mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-21 16:40:22 +00:00
Captured and compared outgoing requests from CLIProxyAPI against real Claude Code 2.1.63 and fixed all detectable differences: Headers: - Update anthropic-beta to match 2.1.63: replace fine-grained-tool-streaming and prompt-caching-2024-07-31 with context-management-2025-06-27 and prompt-caching-scope-2026-01-05 - Remove X-Stainless-Helper-Method header (real Claude Code does not send it) - Update default User-Agent from "claude-cli/2.1.44 (external, sdk-cli)" to "claude-cli/2.1.63 (external, cli)" - Force Claude Code User-Agent for non-Claude clients to avoid leaking real client identity (e.g. curl, OpenAI SDKs) during cloaking Body: - Inject x-anthropic-billing-header as system[0] (matches real format) - Change system prompt identifier from "You are Claude Code..." to "You are a Claude agent, built on Anthropic's Claude Agent SDK." - Add cache_control with ttl:"1h" to match real request format - Fix user_id format: user_[64hex]_account_[uuid]_session_[uuid] (was missing account UUID) - Disable tool name prefix (set claudeToolPrefix to empty string) TLS: - Switch utls fingerprint from HelloFirefox_Auto to HelloChrome_Auto (closer to Node.js/OpenSSL used by real Claude Code) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
168 lines
4.7 KiB
Go
168 lines
4.7 KiB
Go
// Package claude provides authentication functionality for Anthropic's Claude API.
|
|
// This file implements a custom HTTP transport using utls to bypass TLS fingerprinting.
|
|
package claude
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
|
|
tls "github.com/refraction-networking/utls"
|
|
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/net/proxy"
|
|
)
|
|
|
|
// utlsRoundTripper implements http.RoundTripper using utls with Chrome fingerprint
|
|
// to bypass Cloudflare's TLS fingerprinting on Anthropic domains.
|
|
type utlsRoundTripper struct {
|
|
// mu protects the connections map and pending map
|
|
mu sync.Mutex
|
|
// connections caches HTTP/2 client connections per host
|
|
connections map[string]*http2.ClientConn
|
|
// pending tracks hosts that are currently being connected to (prevents race condition)
|
|
pending map[string]*sync.Cond
|
|
// dialer is used to create network connections, supporting proxies
|
|
dialer proxy.Dialer
|
|
}
|
|
|
|
// newUtlsRoundTripper creates a new utls-based round tripper with optional proxy support
|
|
func newUtlsRoundTripper(cfg *config.SDKConfig) *utlsRoundTripper {
|
|
var dialer proxy.Dialer = proxy.Direct
|
|
if cfg != nil && cfg.ProxyURL != "" {
|
|
proxyURL, err := url.Parse(cfg.ProxyURL)
|
|
if err != nil {
|
|
log.Errorf("failed to parse proxy URL %q: %v", cfg.ProxyURL, err)
|
|
} else {
|
|
pDialer, err := proxy.FromURL(proxyURL, proxy.Direct)
|
|
if err != nil {
|
|
log.Errorf("failed to create proxy dialer for %q: %v", cfg.ProxyURL, err)
|
|
} else {
|
|
dialer = pDialer
|
|
}
|
|
}
|
|
}
|
|
|
|
return &utlsRoundTripper{
|
|
connections: make(map[string]*http2.ClientConn),
|
|
pending: make(map[string]*sync.Cond),
|
|
dialer: dialer,
|
|
}
|
|
}
|
|
|
|
// getOrCreateConnection gets an existing connection or creates a new one.
|
|
// It uses a per-host locking mechanism to prevent multiple goroutines from
|
|
// creating connections to the same host simultaneously.
|
|
func (t *utlsRoundTripper) getOrCreateConnection(host, addr string) (*http2.ClientConn, error) {
|
|
t.mu.Lock()
|
|
|
|
// Check if connection exists and is usable
|
|
if h2Conn, ok := t.connections[host]; ok && h2Conn.CanTakeNewRequest() {
|
|
t.mu.Unlock()
|
|
return h2Conn, nil
|
|
}
|
|
|
|
// Check if another goroutine is already creating a connection
|
|
if cond, ok := t.pending[host]; ok {
|
|
// Wait for the other goroutine to finish
|
|
cond.Wait()
|
|
// Check if connection is now available
|
|
if h2Conn, ok := t.connections[host]; ok && h2Conn.CanTakeNewRequest() {
|
|
t.mu.Unlock()
|
|
return h2Conn, nil
|
|
}
|
|
// Connection still not available, we'll create one
|
|
}
|
|
|
|
// Mark this host as pending
|
|
cond := sync.NewCond(&t.mu)
|
|
t.pending[host] = cond
|
|
t.mu.Unlock()
|
|
|
|
// Create connection outside the lock
|
|
h2Conn, err := t.createConnection(host, addr)
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
// Remove pending marker and wake up waiting goroutines
|
|
delete(t.pending, host)
|
|
cond.Broadcast()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store the new connection
|
|
t.connections[host] = h2Conn
|
|
return h2Conn, nil
|
|
}
|
|
|
|
// createConnection creates a new HTTP/2 connection with Chrome TLS fingerprint.
|
|
// Chrome's TLS fingerprint is closer to Node.js/OpenSSL (which real Claude Code uses)
|
|
// than Firefox, reducing the mismatch between TLS layer and HTTP headers.
|
|
func (t *utlsRoundTripper) createConnection(host, addr string) (*http2.ClientConn, error) {
|
|
conn, err := t.dialer.Dial("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConfig := &tls.Config{ServerName: host}
|
|
tlsConn := tls.UClient(conn, tlsConfig, tls.HelloChrome_Auto)
|
|
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
tr := &http2.Transport{}
|
|
h2Conn, err := tr.NewClientConn(tlsConn)
|
|
if err != nil {
|
|
tlsConn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return h2Conn, nil
|
|
}
|
|
|
|
// RoundTrip implements http.RoundTripper
|
|
func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
host := req.URL.Host
|
|
addr := host
|
|
if !strings.Contains(addr, ":") {
|
|
addr += ":443"
|
|
}
|
|
|
|
// Get hostname without port for TLS ServerName
|
|
hostname := req.URL.Hostname()
|
|
|
|
h2Conn, err := t.getOrCreateConnection(hostname, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := h2Conn.RoundTrip(req)
|
|
if err != nil {
|
|
// Connection failed, remove it from cache
|
|
t.mu.Lock()
|
|
if cached, ok := t.connections[hostname]; ok && cached == h2Conn {
|
|
delete(t.connections, hostname)
|
|
}
|
|
t.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// NewAnthropicHttpClient creates an HTTP client that bypasses TLS fingerprinting
|
|
// for Anthropic domains by using utls with Chrome fingerprint.
|
|
// It accepts optional SDK configuration for proxy settings.
|
|
func NewAnthropicHttpClient(cfg *config.SDKConfig) *http.Client {
|
|
return &http.Client{
|
|
Transport: newUtlsRoundTripper(cfg),
|
|
}
|
|
}
|