反编译本地 Claude Code 2.1.145 二进制 (Bun 1.3.14 打包, @anthropic-ai/sdk@0.94.0 嵌入) 提取真实指纹,系统性升级 mimicry。 核心改动: - 新增 ClaudeCodeBundle struct 作为单一事实源,DefaultBundle 描述当前 伪装目标的完整快照 (CLIVersion/SDKVersion/RuntimeVersion/OS/Arch) - DefaultCLIVersion/DefaultStainlessPackageVersion/CLICurrentVersion/ DefaultHeaders 全部派生自 DefaultBundle,消除三处 (2.1.92, 2.1.104, 0.70.0, 0.81.0) 版本分裂 - CLI 版本 2.1.92/2.1.104 -> 2.1.145 - SDK 版本 0.70.0/0.81.0 -> 0.94.0 - 新增 12 个 2.1.145 反编译确认的 anthropic-beta token: advanced-tool-use, tool-search-tool, mcp-servers, mcp-client, mid-conversation-system, afk-mode, cache-diagnosis, context-hint, environments, managed-agents, skills, compact - FullClaudeCodeMimicryBetas() 从 7 个 token 升级到 21 个 ordered list - 修正 BetaTokenEfficientTools 错日期 (2026-03-28 -> 2025-02-19) - 从默认 beta header 移除已 GA 的 BetaFineGrainedToolStreaming / BetaTokenEfficientTools (常量保留供客户端显式 merge) - claudemask.RequiredNodeHeaders 加 X-Claude-Code-Session-Id 强制 新增 ensureClaudeCodeSessionID helper (claude_code_session_id.go): - 真实 CLI 在 SDK 内强制 X-Claude-Code-Session-Id:y_(),缺失被判第三方 - OAuth mimic 路径: metadata.user_id 派生 -> canonical UUID 写入 -> 兜底 uuid.NewString() - API key passthrough 路径: 不从 body 派生,保护客户端原始语义 - 所有路径均对客户端传入的非法 UUID 执行删除 (避免恶意值上游透传) - 所有写入 header 的 session-id 都通过 uuid.Parse 校验 测试: - 新增 14 个 ensureClaudeCodeSessionID 单元测试,含恶意 UUID 注入拒绝 + API key 路径隔离 + canonical 形式校验 - 新增 3 个 bundle 派生一致性测试 - mask_test 加 session-id 缺失校验 case - 老 UA 断言 2.1.104 -> 2.1.145 不在范围: - TLS 指纹 (utls 已处理) - Bun.hash vs xxHash64 算法验证 (需 golden vectors,独立项目) References: - VERSION:2.1.145 BUILD_TIME:2026-05-19T01:36:35Z GIT_SHA:daa4c3755d45ab0cf97bb41db8c03bd2dfd2ff5f
196 lines
6.9 KiB
Go
196 lines
6.9 KiB
Go
package claudemask
|
||
|
||
import (
|
||
"net/http"
|
||
"testing"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||
)
|
||
|
||
func TestValidateNodeEmulation_ValidRequest(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 设置所有必需的 Node.js 头
|
||
req.Header.Set("User-Agent", claude.DefaultUserAgent())
|
||
req.Header.Set("X-Stainless-Lang", claude.DefaultStainlessLang)
|
||
req.Header.Set("X-Stainless-Runtime", claude.DefaultStainlessRuntime)
|
||
req.Header.Set("X-Stainless-Runtime-Version", claude.DefaultStainlessRuntimeVersion)
|
||
req.Header.Set("X-Stainless-Package-Version", claude.DefaultStainlessPackageVersion)
|
||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||
// 2.1.145 强制:X-Claude-Code-Session-Id (UUID)
|
||
req.Header.Set("X-Claude-Code-Session-Id", "01970000-0000-7000-8000-000000000001")
|
||
|
||
isValid, errors := ValidateNodeEmulation(req)
|
||
|
||
if !isValid {
|
||
t.Errorf("expected valid emulation, got errors: %v", errors)
|
||
}
|
||
if len(errors) > 0 {
|
||
t.Errorf("expected no errors, got: %v", errors)
|
||
}
|
||
}
|
||
|
||
func TestValidateNodeEmulation_MissingHeaders(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 只设置部分头
|
||
req.Header.Set("User-Agent", claude.DefaultUserAgent())
|
||
|
||
isValid, errors := ValidateNodeEmulation(req)
|
||
|
||
if isValid {
|
||
t.Error("expected invalid emulation due to missing headers")
|
||
}
|
||
if len(errors) == 0 {
|
||
t.Error("expected validation errors")
|
||
}
|
||
if len(errors) < 8 {
|
||
t.Errorf("expected at least 8 missing headers, got %d errors", len(errors))
|
||
}
|
||
}
|
||
|
||
// TestValidateNodeEmulation_MissingSessionID 验证 X-Claude-Code-Session-Id
|
||
// 是强制头:缺失则验证失败。
|
||
func TestValidateNodeEmulation_MissingSessionID(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 设置所有 Stainless 头,但缺 session-id
|
||
req.Header.Set("User-Agent", claude.DefaultUserAgent())
|
||
req.Header.Set("X-Stainless-Lang", claude.DefaultStainlessLang)
|
||
req.Header.Set("X-Stainless-Runtime", claude.DefaultStainlessRuntime)
|
||
req.Header.Set("X-Stainless-Runtime-Version", claude.DefaultStainlessRuntimeVersion)
|
||
req.Header.Set("X-Stainless-Package-Version", claude.DefaultStainlessPackageVersion)
|
||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||
|
||
isValid, errs := ValidateNodeEmulation(req)
|
||
if isValid {
|
||
t.Fatal("expected invalid due to missing X-Claude-Code-Session-Id")
|
||
}
|
||
foundSessionErr := false
|
||
for _, e := range errs {
|
||
if e == "missing required header: X-Claude-Code-Session-Id" {
|
||
foundSessionErr = true
|
||
break
|
||
}
|
||
}
|
||
if !foundSessionErr {
|
||
t.Fatalf("expected X-Claude-Code-Session-Id error, got: %v", errs)
|
||
}
|
||
}
|
||
|
||
func TestValidateNodeEmulation_InvalidRuntime(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 设置所有必需的头,但 runtime 错误
|
||
req.Header.Set("User-Agent", claude.DefaultUserAgent())
|
||
req.Header.Set("X-Stainless-Lang", claude.DefaultStainlessLang)
|
||
req.Header.Set("X-Stainless-Runtime", "go") // ❌ 应为 "node"
|
||
req.Header.Set("X-Stainless-Runtime-Version", claude.DefaultStainlessRuntimeVersion)
|
||
req.Header.Set("X-Stainless-Package-Version", claude.DefaultStainlessPackageVersion)
|
||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||
|
||
isValid, errs := ValidateNodeEmulation(req)
|
||
|
||
if isValid {
|
||
t.Error("expected invalid due to runtime=go")
|
||
}
|
||
if len(errs) == 0 {
|
||
t.Error("expected validation error for runtime")
|
||
}
|
||
}
|
||
|
||
func TestValidateNodeEmulation_GoClientUA(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// User-Agent 包含 Go 指示
|
||
req.Header.Set("User-Agent", "Go-http-client/2.0") // ❌ 包含 Go 指示
|
||
|
||
isValid, _ := ValidateNodeEmulation(req)
|
||
|
||
if isValid {
|
||
t.Error("expected invalid due to Go-http-client in User-Agent")
|
||
}
|
||
}
|
||
|
||
func TestValidateNodeEmulation_MissingUserAgent(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 不设置 User-Agent
|
||
req.Header.Set("X-Stainless-Lang", claude.DefaultStainlessLang)
|
||
req.Header.Set("X-Stainless-Runtime", claude.DefaultStainlessRuntime)
|
||
req.Header.Set("X-Stainless-Runtime-Version", claude.DefaultStainlessRuntimeVersion)
|
||
req.Header.Set("X-Stainless-Package-Version", claude.DefaultStainlessPackageVersion)
|
||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||
|
||
isValid, errs := ValidateNodeEmulation(req)
|
||
|
||
if isValid {
|
||
t.Error("expected invalid due to missing User-Agent")
|
||
}
|
||
if len(errs) == 0 {
|
||
t.Error("expected validation error for missing User-Agent")
|
||
}
|
||
}
|
||
|
||
func TestCleanRequest_FixesGoUserAgent(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 设置不正确的 User-Agent
|
||
req.Header.Set("User-Agent", "Go-http-client/2.0")
|
||
|
||
CleanRequest(req)
|
||
|
||
ua := req.Header.Get("User-Agent")
|
||
if ua != claude.DefaultUserAgent() {
|
||
t.Errorf("expected fixed User-Agent, got: %s", ua)
|
||
}
|
||
}
|
||
|
||
func TestCleanRequest_FixesMissingUserAgent(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 不设置 User-Agent
|
||
CleanRequest(req)
|
||
|
||
ua := req.Header.Get("User-Agent")
|
||
if ua != claude.DefaultUserAgent() {
|
||
t.Errorf("expected User-Agent to be set, got: %s", ua)
|
||
}
|
||
}
|
||
|
||
func TestValidateAndClean(t *testing.T) {
|
||
req, _ := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
|
||
// 设置带有 Go 指示的 User-Agent,其他头正确
|
||
req.Header.Set("User-Agent", "Go-http-client/2.0")
|
||
req.Header.Set("X-Stainless-Lang", claude.DefaultStainlessLang)
|
||
req.Header.Set("X-Stainless-Runtime", claude.DefaultStainlessRuntime)
|
||
req.Header.Set("X-Stainless-Runtime-Version", claude.DefaultStainlessRuntimeVersion)
|
||
req.Header.Set("X-Stainless-Package-Version", claude.DefaultStainlessPackageVersion)
|
||
req.Header.Set("X-Stainless-OS", claude.DefaultStainlessOS)
|
||
req.Header.Set("X-Stainless-Arch", claude.DefaultStainlessArch)
|
||
req.Header.Set("anthropic-version", claude.DefaultAnthropicVersion)
|
||
// 2.1.145 强制:X-Claude-Code-Session-Id
|
||
req.Header.Set("X-Claude-Code-Session-Id", "01970000-0000-7000-8000-000000000002")
|
||
|
||
isValid, errors := ValidateAndClean(req)
|
||
|
||
// 清理后应该有效
|
||
if !isValid {
|
||
t.Errorf("expected valid after cleaning, got errors: %v", errors)
|
||
}
|
||
|
||
ua := req.Header.Get("User-Agent")
|
||
if ua != claude.DefaultUserAgent() {
|
||
t.Errorf("expected cleaned User-Agent, got: %s", ua)
|
||
}
|
||
}
|