// web UI 相关 package main import ( "net/http" "net/http/httptest" "net/url" "testing" ) // 测试期间换一个管理口令 func withAuthToken(t *testing.T, token string) { t.Helper() streamers.Lock() old := config.AuthToken config.AuthToken = token streamers.Unlock() t.Cleanup(func() { streamers.Lock() config.AuthToken = old streamers.Unlock() }) } // 页面上发给服务端的 X-Auth-Token 是 encodeURIComponent 过的口令, // 这里用 url.QueryEscape 造出同样的形状(对中文而言两者产出一致) func pageHeader(r *http.Request, token string) *http.Request { r.Header.Set("X-Auth-Token", url.QueryEscape(token)) return r } func TestCheckAuthTokenPlainASCII(t *testing.T) { withAuthToken(t, "a1b2c3d4e5f6") r := httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("X-Auth-Token", "a1b2c3d4e5f6") if !checkAuthToken(r) { t.Fatal("ASCII 口令应该通过") } // 带 % 的口令:页面会编码成 %25,服务端解码后要能对上 withAuthToken(t, "ab%20cd") r = pageHeader(httptest.NewRequest(http.MethodGet, "/api/status", nil), "ab%20cd") if r.Header.Get("X-Auth-Token") != "ab%2520cd" { t.Fatalf("编码结果不对:%q", r.Header.Get("X-Auth-Token")) } if !checkAuthToken(r) { t.Fatal("含 % 的口令应该通过") } } func TestCheckAuthTokenNonASCII(t *testing.T) { // 中文口令是最容易踩的:原先直接把中文塞进请求头,浏览器会抛 // TypeError(Failed to read the 'headers' property…),请求根本发不出去, // 页面上只显示「无法连接到服务器」,用户被彻底锁在门外 withAuthToken(t, "我的口令abc123") r := pageHeader(httptest.NewRequest(http.MethodGet, "/api/status", nil), "我的口令abc123") if r.Header.Get("X-Auth-Token") == "我的口令abc123" { t.Fatal("测试前提不成立:头里不该是原文") } if !checkAuthToken(r) { t.Fatal("编码后的中文口令应该通过") } // 手写 curl 按文档原样发中文口令,也要能通过 r = httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("X-Auth-Token", "我的口令abc123") if !checkAuthToken(r) { t.Fatal("原样的中文口令也应该通过") } } func TestCheckAuthTokenAllSources(t *testing.T) { withAuthToken(t, "口令abc123") // Authorization: Bearer r := httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("Authorization", "Bearer "+url.QueryEscape("口令abc123")) if !checkAuthToken(r) { t.Fatal("Bearer 写法应该通过") } // ?token=(net/http 已经把查询串解码好了) r = httptest.NewRequest(http.MethodGet, "/api/status?token="+url.QueryEscape("口令abc123"), nil) if !checkAuthToken(r) { t.Fatal("查询参数写法应该通过") } // 下载链接里的 ?token= 也是这个路径,混着 % 的口令同样要对上 withAuthToken(t, "口令%20abc") r = httptest.NewRequest(http.MethodGet, "/api/file?token="+url.QueryEscape("口令%20abc"), nil) if !checkAuthToken(r) { t.Fatal("查询参数里的混合口令应该通过") } } func TestCheckAuthTokenRejects(t *testing.T) { withAuthToken(t, "正确的口令") cases := map[string]*http.Request{ "口令错了": func() *http.Request { return pageHeader(httptest.NewRequest(http.MethodGet, "/api/status", nil), "错误的口令") }(), "没带口令": httptest.NewRequest(http.MethodGet, "/api/status", nil), "空口令头": func() *http.Request { r := httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("X-Auth-Token", "") return r }(), "编码了但不是口令": func() *http.Request { r := httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("X-Auth-Token", "%E5%8F%A3%E4%BB%A4") return r }(), } for name, r := range cases { if checkAuthToken(r) { t.Fatalf("%s:不该通过", name) } } // 口令没配时一律拒绝,不能因为「都是空」就放行 withAuthToken(t, "") r := httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("X-Auth-Token", "") if checkAuthToken(r) { t.Fatal("口令为空时不该通过") } } func TestAuthTokenCandidatesOrder(t *testing.T) { // 三种写法同时出现时按 Bearer → X-Auth-Token → ?token= 的顺序给候选, // 顺序本身不影响结果,但别写出重复项(重复比较纯属浪费) r := httptest.NewRequest(http.MethodGet, "/api/status?token=qqq", nil) r.Header.Set("Authorization", "Bearer aaa") r.Header.Set("X-Auth-Token", "bbb") got := authTokenCandidates(r) want := []string{"aaa", "bbb", "qqq"} if len(got) != len(want) { t.Fatalf("候选数量不对:%q", got) } for i := range want { if got[i] != want[i] { t.Fatalf("候选顺序不对:%q,想要 %q", got, want) } } // Authorization 不是 Bearer 前缀时不算数 r = httptest.NewRequest(http.MethodGet, "/api/status", nil) r.Header.Set("Authorization", "Basic zzz") r.Header.Set("X-Auth-Token", "bbb") got = authTokenCandidates(r) if len(got) != 1 || got[0] != "bbb" { t.Fatalf("非 Bearer 的 Authorization 不该当候选:%q", got) } }