sub2api/backend/internal/service/claude_code_session_id.go
win 92433656f5 chore: merge upstream Wei-Shaw/sub2api v0.1.128 — keep fork customizations
Upstream 新功能 (34 commits, ~main..origin/main):
- feat(email): 通知邮件模板服务、模板编辑器、订阅/余额提醒邮件
- feat(notification): NotificationEmailService 注入到 Balance/Payment/Setting
- feat(payment): 支付成功通知邮件
- feat(usage): 用户 API Key 用量页支持按日明细
- feat(openai-gateway): Codex OAuth 浏览器 UA 自动改写规避 Cloudflare 质询
- feat(admin): 邮件模板管理接口
- fix(auth): 停用/删除分组后阻断 API Key
- fix(group): 修正分组账号可用计数口径
- fix(openai): /v1/responses respect force chat completions, images n 参数透传
- test(repository): AES Encryptor 单元测试
- chore: VERSION 0.1.128

冲突解决 (backend/cmd/server/wire_gen.go):
- 引入 upstream 新 wire providers: notificationEmailService,
  ProvidePaymentService(10 args), ProvideAdminSettingHandler(8 args)
- 保留 fork 独有依赖: rpmTokenBucketService (RPM 平滑),
  NewOpsHandler 3 参数版本 (requestEventBus, opsLogBroadcaster)
- ProvideBalanceNotifyService 接受 4 参数 (含 notificationEmailService)

修复 session-id helper 设计 (claude_code_session_id.go):
- 发现: TestGatewayService_AnthropicOAuth_InjectsClaudeCodeSessionHeaderFromMetadata
  在 OAuth + mimicClaudeCode=false 场景失败
- 重新审视设计原则: OAuth 凭证本身就是 Claude Code 客户端,可信任 metadata
  派生 session_id;不应受 mimicClaudeCode 标志阻止
- 修复: metadata 派生只看 tokenType=="oauth";UUID 兜底仍需 oauth && mimic
- 更新测试: OAuthNonMimicDerivesFromMetadata 取代原 IgnoresMetadata

所有 fork 独有功能保留:
- Claude Code 2.1.145 mimicry bundle (上个 commit 引入)
- RPM token bucket smoothing (commit 95814974)
- Windsurf/Antigravity/Omniroute 定制
- claudemask/ 校验包 (upstream 已删除,我们仍在 gateway_service 中使用)

不在范围:
- 不修复 baseline 既存的 2 个测试失败 (TestProxyImportData...,
  TestWindsurfTierAccessService_Snapshot_HappyPath) - 与 merge 无关
2026-05-20 17:50:44 +08:00

77 lines
2.8 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"
"github.com/google/uuid"
"github.com/tidwall/gjson"
)
// ensureClaudeCodeSessionID 确保 X-Claude-Code-Session-Id header 被合理填充。
//
// 真实 Claude Code 2.1.145 CLI 在 SDK 内会始终设置 X-Claude-Code-Session-Id 为
// 当前 CLI 会话的 UUID源码`"X-Claude-Code-Session-Id":y_()`)。上游若发现
// 该头缺失,可能将请求识别为非官方 Claude Code 第三方调用。
//
// 行为按 tokenType / mimicClaudeCode 分两条路径:
//
// OAuth 路径 (tokenType == "oauth"):
// OAuth 账号本身就是真实 Claude Code 客户端的凭证,可以信任 body 中的
// metadata.user_id 派生 session id。
// 1. metadata.user_id 派生 SessionID 是合法 UUID → canonical 写入
// 2. header 已有合法 UUID → canonical 保留
// 3. mimicClaudeCode == true → 兜底生成新 UUID
// (mimicClaudeCode == false 且无 metadata 时不强制注入)
//
// API key 透传路径 (tokenType != "oauth"):
// - 不从 body metadata 派生 header避免污染客户端原始语义
// - 若客户端在 header 中传入 X-Claude-Code-Session-Id
// 合法 UUID → canonical 保留
// 非法值 → 删除(不向上游转发恶意值)
// - 不兜底生成
//
// 安全说明metadata.user_id 由客户端控制ParseMetadataUserID 的正则仅约束字符集,
// 不保证 UUID 结构。因此所有写入 header 的 session id 都必须经 uuid.Parse 校验。
func ensureClaudeCodeSessionID(req *http.Request, body []byte, tokenType string, mimicClaudeCode bool) {
if req == nil {
return
}
if req.Header == nil {
req.Header = make(http.Header)
}
isOAuth := tokenType == "oauth"
// OAuth 路径:从 metadata 派生OAuth 凭证可信任)。
if isOAuth {
if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" {
if parsed := ParseMetadataUserID(uid); parsed != nil {
if id, err := uuid.Parse(parsed.SessionID); err == nil {
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", id.String())
return
}
}
}
}
// 处理客户端已传入的 header合法 → canonicalize非法 → 删除。
// 这条规则对所有路径生效,避免恶意非 UUID 值向上游透传。
if existing := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id"); existing != "" {
if id, err := uuid.Parse(existing); err == nil {
canonical := id.String()
if canonical != existing {
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", canonical)
}
return
}
// 非法 UUID删除避免透传
req.Header.Del("X-Claude-Code-Session-Id")
}
// OAuth mimic 兜底生成(仅 mimic 场景API key/非 mimic 不污染)。
// uuid.NewString() 走 crypto/rand。
if isOAuth && mimicClaudeCode {
setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", uuid.NewString())
}
}