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

196 lines
6.9 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 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)
}
}