sub2api/backend/internal/service/claude_code_session_id_test.go
win 0fefedf9cd feat(claude-mimic): upgrade Claude Code mimicry to 2.1.145 via bundle abstraction
反编译本地 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
2026-05-20 17:18:47 +08:00

201 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}