149 lines
4.6 KiB
Go
149 lines
4.6 KiB
Go
package turnstile
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"html/template"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
type captureTransport struct {
|
|
remoteIP string
|
|
}
|
|
|
|
func (t *captureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
body, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vals, err := url.ParseQuery(string(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.remoteIP = vals.Get("remoteip")
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader([]byte(`{"success":true}`))),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}
|
|
|
|
func TestVerifyTurnstileSendsTrustedClientIP(t *testing.T) {
|
|
transport := &captureTransport{}
|
|
m := Middleware{
|
|
SecretKey: "secret",
|
|
httpClient: &http.Client{Transport: transport},
|
|
}
|
|
|
|
req := httptestNewRequestWithTrustedClientIP("192.0.2.1")
|
|
req.RemoteAddr = "203.0.113.1:443"
|
|
ok, err := m.verifyTurnstile(req, "turnstile-token")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("expected successful verification")
|
|
}
|
|
if transport.remoteIP != "192.0.2.1" {
|
|
t.Fatalf("remoteip = %q, want 192.0.2.1", transport.remoteIP)
|
|
}
|
|
}
|
|
|
|
func httptestNewRequestWithTrustedClientIP(ip string) *http.Request {
|
|
req := httptest.NewRequest(http.MethodPost, "https://example.com/__turnstile__/verify", nil)
|
|
vars := map[string]any{caddyhttp.ClientIPVarKey: ip}
|
|
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
func TestChallengeReason(t *testing.T) {
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
enabled := Middleware{
|
|
ForgejoCookieName: "i_like_gitea",
|
|
sessionPasses: signer,
|
|
}
|
|
disabled := Middleware{}
|
|
|
|
anon := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil)
|
|
if got := enabled.challengeReason(anon); got != challengeAnonymous {
|
|
t.Fatalf("no credentials: got %q, want %q", got, challengeAnonymous)
|
|
}
|
|
if got := disabled.challengeReason(anon); got != challengeAnonymous {
|
|
t.Fatalf("forgejo disabled: got %q, want %q", got, challengeAnonymous)
|
|
}
|
|
|
|
withCookie := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil)
|
|
withCookie.AddCookie(&http.Cookie{Name: "i_like_gitea", Value: "stale-session"})
|
|
if got := enabled.challengeReason(withCookie); got != challengeInvalid {
|
|
t.Fatalf("invalid session cookie: got %q, want %q", got, challengeInvalid)
|
|
}
|
|
|
|
withRemember := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil)
|
|
enabled.ForgejoRememberCookieName = "persistent"
|
|
withRemember.AddCookie(&http.Cookie{Name: "persistent", Value: "remember-token"})
|
|
if got := enabled.challengeReason(withRemember); got != challengeInvalid {
|
|
t.Fatalf("invalid remember-me cookie: got %q, want %q", got, challengeInvalid)
|
|
}
|
|
|
|
withBearer := httptest.NewRequest(http.MethodGet, "https://example.com/repo", nil)
|
|
withBearer.Header.Set("Authorization", "Bearer bad-token")
|
|
if got := enabled.challengeReason(withBearer); got != challengeInvalid {
|
|
t.Fatalf("invalid bearer: got %q, want %q", got, challengeInvalid)
|
|
}
|
|
if got := disabled.challengeReason(withBearer); got != challengeAnonymous {
|
|
t.Fatalf("bearer ignored when forgejo disabled: got %q, want %q", got, challengeAnonymous)
|
|
}
|
|
}
|
|
|
|
func TestServeChallengeSetsHeaderAndOK(t *testing.T) {
|
|
tmpl, err := template.New("challenge").Parse(challengeHTML)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer, err := newSessionPassSigner("test-secret-key-please-change", "__turnstile_session_pass", time.Hour)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := Middleware{
|
|
SiteKey: "site-key",
|
|
verify: "/__turnstile__/verify",
|
|
tmpl: tmpl,
|
|
ForgejoCookieName: "i_like_gitea",
|
|
sessionPasses: signer,
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo", nil)
|
|
if err := m.serveChallenge(rec, req, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if got := rec.Header().Get(challengeHeader); got != challengeAnonymous {
|
|
t.Fatalf("header = %q, want %q", got, challengeAnonymous)
|
|
}
|
|
|
|
rec = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodGet, "https://example.com/owner/repo", nil)
|
|
req.Header.Set("Authorization", "token bad")
|
|
if err := m.serveChallenge(rec, req, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if got := rec.Header().Get(challengeHeader); got != challengeInvalid {
|
|
t.Fatalf("header = %q, want %q", got, challengeInvalid)
|
|
}
|
|
}
|