反编译本地 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
201 lines
7.2 KiB
Go
201 lines
7.2 KiB
Go
package service
|
||
|
||
import (
|
||
"net/http"
|
||
"testing"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
const (
|
||
testValidUUID = "01970000-0000-7000-8000-000000000001"
|
||
testValidUUIDAlt = "01970000-0000-7000-8000-000000000002"
|
||
)
|
||
|
||
func newReq(t *testing.T) *http.Request {
|
||
t.Helper()
|
||
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", nil)
|
||
if err != nil {
|
||
t.Fatalf("NewRequest: %v", err)
|
||
}
|
||
return req
|
||
}
|
||
|
||
func TestEnsureClaudeCodeSessionID_NilRequest(t *testing.T) {
|
||
// Should not panic.
|
||
ensureClaudeCodeSessionID(nil, nil, "oauth", true)
|
||
}
|
||
|
||
func TestEnsureClaudeCodeSessionID_FromMetadataJSON(t *testing.T) {
|
||
req := newReq(t)
|
||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
|
||
ensureClaudeCodeSessionID(req, body, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != testValidUUID {
|
||
t.Fatalf("session_id = %q, want %q", got, testValidUUID)
|
||
}
|
||
}
|
||
|
||
func TestEnsureClaudeCodeSessionID_FromMetadataLegacy(t *testing.T) {
|
||
req := newReq(t)
|
||
// legacy format: user_{64hex}_account_{uuid}_session_{uuid}
|
||
dev := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||
acc := "11111111-2222-3333-4444-555555555555"
|
||
uid := "user_" + dev + "_account_" + acc + "_session_" + testValidUUID
|
||
body := []byte(`{"metadata":{"user_id":"` + uid + `"}}`)
|
||
ensureClaudeCodeSessionID(req, body, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != testValidUUID {
|
||
t.Fatalf("session_id = %q, want %q", got, testValidUUID)
|
||
}
|
||
}
|
||
|
||
// 安全测试:metadata.user_id 中的 session_id 不是合法 UUID 时,
|
||
// 必须 fallback 到 OAuth UUID 兜底,而不是写入恶意值。
|
||
func TestEnsureClaudeCodeSessionID_RejectsMalformedMetadataUUID(t *testing.T) {
|
||
req := newReq(t)
|
||
// session_id 字段是 36 字符但非 UUID 格式(凑数 hex+dash)
|
||
bogus := "abcdefab-0000-0000-0000-not-a-real-uuid"
|
||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + bogus + `\"}"}}`)
|
||
ensureClaudeCodeSessionID(req, body, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got == bogus {
|
||
t.Fatalf("malformed metadata session_id was written verbatim: %q", got)
|
||
}
|
||
if got == "" {
|
||
t.Fatalf("expected OAuth mimic to fallback to UUID, got empty")
|
||
}
|
||
if _, err := uuid.Parse(got); err != nil {
|
||
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
|
||
}
|
||
}
|
||
|
||
func TestEnsureClaudeCodeSessionID_PreservesExistingValidHeader(t *testing.T) {
|
||
req := newReq(t)
|
||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", testValidUUID)
|
||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != testValidUUID {
|
||
t.Fatalf("session_id = %q, want preserved %q", got, testValidUUID)
|
||
}
|
||
}
|
||
|
||
func TestEnsureClaudeCodeSessionID_OverwritesInvalidHeader(t *testing.T) {
|
||
req := newReq(t)
|
||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", "not-a-uuid")
|
||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got == "not-a-uuid" {
|
||
t.Fatalf("invalid header was not overwritten: %q", got)
|
||
}
|
||
if _, err := uuid.Parse(got); err != nil {
|
||
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
|
||
}
|
||
}
|
||
|
||
func TestEnsureClaudeCodeSessionID_OAuthMimicFallback(t *testing.T) {
|
||
req := newReq(t)
|
||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got == "" {
|
||
t.Fatalf("expected OAuth mimic fallback to set X-Claude-Code-Session-Id")
|
||
}
|
||
if _, err := uuid.Parse(got); err != nil {
|
||
t.Fatalf("fallback session_id is not a valid UUID: %q (err=%v)", got, err)
|
||
}
|
||
}
|
||
|
||
// API key passthrough 路径必须不被污染:缺失 session-id 时不强制生成。
|
||
func TestEnsureClaudeCodeSessionID_DoesNotPolluteAPIKey(t *testing.T) {
|
||
req := newReq(t)
|
||
ensureClaudeCodeSessionID(req, nil, "api_key", false)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != "" {
|
||
t.Fatalf("API key path should NOT auto-generate session-id, got %q", got)
|
||
}
|
||
}
|
||
|
||
// API key 路径即使 body 中有合法 metadata.user_id,也不应该派生 session-id 头。
|
||
// 这是 fresh code review 发现的 C1 修复:保护客户端原始语义。
|
||
func TestEnsureClaudeCodeSessionID_APIKeyIgnoresMetadata(t *testing.T) {
|
||
req := newReq(t)
|
||
// 客户端传入合法的 metadata.user_id,但 tokenType=api_key
|
||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
|
||
ensureClaudeCodeSessionID(req, body, "api_key", false)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != "" {
|
||
t.Fatalf("API key path must NOT derive session-id from metadata, got %q", got)
|
||
}
|
||
}
|
||
|
||
// OAuth 但非 mimic 模式也不应该从 metadata 派生 header。
|
||
func TestEnsureClaudeCodeSessionID_OAuthNonMimicIgnoresMetadata(t *testing.T) {
|
||
req := newReq(t)
|
||
body := []byte(`{"metadata":{"user_id":"{\"device_id\":\"abc\",\"account_uuid\":\"\",\"session_id\":\"` + testValidUUID + `\"}"}}`)
|
||
ensureClaudeCodeSessionID(req, body, "oauth", false)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != "" {
|
||
t.Fatalf("Non-mimic OAuth must NOT derive session-id from metadata, got %q", got)
|
||
}
|
||
}
|
||
|
||
// API key 路径若客户端传入了非法 UUID header,必须删除避免向上游透传。
|
||
// 这是 C2 修复:UUID 校验承诺要对所有路径生效。
|
||
func TestEnsureClaudeCodeSessionID_APIKeyDeletesInvalidHeader(t *testing.T) {
|
||
req := newReq(t)
|
||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", "not-a-uuid")
|
||
ensureClaudeCodeSessionID(req, nil, "api_key", false)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != "" {
|
||
t.Fatalf("API key path must delete invalid header, got %q", got)
|
||
}
|
||
}
|
||
|
||
// API key 路径若客户端传入了合法 UUID header,规范化保留(不删除)。
|
||
func TestEnsureClaudeCodeSessionID_APIKeyPreservesValidHeader(t *testing.T) {
|
||
req := newReq(t)
|
||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", testValidUUID)
|
||
ensureClaudeCodeSessionID(req, nil, "api_key", false)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != testValidUUID {
|
||
t.Fatalf("API key path must preserve valid client header, got %q want %q", got, testValidUUID)
|
||
}
|
||
}
|
||
|
||
// OAuth 但非 mimic 场景:保留客户端原始语义,不自动生成。
|
||
func TestEnsureClaudeCodeSessionID_OAuthNonMimicDoesNotForce(t *testing.T) {
|
||
req := newReq(t)
|
||
ensureClaudeCodeSessionID(req, nil, "oauth", false)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
if got != "" {
|
||
t.Fatalf("non-mimic OAuth should not auto-generate, got %q", got)
|
||
}
|
||
}
|
||
|
||
// 验证不同 UUID 输入会被规范化为 canonical 小写形式。
|
||
func TestEnsureClaudeCodeSessionID_CanonicalForm(t *testing.T) {
|
||
req := newReq(t)
|
||
// 大写 UUID 输入
|
||
upper := "01970000-0000-7000-8000-ABCDEF000003"
|
||
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", upper)
|
||
ensureClaudeCodeSessionID(req, nil, "oauth", true)
|
||
|
||
got := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id")
|
||
want := "01970000-0000-7000-8000-abcdef000003"
|
||
if got != want {
|
||
t.Fatalf("session_id = %q, want canonical %q", got, want)
|
||
}
|
||
}
|