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
169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"sneak.berlin/go/pixa/internal/config"
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
"sneak.berlin/go/pixa/internal/session"
|
|
)
|
|
|
|
// Form field names and a throwaway source image URL for the body-limit
|
|
// tests.
|
|
const (
|
|
sourceURLField = "url"
|
|
testSourceURL = "https://example.com/a.jpg"
|
|
)
|
|
|
|
// newBodyLimitTestRouter mirrors the production wiring for the form POST
|
|
// routes (see server.SetupRoutes): LimitBody sits in front of the CSRF
|
|
// middleware, which sits in front of the handlers. maxBytes is the body
|
|
// cap under test, so a test can trip the limit with a small body.
|
|
func newBodyLimitTestRouter(
|
|
t *testing.T, maxBytes int64,
|
|
) (*Handlers, http.Handler) {
|
|
t.Helper()
|
|
|
|
cfg := &config.Config{SigningKey: testSigningKey, Debug: true}
|
|
|
|
keys, err := seal.DeriveKeys(testSigningKey)
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
|
}
|
|
|
|
h := &Handlers{
|
|
log: slog.New(slog.DiscardHandler),
|
|
config: cfg,
|
|
sessMgr: session.NewManager(keys.SessionHashKey, keys.SessionBlockKey),
|
|
encGen: encurl.NewGenerator(keys.EncryptedURLKey),
|
|
csrfProtect: newCSRFProtect(keys.CSRFKey, cfg.Debug),
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(h.LimitBody(maxBytes))
|
|
r.Use(h.CSRF())
|
|
r.Get("/", h.HandleRoot())
|
|
r.Post("/", h.HandleRoot())
|
|
r.Post("/generate", h.HandleGenerateURL())
|
|
})
|
|
|
|
return h, r
|
|
}
|
|
|
|
// TestOversizedLoginPostRejectedBeforeCSRF is the core regression: an
|
|
// oversized POST / carrying an otherwise valid CSRF cookie and token must
|
|
// be rejected with 413. If the body limit ran after CSRF, the truncated
|
|
// body would read as a missing token and return 403; if it ran after the
|
|
// handler, a valid token would return 303. Getting 413 proves the limit
|
|
// fires before CSRF parses the form.
|
|
func TestOversizedLoginPostRejectedBeforeCSRF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, srv := newBodyLimitTestRouter(t, 16)
|
|
|
|
cookies, token := csrfCredentials(t, srv, nil)
|
|
|
|
rec := postForm(srv, "/", cookies, url.Values{
|
|
loginKeyField: {testSigningKey},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Errorf("oversized POST / status = %d, want %d",
|
|
rec.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
}
|
|
|
|
// TestOversizedGeneratePostRejectedBeforeCSRF is the same regression for
|
|
// POST /generate, which also parses a form behind CSRF.
|
|
func TestOversizedGeneratePostRejectedBeforeCSRF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newBodyLimitTestRouter(t, 16)
|
|
|
|
sessionCookie := newSessionCookie(t, h)
|
|
|
|
cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie})
|
|
cookies = append(cookies, sessionCookie)
|
|
|
|
rec := postForm(srv, "/generate", cookies, url.Values{
|
|
sourceURLField: {testSourceURL},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Errorf("oversized POST /generate status = %d, want %d",
|
|
rec.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
}
|
|
|
|
// TestWithinLimitLoginPostSucceeds verifies the limit does not disturb a
|
|
// normal request: under the production cap, a valid login still parses and
|
|
// establishes a session (303). This guards against the body limit
|
|
// consuming or corrupting the form the CSRF check and handler depend on.
|
|
func TestWithinLimitLoginPostSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, srv := newBodyLimitTestRouter(t, MaxFormBytes)
|
|
|
|
cookies, token := csrfCredentials(t, srv, nil)
|
|
|
|
rec := postForm(srv, "/", cookies, url.Values{
|
|
loginKeyField: {testSigningKey},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("within-limit POST / status = %d, want %d",
|
|
rec.Code, http.StatusSeeOther)
|
|
}
|
|
|
|
var authed bool
|
|
|
|
for _, c := range rec.Result().Cookies() {
|
|
if c.Name == session.CookieName && c.Value != "" {
|
|
authed = true
|
|
}
|
|
}
|
|
|
|
if !authed {
|
|
t.Error("within-limit valid login did not set a session cookie")
|
|
}
|
|
}
|
|
|
|
// TestWithinLimitGeneratePostSucceeds is the same non-regression check for
|
|
// POST /generate.
|
|
func TestWithinLimitGeneratePostSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newBodyLimitTestRouter(t, MaxFormBytes)
|
|
|
|
sessionCookie := newSessionCookie(t, h)
|
|
|
|
cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie})
|
|
cookies = append(cookies, sessionCookie)
|
|
|
|
rec := postForm(srv, "/generate", cookies, url.Values{
|
|
sourceURLField: {testSourceURL},
|
|
"format": {"jpeg"},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("within-limit POST /generate status = %d, want %d",
|
|
rec.Code, http.StatusOK)
|
|
}
|
|
|
|
if !strings.Contains(rec.Body.String(), "/v1/e/") {
|
|
t.Error("within-limit generate response did not contain a generated URL")
|
|
}
|
|
}
|