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 无关
77 lines
2.8 KiB
Go
77 lines
2.8 KiB
Go
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())
|
||
}
|
||
}
|