Files
pixa/internal/handlers/imageenc.go
T
sneak 65560f7af8
check / check (push) Canceled after 0s
Serve both image routes with one function and one error mapping (closes #316)
HandleImage parses the path and verifies the signature, and HandleImageEnc
decrypts the token. Each then calls serveImage, which chooses the format for
auto, gets the image and serves it: the headers, ETag and 304, HEAD, the copy
and the log line. respondImageError answers the image service's errors on both
routes, and handleImageError is gone. /v1/image/ now answers 504 when the
upstream sends no response headers within upstream_fetch_timeout, as README.md
documented for /v1/e/. respondImageError logs a 500 at error level and every
other answer at warn. A failed write is logged once, with the request and the
bytes served. README.md lists each image route's answers as bullets. Tests send
the same errors through both routes.

Model: opus-5-5
2026-10-26 13:45:47 +00:00

78 lines
2.2 KiB
Go

package handlers
import (
"errors"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"sneak.berlin/go/pixa/internal/encurl"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// HandleImageEnc handles requests to /v1/e/{token}/* for encrypted
// image URLs. The trailing path (e.g., /img.jpg) is ignored but helps
// browsers identify the content type.
func (s *Handlers) HandleImageEnc() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if s.refuseBlockedReferer(w, r) {
return
}
req, expires, ok := s.parseImageEncRequest(w, r)
if !ok {
return
}
s.serveImage(w, r, req, expires)
}
}
// parseImageEncRequest decrypts the token of an encrypted image URL into an
// image request and checks it, and returns it with the token's expiry. On a
// token that is missing, does not decrypt, has expired or asks for something
// not valid, it writes an error response and returns false.
func (s *Handlers) parseImageEncRequest(
w http.ResponseWriter, r *http.Request,
) (*imagerequest.Request, time.Time, bool) {
// Extract token from URL
token := chi.URLParam(r, "token")
if token == "" {
s.respondError(w, "missing token", http.StatusBadRequest)
return nil, time.Time{}, false
}
// Decrypt and validate the payload
req, expires, err := s.encGen.Parse(token)
if err != nil {
if errors.Is(err, encurl.ErrExpired) {
s.log.Debug("encrypted URL expired", "error", err)
s.respondError(w, "URL has expired", http.StatusGone)
return nil, time.Time{}, false
}
s.log.Debug("failed to decrypt URL", "error", err)
s.respondError(w, "invalid encrypted URL", http.StatusBadRequest)
return nil, time.Time{}, false
}
// Apply the same dimension and fit-mode bounds as the plain image
// route: a sealed payload is trusted for its origin, not for staying
// within limits, so an over-limit size or unknown fit mode is a 400
// here rather than an out-of-memory or a 500 from the processor.
err = imagerequest.ValidateImageRequest(req)
if err != nil {
s.log.Debug("encrypted URL failed validation", "error", err)
s.respondError(w, "invalid encrypted URL: "+err.Error(),
http.StatusBadRequest)
return nil, time.Time{}, false
}
return req, expires, true
}