check / check (push) Canceled after 0s
seal.DeriveKeys derives the session hash and block keys, the encrypted URL key and the CSRF key from the signing key, with their salts declared beside it, and returns them as one seal.Keys. internal/providers calls it once at startup and provides the value to the fx graph. The session manager, the encrypted URL generator and the CSRF protection each take their own key. session.NewManager, encurl.NewGenerator, newCSRFProtect and handlers.New return no error, as nothing in them can fail now. Each key is derived as before, with seal.DeriveKey and the same salt. The signing key itself stays the HMAC key of signed URLs and the login password. README.md says under signing_key what it protects. Model: opus-5-5
161 lines
4.3 KiB
Go
161 lines
4.3 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"image/jpeg"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
)
|
|
|
|
// TestImageProxyRefusesComputedSideAboveMaximum requests sizes of a 3000x600
|
|
// and a 600x3000 PNG through both image routes. A size with one side 0, whose
|
|
// side computed from the source's proportions would be above 8192, is refused
|
|
// with 400 and the JSON error body. The sizes after those refusals are served
|
|
// as before, which also shows that a refusal is not cached as a failed fetch of
|
|
// the source: orig, a size with both sides given, and a size with one side 0
|
|
// whose computed side stays within 8192.
|
|
func TestImageProxyRefusesComputedSideAboveMaximum(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// The upstream paths of the two sources
|
|
const (
|
|
widePath = "/wide.png"
|
|
tallPath = "/tall.png"
|
|
)
|
|
|
|
wide := encodeTestPNG(t, 3000, 600)
|
|
tall := encodeTestPNG(t, 600, 3000)
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
if r.URL.Path == tallPath {
|
|
_, _ = w.Write(tall)
|
|
|
|
return
|
|
}
|
|
|
|
_, _ = w.Write(wide)
|
|
}))
|
|
t.Cleanup(upstream.Close)
|
|
|
|
s, _, _ := startImageProxy(t, upstream)
|
|
|
|
keys, err := seal.DeriveKeys(testSigningKey)
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys: %v", err)
|
|
}
|
|
|
|
generator := encurl.NewGenerator(keys.EncryptedURLKey)
|
|
|
|
tests := []struct {
|
|
path string
|
|
// size is the size of the /v1/image/ URL, and width and height are
|
|
// those of the encrypted URL
|
|
size string
|
|
width, height int
|
|
// wantWidth and wantHeight are the served image's, 0 for a refusal
|
|
wantWidth, wantHeight int
|
|
}{
|
|
{widePath, "0x8192", 0, 8192, 0, 0},
|
|
{tallPath, "8192x0", 8192, 0, 0, 0},
|
|
{widePath, "orig", 0, 0, 3000, 600},
|
|
{widePath, "300x200", 300, 200, 300, 200},
|
|
{widePath, "0x300", 0, 300, 1500, 300},
|
|
{tallPath, "300x0", 300, 0, 300, 1500},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
token, err := generator.Generate(&imagerequest.Request{
|
|
Source: imagerequest.Source{Host: upstreamHost, Path: tt.path},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: tt.width, Height: tt.height},
|
|
Format: imagerequest.FormatJPEG,
|
|
},
|
|
}, time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("encurl Generate: %v", err)
|
|
}
|
|
|
|
for _, target := range []string{
|
|
"/v1/image/" + upstreamHost + tt.path + "/" + tt.size + ".jpeg",
|
|
"/v1/e/" + token + "/img.jpg",
|
|
} {
|
|
rec := httptest.NewRecorder()
|
|
s.ServeHTTP(rec, httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, target, nil))
|
|
t.Logf("GET %s: %d, Content-Type %s",
|
|
target, rec.Code, rec.Header().Get("Content-Type"))
|
|
|
|
if tt.wantWidth == 0 {
|
|
checkSizeRefused(t, rec)
|
|
} else {
|
|
checkServedSize(t, rec, tt.wantWidth, tt.wantHeight)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkSizeRefused checks that rec answers 400 with the JSON error body that
|
|
// refuses a size whose side computed from the source's proportions is above
|
|
// 8192.
|
|
func checkSizeRefused(t *testing.T, rec *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
|
|
const wantError = "size too large for the source: " +
|
|
"the side computed from its proportions is above 8192"
|
|
|
|
var body struct {
|
|
Error string `json:"error"`
|
|
Status int `json:"status"`
|
|
}
|
|
|
|
err := json.Unmarshal(rec.Body.Bytes(), &body)
|
|
if err != nil {
|
|
t.Errorf("status %d, body is not JSON: %v", rec.Code, err)
|
|
|
|
return
|
|
}
|
|
|
|
if rec.Code != http.StatusBadRequest || body.Status != http.StatusBadRequest ||
|
|
body.Error != wantError {
|
|
t.Errorf("status %d, body %+v, want %d and the error %q",
|
|
rec.Code, body, http.StatusBadRequest, wantError)
|
|
}
|
|
}
|
|
|
|
// checkServedSize checks that rec answers 200 with a JPEG of wantWidth by
|
|
// wantHeight.
|
|
func checkServedSize(
|
|
t *testing.T, rec *httptest.ResponseRecorder, wantWidth, wantHeight int,
|
|
) {
|
|
t.Helper()
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status %d, want %d; body %s",
|
|
rec.Code, http.StatusOK, rec.Body.String())
|
|
|
|
return
|
|
}
|
|
|
|
served, err := jpeg.DecodeConfig(bytes.NewReader(rec.Body.Bytes()))
|
|
if err != nil {
|
|
t.Errorf("decoding the served JPEG: %v", err)
|
|
|
|
return
|
|
}
|
|
|
|
if served.Width != wantWidth || served.Height != wantHeight {
|
|
t.Errorf("served %dx%d, want %dx%d",
|
|
served.Width, served.Height, wantWidth, wantHeight)
|
|
}
|
|
}
|