135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|