check / check (push) Canceled after 0s
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. 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 the other answers at warn. The image service alone logs a source refused for its first bytes. A failed write is logged once, at warn, 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
79 lines
2.2 KiB
Go
79 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("invalid encrypted 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. This refuses an over-limit size before the source is
|
|
// fetched. An unknown fit mode is a 400 here rather than 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
|
|
}
|