identity_service.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. package service
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/sha256"
  6. "encoding/hex"
  7. "fmt"
  8. "log/slog"
  9. "net/http"
  10. "regexp"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  16. "github.com/tidwall/gjson"
  17. "github.com/tidwall/sjson"
  18. )
  19. // 预编译正则表达式(避免每次调用重新编译)
  20. var (
  21. // 匹配 User-Agent 版本号: xxx/x.y.z
  22. userAgentVersionRegex = regexp.MustCompile(`/(\d+)\.(\d+)\.(\d+)`)
  23. // fingerprintUserAgentPattern 校验可写入账号级持久身份的 User-Agent 形态:
  24. // <product>/<major>.<minor>.<patch> 之后必须紧跟空白或字符串结束。
  25. // 版本号带 -local / -dev / +build 等后缀的本地构建一律不接受。
  26. fingerprintUserAgentPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+/\d+\.\d+\.\d+(\s|$)`)
  27. // claudeCLIUAVersionPrefixRegex 匹配 claude-cli UA 开头的 "claude-cli/x.y.z" 版本号段,
  28. // 供版本下限抬升时就地替换版本号使用;UA 其余部分(括号内的真实客户端形态等)原样保留。
  29. claudeCLIUAVersionPrefixRegex = regexp.MustCompile(`(?i)^(claude-cli)/\d+\.\d+\.\d+`)
  30. )
  31. const (
  32. // claudeCLIUserAgentProduct 是官方 Claude Code CLI 的产品名(小写)。
  33. claudeCLIUserAgentProduct = "claude-cli"
  34. // maxFingerprintUserAgentLength 限制写入缓存的 User-Agent 长度。
  35. maxFingerprintUserAgentLength = 256
  36. // maxClaudeCLIMajorVersionSkew 是 claude-cli 主版本号相对 sub2api 自身伪装
  37. // 版本(claude.CLIVersion())允许的最大超前量。给足两个大版本的升级
  38. // 窗口,同时挡掉 999 这类哨兵版本号。
  39. maxClaudeCLIMajorVersionSkew = 2
  40. )
  41. // isAcceptableFingerprintUserAgent 判断 User-Agent 是否可作为账号级持久身份写入缓存。
  42. //
  43. // 指纹是账号级、“只升不降”、活跃账号懒续期后近乎永不过期的持久状态,且系统内
  44. // 没有重置入口。一旦写入畸形或哨兵版本(如 claude-cli/999.0.0-local),该账号
  45. // 此后所有上游请求都会在 HTTP 头与请求体 cc_version 两处声称这个不存在的版本,
  46. // 被上游判定为非正版客户端并持续返回不带限流重置头的 429;无重置头又会落到 5 秒
  47. // 兜底冷却,账号池收缩后对外表现为 503 风暴。
  48. //
  49. // 校验必须放在创建与升级两条路径的共同入口:只在 isNewerVersion 处加校验是不够的,
  50. // createFingerprintFromHeaders 首次创建时同样会原样保存畸形 UA,删键恢复后账号可被
  51. // 同一客户端立即再次毒化。
  52. func isAcceptableFingerprintUserAgent(ua string) bool {
  53. ua = strings.TrimSpace(ua)
  54. if ua == "" || len(ua) > maxFingerprintUserAgentLength {
  55. return false
  56. }
  57. if !fingerprintUserAgentPattern.MatchString(ua) {
  58. return false
  59. }
  60. // 非 claude-cli 产品不做版本区间约束:形态合法即可,避免误伤其他合法客户端。
  61. if extractProduct(ua) != claudeCLIUserAgentProduct {
  62. return true
  63. }
  64. major, _, _, ok := parseUserAgentVersion(ua)
  65. if !ok {
  66. return false
  67. }
  68. currentMajor, _, _, currentOK := parseUserAgentVersion(claudeCLIUserAgentProduct + "/" + claude.CLIVersion())
  69. if !currentOK {
  70. return true
  71. }
  72. return major <= currentMajor+maxClaudeCLIMajorVersionSkew
  73. }
  74. // floorClaudeCLIUserAgentVersion 把 claude-cli UA 中的版本号抬升到 claude.CLICurrentVersion
  75. // 下限:低于下限时就地升到下限并返回 changed=true,否则原样返回。
  76. //
  77. // 为什么需要这条:账号级指纹"只升不降"、活跃账号懒续期后近乎永不过期,且系统内没有重置
  78. // 入口,defaultFingerprint 只在首次创建指纹时使用。存量账号缓存里的 claude-cli 版本停留在
  79. // 历史值(如 claude-cli/2.1.220),客户端送来更旧的版本时 isNewerVersion 不会触发升级——
  80. // 仅升 CLICurrentVersion 常量对所有已有账号完全无效,上游按指纹 UA 做客户端版本闸门
  81. // (如 Fable 5.1 要求 >= 2.1.251)时旧指纹永远过不去。
  82. //
  83. // 约束:
  84. // - 只对 claude-cli 产品生效,其它产品一律不动,避免误伤别的合法客户端;
  85. // - 只升不降:版本等于或高于 CLICurrentVersion(含客户端上报的更新版本)时不做任何改动;
  86. // - 只替换 claude-cli/ 后的版本号段,UA 其余部分(如 "(external, claude-desktop-3p,
  87. // agent-sdk/0.3.100)")原样保留,不重建整个字符串、不退化为 defaultFingerprint.UserAgent;
  88. // - X-Stainless-* 字段不在此处理,维持调用方的既有 merge 语义。
  89. func floorClaudeCLIUserAgentVersion(ua string) (string, bool) {
  90. if extractProduct(ua) != claudeCLIUserAgentProduct {
  91. return ua, false
  92. }
  93. floorUA := claudeCLIUserAgentProduct + "/" + claude.CLICurrentVersion
  94. // isNewerVersion(floor, ua) 为 true 当且仅当下限版本严格高于 ua:
  95. // ua 等于或高于下限、产品名不一致、或版本无法解析时都不做改动。
  96. if !isNewerVersion(floorUA, ua) {
  97. return ua, false
  98. }
  99. floored := claudeCLIUAVersionPrefixRegex.ReplaceAllString(ua, "${1}/"+claude.CLICurrentVersion)
  100. if floored == ua {
  101. return ua, false
  102. }
  103. return floored, true
  104. }
  105. // 默认指纹值(当客户端未提供时使用)
  106. var defaultFingerprint = Fingerprint{
  107. UserAgent: "claude-cli/" + claude.CLIVersion() + " (external, cli)",
  108. StainlessLang: "js",
  109. StainlessPackageVersion: "0.94.0",
  110. StainlessOS: "Linux",
  111. StainlessArch: "arm64",
  112. StainlessRuntime: "node",
  113. StainlessRuntimeVersion: "v24.3.0",
  114. }
  115. // Fingerprint represents account fingerprint data
  116. type Fingerprint struct {
  117. ClientID string
  118. UserAgent string
  119. StainlessLang string
  120. StainlessPackageVersion string
  121. StainlessOS string
  122. StainlessArch string
  123. StainlessRuntime string
  124. StainlessRuntimeVersion string
  125. UpdatedAt int64 `json:",omitempty"` // Unix timestamp,用于判断是否需要续期TTL
  126. }
  127. // IdentityCache defines cache operations for identity service
  128. type IdentityCache interface {
  129. GetFingerprint(ctx context.Context, accountID int64) (*Fingerprint, error)
  130. SetFingerprint(ctx context.Context, accountID int64, fp *Fingerprint) error
  131. // GetMaskedSessionID 获取固定的会话ID(用于会话ID伪装功能)
  132. // 返回的 sessionID 是一个 UUID 格式的字符串
  133. // 如果不存在或已过期(15分钟无请求),返回空字符串
  134. GetMaskedSessionID(ctx context.Context, accountID int64) (string, error)
  135. // SetMaskedSessionID 设置固定的会话ID,TTL 为 15 分钟
  136. // 每次调用都会刷新 TTL
  137. SetMaskedSessionID(ctx context.Context, accountID int64, sessionID string) error
  138. }
  139. // IdentityService 管理OAuth账号的请求身份指纹
  140. type IdentityService struct {
  141. cache IdentityCache
  142. }
  143. // NewIdentityService 创建新的IdentityService
  144. func NewIdentityService(cache IdentityCache) *IdentityService {
  145. return &IdentityService{cache: cache}
  146. }
  147. // GetOrCreateFingerprint 获取或创建账号的指纹
  148. // 如果缓存存在,检测user-agent版本,新版本则更新
  149. // 如果缓存不存在,生成随机ClientID并从请求头创建指纹,然后缓存
  150. func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID int64, headers http.Header) (*Fingerprint, error) {
  151. // 入口统一校验:创建与升级两条路径共用,任一路径漏掉都会让畸形 UA 被持久化。
  152. clientUA := strings.TrimSpace(headers.Get("User-Agent"))
  153. uaAcceptable := isAcceptableFingerprintUserAgent(clientUA)
  154. // 尝试从缓存获取指纹
  155. cached, err := s.cache.GetFingerprint(ctx, accountID)
  156. if err == nil && cached != nil {
  157. needWrite := false
  158. // 只在真正阻止了一次写入时记录,便于定位污染源,同时避免被毒化客户端的
  159. // 高频重试刷屏(无重置头的 429 会落到 5 秒兜底冷却,重试相当密集)。
  160. if !uaAcceptable && clientUA != "" && isNewerVersion(clientUA, cached.UserAgent) {
  161. logger.LegacyPrintf("service.identity",
  162. "Rejected fingerprint user-agent for account %d: %q (malformed or implausible version)",
  163. accountID, clientUA)
  164. }
  165. if !isAcceptableFingerprintUserAgent(cached.UserAgent) {
  166. // 自愈:缓存中已是畸形/哨兵 UA(本次加固之前写入的)。指纹在活跃账号上
  167. // 懒续期后近乎永不过期,且系统内没有重置入口——不在读取时纠正,存量被
  168. // 毒化的账号就只能靠手工删 Redis 键恢复。
  169. poisoned := cached.UserAgent
  170. if uaAcceptable {
  171. mergeHeadersIntoFingerprint(cached, headers)
  172. } else {
  173. cached.UserAgent = defaultFingerprint.UserAgent
  174. }
  175. needWrite = true
  176. logger.LegacyPrintf("service.identity",
  177. "Replaced malformed cached fingerprint for account %d: %q -> %q",
  178. accountID, poisoned, cached.UserAgent)
  179. } else {
  180. // 客户端送来更新版本时的常规升级:merge 语义 — 仅更新请求中实际携带的字段,
  181. // 保留缓存值,避免缺失的头被硬编码默认值覆盖(如新 CLI 版本 + 旧 SDK 默认值的不一致)
  182. if uaAcceptable && isNewerVersion(clientUA, cached.UserAgent) {
  183. mergeHeadersIntoFingerprint(cached, headers)
  184. needWrite = true
  185. logger.LegacyPrintf("service.identity", "Updated fingerprint for account %d: %s (merge update)", accountID, clientUA)
  186. }
  187. }
  188. // 版本下限抬升(floor):heal 与 healthy 两条缓存命中路径共同的后置条件,与
  189. // 上面的客户端常规升级相互独立、二者取更新者。客户端送来更旧版本时
  190. // isNewerVersion 不触发,此处仍能把低于 CLICurrentVersion 的存量指纹就地抬到
  191. // 下限并持久化,否则升 CLICurrentVersion 对所有已有账号无效,新模型的客户端
  192. // 版本闸门(如 Fable 5.1 要求 >= 2.1.251)永远过不去。自愈路径同样必须经过:
  193. // 畸形缓存 + 合法但过旧的客户端 UA 首次读取就要落到下限,不能先落库一个
  194. // 过旧版本、等下一次读取才纠正。
  195. if flooredUA, changed := floorClaudeCLIUserAgentVersion(cached.UserAgent); changed {
  196. cached.UserAgent = flooredUA
  197. needWrite = true
  198. logger.LegacyPrintf("service.identity",
  199. "Floored cached fingerprint claude-cli version for account %d: %s", accountID, flooredUA)
  200. }
  201. if !needWrite && time.Since(time.Unix(cached.UpdatedAt, 0)) > 24*time.Hour {
  202. // 距上次写入超过24小时,续期TTL
  203. needWrite = true
  204. }
  205. if needWrite {
  206. cached.UpdatedAt = time.Now().Unix()
  207. if err := s.cache.SetFingerprint(ctx, accountID, cached); err != nil {
  208. logger.LegacyPrintf("service.identity", "Warning: failed to refresh fingerprint for account %d: %v", accountID, err)
  209. }
  210. }
  211. return cached, nil
  212. }
  213. // 缓存不存在或解析失败,创建新指纹。首次创建同样是持久化写入,
  214. // 畸形 UA 在这里落库后就成了账号的长期身份,必须同样拒绝。
  215. if !uaAcceptable && clientUA != "" {
  216. logger.LegacyPrintf("service.identity",
  217. "Rejected fingerprint user-agent for account %d: %q (malformed or implausible version)",
  218. accountID, clientUA)
  219. }
  220. fp := s.createFingerprintFromHeaders(headers)
  221. // 生成随机ClientID
  222. fp.ClientID = generateClientID()
  223. fp.UpdatedAt = time.Now().Unix()
  224. // 保存到缓存(7天TTL,每24小时自动续期)
  225. if err := s.cache.SetFingerprint(ctx, accountID, fp); err != nil {
  226. logger.LegacyPrintf("service.identity", "Warning: failed to cache fingerprint for account %d: %v", accountID, err)
  227. }
  228. logger.LegacyPrintf("service.identity", "Created new fingerprint for account %d with client_id: %s", accountID, fp.ClientID)
  229. return fp, nil
  230. }
  231. // createFingerprintFromHeaders 从请求头创建指纹
  232. func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fingerprint {
  233. fp := &Fingerprint{}
  234. // 获取User-Agent:只接受形态合法且版本合理的值,否则回退默认指纹。
  235. // 首次创建同样是持久化写入,必须与升级路径共用同一套校验。
  236. if ua := strings.TrimSpace(headers.Get("User-Agent")); isAcceptableFingerprintUserAgent(ua) {
  237. // 首次创建与缓存命中路径共用同一个版本下限:合法但过旧的 claude-cli UA
  238. // 落库时同样不能低于 CLICurrentVersion,否则新账号一开始就带着过旧的持久身份。
  239. fp.UserAgent, _ = floorClaudeCLIUserAgentVersion(ua)
  240. } else {
  241. fp.UserAgent = defaultFingerprint.UserAgent
  242. }
  243. // 获取x-stainless-*头,如果没有则使用默认值
  244. fp.StainlessLang = getHeaderOrDefault(headers, "X-Stainless-Lang", defaultFingerprint.StainlessLang)
  245. fp.StainlessPackageVersion = getHeaderOrDefault(headers, "X-Stainless-Package-Version", defaultFingerprint.StainlessPackageVersion)
  246. fp.StainlessOS = getHeaderOrDefault(headers, "X-Stainless-OS", defaultFingerprint.StainlessOS)
  247. fp.StainlessArch = getHeaderOrDefault(headers, "X-Stainless-Arch", defaultFingerprint.StainlessArch)
  248. fp.StainlessRuntime = getHeaderOrDefault(headers, "X-Stainless-Runtime", defaultFingerprint.StainlessRuntime)
  249. fp.StainlessRuntimeVersion = getHeaderOrDefault(headers, "X-Stainless-Runtime-Version", defaultFingerprint.StainlessRuntimeVersion)
  250. return fp
  251. }
  252. // mergeHeadersIntoFingerprint 将请求头中实际存在的字段合并到现有指纹中(用于版本升级场景)
  253. // 关键语义:请求中有的字段 → 用新值覆盖;缺失的头 → 保留缓存中的已有值
  254. // 与 createFingerprintFromHeaders 的区别:后者用于首次创建,缺失头回退到 defaultFingerprint;
  255. // 本函数用于升级更新,缺失头保留缓存值,避免将已知的真实值退化为硬编码默认值
  256. func mergeHeadersIntoFingerprint(fp *Fingerprint, headers http.Header) {
  257. // User-Agent:版本升级的触发条件,一定存在
  258. if ua := headers.Get("User-Agent"); ua != "" {
  259. fp.UserAgent = ua
  260. }
  261. // X-Stainless-* 头:仅在请求中实际携带时才更新,否则保留缓存值
  262. mergeHeader(headers, "X-Stainless-Lang", &fp.StainlessLang)
  263. mergeHeader(headers, "X-Stainless-Package-Version", &fp.StainlessPackageVersion)
  264. mergeHeader(headers, "X-Stainless-OS", &fp.StainlessOS)
  265. mergeHeader(headers, "X-Stainless-Arch", &fp.StainlessArch)
  266. mergeHeader(headers, "X-Stainless-Runtime", &fp.StainlessRuntime)
  267. mergeHeader(headers, "X-Stainless-Runtime-Version", &fp.StainlessRuntimeVersion)
  268. }
  269. // mergeHeader 如果请求头中存在该字段则更新目标值,否则保留原值
  270. func mergeHeader(headers http.Header, key string, target *string) {
  271. if v := headers.Get(key); v != "" {
  272. *target = v
  273. }
  274. }
  275. // getHeaderOrDefault 获取header值,如果不存在则返回默认值
  276. func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
  277. if v := headers.Get(key); v != "" {
  278. return v
  279. }
  280. return defaultValue
  281. }
  282. // ApplyFingerprint 将指纹应用到请求头(覆盖原有的x-stainless-*头)
  283. // 使用 setHeaderRaw 保持原始大小写(如 X-Stainless-OS 而非 X-Stainless-Os)
  284. func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) {
  285. if fp == nil {
  286. return
  287. }
  288. // 设置user-agent
  289. if fp.UserAgent != "" {
  290. setHeaderRaw(req.Header, "User-Agent", fp.UserAgent)
  291. }
  292. // 设置x-stainless-*头(保持与 claude.DefaultHeaders 一致的大小写)
  293. if fp.StainlessLang != "" {
  294. setHeaderRaw(req.Header, "X-Stainless-Lang", fp.StainlessLang)
  295. }
  296. if fp.StainlessPackageVersion != "" {
  297. setHeaderRaw(req.Header, "X-Stainless-Package-Version", fp.StainlessPackageVersion)
  298. }
  299. if fp.StainlessOS != "" {
  300. setHeaderRaw(req.Header, "X-Stainless-OS", fp.StainlessOS)
  301. }
  302. if fp.StainlessArch != "" {
  303. setHeaderRaw(req.Header, "X-Stainless-Arch", fp.StainlessArch)
  304. }
  305. if fp.StainlessRuntime != "" {
  306. setHeaderRaw(req.Header, "X-Stainless-Runtime", fp.StainlessRuntime)
  307. }
  308. if fp.StainlessRuntimeVersion != "" {
  309. setHeaderRaw(req.Header, "X-Stainless-Runtime-Version", fp.StainlessRuntimeVersion)
  310. }
  311. }
  312. // RewriteUserID 重写body中的metadata.user_id
  313. // 支持旧拼接格式和新 JSON 格式的 user_id 解析,
  314. // 根据 fingerprintUA 版本选择输出格式。
  315. //
  316. // 重要:此函数使用 json.RawMessage 保留其他字段的原始字节,
  317. // 避免重新序列化导致 thinking 块等内容被修改。
  318. func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUID, cachedClientID, fingerprintUA string) ([]byte, error) {
  319. if len(body) == 0 || accountUUID == "" || cachedClientID == "" {
  320. return body, nil
  321. }
  322. metadata := gjson.GetBytes(body, "metadata")
  323. if !metadata.Exists() || metadata.Type == gjson.Null {
  324. return body, nil
  325. }
  326. if !strings.HasPrefix(strings.TrimSpace(metadata.Raw), "{") {
  327. return body, nil
  328. }
  329. userIDResult := metadata.Get("user_id")
  330. if !userIDResult.Exists() || userIDResult.Type != gjson.String {
  331. return body, nil
  332. }
  333. userID := userIDResult.String()
  334. if userID == "" {
  335. return body, nil
  336. }
  337. // 解析 user_id(兼容旧拼接格式和新 JSON 格式)
  338. parsed := ParseMetadataUserID(userID)
  339. if parsed == nil {
  340. return body, nil
  341. }
  342. sessionTail := parsed.SessionID // 原始session UUID
  343. // 生成新的session hash: SHA256(accountID::sessionTail) -> UUID格式
  344. seed := fmt.Sprintf("%d::%s", accountID, sessionTail)
  345. newSessionHash := generateUUIDFromSeed(seed)
  346. // 根据客户端版本选择输出格式
  347. version := ExtractCLIVersion(fingerprintUA)
  348. newUserID := FormatMetadataUserID(cachedClientID, accountUUID, newSessionHash, version)
  349. if newUserID == userID {
  350. return body, nil
  351. }
  352. newBody, err := sjson.SetBytes(body, "metadata.user_id", newUserID)
  353. if err != nil {
  354. return body, nil
  355. }
  356. return newBody, nil
  357. }
  358. // RewriteUserIDWithMasking 重写body中的metadata.user_id,支持会话ID伪装
  359. // 如果账号启用了会话ID伪装(session_id_masking_enabled),
  360. // 则在完成常规重写后,将 session 部分替换为固定的伪装ID(15分钟内保持不变)
  361. //
  362. // 重要:此函数使用 json.RawMessage 保留其他字段的原始字节,
  363. // 避免重新序列化导致 thinking 块等内容被修改。
  364. func (s *IdentityService) RewriteUserIDWithMasking(ctx context.Context, body []byte, account *Account, accountUUID, cachedClientID, fingerprintUA string) ([]byte, error) {
  365. // 先执行常规的 RewriteUserID 逻辑
  366. newBody, err := s.RewriteUserID(body, account.ID, accountUUID, cachedClientID, fingerprintUA)
  367. if err != nil {
  368. return newBody, err
  369. }
  370. // 检查是否启用会话ID伪装
  371. if !account.IsSessionIDMaskingEnabled() {
  372. return newBody, nil
  373. }
  374. metadata := gjson.GetBytes(newBody, "metadata")
  375. if !metadata.Exists() || metadata.Type == gjson.Null {
  376. return newBody, nil
  377. }
  378. if !strings.HasPrefix(strings.TrimSpace(metadata.Raw), "{") {
  379. return newBody, nil
  380. }
  381. userIDResult := metadata.Get("user_id")
  382. if !userIDResult.Exists() || userIDResult.Type != gjson.String {
  383. return newBody, nil
  384. }
  385. userID := userIDResult.String()
  386. if userID == "" {
  387. return newBody, nil
  388. }
  389. // 解析已重写的 user_id
  390. uidParsed := ParseMetadataUserID(userID)
  391. if uidParsed == nil {
  392. return newBody, nil
  393. }
  394. // 获取或生成固定的伪装 session ID
  395. maskedSessionID, err := s.cache.GetMaskedSessionID(ctx, account.ID)
  396. if err != nil {
  397. logger.LegacyPrintf("service.identity", "Warning: failed to get masked session ID for account %d: %v", account.ID, err)
  398. return newBody, nil
  399. }
  400. if maskedSessionID == "" {
  401. // 首次或已过期,生成新的伪装 session ID
  402. maskedSessionID = generateRandomUUID()
  403. logger.LegacyPrintf("service.identity", "Generated new masked session ID for account %d: %s", account.ID, maskedSessionID)
  404. }
  405. // 刷新 TTL(每次请求都刷新,保持 15 分钟有效期)
  406. if err := s.cache.SetMaskedSessionID(ctx, account.ID, maskedSessionID); err != nil {
  407. logger.LegacyPrintf("service.identity", "Warning: failed to set masked session ID for account %d: %v", account.ID, err)
  408. }
  409. // 用 FormatMetadataUserID 重建(保持与 RewriteUserID 相同的格式)
  410. version := ExtractCLIVersion(fingerprintUA)
  411. newUserID := FormatMetadataUserID(uidParsed.DeviceID, uidParsed.AccountUUID, maskedSessionID, version)
  412. slog.Debug("session_id_masking_applied",
  413. "account_id", account.ID,
  414. "before", userID,
  415. "after", newUserID,
  416. )
  417. if newUserID == userID {
  418. return newBody, nil
  419. }
  420. maskedBody, setErr := sjson.SetBytes(newBody, "metadata.user_id", newUserID)
  421. if setErr != nil {
  422. return newBody, nil
  423. }
  424. return maskedBody, nil
  425. }
  426. // generateRandomUUID 生成随机 UUID v4 格式字符串
  427. func generateRandomUUID() string {
  428. b := make([]byte, 16)
  429. if _, err := rand.Read(b); err != nil {
  430. // fallback: 使用时间戳生成
  431. h := sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
  432. b = h[:16]
  433. }
  434. // 设置 UUID v4 版本和变体位
  435. b[6] = (b[6] & 0x0f) | 0x40
  436. b[8] = (b[8] & 0x3f) | 0x80
  437. return fmt.Sprintf("%x-%x-%x-%x-%x",
  438. b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
  439. }
  440. // generateClientID 生成64位十六进制客户端ID(32字节随机数)
  441. func generateClientID() string {
  442. b := make([]byte, 32)
  443. if _, err := rand.Read(b); err != nil {
  444. // 极罕见的情况,使用时间戳+固定值作为fallback
  445. logger.LegacyPrintf("service.identity", "Warning: crypto/rand.Read failed: %v, using fallback", err)
  446. // 使用SHA256(当前纳秒时间)作为fallback
  447. h := sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
  448. return hex.EncodeToString(h[:])
  449. }
  450. return hex.EncodeToString(b)
  451. }
  452. // generateUUIDFromSeed 从种子生成确定性UUID v4格式字符串
  453. func generateUUIDFromSeed(seed string) string {
  454. hash := sha256.Sum256([]byte(seed))
  455. bytes := hash[:16]
  456. // 设置UUID v4版本和变体位
  457. bytes[6] = (bytes[6] & 0x0f) | 0x40
  458. bytes[8] = (bytes[8] & 0x3f) | 0x80
  459. return fmt.Sprintf("%x-%x-%x-%x-%x",
  460. bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16])
  461. }
  462. // parseUserAgentVersion 解析user-agent版本号
  463. // 例如:claude-cli/2.1.2 -> (2, 1, 2)
  464. func parseUserAgentVersion(ua string) (major, minor, patch int, ok bool) {
  465. // 匹配 xxx/x.y.z 格式
  466. matches := userAgentVersionRegex.FindStringSubmatch(ua)
  467. if len(matches) != 4 {
  468. return 0, 0, 0, false
  469. }
  470. major, _ = strconv.Atoi(matches[1])
  471. minor, _ = strconv.Atoi(matches[2])
  472. patch, _ = strconv.Atoi(matches[3])
  473. return major, minor, patch, true
  474. }
  475. // extractProduct 提取 User-Agent 中 "/" 前的产品名
  476. // 例如:claude-cli/2.1.22 (external, cli) -> "claude-cli"
  477. func extractProduct(ua string) string {
  478. if idx := strings.Index(ua, "/"); idx > 0 {
  479. return strings.ToLower(ua[:idx])
  480. }
  481. return ""
  482. }
  483. // isNewerVersion 比较版本号,判断newUA是否比cachedUA更新
  484. // 要求产品名一致(防止浏览器 UA 如 Mozilla/5.0 误判为更新版本)
  485. func isNewerVersion(newUA, cachedUA string) bool {
  486. // 校验产品名一致性
  487. newProduct := extractProduct(newUA)
  488. cachedProduct := extractProduct(cachedUA)
  489. if newProduct == "" || cachedProduct == "" || newProduct != cachedProduct {
  490. return false
  491. }
  492. newMajor, newMinor, newPatch, newOk := parseUserAgentVersion(newUA)
  493. cachedMajor, cachedMinor, cachedPatch, cachedOk := parseUserAgentVersion(cachedUA)
  494. if !newOk || !cachedOk {
  495. return false
  496. }
  497. // 比较版本号
  498. if newMajor > cachedMajor {
  499. return true
  500. }
  501. if newMajor < cachedMajor {
  502. return false
  503. }
  504. if newMinor > cachedMinor {
  505. return true
  506. }
  507. if newMinor < cachedMinor {
  508. return false
  509. }
  510. return newPatch > cachedPatch
  511. }