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 mimic 路径 (tokenType == "oauth" && mimicClaudeCode): // 1. body 中 metadata.user_id 派生的 SessionID 是合法 UUID → canonicalize 写入 // 2. 请求 header 中已有合法 UUID → canonicalize 保留 // 3. 否则 → 兜底生成 UUID // // API key 透传 / 非 mimic 路径: // - 不从 body 合成 header(避免污染客户端原始语义) // - 但若客户端在 header 中传入了 X-Claude-Code-Session-Id: // 合法 UUID → canonicalize 保留 // 非法值 → 删除(不向上游转发恶意值,符合 UUID 校验承诺) // - 不兜底生成 // // 安全说明: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) } isOAuthMimic := tokenType == "oauth" && mimicClaudeCode // OAuth mimic 路径:从 metadata 派生(仅在 mimic 场景写 header)。 if isOAuthMimic { 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 不污染)。 // uuid.NewString() 走 crypto/rand。 if isOAuthMimic { setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", uuid.NewString()) } }