1 Commits
Author SHA1 Message Date
sneak 2670d9002f 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:34:19 +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 of the // the format auto. The first is signed with testSigningKey, the key the
// image service newSignedHostServer builds, and the second is made with h's // handlers that newSignedHostServer builds check signatures with. The second
// generator. // is made with h's 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 URL with an upstream error. // each of hosts, and answers any other source 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 of the image service newSignedHostServer builds. It takes a Handlers // the key the handlers that newSignedHostServer builds check signatures with.
// it does not use, so that it has the type of encPhotoURL. // It takes a Handlers 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()
+2 -1
View File
@@ -133,7 +133,8 @@ func parseImageComponents(remainder string) (*Request, error) {
return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host) return nil, fmt.Errorf("%w: %q", ErrInvalidHost, host)
} }
// Go's URL parser refuses brackets around anything but an IPv6 address // The allowlist check and the fetcher drop the brackets, so brackets
// 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, as Get writes // given quality and fit mode. Each call returns a new request, which a test
// to the request it is given. // may change.
func photoVariant( func photoVariant(
fixtures *TestFixtures, quality int, fit imagerequest.Fit, fixtures *TestFixtures, quality int, fit imagerequest.Fit,
) *imagerequest.Request { ) *imagerequest.Request {
+4 -5
View File
@@ -17,7 +17,7 @@ const (
testPathUpload = "/uploads/image.jpg" testPathUpload = "/uploads/image.jpg"
) )
func TestService_Get_AllowlistedHost(t *testing.T) { func TestService_Get_GoodHost(t *testing.T) {
t.Parallel() t.Parallel()
svc, fixtures := SetupTestService(t) svc, fixtures := SetupTestService(t)
@@ -58,10 +58,9 @@ func TestService_Get_AllowlistedHost(t *testing.T) {
} }
} }
// TestService_Get_NonAllowlistedHost checks that the image service serves a // TestService_Get_OtherHost checks that the image service serves a second host
// host that is not on the allowlist as any other: the /v1/image/ handler checks // as it serves GoodHost.
// the signature for such a host before it asks the service for the image. func TestService_Get_OtherHost(t *testing.T) {
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 // allowlisted host, valid JPEG GoodHostJPEG string // valid JPEG on GoodHost
GoodHostPNG string // allowlisted host, valid PNG GoodHostPNG string // valid PNG on GoodHost
GoodHostGIF string // allowlisted host, valid GIF GoodHostGIF string // valid GIF on GoodHost
OtherHostJPEG string // non-allowlisted host, valid JPEG OtherHostJPEG string // valid JPEG on OtherHost
OtherHostPNG string // non-allowlisted host, valid PNG OtherHostPNG string // valid PNG on OtherHost
// 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 // allowlisted hostname GoodHost string // a host the mock fetcher serves
OtherHost string // non-allowlisted hostname OtherHost string // a second host the mock fetcher serves
} }
// DefaultFixtures returns the standard test fixture paths. // DefaultFixtures returns the standard test fixture paths.