158 lines
5.3 KiB
Go
158 lines
5.3 KiB
Go
package service
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
)
|
||
|
||
// headerWireCasing 定义每个白名单 header 在真实 Claude CLI 抓包中的准确大小写。
|
||
// Go 的 HTTP server 解析请求时会将所有 header key 转为 Canonical 形式(如 x-app → X-App),
|
||
// 此 map 用于在转发时恢复到真实的 wire format。
|
||
//
|
||
// 来源:对真实 Claude CLI (claude-cli/2.1.81) 到 api.anthropic.com 的 HTTPS 流量抓包。
|
||
var headerWireCasing = map[string]string{
|
||
// Title case
|
||
"accept": "Accept",
|
||
"user-agent": "User-Agent",
|
||
|
||
// X-Stainless-* 保持 SDK 原始大小写
|
||
"x-stainless-retry-count": "X-Stainless-Retry-Count",
|
||
"x-stainless-timeout": "X-Stainless-Timeout",
|
||
"x-stainless-lang": "X-Stainless-Lang",
|
||
"x-stainless-package-version": "X-Stainless-Package-Version",
|
||
"x-stainless-os": "X-Stainless-OS",
|
||
"x-stainless-arch": "X-Stainless-Arch",
|
||
"x-stainless-runtime": "X-Stainless-Runtime",
|
||
"x-stainless-runtime-version": "X-Stainless-Runtime-Version",
|
||
"x-stainless-helper-method": "x-stainless-helper-method",
|
||
|
||
// Anthropic SDK 自身设置的 header,全小写
|
||
"anthropic-dangerous-direct-browser-access": "anthropic-dangerous-direct-browser-access",
|
||
"anthropic-version": "anthropic-version",
|
||
"anthropic-beta": "anthropic-beta",
|
||
"x-app": "x-app",
|
||
"content-type": "content-type",
|
||
"accept-language": "accept-language",
|
||
"sec-fetch-mode": "sec-fetch-mode",
|
||
"accept-encoding": "accept-encoding",
|
||
"authorization": "authorization",
|
||
}
|
||
|
||
// headerWireOrder 定义真实 Claude CLI 发送 header 的顺序(基于抓包)。
|
||
// 用于 debug log 按此顺序输出,便于与抓包结果直接对比。
|
||
var headerWireOrder = []string{
|
||
"Accept",
|
||
"X-Stainless-Retry-Count",
|
||
"X-Stainless-Timeout",
|
||
"X-Stainless-Lang",
|
||
"X-Stainless-Package-Version",
|
||
"X-Stainless-OS",
|
||
"X-Stainless-Arch",
|
||
"X-Stainless-Runtime",
|
||
"X-Stainless-Runtime-Version",
|
||
"anthropic-dangerous-direct-browser-access",
|
||
"anthropic-version",
|
||
"authorization",
|
||
"x-app",
|
||
"User-Agent",
|
||
"content-type",
|
||
"anthropic-beta",
|
||
"accept-language",
|
||
"sec-fetch-mode",
|
||
"accept-encoding",
|
||
"x-stainless-helper-method",
|
||
}
|
||
|
||
// headerWireOrderSet 用于快速判断某个 key 是否在 headerWireOrder 中(按 lowercase 匹配)。
|
||
var headerWireOrderSet map[string]struct{}
|
||
|
||
func init() {
|
||
headerWireOrderSet = make(map[string]struct{}, len(headerWireOrder))
|
||
for _, k := range headerWireOrder {
|
||
headerWireOrderSet[strings.ToLower(k)] = struct{}{}
|
||
}
|
||
}
|
||
|
||
// resolveWireCasing 将 Go canonical key(如 X-Stainless-Os)映射为真实 wire casing(如 X-Stainless-OS)。
|
||
// 如果 map 中没有对应条目,返回原始 key 不变。
|
||
func resolveWireCasing(key string) string {
|
||
if wk, ok := headerWireCasing[strings.ToLower(key)]; ok {
|
||
return wk
|
||
}
|
||
return key
|
||
}
|
||
|
||
// setHeaderRaw sets a header bypassing Go's canonical-case normalization.
|
||
// The key is stored exactly as provided, preserving original casing.
|
||
//
|
||
// It first removes any existing value under the canonical key, the wire casing key,
|
||
// and the exact raw key, preventing duplicates from any source.
|
||
func setHeaderRaw(h http.Header, key, value string) {
|
||
h.Del(key) // remove canonical form (e.g. "Anthropic-Beta")
|
||
if wk := resolveWireCasing(key); wk != key {
|
||
delete(h, wk) // remove wire casing form if different
|
||
}
|
||
delete(h, key) // remove exact raw key if it differs from canonical
|
||
h[key] = []string{value}
|
||
}
|
||
|
||
// addHeaderRaw appends a header value bypassing Go's canonical-case normalization.
|
||
func addHeaderRaw(h http.Header, key, value string) {
|
||
h[key] = append(h[key], value)
|
||
}
|
||
|
||
// getHeaderRaw reads a header value, trying multiple key forms to handle the mismatch
|
||
// between Go canonical keys, wire casing keys, and raw keys:
|
||
// 1. exact key as provided
|
||
// 2. wire casing form (from headerWireCasing)
|
||
// 3. Go canonical form (via http.Header.Get)
|
||
func getHeaderRaw(h http.Header, key string) string {
|
||
// 1. exact key
|
||
if vals := h[key]; len(vals) > 0 {
|
||
return vals[0]
|
||
}
|
||
// 2. wire casing (e.g. looking up "Anthropic-Dangerous-Direct-Browser-Access" finds "anthropic-dangerous-direct-browser-access")
|
||
if wk := resolveWireCasing(key); wk != key {
|
||
if vals := h[wk]; len(vals) > 0 {
|
||
return vals[0]
|
||
}
|
||
}
|
||
// 3. canonical fallback
|
||
return h.Get(key)
|
||
}
|
||
|
||
// sortHeadersByWireOrder 按照真实 Claude CLI 的 header 顺序返回排序后的 key 列表。
|
||
// 在 headerWireOrder 中定义的 key 按其顺序排列,未定义的 key 追加到末尾。
|
||
func sortHeadersByWireOrder(h http.Header) []string {
|
||
// 构建 lowercase -> actual map key 的映射
|
||
present := make(map[string]string, len(h))
|
||
for k := range h {
|
||
present[strings.ToLower(k)] = k
|
||
}
|
||
|
||
result := make([]string, 0, len(h))
|
||
seen := make(map[string]struct{}, len(h))
|
||
|
||
// 先按 wire order 输出
|
||
for _, wk := range headerWireOrder {
|
||
lk := strings.ToLower(wk)
|
||
if actual, ok := present[lk]; ok {
|
||
if _, dup := seen[lk]; !dup {
|
||
result = append(result, actual)
|
||
seen[lk] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 再追加不在 wire order 中的 header
|
||
for k := range h {
|
||
lk := strings.ToLower(k)
|
||
if _, ok := seen[lk]; !ok {
|
||
result = append(result, k)
|
||
seen[lk] = struct{}{}
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|