| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- //go:build unit
- package service
- import (
- "net/http"
- "strings"
- "testing"
- "github.com/stretchr/testify/require"
- )
- func headerOverrideTestAccount(platform, accountType string, credentials map[string]any) *Account {
- return &Account{
- Platform: platform,
- Type: accountType,
- Credentials: credentials,
- }
- }
- func TestIsHeaderOverrideEligible(t *testing.T) {
- tests := []struct {
- name string
- platform string
- accType string
- want bool
- }{
- {"anthropic apikey", PlatformAnthropic, AccountTypeAPIKey, true},
- {"openai apikey", PlatformOpenAI, AccountTypeAPIKey, true},
- {"kimi apikey", PlatformKimi, AccountTypeAPIKey, true},
- {"zhipu apikey", PlatformZhipu, AccountTypeAPIKey, true},
- {"deepseek apikey", PlatformDeepseek, AccountTypeAPIKey, true},
- {"anthropic oauth", PlatformAnthropic, AccountTypeOAuth, false},
- {"openai oauth", PlatformOpenAI, AccountTypeOAuth, false},
- {"kimi oauth", PlatformKimi, AccountTypeOAuth, false},
- {"zhipu oauth", PlatformZhipu, AccountTypeOAuth, false},
- {"deepseek oauth", PlatformDeepseek, AccountTypeOAuth, false},
- {"gemini apikey", PlatformGemini, AccountTypeAPIKey, false},
- {"grok apikey", PlatformGrok, AccountTypeAPIKey, true},
- {"grok oauth", PlatformGrok, AccountTypeOAuth, true},
- {"antigravity apikey", PlatformAntigravity, AccountTypeAPIKey, false},
- {"anthropic bedrock", PlatformAnthropic, AccountTypeBedrock, false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- acc := headerOverrideTestAccount(tt.platform, tt.accType, nil)
- require.Equal(t, tt.want, acc.IsHeaderOverrideEligible())
- })
- }
- var nilAccount *Account
- require.False(t, nilAccount.IsHeaderOverrideEligible())
- require.False(t, nilAccount.IsHeaderOverrideEnabled())
- require.Nil(t, nilAccount.GetHeaderOverrides())
- }
- func TestIsHeaderOverrideEnabled(t *testing.T) {
- acc := headerOverrideTestAccount(PlatformAnthropic, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- })
- require.True(t, acc.IsHeaderOverrideEnabled())
- // 未配置 / 非 bool / false 均视为未启用
- require.False(t, headerOverrideTestAccount(PlatformAnthropic, AccountTypeAPIKey, nil).IsHeaderOverrideEnabled())
- require.False(t, headerOverrideTestAccount(PlatformAnthropic, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: "true",
- }).IsHeaderOverrideEnabled())
- require.False(t, headerOverrideTestAccount(PlatformAnthropic, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: false,
- }).IsHeaderOverrideEnabled())
- // 不符合平台/类型条件时即使配置了 true 也不启用
- require.False(t, headerOverrideTestAccount(PlatformAnthropic, AccountTypeOAuth, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- }).IsHeaderOverrideEnabled())
- require.False(t, headerOverrideTestAccount(PlatformGemini, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- }).IsHeaderOverrideEnabled())
- }
- func TestGetHeaderOverrides(t *testing.T) {
- acc := headerOverrideTestAccount(PlatformOpenAI, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{
- "User-Agent": "my-agent/1.0", // 大写 key 归一化为小写
- " X-App ": "cli", // 名称去空白
- "x-empty": "", // 空 value(模板占位)跳过
- "authorization": "Bearer leaked", // 禁止覆写的头跳过
- "bad name": "value", // 非法 header 名跳过
- "x-padded": " padded ", // value 去空白
- },
- })
- overrides := acc.GetHeaderOverrides()
- require.Equal(t, map[string]string{
- "user-agent": "my-agent/1.0",
- "x-app": "cli",
- "x-padded": "padded",
- }, overrides)
- // 未启用时返回 nil
- disabled := headerOverrideTestAccount(PlatformOpenAI, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrides: map[string]any{"user-agent": "x"},
- })
- require.Nil(t, disabled.GetHeaderOverrides())
- // 启用但全部为空 value 时返回 nil
- empty := headerOverrideTestAccount(PlatformOpenAI, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{"user-agent": ""},
- })
- require.Nil(t, empty.GetHeaderOverrides())
- // 未经 Normalize 落库的超长数据 / WebSocket 握手头在应用时被防御性跳过
- oversizedValue := strings.Repeat("a", maxHeaderOverrideValueLength+1)
- defensive := headerOverrideTestAccount(PlatformOpenAI, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{
- "x-big": oversizedValue,
- "sec-websocket-key": "forged",
- "content-type": "application/json", // 名单扩充前落库的数据也要被拦截
- "x-claude-code-session-id": "pinned-session",
- "x-ok": "ok",
- },
- })
- require.Equal(t, map[string]string{"x-ok": "ok"}, defensive.GetHeaderOverrides())
- }
- func TestApplyHeaderOverrides(t *testing.T) {
- acc := headerOverrideTestAccount(PlatformAnthropic, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{
- "user-agent": "override-agent/2.0",
- "anthropic-beta": "custom-beta-1",
- "x-custom": "custom-value",
- },
- })
- h := http.Header{}
- // 模拟转发链路:canonical key 与 wire casing 原样 key 混合存在
- h.Set("User-Agent", "claude-cli/2.1.161 (external, cli)")
- h["anthropic-beta"] = []string{"claude-code-20250219,oauth-2025-04-20"} // 非 canonical 原样 key
- h.Set("Content-Type", "application/json")
- acc.ApplyHeaderOverrides(h)
- // user-agent 覆盖且只有一个值(已知头恢复 wire casing)
- require.Equal(t, []string{"override-agent/2.0"}, h["User-Agent"])
- // anthropic-beta:非 canonical 旧值被清除,写入 wire casing(小写)
- require.Equal(t, []string{"custom-beta-1"}, h["anthropic-beta"])
- require.Empty(t, h["Anthropic-Beta"])
- // 新增头(未知头以小写原样键写入,与转发链路 wire casing 约定一致)
- require.Equal(t, []string{"custom-value"}, h["x-custom"])
- require.Equal(t, "custom-value", getHeaderRaw(h, "x-custom"))
- // 未覆写的头不受影响
- require.Equal(t, "application/json", h.Get("Content-Type"))
- // 覆盖后不存在任何大小写重复
- count := 0
- for k := range h {
- if k == "anthropic-beta" || k == "Anthropic-Beta" {
- count++
- }
- }
- require.Equal(t, 1, count)
- }
- func TestApplyHeaderOverridesNoOpPaths(t *testing.T) {
- baseline := func() http.Header {
- h := http.Header{}
- h.Set("User-Agent", "orig")
- return h
- }
- // OAuth 账号:即使配置了覆写也不生效
- oauth := headerOverrideTestAccount(PlatformAnthropic, AccountTypeOAuth, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{"user-agent": "hacked"},
- })
- h := baseline()
- oauth.ApplyHeaderOverrides(h)
- require.Equal(t, "orig", h.Get("User-Agent"))
- // 未启用开关
- off := headerOverrideTestAccount(PlatformAnthropic, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrides: map[string]any{"user-agent": "hacked"},
- })
- h = baseline()
- off.ApplyHeaderOverrides(h)
- require.Equal(t, "orig", h.Get("User-Agent"))
- // 禁止覆写的头(authorization / x-api-key / host 等)不会被应用
- blocked := headerOverrideTestAccount(PlatformOpenAI, AccountTypeAPIKey, map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{
- "Authorization": "Bearer evil",
- "X-Api-Key": "evil",
- "Host": "evil.example.com",
- "Content-Length": "0",
- },
- })
- h = http.Header{}
- h.Set("Authorization", "Bearer real-key")
- blocked.ApplyHeaderOverrides(h)
- require.Equal(t, "Bearer real-key", h.Get("Authorization"))
- require.Empty(t, h.Get("X-Api-Key"))
- require.Empty(t, h.Get("Host"))
- // nil header 不 panic
- blocked.ApplyHeaderOverrides(nil)
- }
- func TestNormalizeHeaderOverrideCredentials(t *testing.T) {
- t.Run("nil credentials no-op", func(t *testing.T) {
- require.NoError(t, NormalizeHeaderOverrideCredentials(nil))
- })
- t.Run("missing keys no-op", func(t *testing.T) {
- creds := map[string]any{"api_key": "sk-xxx"}
- require.NoError(t, NormalizeHeaderOverrideCredentials(creds))
- _, exists := creds[credKeyHeaderOverrides]
- require.False(t, exists)
- })
- t.Run("normalizes names and values", func(t *testing.T) {
- creds := map[string]any{
- credKeyHeaderOverrideEnabled: true,
- credKeyHeaderOverrides: map[string]any{
- " User-Agent ": " my-agent ",
- "X-App": "",
- "": "", // 完全空行被丢弃
- },
- }
- require.NoError(t, NormalizeHeaderOverrideCredentials(creds))
- require.Equal(t, map[string]any{
- "user-agent": "my-agent",
- "x-app": "",
- }, creds[credKeyHeaderOverrides])
- })
- t.Run("accepts map[string]string input", func(t *testing.T) {
- creds := map[string]any{
- credKeyHeaderOverrides: map[string]string{"X-App": "cli"},
- }
- require.NoError(t, NormalizeHeaderOverrideCredentials(creds))
- require.Equal(t, map[string]any{"x-app": "cli"}, creds[credKeyHeaderOverrides])
- })
- t.Run("rejects non-bool enabled", func(t *testing.T) {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrideEnabled: "yes",
- })
- require.Error(t, err)
- })
- t.Run("rejects non-object overrides", func(t *testing.T) {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: []any{"user-agent"},
- })
- require.Error(t, err)
- })
- t.Run("rejects non-string value", func(t *testing.T) {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{"x-app": 123},
- })
- require.Error(t, err)
- })
- t.Run("rejects invalid header name", func(t *testing.T) {
- for _, name := range []string{"bad name", "bad:name", "bad\nname", "值"} {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{name: "v"},
- })
- require.Error(t, err, "name %q should be rejected", name)
- }
- })
- t.Run("rejects empty name with value", func(t *testing.T) {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{" ": "v"},
- })
- require.Error(t, err)
- })
- t.Run("rejects blocked headers", func(t *testing.T) {
- for _, name := range []string{
- "Authorization", "x-api-key", "Host", "content-length", "Transfer-Encoding",
- "connection", "accept-encoding", "Sec-WebSocket-Key", "session_id",
- "conversation_id", "x-codex-turn-state", "chatgpt-account-id",
- "Content-Type", "Cookie", "x-goog-api-key",
- "X-Claude-Code-Session-Id", "x-client-request-id",
- } {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{name: "v"},
- })
- require.Error(t, err, "blocked header %q should be rejected", name)
- }
- })
- t.Run("allows tab inside value", func(t *testing.T) {
- creds := map[string]any{
- credKeyHeaderOverrides: map[string]any{"x-app": "a\tb"},
- }
- require.NoError(t, NormalizeHeaderOverrideCredentials(creds))
- require.Equal(t, map[string]any{"x-app": "a\tb"}, creds[credKeyHeaderOverrides])
- })
- t.Run("rejects invalid value", func(t *testing.T) {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{"x-app": "bad\nvalue"},
- })
- require.Error(t, err)
- })
- t.Run("rejects duplicate names case-insensitively", func(t *testing.T) {
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{
- "User-Agent": "a",
- "user-agent": "b",
- },
- })
- require.Error(t, err)
- })
- t.Run("rejects too many entries", func(t *testing.T) {
- entries := make(map[string]any, maxHeaderOverrideEntries+1)
- for i := 0; i <= maxHeaderOverrideEntries; i++ {
- entries["x-h-"+string(rune('a'+i%26))+string(rune('a'+(i/26)%26))+string(rune('a'+(i/676)%26))] = "v"
- }
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: entries,
- })
- require.Error(t, err)
- })
- t.Run("rejects oversized value", func(t *testing.T) {
- big := make([]byte, maxHeaderOverrideValueLength+1)
- for i := range big {
- big[i] = 'a'
- }
- err := NormalizeHeaderOverrideCredentials(map[string]any{
- credKeyHeaderOverrides: map[string]any{"x-app": string(big)},
- })
- require.Error(t, err)
- })
- }
|