mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
将 codex_cli_only 客户端识别从「单一 strict 开关 + 固定 OR 头集合」重构为 可逐项管理的引擎指纹信号列表,加固整条判定链,并补齐账号级 app-server 控制、 对齐前端设置文案。 判定链(每步可短路): - 账号未开 codex_cli_only → 不限制;gateway.force_codex_cli → 旁路放行 - 全局黑名单命中(OR 宽 deny)→ 立即拒 - 身份候选:官方 UA(strict,仅前缀)/ 官方 originator(OR)/ 全局白名单(双因子 AND) / 全局 app-server 开关 OR 账号 app-server 开关;均不命中 → 拒 - 版本门(仅官方候选):UA 须可解析引擎版本,再校验 [min,max] 区间 - 引擎指纹 AND 硬门:按信号列表逐条勾选 AND、每条行内变体 OR;无 Required 信号 → 放行 引擎指纹信号列表(唯一真源) - 新增 openai.EngineFingerprintSignal 类型 + EvaluateEngineFingerprint 求值器 (勾选 AND / 行内变体 OR / 无勾选 → 放行) - CodexRestrictionPolicy 增 EngineFingerprintSignals;信号列表单一决定是否启用指纹门, 不再保留独立「要求引擎指纹」总开关(与「信号全不选」语义重复) - 新设置键 codex_cli_only_engine_fingerprint_signals(默认只勾 x-codex- 前缀); 旧 body 指纹开关幂等迁移并入信号列表;wire 接线 - 黑/白名单自由条目、命名预设、版本区间 全局设置管线 - gateway 缺 settingService(仅测试/误配可达)时指纹门回退默认种子信号、失败关闭, 不再因零值 policy(nil 信号)失败开放 账号级 Codex app-server(替换已失效的 ClaudeCode 放行机制) - account.IsCodexCLIOnlyAppServerAllowed() 读 extra.codex_cli_only_allow_app_server, 仅在 codex_cli_only 开启时生效;候选身份门「全局 OR 账号」,与旧系统双层控制对齐 - 移除已无入口的 claude_code 预设机制(allowedClientRegistry / MatchAllowedClients / 账号 GetCodexCLIOnlyAllowedClients / reason);白名单 AllowedClientEntry / IsAllowedClientMatch 保留 门加固(反伪 + 写入校验) - 官方 UA 访问门改 strict:IsCodexOfficialClientRequestStrict 仅前缀匹配,收窄「浏览器前缀 + 中段 codex token」伪造面(strict 仍保留 Codex 家族前缀与 UA 尾部兜底,故对「任意前缀 + 官方尾部 (name;ver)」仍放行——与 UA 可伪造、真正反伪靠引擎指纹门的设计一致) - 官方客户端识别扩展:新增 codex-tui/、codex_vscode_copilot/ 前缀 + UA 尾部 (name;ver) 兜底 (恢复 CODEX_INTERNAL_ORIGINATOR_OVERRIDE 的真实 client,如 cccc→codex-tui),originator 改 精确集。该识别经 IsCodexOfficialClientByHeaders 被 passthrough 复用,故透传的官方判定一并 修正(codex-tui 等不再被误改写 UA)——非「行为不变」,属有意修正 - 白名单写入校验 ValidateCodexWhitelistEntriesJSON + AllowedClientEntry.IsWhitelistable: 双因子 AND 条目须可命中(非空 originator + 非空 ua_contains),拒绝写入会静默失效的死规则; 黑名单(OR 宽 deny,允许 originator-only)不受约束 管理端 / 前端 - handler / DTO / settings_view / 契约测试;gateway 接入判定链 - 信号列表编辑器(替换 body 开关)、api 类型、SettingsView;无勾选给常驻警告 - Create/Edit/Bulk 三弹窗「Codex Only」下新增 app-server 开关(OR 合并全局) - 文案:UA/Originator → User-Agent/Originator;黑/白名单重命名为 User-Agent/Originator 黑/白名单; 「允许 App Server 第三方客户端」→「Codex app-server」+ 简介示例;i18n zh/en 同步 - 移除死代码 HasCodex*Fingerprint helper 测试:引擎指纹求值器 / 账号 app-server(OR 语义)/ detector(含 N1 strict、失败关闭)/ 白名单写入校验 / BulkEdit spec 等;后端 build + service/openai/admin 单测全绿,前端 vue-tsc + vitest 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
186 lines
8.4 KiB
Go
186 lines
8.4 KiB
Go
package openai
|
||
|
||
import "testing"
|
||
|
||
func TestIsCodexCLIRequest(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
ua string
|
||
want bool
|
||
}{
|
||
{name: "codex_cli_rs 前缀", ua: "codex_cli_rs/0.1.0", want: true},
|
||
{name: "codex_vscode 前缀", ua: "codex_vscode/1.2.3", want: true},
|
||
{name: "大小写混合", ua: "Codex_CLI_Rs/0.1.0", want: true},
|
||
{name: "复合 UA 包含 codex", ua: "Mozilla/5.0 codex_cli_rs/0.1.0", want: true},
|
||
{name: "空白包裹", ua: " codex_vscode/1.2.3 ", want: true},
|
||
{name: "非 codex", ua: "curl/8.0.1", want: false},
|
||
{name: "空字符串", ua: "", want: false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := IsCodexCLIRequest(tt.ua)
|
||
if got != tt.want {
|
||
t.Fatalf("IsCodexCLIRequest(%q) = %v, want %v", tt.ua, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestCodexUATrailerName(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
ua string
|
||
want string
|
||
}{
|
||
// 典型 cccc override 场景:前缀改写但尾部保留真实 clientInfo.name
|
||
{name: "cccc override → codex-tui", ua: "cccc/0.141.0 (mac os 14.6.1; arm64) apple_terminal/453 (codex-tui; 0.141.0)", want: "codex-tui"},
|
||
{name: "cccc override Ubuntu", ua: "cccc/0.139.0 (ubuntu 22.4.0; x86_64) screen (codex-tui; 0.139.0)", want: "codex-tui"},
|
||
// 官方客户端自报名称
|
||
{name: "Codex Desktop 自报(小写后)", ua: "codex desktop/0.142.0 (mac os 26.0.1; arm64) unknown (codex desktop; 26.616.71553)", want: "codex desktop"},
|
||
{name: "codex-tui 自报", ua: "codex-tui/0.141.0 (mac os 15.5.0; arm64) ghostty/1.3.1 (codex-tui; 0.141.0)", want: "codex-tui"},
|
||
{name: "codex_exec 自报", ua: "codex_exec/0.141.0 (mac os 14.7.3; arm64) apple_terminal (codex_exec; 0.141.0)", want: "codex_exec"},
|
||
// 无括号/无尾部组
|
||
{name: "curl 无括号", ua: "curl/8.0.1", want: ""},
|
||
{name: "空字符串", ua: "", want: ""},
|
||
// 非 codex 尾部
|
||
{name: "非 codex 尾部不影响", ua: "evil/0.1.0 (linux; x86_64) bash (evil; 0.1.0)", want: "evil"},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := codexUATrailerName(tt.ua)
|
||
if got != tt.want {
|
||
t.Fatalf("codexUATrailerName(%q) = %q, want %q", tt.ua, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsCodexOfficialClientRequest(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
ua string
|
||
want bool
|
||
}{
|
||
{name: "codex_cli_rs 前缀", ua: "codex_cli_rs/0.98.0", want: true},
|
||
{name: "codex_vscode 前缀", ua: "codex_vscode/1.0.0", want: true},
|
||
{name: "codex_vscode_copilot 变体前缀", ua: "codex_vscode_copilot/0.140.0", want: true},
|
||
{name: "codex_app 前缀", ua: "codex_app/0.1.0", want: true},
|
||
{name: "codex_chatgpt_desktop 前缀", ua: "codex_chatgpt_desktop/1.0.0", want: true},
|
||
{name: "codex_atlas 前缀", ua: "codex_atlas/1.0.0", want: true},
|
||
{name: "codex_exec 前缀", ua: "codex_exec/0.1.0", want: true},
|
||
{name: "codex_sdk_ts 前缀", ua: "codex_sdk_ts/0.1.0", want: true},
|
||
{name: "Codex 桌面 UA", ua: "Codex Desktop/1.2.3", want: true},
|
||
{name: "codex-tui 连字符前缀(真实流量占比最高)", ua: "codex-tui/0.141.0 (Mac OS 15.5.0; arm64) ghostty/1.3.1 (codex-tui; 0.141.0)", want: true},
|
||
{name: "复合 UA 包含 codex_app", ua: "Mozilla/5.0 codex_app/0.1.0", want: true},
|
||
{name: "大小写混合", ua: "Codex_VSCode/1.2.3", want: true},
|
||
// UA 尾部兜底:cccc 是生产中 CODEX_INTERNAL_ORIGINATOR_OVERRIDE=cccc 的真实 codex-tui。
|
||
// 审计 10GB/23天 中占非 codex 的 80.9%(494/611)、全 openai 流量的 5.3%——若不兜底会误杀。
|
||
{name: "cccc override Mac → 尾部兜底放行", ua: "cccc/0.141.0 (Mac OS 14.6.1; arm64) Apple_Terminal/453 (codex-tui; 0.141.0)", want: true},
|
||
{name: "cccc override Ubuntu → 尾部兜底放行", ua: "cccc/0.139.0 (Ubuntu 22.4.0; x86_64) screen (codex-tui; 0.139.0)", want: true},
|
||
{name: "cccc override iTerm → 尾部兜底放行", ua: "cccc/0.137.0 (Mac OS 26.1.0; arm64) iTerm.app/3.4.22 (codex-tui; 0.137.0)", want: true},
|
||
// 非 codex 尾部不应放行
|
||
{name: "完全伪造尾部应拒", ua: "evil/0.1.0 (Linux; x86_64) bash (evil; 0.1.0)", want: false},
|
||
{name: "非 codex", ua: "curl/8.0.1", want: false},
|
||
{name: "空字符串", ua: "", want: false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := IsCodexOfficialClientRequest(tt.ua)
|
||
if got != tt.want {
|
||
t.Fatalf("IsCodexOfficialClientRequest(%q) = %v, want %v", tt.ua, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsCodexOfficialClientOriginator(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
originator string
|
||
want bool
|
||
}{
|
||
{name: "codex_cli_rs", originator: "codex_cli_rs", want: true},
|
||
{name: "codex_vscode", originator: "codex_vscode", want: true},
|
||
{name: "codex_app", originator: "codex_app", want: true},
|
||
{name: "codex_chatgpt_desktop", originator: "codex_chatgpt_desktop", want: true},
|
||
{name: "codex_atlas", originator: "codex_atlas", want: true},
|
||
{name: "codex_exec", originator: "codex_exec", want: true},
|
||
{name: "codex_sdk_ts", originator: "codex_sdk_ts", want: true},
|
||
{name: "Codex 前缀", originator: "Codex Desktop", want: true},
|
||
{name: "codex-tui 连字符(真实流量占比最高)", originator: "codex-tui", want: true},
|
||
{name: "空白包裹", originator: " codex_vscode ", want: true},
|
||
{name: "伪造含 codex_ 子串应拒(L2 收紧)", originator: "evil-codex_cli", want: false},
|
||
{name: "codex_ 混入中段应拒(L2 收紧)", originator: "my_codex_thing", want: false},
|
||
{name: "非 codex", originator: "my_client", want: false},
|
||
{name: "空字符串", originator: "", want: false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := IsCodexOfficialClientOriginator(tt.originator)
|
||
if got != tt.want {
|
||
t.Fatalf("IsCodexOfficialClientOriginator(%q) = %v, want %v", tt.originator, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsCodexOfficialClientRequestStrict(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
ua string
|
||
want bool
|
||
}{
|
||
// 前缀开头:与 lax 版一致放行
|
||
{name: "codex_cli_rs 前缀开头", ua: "codex_cli_rs/0.141.0 (x)", want: true},
|
||
{name: "codex_vscode 前缀开头", ua: "codex_vscode/1.0.0", want: true},
|
||
{name: "codex_app 前缀开头", ua: "codex_app/2.1.0", want: true},
|
||
{name: "Codex 家族前缀保留", ua: "Codex Desktop/1.2.3", want: true},
|
||
{name: "大小写混合前缀开头", ua: "Codex_CLI_Rs/0.141.0", want: true},
|
||
// UA 尾部兜底保留:cccc override 真实 codex-tui 仍放行
|
||
{name: "cccc override 尾部兜底仍放行", ua: "cccc/0.141.0 (Mac OS 14.6.1; arm64) Apple_Terminal/453 (codex-tui; 0.141.0)", want: true},
|
||
// N1 收紧:codex token 不在行首(子串)不再算官方——lax 版会因 Contains 误判 true
|
||
{name: "浏览器前缀+中段 codex_app 收紧→拒", ua: "Mozilla/5.0 codex_app/0.141.0", want: false},
|
||
{name: "中段 codex_cli_rs 收紧→拒", ua: "evilclient/1.0 codex_cli_rs/0.141.0", want: false},
|
||
{name: "非 codex", ua: "curl/8.0.1", want: false},
|
||
{name: "空字符串", ua: "", want: false},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := IsCodexOfficialClientRequestStrict(tt.ua)
|
||
if got != tt.want {
|
||
t.Fatalf("IsCodexOfficialClientRequestStrict(%q) = %v, want %v", tt.ua, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsCodexOfficialClientByHeaders(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
ua string
|
||
originator string
|
||
want bool
|
||
}{
|
||
{name: "仅 originator 命中 desktop", originator: "Codex Desktop", want: true},
|
||
{name: "仅 originator 命中 vscode", originator: "codex_vscode", want: true},
|
||
{name: "仅 ua 命中 desktop", ua: "Codex Desktop/1.2.3", want: true},
|
||
{name: "仅 originator 命中 codex-tui", originator: "codex-tui", want: true},
|
||
{name: "仅 ua 命中 codex-tui", ua: "codex-tui/0.141.0 (Mac OS 15.5.0; arm64) ghostty/1.3.1", want: true},
|
||
// cccc:originator 不命中精确集,但 UA 尾部兜底恢复真实 codex-tui
|
||
{name: "cccc override → UA 尾部兜底放行(审计 5.3% 误杀场景)", ua: "cccc/0.141.0 (Mac OS 14.6.1; arm64) Apple_Terminal/453 (codex-tui; 0.141.0)", originator: "cccc", want: true},
|
||
{name: "ua 与 originator 都未命中", ua: "curl/8.0.1", originator: "my_client", want: false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := IsCodexOfficialClientByHeaders(tt.ua, tt.originator)
|
||
if got != tt.want {
|
||
t.Fatalf("IsCodexOfficialClientByHeaders(%q, %q) = %v, want %v", tt.ua, tt.originator, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|