account_header_override.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. package service
  2. import (
  3. "net/http"
  4. "strings"
  5. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  6. "golang.org/x/net/http/httpguts"
  7. )
  8. // 请求头覆写(header override):对 Anthropic / OpenAI / Kimi / Zhipu / DeepSeek
  9. // 平台的 api_key 账号,以及 Grok 平台的 api_key / oauth 账号生效。
  10. // 管理员在账号上配置一组 header name -> value,转发到上游前用配置值覆盖同名请求头
  11. // (匹配不区分大小写);value 为空的条目视为"未填写",不参与覆盖。
  12. const (
  13. credKeyHeaderOverrideEnabled = "header_override_enabled"
  14. credKeyHeaderOverrides = "header_overrides"
  15. maxHeaderOverrideEntries = 64
  16. maxHeaderOverrideNameLength = 200
  17. maxHeaderOverrideValueLength = 8192
  18. )
  19. // headerOverrideBlockedNames 禁止覆写的请求头(小写)。
  20. // - 连接控制/逐跳头:由 HTTP 栈管理,覆写会破坏请求传输;
  21. // - host/content-length:由 Go 的 Request.Host / ContentLength 字段管理,header 覆写不生效或产生冲突;
  22. // - content-type:承载报文框架信息(multipart boundary 为每请求随机值),静态覆写必然与 body 不匹配;
  23. // - authorization/x-api-key/cookie 等:上游认证头由账号凭据统一注入,禁止通过覆写篡改或重新引入;
  24. // - accept-encoding:强制压缩会破坏网关对上游流式响应(SSE/usage)的解析;
  25. // - sec-websocket-*:WebSocket 握手头由拨号器管理(OpenAI WS 模式);
  26. // - session_id/x-claude-code-session-id/x-grok-conv-id 等:逐请求会话隔离头,
  27. // 固定值会造成会话串扰。
  28. var headerOverrideBlockedNames = map[string]struct{}{
  29. "host": {},
  30. "content-length": {},
  31. "content-type": {},
  32. "transfer-encoding": {},
  33. "connection": {},
  34. "keep-alive": {},
  35. "proxy-authenticate": {},
  36. "proxy-authorization": {},
  37. "proxy-connection": {},
  38. "te": {},
  39. "trailer": {},
  40. "upgrade": {},
  41. "authorization": {},
  42. "x-api-key": {},
  43. "x-goog-api-key": {},
  44. "cookie": {},
  45. "accept-encoding": {},
  46. "sec-websocket-key": {},
  47. "sec-websocket-version": {},
  48. "sec-websocket-extensions": {},
  49. "sec-websocket-protocol": {},
  50. "sec-websocket-accept": {},
  51. "session_id": {},
  52. "conversation_id": {},
  53. "x-codex-turn-state": {},
  54. "x-codex-turn-metadata": {},
  55. "chatgpt-account-id": {},
  56. "x-claude-code-session-id": {},
  57. "x-client-request-id": {},
  58. "x-grok-conv-id": {},
  59. }
  60. func isHeaderOverrideBlockedName(lowerName string) bool {
  61. _, blocked := headerOverrideBlockedNames[lowerName]
  62. return blocked
  63. }
  64. // IsHeaderOverrideEligible 报告账号类型是否支持请求头覆写。
  65. // Anthropic / OpenAI / Kimi / Zhipu / DeepSeek 仅开放 api_key 账号;
  66. // Grok 额外开放 oauth 账号——
  67. // 订阅流量改发自定义转发地址时,通常需要补充中间层要求的准入头。
  68. func (a *Account) IsHeaderOverrideEligible() bool {
  69. if a == nil {
  70. return false
  71. }
  72. switch a.Platform {
  73. case PlatformAnthropic, PlatformOpenAI, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
  74. return a.Type == AccountTypeAPIKey
  75. case PlatformGrok:
  76. return a.Type == AccountTypeAPIKey || a.Type == AccountTypeOAuth
  77. default:
  78. return false
  79. }
  80. }
  81. // IsHeaderOverrideEnabled 报告账号是否启用了请求头覆写。
  82. func (a *Account) IsHeaderOverrideEnabled() bool {
  83. if !a.IsHeaderOverrideEligible() || a.Credentials == nil {
  84. return false
  85. }
  86. enabled, ok := a.Credentials[credKeyHeaderOverrideEnabled].(bool)
  87. return ok && enabled
  88. }
  89. // GetHeaderOverrides 返回生效的请求头覆写表(key 统一小写)。
  90. // 未启用、不符合平台/类型条件或配置为空时返回 nil。
  91. // 空 value 的条目(模板占位)与非法/禁止的 header 名会被跳过。
  92. // 结果带热路径缓存(同 GetModelMapping 先例):同一 credentials 映射在
  93. // 一次请求 / 一条 WS 会话内的多次调用只做一次解析与校验。
  94. func (a *Account) GetHeaderOverrides() map[string]string {
  95. if !a.IsHeaderOverrideEnabled() {
  96. return nil
  97. }
  98. rawMapping, rawIsAnyMap := a.Credentials[credKeyHeaderOverrides].(map[string]any)
  99. if !rawIsAnyMap {
  100. // 非 JSON 反序列化产物(如直接注入的 map[string]string):直接解析,不缓存
  101. return resolveHeaderOverrides(stringMappingFromRaw(a.Credentials[credKeyHeaderOverrides]))
  102. }
  103. credentialsPtr := mapPtr(a.Credentials)
  104. rawPtr := mapPtr(rawMapping)
  105. rawLen := len(rawMapping)
  106. rawSig := uint64(0)
  107. rawSigReady := false
  108. if a.headerOverrideCacheReady &&
  109. a.headerOverrideCacheCredentialsPtr == credentialsPtr &&
  110. a.headerOverrideCacheRawPtr == rawPtr &&
  111. a.headerOverrideCacheRawLen == rawLen {
  112. rawSig = modelMappingSignature(rawMapping)
  113. rawSigReady = true
  114. if a.headerOverrideCacheRawSig == rawSig {
  115. return a.headerOverrideCache
  116. }
  117. }
  118. overrides := resolveHeaderOverrides(stringMappingFromRaw(rawMapping))
  119. if !rawSigReady {
  120. rawSig = modelMappingSignature(rawMapping)
  121. }
  122. a.headerOverrideCache = overrides
  123. a.headerOverrideCacheReady = true
  124. a.headerOverrideCacheCredentialsPtr = credentialsPtr
  125. a.headerOverrideCacheRawPtr = rawPtr
  126. a.headerOverrideCacheRawLen = rawLen
  127. a.headerOverrideCacheRawSig = rawSig
  128. return overrides
  129. }
  130. // resolveHeaderOverrides 解析并防御性过滤原始覆写表:保存路径已做校验,
  131. // 这里兜底未经 Normalize 落库的数据(含名单扩充前保存的旧配置),非法条目直接跳过。
  132. func resolveHeaderOverrides(raw map[string]string) map[string]string {
  133. if len(raw) == 0 {
  134. return nil
  135. }
  136. result := make(map[string]string, len(raw))
  137. for name, value := range raw {
  138. lowerName, value, err := normalizeHeaderOverrideEntry(name, value)
  139. if err != nil || lowerName == "" || value == "" {
  140. continue
  141. }
  142. result[lowerName] = value
  143. }
  144. if len(result) == 0 {
  145. return nil
  146. }
  147. return result
  148. }
  149. // HeaderOverrideValue 返回指定 header(小写名)的生效覆写值。
  150. // 供转发链路在 header 写入前感知覆写结果(如 anthropic-beta 需要参与 body 净化)。
  151. func (a *Account) HeaderOverrideValue(lowerName string) (string, bool) {
  152. value, ok := a.GetHeaderOverrides()[lowerName]
  153. return value, ok
  154. }
  155. // ApplyHeaderOverrides 将账号配置的请求头覆写应用到出站请求头。
  156. // 对每个覆写条目:先删除所有大小写变体(转发链路会以 wire casing 直接写入 map,
  157. // 可能存在非 canonical key),再按已知 wire casing 写入,避免产生重复头。
  158. // 账号未启用或不符合条件时为 no-op,可安全地在 OAuth/api_key 共用的构建器中调用。
  159. func (a *Account) ApplyHeaderOverrides(h http.Header) {
  160. if h == nil {
  161. return
  162. }
  163. overrides := a.GetHeaderOverrides()
  164. if len(overrides) == 0 {
  165. return
  166. }
  167. // 覆写名两两不同(大小写不敏感)且各自只操作同名键,应用顺序不影响结果。
  168. // 全量 EqualFold 扫描兜底删除任意 casing 的既有键:透传链路可能保留客户端
  169. // 原始 casing,非 canonical/wire casing 的键 deleteHeaderAllForms 覆盖不到。
  170. for name, value := range overrides {
  171. for existing := range h {
  172. if strings.EqualFold(existing, name) {
  173. delete(h, existing)
  174. }
  175. }
  176. h[resolveWireCasing(name)] = []string{value}
  177. }
  178. }
  179. // NormalizeHeaderOverrideCredentials 校验并原地规范化 credentials 中的请求头覆写字段。
  180. // 供账号创建/更新/批量更新的保存路径调用;credentials 未携带相关字段时为 no-op。
  181. // 规范化内容:header 名转小写并去除首尾空白,value 去除首尾空白,丢弃名和值均为空的条目。
  182. func NormalizeHeaderOverrideCredentials(credentials map[string]any) error {
  183. if credentials == nil {
  184. return nil
  185. }
  186. if raw, ok := credentials[credKeyHeaderOverrideEnabled]; ok && raw != nil {
  187. if _, isBool := raw.(bool); !isBool {
  188. return infraerrors.New(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  189. "header_override_enabled must be a boolean")
  190. }
  191. }
  192. raw, ok := credentials[credKeyHeaderOverrides]
  193. if !ok || raw == nil {
  194. return nil
  195. }
  196. var entries map[string]any
  197. switch m := raw.(type) {
  198. case map[string]any:
  199. entries = m
  200. case map[string]string:
  201. entries = make(map[string]any, len(m))
  202. for k, v := range m {
  203. entries[k] = v
  204. }
  205. default:
  206. return infraerrors.New(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  207. "header_overrides must be an object of header name to string value")
  208. }
  209. if len(entries) > maxHeaderOverrideEntries {
  210. return infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  211. "header_overrides supports at most %d entries", maxHeaderOverrideEntries)
  212. }
  213. normalized := make(map[string]any, len(entries))
  214. for name, rawValue := range entries {
  215. value, isString := rawValue.(string)
  216. if !isString {
  217. return infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  218. "header %q value must be a string", name)
  219. }
  220. lowerName, value, err := normalizeHeaderOverrideEntry(name, value)
  221. if err != nil {
  222. return err
  223. }
  224. if lowerName == "" {
  225. continue // 丢弃完全为空的占位行
  226. }
  227. if _, dup := normalized[lowerName]; dup {
  228. return infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  229. "duplicate header name %q (matching is case-insensitive)", lowerName)
  230. }
  231. normalized[lowerName] = value
  232. }
  233. credentials[credKeyHeaderOverrides] = normalized
  234. return nil
  235. }
  236. // normalizeHeaderOverrideEntry 校验并规范化单个覆写条目,保存路径(Normalize,err → 400)
  237. // 与应用路径(resolveHeaderOverrides,err → 跳过)共用同一套规则,避免两处校验漂移。
  238. // 名和值均为空表示空占位行,返回 ("", "", nil);空 value 的具名条目合法(模板占位)。
  239. func normalizeHeaderOverrideEntry(name, value string) (string, string, error) {
  240. lowerName := strings.ToLower(strings.TrimSpace(name))
  241. value = strings.TrimSpace(value)
  242. if lowerName == "" {
  243. if value == "" {
  244. return "", "", nil
  245. }
  246. return "", "", infraerrors.New(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  247. "header name must not be empty")
  248. }
  249. if len(lowerName) > maxHeaderOverrideNameLength {
  250. return "", "", infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  251. "header name %q exceeds %d characters", lowerName, maxHeaderOverrideNameLength)
  252. }
  253. if !httpguts.ValidHeaderFieldName(lowerName) {
  254. return "", "", infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  255. "invalid header name %q", lowerName)
  256. }
  257. if isHeaderOverrideBlockedName(lowerName) {
  258. return "", "", infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  259. "header %q is not allowed to be overridden", lowerName)
  260. }
  261. if len(value) > maxHeaderOverrideValueLength {
  262. return "", "", infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  263. "header %q value exceeds %d characters", lowerName, maxHeaderOverrideValueLength)
  264. }
  265. if !httpguts.ValidHeaderFieldValue(value) {
  266. return "", "", infraerrors.Newf(http.StatusBadRequest, "INVALID_HEADER_OVERRIDE",
  267. "header %q has an invalid value", lowerName)
  268. }
  269. return lowerName, value, nil
  270. }