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
This commit is contained in:
2026-10-26 12:18:23 +00:00
parent 63690a9ec9
commit 8a03d0dbe7
46 changed files with 753 additions and 908 deletions
+45 -53
View File
@@ -34,11 +34,11 @@ func TestSigner_Sign(t *testing.T) {
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Unix(1704067200, 0), // Fixed timestamp for reproducibility
}
expires := time.Unix(1704067200, 0) // Fixed timestamp for reproducibility
sig1 := signer.Sign(req)
sig2 := signer.Sign(req)
sig1 := signer.Sign(req, expires)
sig2 := signer.Sign(req, expires)
// Same input should produce same signature
if sig1 != sig2 {
@@ -62,18 +62,17 @@ func TestSigner_Sign(t *testing.T) {
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Unix(1704067200, 0),
}
sig3 := signer.Sign(req2)
sig3 := signer.Sign(req2, expires)
if sig1 == sig3 {
t.Error("Sign() produced same signature for different input")
}
}
// validVerifyRequest returns a fully-populated request that verifies
// successfully once signed.
func validVerifyRequest() *imagerequest.Request {
// validVerifyRequest returns a fully-populated request, and an expiry an hour
// ahead, that verify successfully once signed.
func validVerifyRequest() (*imagerequest.Request, time.Time) {
return &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
@@ -83,13 +82,13 @@ func validVerifyRequest() *imagerequest.Request {
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Now().Add(1 * time.Hour),
}
}, time.Now().Add(1 * time.Hour)
}
type verifyCase struct {
name string
setup func() *imagerequest.Request
name string
// setup returns the request, the signature and the expiry to verify
setup func() (*imagerequest.Request, string, time.Time)
wantErr error
}
@@ -97,54 +96,49 @@ func verifyCases(signer *signature.Signer) []verifyCase {
return []verifyCase{
{
name: "valid signature",
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Signature = signer.Sign(req)
setup: func() (*imagerequest.Request, string, time.Time) {
req, expires := validVerifyRequest()
return req
return req, signer.Sign(req, expires), expires
},
wantErr: nil,
},
{
name: "expired signature",
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Expires = time.Now().Add(-1 * time.Hour)
req.Signature = signer.Sign(req)
setup: func() (*imagerequest.Request, string, time.Time) {
req, _ := validVerifyRequest()
expires := time.Now().Add(-1 * time.Hour)
return req
return req, signer.Sign(req, expires), expires
},
wantErr: signature.ErrExpired,
},
{
name: "invalid signature",
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Signature = "invalid-signature"
setup: func() (*imagerequest.Request, string, time.Time) {
req, expires := validVerifyRequest()
return req
return req, "invalid-signature", expires
},
wantErr: signature.ErrInvalid,
},
{
name: "missing expiration",
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Expires = time.Time{}
req.Signature = "some-signature"
setup: func() (*imagerequest.Request, string, time.Time) {
req, _ := validVerifyRequest()
return req
return req, "some-signature", time.Time{}
},
wantErr: signature.ErrMissingExpiration,
},
{
name: "tampered request",
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Signature = signer.Sign(req)
setup: func() (*imagerequest.Request, string, time.Time) {
req, expires := validVerifyRequest()
sig := signer.Sign(req, expires)
req.Source.Path = "/photos/secret.jpg"
return req
return req, sig, expires
},
wantErr: signature.ErrInvalid,
},
@@ -160,8 +154,8 @@ func TestSigner_Verify(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := tt.setup()
err := signer.Verify(req)
req, sig, expires := tt.setup()
err := signer.Verify(req, sig, expires)
if tt.wantErr == nil {
if err != nil {
@@ -254,8 +248,9 @@ func TestSigner_Verify_ExactMatchOnly(t *testing.T) {
signer := signature.New("test-secret-key")
// Base request that we'll sign, then tamper with individual fields.
baseReq := func() *imagerequest.Request {
// Base request that we'll sign, then tamper with individual fields; it
// is returned with its signature and expiry.
baseReq := func() (*imagerequest.Request, string, time.Time) {
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
@@ -266,21 +261,20 @@ func TestSigner_Verify_ExactMatchOnly(t *testing.T) {
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Now().Add(1 * time.Hour),
}
req.Signature = signer.Sign(req)
expires := time.Now().Add(1 * time.Hour)
return req
return req, signer.Sign(req, expires), expires
}
for _, tt := range exactMatchTamperCases() {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := baseReq()
req, sig, expires := baseReq()
tt.tamper(req)
err := signer.Verify(req)
err := signer.Verify(req, sig, expires)
if !errors.Is(err, signature.ErrInvalid) {
t.Errorf("Verify() = %v, want %v", err, signature.ErrInvalid)
}
@@ -291,9 +285,9 @@ func TestSigner_Verify_ExactMatchOnly(t *testing.T) {
t.Run("unmodified request passes", func(t *testing.T) {
t.Parallel()
req := baseReq()
req, sig, expires := baseReq()
err := signer.Verify(req)
err := signer.Verify(req, sig, expires)
if err != nil {
t.Errorf("Verify() unmodified request failed: %v", err)
}
@@ -329,10 +323,9 @@ func TestSigner_Sign_ExactHostInData(t *testing.T) {
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Unix(1704067200, 0),
}
sig := signer.Sign(req)
sig := signer.Sign(req, time.Unix(1704067200, 0))
if existing, ok := sigs[sig]; ok {
t.Errorf("hosts %q and %q produced the same signature", existing, host)
}
@@ -356,20 +349,20 @@ func TestSigner_DifferentKeys(t *testing.T) {
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Now().Add(1 * time.Hour),
}
expires := time.Now().Add(1 * time.Hour)
// Sign with key 1
req.Signature = signer1.Sign(req)
sig := signer1.Sign(req, expires)
// Verify with key 1 should succeed
err := signer1.Verify(req)
err := signer1.Verify(req, sig, expires)
if err != nil {
t.Errorf("Verify() with same key failed: %v", err)
}
// Verify with key 2 should fail
err = signer2.Verify(req)
err = signer2.Verify(req, sig, expires)
if !errors.Is(err, signature.ErrInvalid) {
t.Errorf("Verify() with different key should fail, got: %v", err)
}
@@ -395,12 +388,11 @@ func TestSign_ReadmeQueryExample(t *testing.T) {
Quality: 85,
FitMode: testFitCover,
},
Expires: time.Unix(1704067200, 0),
}
const wantSig = "37aFa9GSVCZM1kRY9SzMOk2fOg_M7Wt5gKNR4xM-FYA="
gotSig := signer.Sign(&req)
gotSig := signer.Sign(&req, time.Unix(1704067200, 0))
if gotSig != wantSig {
t.Errorf("Sign() = %q, want %q", gotSig, wantSig)
}