Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1dc65761b | ||
|
|
2005143e3d | ||
|
|
f16b6bd6a6 |
@@ -79,14 +79,14 @@ hosts require an HMAC-SHA256 signature.
|
|||||||
|
|
||||||
Signatures use HMAC-SHA256 and include an expiration timestamp to
|
Signatures use HMAC-SHA256 and include an expiration timestamp to
|
||||||
prevent replay attacks. Signatures are **exact match only**: every
|
prevent replay attacks. Signatures are **exact match only**: every
|
||||||
component (host, path, query, dimensions, format, expiration, quality,
|
component (host, path, query, dimensions, format, expiration) must
|
||||||
fit) must match exactly what was signed. No suffix matching, wildcard
|
match exactly what was signed. No suffix matching, wildcard matching,
|
||||||
matching, or partial matching is supported.
|
or partial matching is supported.
|
||||||
|
|
||||||
**Signed data format** (colon-separated):
|
**Signed data format** (colon-separated):
|
||||||
|
|
||||||
```
|
```
|
||||||
HMAC-SHA256(secret, "host:path:query:width:height:format:expiration:quality:fit")
|
HMAC-SHA256(secret, "host:path:query:width:height:format:expiration")
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
@@ -98,25 +98,18 @@ Where:
|
|||||||
- `height` — requested height in pixels, `0` for original
|
- `height` — requested height in pixels, `0` for original
|
||||||
- `format` — output format (jpeg, png, webp, avif, gif, orig)
|
- `format` — output format (jpeg, png, webp, avif, gif, orig)
|
||||||
- `expiration` — Unix timestamp when signature expires
|
- `expiration` — Unix timestamp when signature expires
|
||||||
- `quality` — the URL's `q` query parameter (1-100), or `85` when the URL
|
|
||||||
has no `q`
|
|
||||||
- `fit` — the URL's `fit` query parameter (cover, contain, fill, inside,
|
|
||||||
outside), or `cover` when the URL has no `fit`
|
|
||||||
|
|
||||||
**Example:** resize `https://cdn.example.com/photos/cat.jpg` to 800x600
|
**Example:** resize
|
||||||
WebP with expiration 1704067200, default quality and fit:
|
`https://cdn.example.com/photos/cat.jpg` to 800x600 WebP with
|
||||||
|
expiration 1704067200:
|
||||||
|
|
||||||
1. Build input:
|
1. Build input:
|
||||||
`cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover`
|
`cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200`
|
||||||
2. Compute HMAC-SHA256 with your secret key
|
2. Compute HMAC-SHA256 with your secret key
|
||||||
3. Base64URL-encode the result
|
3. Base64URL-encode the result
|
||||||
4. URL:
|
4. URL:
|
||||||
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200`
|
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200`
|
||||||
|
|
||||||
For the same image at quality 40 with fit `contain`, the input ends in
|
|
||||||
`:40:contain` and the URL is
|
|
||||||
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200&q=40&fit=contain`.
|
|
||||||
|
|
||||||
**Allowlist patterns:**
|
**Allowlist patterns:**
|
||||||
|
|
||||||
- **Exact match**: `cdn.example.com` — matches only that host
|
- **Exact match**: `cdn.example.com` — matches only that host
|
||||||
|
|||||||
@@ -42,13 +42,6 @@ exhaustion
|
|||||||
the config file is looked for under `/etc/pixa` and `~/.config/pixa`
|
the config file is looked for under `/etc/pixa` and `~/.config/pixa`
|
||||||
instead of the daemon name `pixad`; documented in `README.md` and
|
instead of the daemon name `pixad`; documented in `README.md` and
|
||||||
`config.example.yml`.
|
`config.example.yml`.
|
||||||
- 2026-09-28 quality and fit in the URL signature (closes #60): the signed
|
|
||||||
data is now `host:path:query:width:height:format:expiration:quality:fit`,
|
|
||||||
using `85` and `cover` when the URL has no `q` or `fit`, so one signed
|
|
||||||
URL can no longer be replayed across other quality and fit values to
|
|
||||||
create unauthorized cache entries and transcodes; the known-answer
|
|
||||||
vectors in `internal/signature/golden_test.go` and the README signature
|
|
||||||
specification describe the new format.
|
|
||||||
- 2026-09-28 Docker image healthcheck (closes #111): a `HEALTHCHECK` in
|
- 2026-09-28 Docker image healthcheck (closes #111): a `HEALTHCHECK` in
|
||||||
the runtime stage probing `/.well-known/healthcheck.json` with busybox
|
the runtime stage probing `/.well-known/healthcheck.json` with busybox
|
||||||
`wget`; `script/docker-smoke` (`make docker-smoke`) builds the image,
|
`wget`; `script/docker-smoke` (`make docker-smoke`) builds the image,
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"sneak.berlin/go/pixa/internal/imgcache"
|
|
||||||
"sneak.berlin/go/pixa/internal/signature"
|
|
||||||
)
|
|
||||||
|
|
||||||
// signedHost is not on the allowlist setupTestHandler builds, so a request
|
|
||||||
// for it needs a valid signature. No image is served for it: a request that
|
|
||||||
// passes the signature check gets 502 from the failed fetch, and one that
|
|
||||||
// fails the check gets 401.
|
|
||||||
const signedHost = "signed.example.com"
|
|
||||||
|
|
||||||
// getImage sends a GET for target to the image route of fix and returns the
|
|
||||||
// response status.
|
|
||||||
func getImage(t *testing.T, fix *testFixtures, target string) int {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
r := chi.NewRouter()
|
|
||||||
r.Get("/v1/image/*", fix.handler.HandleImage())
|
|
||||||
|
|
||||||
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, target, nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r.ServeHTTP(rec, req)
|
|
||||||
t.Logf("GET %s: %d", target, rec.Code)
|
|
||||||
|
|
||||||
return rec.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHandleImage_SignatureCoversQualityAndFit signs a URL for quality 85
|
|
||||||
// and fit cover, the values the route uses when a URL has no q or fit, and
|
|
||||||
// sends that signature with each q and fit below.
|
|
||||||
func TestHandleImage_SignatureCoversQualityAndFit(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
expires := time.Now().Add(time.Hour)
|
|
||||||
signer := signature.New("test-signing-key-must-be-32-chars")
|
|
||||||
sig := signer.Sign(&signature.Request{
|
|
||||||
SourceHost: signedHost,
|
|
||||||
SourcePath: "/images/photo.jpg",
|
|
||||||
Width: 50,
|
|
||||||
Height: 50,
|
|
||||||
Format: string(imgcache.FormatJPEG),
|
|
||||||
Quality: 85,
|
|
||||||
FitMode: string(imgcache.FitCover),
|
|
||||||
Expires: expires,
|
|
||||||
})
|
|
||||||
signedURL := fmt.Sprintf("/v1/image/%s/images/photo.jpg/50x50.jpeg?sig=%s&exp=%d",
|
|
||||||
signedHost, sig, expires.Unix())
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
query string
|
|
||||||
wantStatus int
|
|
||||||
}{
|
|
||||||
{"no q or fit", "", http.StatusBadGateway},
|
|
||||||
{"q=85 and fit=cover", "&q=85&fit=cover", http.StatusBadGateway},
|
|
||||||
{"replayed with q=40", "&q=40", http.StatusUnauthorized},
|
|
||||||
{"replayed with fit=contain", "&fit=contain", http.StatusUnauthorized},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
status := getImage(t, setupTestHandler(t), signedURL+tt.query)
|
|
||||||
if status != tt.wantStatus {
|
|
||||||
t.Errorf("status = %d, want %d", status, tt.wantStatus)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHandleImage_GeneratedSignedURLVerifies sends URLs built by the
|
|
||||||
// service's signed-URL generator to the route.
|
|
||||||
func TestHandleImage_GeneratedSignedURLVerifies(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
quality int
|
|
||||||
fitMode imgcache.FitMode
|
|
||||||
}{
|
|
||||||
{"quality 40 and fit contain", 40, imgcache.FitContain},
|
|
||||||
{"quality and fit unset", 0, ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fix := setupTestHandler(t)
|
|
||||||
|
|
||||||
signedURL, err := fix.service.GenerateSignedURL("", &imgcache.ImageRequest{
|
|
||||||
SourceHost: signedHost,
|
|
||||||
SourcePath: "/images/photo.jpg",
|
|
||||||
Size: imgcache.Size{Width: 50, Height: 50},
|
|
||||||
Format: imgcache.FormatJPEG,
|
|
||||||
Quality: tt.quality,
|
|
||||||
FitMode: tt.fitMode,
|
|
||||||
}, time.Hour)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GenerateSignedURL() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
status := getImage(t, fix, signedURL)
|
|
||||||
if status != http.StatusBadGateway {
|
|
||||||
t.Errorf("status = %d, want %d", status, http.StatusBadGateway)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -205,23 +205,12 @@ func (s *Service) ValidateRequest(req *ImageRequest) error {
|
|||||||
return s.signer.Verify(signatureRequest(req))
|
return s.signer.Verify(signatureRequest(req))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSignedURL generates a signed URL for the given request. The URL
|
// GenerateSignedURL generates a signed URL for the given request.
|
||||||
// carries q and fit next to sig and exp, so the image route verifies it for
|
|
||||||
// the quality and fit it was signed with. An unset quality or fit is first
|
|
||||||
// set to 85 or cover, the values the route uses when a URL has no q or fit.
|
|
||||||
func (s *Service) GenerateSignedURL(
|
func (s *Service) GenerateSignedURL(
|
||||||
baseURL string,
|
baseURL string,
|
||||||
req *ImageRequest,
|
req *ImageRequest,
|
||||||
ttl time.Duration,
|
ttl time.Duration,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
if req.Quality == 0 {
|
|
||||||
req.Quality = 85
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.FitMode == "" {
|
|
||||||
req.FitMode = FitCover
|
|
||||||
}
|
|
||||||
|
|
||||||
sigReq := signatureRequest(req)
|
sigReq := signatureRequest(req)
|
||||||
path, sig, exp := s.signer.GenerateSignedURL(sigReq, ttl)
|
path, sig, exp := s.signer.GenerateSignedURL(sigReq, ttl)
|
||||||
|
|
||||||
@@ -229,8 +218,7 @@ func (s *Service) GenerateSignedURL(
|
|||||||
req.Expires = sigReq.Expires
|
req.Expires = sigReq.Expires
|
||||||
req.Signature = sigReq.Signature
|
req.Signature = sigReq.Signature
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s?sig=%s&exp=%d&q=%d&fit=%s",
|
return fmt.Sprintf("%s%s?sig=%s&exp=%d", baseURL, path, sig, exp), nil
|
||||||
baseURL, path, sig, exp, req.Quality, req.FitMode), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadCachedSource attempts to load source content from cache, returning nil
|
// loadCachedSource attempts to load source content from cache, returning nil
|
||||||
@@ -464,8 +452,6 @@ func signatureRequest(req *ImageRequest) *signature.Request {
|
|||||||
Width: req.Size.Width,
|
Width: req.Size.Width,
|
||||||
Height: req.Size.Height,
|
Height: req.Size.Height,
|
||||||
Format: string(req.Format),
|
Format: string(req.Format),
|
||||||
Quality: req.Quality,
|
|
||||||
FitMode: string(req.FitMode),
|
|
||||||
Signature: req.Signature,
|
Signature: req.Signature,
|
||||||
Expires: req.Expires,
|
Expires: req.Expires,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,12 +41,9 @@ func goldenVectors() []goldenVector {
|
|||||||
Width: 800,
|
Width: 800,
|
||||||
Height: 600,
|
Height: 600,
|
||||||
Format: testFormatWebP,
|
Format: testFormatWebP,
|
||||||
Quality: 85,
|
|
||||||
FitMode: testFitCover,
|
|
||||||
},
|
},
|
||||||
// Signed data:
|
// Signed data: "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200"
|
||||||
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover"
|
wantSignature: "x5PfPp8QSDo0cJT96od-AEgrQyOVLfqifH5sst61_-w=",
|
||||||
wantSignature: "kdqeGoW2SX7qnaYtoB970wEnLydn0UnIgQYQLfAnjXQ=",
|
|
||||||
wantSignedPath: testSignedPath,
|
wantSignedPath: testSignedPath,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -58,12 +55,10 @@ func goldenVectors() []goldenVector {
|
|||||||
Width: 800,
|
Width: 800,
|
||||||
Height: 600,
|
Height: 600,
|
||||||
Format: testFormatWebP,
|
Format: testFormatWebP,
|
||||||
Quality: 85,
|
|
||||||
FitMode: testFitCover,
|
|
||||||
},
|
},
|
||||||
// Signed data:
|
// Signed data:
|
||||||
// "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200:85:cover"
|
// "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200"
|
||||||
wantSignature: "pKgVBOTd_Q_EikI7MNQLC9Q8Hurdxzyv3EIYvVhqc2I=",
|
wantSignature: "394_Vf9TdQFkpQ3XKFDQSyxgqKq8N7mApf2S4QaHqyo=",
|
||||||
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg" +
|
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg" +
|
||||||
"%3Ftoken=abc&v=2/800x600.webp",
|
"%3Ftoken=abc&v=2/800x600.webp",
|
||||||
},
|
},
|
||||||
@@ -76,34 +71,11 @@ func goldenVectors() []goldenVector {
|
|||||||
Width: 0,
|
Width: 0,
|
||||||
Height: 0,
|
Height: 0,
|
||||||
Format: testFormatPNG,
|
Format: testFormatPNG,
|
||||||
Quality: 85,
|
|
||||||
FitMode: testFitCover,
|
|
||||||
},
|
},
|
||||||
// Signed data:
|
// Signed data: "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200"
|
||||||
// "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200:85:cover"
|
wantSignature: "7Be7oteeQwvnSPU4bchyQ4ZGYGsAGBKpeEtuQ02ox60=",
|
||||||
wantSignature: "6_rZ0yyVbGZRs8kG7n7HLgLi5Jt8vjiWQljIEL1jbIs=",
|
|
||||||
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg/orig.png",
|
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg/orig.png",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "non-default quality and fit",
|
|
||||||
req: signature.Request{
|
|
||||||
SourceHost: testHost,
|
|
||||||
SourcePath: testPath,
|
|
||||||
SourceQuery: "",
|
|
||||||
Width: 800,
|
|
||||||
Height: 600,
|
|
||||||
Format: testFormatWebP,
|
|
||||||
Quality: 40,
|
|
||||||
FitMode: testFitContain,
|
|
||||||
},
|
|
||||||
// Signed data:
|
|
||||||
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:40:contain"
|
|
||||||
wantSignature: "pGaXpPUbI3A7nMx-4T9bfq9bYWBNL0kY4bxlcv3g1F8=",
|
|
||||||
// The path is the same as for the default quality and fit:
|
|
||||||
// q=40&fit=contain go in the query string next to sig and
|
|
||||||
// exp (imgcache.Service.GenerateSignedURL adds all four).
|
|
||||||
wantSignedPath: testSignedPath,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +84,11 @@ func goldenVectors() []goldenVector {
|
|||||||
// hardcoded signing key.
|
// hardcoded signing key.
|
||||||
//
|
//
|
||||||
// If any of these assertions fail, the signed byte format
|
// If any of these assertions fail, the signed byte format
|
||||||
// ("host:path:query:width:height:format:expiration:quality:fit"), the
|
// ("host:path:query:width:height:format:expiration"), the base64url
|
||||||
// base64url encoding, or the signed URL layout has changed. Update these
|
// encoding, or the signed URL layout has changed. Such a change breaks
|
||||||
// constants only when that change is intended.
|
// every signature already issued to clients, so it must be made
|
||||||
|
// deliberately: update these constants only as part of an intentional,
|
||||||
|
// documented signature format migration.
|
||||||
func TestSigner_GoldenVectors(t *testing.T) {
|
func TestSigner_GoldenVectors(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package signature_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"sneak.berlin/go/pixa/internal/signature"
|
|
||||||
)
|
|
||||||
|
|
||||||
// signedQualityFitRequest returns a request signed for quality 85 and fit
|
|
||||||
// mode "cover", the effective defaults the handler applies before
|
|
||||||
// verification.
|
|
||||||
func signedQualityFitRequest(signer *signature.Signer) *signature.Request {
|
|
||||||
req := &signature.Request{
|
|
||||||
SourceHost: testHost,
|
|
||||||
SourcePath: testPath,
|
|
||||||
Width: 800,
|
|
||||||
Height: 600,
|
|
||||||
Format: testFormatWebP,
|
|
||||||
Quality: 85,
|
|
||||||
FitMode: testFitCover,
|
|
||||||
Expires: time.Now().Add(1 * time.Hour),
|
|
||||||
}
|
|
||||||
req.Signature = signer.Sign(req)
|
|
||||||
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSigner_Verify_QualityAndFitAreSigned proves that quality and fit are
|
|
||||||
// covered by the signature: a URL signed for one quality or fit mode must
|
|
||||||
// not verify when replayed with a different quality or fit mode. This is the
|
|
||||||
// amplification vector from the issue — one signed URL replayed across many
|
|
||||||
// quality and fit values yields many unauthorized cache entries and
|
|
||||||
// transcodes — so it must be rejected.
|
|
||||||
func TestSigner_Verify_QualityAndFitAreSigned(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
signer := signature.New("test-secret-key")
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
tamper func(r *signature.Request)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "replayed with different quality",
|
|
||||||
tamper: func(r *signature.Request) { r.Quality = 40 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "replayed with different fit mode",
|
|
||||||
tamper: func(r *signature.Request) { r.FitMode = testFitContain },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range cases {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
req := signedQualityFitRequest(signer)
|
|
||||||
tt.tamper(req)
|
|
||||||
|
|
||||||
err := signer.Verify(req)
|
|
||||||
if !errors.Is(err, signature.ErrInvalid) {
|
|
||||||
t.Errorf("Verify() = %v, want %v", err, signature.ErrInvalid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,15 +37,6 @@ type Request struct {
|
|||||||
Height int
|
Height int
|
||||||
// Format is the requested output format (e.g. "webp").
|
// Format is the requested output format (e.g. "webp").
|
||||||
Format string
|
Format string
|
||||||
// Quality is the requested output quality (1-100) for lossy formats.
|
|
||||||
// It is the effective value the request resolves to: callers pass the
|
|
||||||
// default quality when the request omits the parameter, so an omitted
|
|
||||||
// quality signs identically to that same value stated explicitly.
|
|
||||||
Quality int
|
|
||||||
// FitMode is how the image is fit into the requested dimensions
|
|
||||||
// (e.g. "cover"). Like Quality it is the effective value: callers pass
|
|
||||||
// the default fit mode when the request omits the parameter.
|
|
||||||
FitMode string
|
|
||||||
// Signature is the HMAC signature to verify.
|
// Signature is the HMAC signature to verify.
|
||||||
Signature string
|
Signature string
|
||||||
// Expires is the signature expiration timestamp.
|
// Expires is the signature expiration timestamp.
|
||||||
@@ -65,8 +56,7 @@ func New(secretKey string) *Signer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sign generates an HMAC-SHA256 signature for the given request.
|
// Sign generates an HMAC-SHA256 signature for the given request.
|
||||||
// The signature covers: host + path + query + width + height + format +
|
// The signature covers: host + path + query + width + height + format + expiration.
|
||||||
// expiration + quality + fit.
|
|
||||||
func (s *Signer) Sign(req *Request) string {
|
func (s *Signer) Sign(req *Request) string {
|
||||||
data := s.buildSignatureData(req)
|
data := s.buildSignatureData(req)
|
||||||
mac := hmac.New(sha256.New, s.secretKey)
|
mac := hmac.New(sha256.New, s.secretKey)
|
||||||
@@ -78,8 +68,7 @@ func (s *Signer) Sign(req *Request) string {
|
|||||||
|
|
||||||
// Verify checks if the signature on the request is valid and not expired.
|
// Verify checks if the signature on the request is valid and not expired.
|
||||||
// Signatures are exact-match only: every component of the signed data
|
// Signatures are exact-match only: every component of the signed data
|
||||||
// (host, path, query, dimensions, format, expiration, quality, fit) must
|
// (host, path, query, dimensions, format, expiration) must match exactly.
|
||||||
// match exactly.
|
|
||||||
// No suffix matching, wildcard matching, or partial matching is supported.
|
// No suffix matching, wildcard matching, or partial matching is supported.
|
||||||
// A signature for "cdn.example.com" will NOT verify for "example.com" or
|
// A signature for "cdn.example.com" will NOT verify for "example.com" or
|
||||||
// "other.cdn.example.com", and vice versa.
|
// "other.cdn.example.com", and vice versa.
|
||||||
@@ -153,13 +142,11 @@ func (s *Signer) GenerateSignedURL(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildSignatureData creates the string to be signed.
|
// buildSignatureData creates the string to be signed.
|
||||||
// Format: "host:path:query:width:height:format:expiration:quality:fit"
|
// Format: "host:path:query:width:height:format:expiration"
|
||||||
// All components are used verbatim (exact match). No normalization,
|
// All components are used verbatim (exact match). No normalization,
|
||||||
// suffix matching, or wildcard expansion is performed. Quality and fit
|
// suffix matching, or wildcard expansion is performed.
|
||||||
// are the effective transform values, so replaying a signed URL with a
|
|
||||||
// different quality or fit mode fails verification.
|
|
||||||
func (s *Signer) buildSignatureData(req *Request) string {
|
func (s *Signer) buildSignatureData(req *Request) string {
|
||||||
return fmt.Sprintf("%s:%s:%s:%d:%d:%s:%d:%d:%s",
|
return fmt.Sprintf("%s:%s:%s:%d:%d:%s:%d",
|
||||||
req.SourceHost,
|
req.SourceHost,
|
||||||
req.SourcePath,
|
req.SourcePath,
|
||||||
req.SourceQuery,
|
req.SourceQuery,
|
||||||
@@ -167,8 +154,6 @@ func (s *Signer) buildSignatureData(req *Request) string {
|
|||||||
req.Height,
|
req.Height,
|
||||||
req.Format,
|
req.Format,
|
||||||
req.Expires.Unix(),
|
req.Expires.Unix(),
|
||||||
req.Quality,
|
|
||||||
req.FitMode,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ const (
|
|||||||
testPath = "/photos/cat.jpg"
|
testPath = "/photos/cat.jpg"
|
||||||
testFormatWebP = "webp"
|
testFormatWebP = "webp"
|
||||||
testFormatPNG = "png"
|
testFormatPNG = "png"
|
||||||
testFitCover = "cover"
|
|
||||||
testFitContain = "contain"
|
|
||||||
testSignedPath = "/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp"
|
testSignedPath = "/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp"
|
||||||
testSig = "abc123"
|
testSig = "abc123"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user