返回 DeepSeek-Reasonix
launch_token_gate_test.go
根目录 / internal / serve / launch_token_gate_test.go
1 package serve
2
3 import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8
9 "reasonix/internal/config"
10 "reasonix/internal/control"
11 )
12
13 func gateDo(t *testing.T, h http.Handler, method, target, body string, hdr map[string]string) *httptest.ResponseRecorder {
14 t.Helper()
15 req := httptest.NewRequest(method, target, strings.NewReader(body))
16 req.Host = "127.0.0.1:8787"
17 req.Header.Set("Content-Type", "application/json")
18 for k, v := range hdr {
19 req.Header.Set(k, v)
20 }
21 rec := httptest.NewRecorder()
22 h.ServeHTTP(rec, req)
23 return rec
24 }
25
26 func TestAuthDisabledGateHoldsEveryMutationSpelling(t *testing.T) {
27 bc := NewBroadcaster()
28 s := New(control.New(control.Options{Sink: bc}), bc, config.ServeConfig{AuthMode: "none"})
29 h := s.Handler()
30 tok := s.AuthToken()
31 cases := []struct {
32 name, method, target string
33 hdr map[string]string
34 want int
35 }{
36 {"PUT", http.MethodPut, "/approve", nil, 403},
37 {"PATCH inbox", http.MethodPatch, "/inbox/items/x", nil, 403},
38 {"DELETE inbox", http.MethodDelete, "/inbox/items/x", nil, 403},
39 {"PROPFIND", "PROPFIND", "/approve", nil, 403},
40 {"override header", http.MethodPost, "/approve", map[string]string{"X-HTTP-Method-Override": "GET"}, 403},
41 {"dot path", http.MethodPost, "/./approve", nil, 403},
42 {"double slash", http.MethodPost, "//approve", nil, 403},
43 {"query token ignored", http.MethodPost, "/approve?token=" + tok, nil, 403},
44 {"wrong bearer", http.MethodPost, "/approve", map[string]string{"Authorization": "Bearer x" + tok}, 403},
45 {"basic scheme", http.MethodPost, "/approve", map[string]string{"Authorization": "Basic " + tok}, 403},
46 {"wrong cookie", http.MethodPost, "/approve", map[string]string{"Cookie": cookieToken + "=nope"}, 403},
47 {"lowercase bearer", http.MethodPost, "/approve", map[string]string{"Authorization": "bearer " + tok}, 400},
48 {"cookie", http.MethodPost, "/approve", map[string]string{"Cookie": cookieToken + "=" + tok}, 400},
49 {"OPTIONS passes gate", http.MethodOptions, "/approve", nil, 405},
50 {"HEAD falls to GET / shell", http.MethodHead, "/approve", nil, 200},
51 }
52 for _, c := range cases {
53 rec := gateDo(t, h, c.method, c.target, `{"allow":true}`, c.hdr)
54 if rec.Code != c.want {
55 t.Errorf("%s: %s %s = %d body=%q, want %d", c.name, c.method, c.target, rec.Code, strings.TrimSpace(rec.Body.String()), c.want)
56 }
57 }
58 if rec := gateDo(t, h, http.MethodPost, "/auth/token", `{"token":""}`, nil); rec.Code != http.StatusUnauthorized {
59 t.Errorf("/auth/token with empty token = %d, want 401", rec.Code)
60 }
61 if rec := gateDo(t, h, http.MethodGet, "/auth/token", "", nil); rec.Code != http.StatusMethodNotAllowed {
62 t.Errorf("GET /auth/token = %d, want 405", rec.Code)
63 }
64 rec := gateDo(t, h, http.MethodPost, "/auth/token", `{"token":"`+tok+`"}`, nil)
65 sc := rec.Header().Get("Set-Cookie")
66 if !strings.Contains(sc, "HttpOnly") || !strings.Contains(sc, "SameSite=Lax") || !strings.Contains(sc, "Max-Age=") {
67 t.Errorf("cookie flags = %q", sc)
68 }
69 }
70
71 // A request to the DNS-rebinding host is refused by hostGuard before the
72 // token gate, even with no token.
73 func TestAuthDisabledHostAndCSRFGuardsAnswerFirst(t *testing.T) {
74 bc := NewBroadcaster()
75 s := New(control.New(control.Options{Sink: bc}), bc, config.ServeConfig{AuthMode: "none"})
76 req := httptest.NewRequest(http.MethodPost, "/approve", strings.NewReader(`{}`))
77 req.Host = "evil.example:8787"
78 req.Header.Set("Content-Type", "application/json")
79 rec := httptest.NewRecorder()
80 s.Handler().ServeHTTP(rec, req)
81 if rec.Code != http.StatusMisdirectedRequest {
82 t.Fatalf("rebinding POST = %d, want 421", rec.Code)
83 }
84 req = httptest.NewRequest(http.MethodPost, "/approve", strings.NewReader(`{}`))
85 req.Host = "127.0.0.1"
86 req.Header.Set("Content-Type", "text/plain")
87 rec = httptest.NewRecorder()
88 s.Handler().ServeHTTP(rec, req)
89 if rec.Code != http.StatusUnsupportedMediaType {
90 t.Fatalf("text/plain POST = %d, want 415 from csrfGuard before token gate", rec.Code)
91 }
92 }
93
93 lines GO