1 Commits
Author SHA1 Message Date
sneak 8a03d0dbe7 Take the signature, the expiry and the URL scheme off the image request (closes #314)
check / check (push) Canceled after 0s
The image request now holds only its source and its transform, exactly what
the variant key covers. The /v1/image/ handler checks the allowlist and
verifies the signature itself with Signer.Verify(req, sig, expires). Both
image handlers keep the URL's expiry as a local value for Cache-Control. The
image service holds no signer, allowlist or AllowHTTP, and ValidateRequest is
gone. The fetcher takes the source and builds its URL with Source.URL from its
own AllowHTTP, so no URL string is built and parsed again. chooseAutoFormat
returns the format it chooses instead of writing into the request. The signed
URL format and every route's answers are unchanged.

Model: opus-5-5
2026-10-26 12:18:23 +00:00
7 changed files with 21 additions and 21 deletions
@@ -75,9 +75,9 @@ func TestFormatForAccept(t *testing.T) {
// autoPhotoURLs returns a signed /v1/image/ URL and an encrypted /v1/e/ URL, // autoPhotoURLs returns a signed /v1/image/ URL and an encrypted /v1/e/ URL,
// both valid for a minute, for the JPEG at photoPath on signedHost at 50x50 in // both valid for a minute, for the JPEG at photoPath on signedHost at 50x50 in
// the format auto. The first is signed with testSigningKey, the key the // the format auto. The first is signed with testSigningKey, the key of the
// handlers that newSignedHostServer builds check signatures with. The second // image service newSignedHostServer builds, and the second is made with h's
// is made with h's generator. // generator.
func autoPhotoURLs(t *testing.T, h *Handlers) (string, string) { func autoPhotoURLs(t *testing.T, h *Handlers) (string, string) {
t.Helper() t.Helper()
@@ -67,7 +67,7 @@ func newImageRoute(t *testing.T, fetcher httpfetcher.Fetcher) http.Handler {
} }
// newPhotoFetcher returns a mock fetcher that serves a JPEG at photoPath on // newPhotoFetcher returns a mock fetcher that serves a JPEG at photoPath on
// each of hosts, and answers any other source with an upstream error. // each of hosts, and answers any other URL with an upstream error.
func newPhotoFetcher(t *testing.T, hosts ...string) *httpfetcher.MockFetcher { func newPhotoFetcher(t *testing.T, hosts ...string) *httpfetcher.MockFetcher {
t.Helper() t.Helper()
@@ -18,8 +18,8 @@ import (
// signedPhotoURL returns a signed /v1/image/ URL, valid for a minute, for the // signedPhotoURL returns a signed /v1/image/ URL, valid for a minute, for the
// JPEG at photoPath on signedHost at 50x50. It is signed with testSigningKey, // JPEG at photoPath on signedHost at 50x50. It is signed with testSigningKey,
// the key the handlers that newSignedHostServer builds check signatures with. // the key of the image service newSignedHostServer builds. It takes a Handlers
// It takes a Handlers it does not use, so that it has the type of encPhotoURL. // it does not use, so that it has the type of encPhotoURL.
func signedPhotoURL(t *testing.T, _ *Handlers) string { func signedPhotoURL(t *testing.T, _ *Handlers) string {
t.Helper() t.Helper()
+1 -2
View File
@@ -133,8 +133,7 @@ func parseImageComponents(remainder string) (*Request, error) {
return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host) return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host)
} }
// The allowlist check and the fetcher drop the brackets, so brackets // Go's URL parser refuses brackets around anything but an IPv6 address
// around anything but an IPv6 address would give a host a second spelling
if inBrackets := hostMatch[1]; inBrackets != "" { if inBrackets := hostMatch[1]; inBrackets != "" {
addr, err := netip.ParseAddr(inBrackets) addr, err := netip.ParseAddr(inBrackets)
if err != nil || !addr.Is6() { if err != nil || !addr.Is6() {
@@ -79,8 +79,8 @@ func setupHeldFetchService(t *testing.T) (*Service, *TestFixtures, *heldFetcher)
} }
// photoVariant asks for the test photo, 100x100, at 50x25 as a JPEG of the // photoVariant asks for the test photo, 100x100, at 50x25 as a JPEG of the
// given quality and fit mode. Each call returns a new request, which a test // given quality and fit mode. Each call returns a new request, as Get writes
// may change. // to the request it is given.
func photoVariant( func photoVariant(
fixtures *TestFixtures, quality int, fit imagerequest.Fit, fixtures *TestFixtures, quality int, fit imagerequest.Fit,
) *imagerequest.Request { ) *imagerequest.Request {
+5 -4
View File
@@ -17,7 +17,7 @@ const (
testPathUpload = "/uploads/image.jpg" testPathUpload = "/uploads/image.jpg"
) )
func TestService_Get_GoodHost(t *testing.T) { func TestService_Get_AllowlistedHost(t *testing.T) {
t.Parallel() t.Parallel()
svc, fixtures := SetupTestService(t) svc, fixtures := SetupTestService(t)
@@ -58,9 +58,10 @@ func TestService_Get_GoodHost(t *testing.T) {
} }
} }
// TestService_Get_OtherHost checks that the image service serves a second host // TestService_Get_NonAllowlistedHost checks that the image service serves a
// as it serves GoodHost. // host that is not on the allowlist as any other: the /v1/image/ handler checks
func TestService_Get_OtherHost(t *testing.T) { // the signature for such a host before it asks the service for the image.
func TestService_Get_NonAllowlistedHost(t *testing.T) {
t.Parallel() t.Parallel()
svc, fixtures := SetupTestService(t) svc, fixtures := SetupTestService(t)
+7 -7
View File
@@ -31,11 +31,11 @@ const (
// TestFixtures contains paths to test files in the mock filesystem. // TestFixtures contains paths to test files in the mock filesystem.
type TestFixtures struct { type TestFixtures struct {
// Valid image files // Valid image files
GoodHostJPEG string // valid JPEG on GoodHost GoodHostJPEG string // allowlisted host, valid JPEG
GoodHostPNG string // valid PNG on GoodHost GoodHostPNG string // allowlisted host, valid PNG
GoodHostGIF string // valid GIF on GoodHost GoodHostGIF string // allowlisted host, valid GIF
OtherHostJPEG string // valid JPEG on OtherHost OtherHostJPEG string // non-allowlisted host, valid JPEG
OtherHostPNG string // valid PNG on OtherHost OtherHostPNG string // non-allowlisted host, valid PNG
// Invalid/edge case files // Invalid/edge case files
InvalidFile string // file with wrong magic bytes InvalidFile string // file with wrong magic bytes
@@ -43,8 +43,8 @@ type TestFixtures struct {
TextFile string // text file masquerading as image TextFile string // text file masquerading as image
// Hostnames // Hostnames
GoodHost string // a host the mock fetcher serves GoodHost string // allowlisted hostname
OtherHost string // a second host the mock fetcher serves OtherHost string // non-allowlisted hostname
} }
// DefaultFixtures returns the standard test fixture paths. // DefaultFixtures returns the standard test fixture paths.