c4c1d4daa4
Sniffs a real Claude Code request on startup to capture exact HTTP headers, then replays them for all proxied requests. Injects the billing header with per-request SHA256 fingerprint into the system prompt. Uses utls with Chrome TLS fingerprint to pass Cloudflare's bot detection on api.anthropic.com. Supports both streaming (SSE) and non-streaming modes, round-robin credential selection with automatic failover, and loading OAuth tokens from both cli-proxy-api auth files and native ~/.claude/.credentials.json.
91 lines
2.4 KiB
Go
91 lines
2.4 KiB
Go
package proxy
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"unicode/utf16"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
const fingerprintSalt = "59cf53e54c78"
|
|
|
|
func computeFingerprint(firstUserMessage string, version string) string {
|
|
indices := []int{4, 7, 20}
|
|
runes := utf16.Encode([]rune(firstUserMessage))
|
|
var chars string
|
|
for _, i := range indices {
|
|
if i < len(runes) {
|
|
chars += string(rune(runes[i]))
|
|
} else {
|
|
chars += "0"
|
|
}
|
|
}
|
|
input := fingerprintSalt + chars + version
|
|
hash := sha256.Sum256([]byte(input))
|
|
return hex.EncodeToString(hash[:])[:3]
|
|
}
|
|
|
|
func extractFirstUserMessage(body []byte) string {
|
|
messages := gjson.GetBytes(body, "messages")
|
|
if !messages.Exists() || !messages.IsArray() {
|
|
return ""
|
|
}
|
|
for _, msg := range messages.Array() {
|
|
if msg.Get("role").String() != "user" {
|
|
continue
|
|
}
|
|
content := msg.Get("content")
|
|
if content.Type == gjson.String {
|
|
return content.String()
|
|
}
|
|
if content.IsArray() {
|
|
for _, block := range content.Array() {
|
|
if block.Get("type").String() == "text" {
|
|
return block.Get("text").String()
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func buildBillingHeader(body []byte, version string) string {
|
|
userMsg := extractFirstUserMessage(body)
|
|
fp := computeFingerprint(userMsg, version)
|
|
return fmt.Sprintf("x-anthropic-billing-header: cc_version=%s.%s; cc_entrypoint=cli; cch=00000;", version, fp)
|
|
}
|
|
|
|
func injectBillingHeader(body []byte, version string) []byte {
|
|
header := buildBillingHeader(body, version)
|
|
billingBlock := map[string]interface{}{"type": "text", "text": header}
|
|
billingJSON, _ := json.Marshal(billingBlock)
|
|
|
|
existing := gjson.GetBytes(body, "system")
|
|
if !existing.Exists() {
|
|
body, _ = sjson.SetRawBytes(body, "system", []byte("["+string(billingJSON)+"]"))
|
|
return body
|
|
}
|
|
|
|
if existing.IsArray() {
|
|
items := make([]json.RawMessage, 0, len(existing.Array())+1)
|
|
items = append(items, billingJSON)
|
|
for _, item := range existing.Array() {
|
|
items = append(items, json.RawMessage(item.Raw))
|
|
}
|
|
systemJSON, _ := json.Marshal(items)
|
|
body, _ = sjson.SetRawBytes(body, "system", systemJSON)
|
|
return body
|
|
}
|
|
|
|
origText := existing.String()
|
|
origBlock := map[string]string{"type": "text", "text": origText}
|
|
origJSON, _ := json.Marshal(origBlock)
|
|
body, _ = sjson.SetRawBytes(body, "system", []byte("["+string(billingJSON)+","+string(origJSON)+"]"))
|
|
return body
|
|
}
|