package turnstile import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestCookieRoundTrip(t *testing.T) { signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour) if err != nil { t.Fatal(err) } rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil) if err := signer.set(rec, req); err != nil { t.Fatal(err) } cookies := rec.Result().Cookies() if len(cookies) != 1 { t.Fatalf("expected 1 cookie, got %d", len(cookies)) } req2 := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo/src/x", nil) req2.AddCookie(cookies[0]) if !signer.valid(req2) { t.Fatal("expected cookie to validate") } } func TestCookieRejectsTamper(t *testing.T) { signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour) if err != nil { t.Fatal(err) } rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil) if err := signer.set(rec, req); err != nil { t.Fatal(err) } c := rec.Result().Cookies()[0] c.Value = c.Value + "x" req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil) req2.AddCookie(c) if signer.valid(req2) { t.Fatal("tampered cookie must not validate") } } func withTrustedClientIP(r *http.Request, ip string) *http.Request { vars := map[string]any{caddyhttp.ClientIPVarKey: ip} ctx := context.WithValue(r.Context(), caddyhttp.VarsCtxKey, vars) return r.WithContext(ctx) } func TestCookieUsesTrustedClientIP(t *testing.T) { signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour) if err != nil { t.Fatal(err) } rec := httptest.NewRecorder() req := withTrustedClientIP(httptest.NewRequest(http.MethodGet, "https://example.com/", nil), "192.0.2.1") req.RemoteAddr = "203.0.113.1:443" if err := signer.set(rec, req); err != nil { t.Fatal(err) } req2 := withTrustedClientIP(httptest.NewRequest(http.MethodGet, "https://example.com/", nil), "192.0.2.1") req2.RemoteAddr = "203.0.113.2:443" req2.AddCookie(rec.Result().Cookies()[0]) if !signer.valid(req2) { t.Fatal("cookie must validate when trusted client IP matches across proxy hops") } req3 := withTrustedClientIP(httptest.NewRequest(http.MethodGet, "https://example.com/", nil), "198.51.100.1") req3.RemoteAddr = "203.0.113.1:443" req3.AddCookie(rec.Result().Cookies()[0]) if signer.valid(req3) { t.Fatal("cookie from different trusted client IP must not validate") } } func TestCookieRejectsIPMismatch(t *testing.T) { signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour) if err != nil { t.Fatal(err) } rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil) req.RemoteAddr = "192.0.2.1:1234" if err := signer.set(rec, req); err != nil { t.Fatal(err) } req2 := httptest.NewRequest(http.MethodGet, "https://example.com/", nil) req2.RemoteAddr = "198.51.100.1:5678" req2.AddCookie(rec.Result().Cookies()[0]) if signer.valid(req2) { t.Fatal("cookie from different IP must not validate") } } func TestCookieExpired(t *testing.T) { signer, err := newCookieSigner("test-secret-key-please-change", "__turnstile_pass", time.Hour) if err != nil { t.Fatal(err) } exp := time.Now().Add(-2 * time.Second).Unix() payload := fmt.Sprintf("2|%d|deadbeef|192.0.2.1", exp) mac := hmac.New(sha256.New, signer.secret) mac.Write([]byte(payload)) sig := hex.EncodeToString(mac.Sum(nil)) value := base64.RawURLEncoding.EncodeToString([]byte(payload + "|" + sig)) req := httptest.NewRequest(http.MethodGet, "https://example.com/", nil) req.AddCookie(&http.Cookie{Name: signer.name, Value: value}) if signer.valid(req) { t.Fatal("expired cookie must not validate") } }