refactor: extract signature package from imgcache
All checks were successful
check / check (push) Successful in 1m46s

Move HMAC-SHA256 request signing out of internal/imgcache/ into its own
internal/signature/ package, per the plan in issue #39. This is one of the
remaining "easily separable" extractions; only the signer is moved here so
the diff stays reviewable.

What moved (from internal/imgcache/signature.go):

- Signer type, its New constructor, Sign, Verify, GenerateSignedURL
- ParseParams (query-string signature/expiration parsing)
- Signature error sentinels

To keep the import edge one-way (imgcache depends on signature, never the
reverse), the package defines a standalone Request type carrying just the
fields the signature covers, instead of importing imgcache.ImageRequest.
imgcache projects its ImageRequest onto signature.Request via a small
unexported helper.

Renames (no stuttering):

- NewSigner -> signature.New
- ParseSignatureParams -> signature.ParseParams
- ErrSignatureRequired/Invalid/Expired -> signature.ErrRequired/Invalid/Expired

The "non-whitelisted host" error message is updated to "non-allowlisted"
for inclusive terminology, consistent with the allowlist rename.

Pure refactor. The bytes fed to the HMAC are unchanged, so signatures remain
compatible. All existing tests move with the package; docker build passes
(fmt-check, lint, test, build).

refs #39
This commit is contained in:
2026-07-25 17:51:30 +07:00
parent b6e9ac2a93
commit 6526b717dd
5 changed files with 223 additions and 168 deletions

View File

@@ -15,6 +15,7 @@ import (
"sneak.berlin/go/pixa/internal/httpfetcher"
"sneak.berlin/go/pixa/internal/imageprocessor"
"sneak.berlin/go/pixa/internal/magic"
"sneak.berlin/go/pixa/internal/signature"
)
// Service implements the ImageCache interface, orchestrating cache, fetcher, and processor.
@@ -22,7 +23,7 @@ type Service struct {
cache *Cache
fetcher httpfetcher.Fetcher
processor *imageprocessor.ImageProcessor
signer *Signer
signer *signature.Signer
allowlist *allowlist.HostAllowList
log *slog.Logger
allowHTTP bool
@@ -69,7 +70,7 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
fetcher = httpfetcher.New(fetcherCfg)
}
signer := NewSigner(cfg.SigningKey)
signer := signature.New(cfg.SigningKey)
log := cfg.Logger
if log == nil {
@@ -397,7 +398,7 @@ func (s *Service) ValidateRequest(req *ImageRequest) error {
}
// Signature required for non-allowed hosts
return s.signer.Verify(req)
return s.signer.Verify(signatureRequest(req))
}
// GenerateSignedURL generates a signed URL for the given request.
@@ -406,11 +407,32 @@ func (s *Service) GenerateSignedURL(
req *ImageRequest,
ttl time.Duration,
) (string, error) {
path, sig, exp := s.signer.GenerateSignedURL(req, ttl)
sigReq := signatureRequest(req)
path, sig, exp := s.signer.GenerateSignedURL(sigReq, ttl)
// Propagate the generated signature and expiration back onto the request.
req.Expires = sigReq.Expires
req.Signature = sigReq.Signature
return fmt.Sprintf("%s%s?sig=%s&exp=%d", baseURL, path, sig, exp), nil
}
// signatureRequest projects an ImageRequest onto the standalone
// signature.Request type used by the signature package. This keeps the
// import edge one-way: imgcache depends on signature, never the reverse.
func signatureRequest(req *ImageRequest) *signature.Request {
return &signature.Request{
SourceHost: req.SourceHost,
SourcePath: req.SourcePath,
SourceQuery: req.SourceQuery,
Width: req.Size.Width,
Height: req.Size.Height,
Format: string(req.Format),
Signature: req.Signature,
Expires: req.Expires,
}
}
// HTTP status codes for error responses.
const (
httpStatusBadGateway = 502