mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-04 17:43:32 +08:00
refactor(runtime): enhance NewUtlsHTTPClient with context-based RoundTripper
- Updated `NewUtlsHTTPClient` to support context-aware RoundTrippers for protected hosts (e.g., Cloudflare bypass). - Replaced `anthropicHosts` with `utlsProtectedHosts` to generalize host handling logic. - Added unit test to validate context-based RoundTripper behavior. - Replaced `NewProxyAwareHTTPClient` with `NewUtlsHTTPClient` in relevant executors for improved TLS fingerprinting. Closes: #3680
This commit is contained in:
@@ -156,7 +156,7 @@ func (e *ClaudeExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
|
||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
return httpClient.Do(httpReq)
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
||||
AuthValue: authValue,
|
||||
})
|
||||
|
||||
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient = reporter.TrackHTTPClient(httpClient)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -437,7 +437,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
AuthValue: authValue,
|
||||
})
|
||||
|
||||
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient = reporter.TrackHTTPClient(httpClient)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -674,7 +674,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
||||
AuthValue: authValue,
|
||||
})
|
||||
|
||||
httpClient := helps.NewUtlsHTTPClient(e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
resp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||
|
||||
@@ -744,7 +744,7 @@ func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
|
||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
return httpClient.Do(httpReq)
|
||||
}
|
||||
|
||||
@@ -821,7 +821,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
||||
AuthType: authType,
|
||||
AuthValue: authValue,
|
||||
})
|
||||
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient = reporter.TrackHTTPClient(httpClient)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -987,7 +987,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
||||
AuthType: authType,
|
||||
AuthValue: authValue,
|
||||
})
|
||||
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient = reporter.TrackHTTPClient(httpClient)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
@@ -1097,7 +1097,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
||||
AuthValue: authValue,
|
||||
})
|
||||
|
||||
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient = reporter.TrackHTTPClient(httpClient)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -128,21 +129,23 @@ func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// anthropicHosts contains the hosts that should use utls Chrome TLS fingerprint.
|
||||
var anthropicHosts = map[string]struct{}{
|
||||
// utlsProtectedHosts contains the hosts that should use utls Chrome TLS fingerprint
|
||||
// to bypass Cloudflare's TLS fingerprinting.
|
||||
var utlsProtectedHosts = map[string]struct{}{
|
||||
"api.anthropic.com": {},
|
||||
"chatgpt.com": {},
|
||||
}
|
||||
|
||||
// fallbackRoundTripper uses utls for Anthropic HTTPS hosts and falls back to
|
||||
// standard transport for all other requests (non-HTTPS or non-Anthropic hosts).
|
||||
// fallbackRoundTripper uses utls for protected HTTPS hosts and falls back to
|
||||
// standard transport for all other requests.
|
||||
type fallbackRoundTripper struct {
|
||||
utls *utlsRoundTripper
|
||||
utls http.RoundTripper
|
||||
fallback http.RoundTripper
|
||||
}
|
||||
|
||||
func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Scheme == "https" {
|
||||
if _, ok := anthropicHosts[strings.ToLower(req.URL.Hostname())]; ok {
|
||||
if _, ok := utlsProtectedHosts[strings.ToLower(req.URL.Hostname())]; ok {
|
||||
return f.utls.RoundTrip(req)
|
||||
}
|
||||
}
|
||||
@@ -150,9 +153,9 @@ func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
|
||||
}
|
||||
|
||||
// NewUtlsHTTPClient creates an HTTP client using utls Chrome TLS fingerprint.
|
||||
// Use this for Claude API requests to match real Claude Code's TLS behavior.
|
||||
// Use this for provider requests that need a Chrome-like TLS fingerprint.
|
||||
// Falls back to standard transport for non-HTTPS requests.
|
||||
func NewUtlsHTTPClient(cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
||||
func NewUtlsHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
||||
var proxyURL string
|
||||
if auth != nil {
|
||||
proxyURL = strings.TrimSpace(auth.ProxyURL)
|
||||
@@ -161,18 +164,20 @@ func NewUtlsHTTPClient(cfg *config.Config, auth *cliproxyauth.Auth, timeout time
|
||||
proxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||
}
|
||||
|
||||
utlsRT := newUtlsRoundTripper(proxyURL)
|
||||
|
||||
var standardTransport http.RoundTripper = &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
var ctxRoundTripper http.RoundTripper
|
||||
if ctx != nil {
|
||||
ctxRoundTripper, _ = ctx.Value("cliproxy.roundtripper").(http.RoundTripper)
|
||||
}
|
||||
|
||||
var utlsRT http.RoundTripper = newUtlsRoundTripper(proxyURL)
|
||||
var standardTransport http.RoundTripper = http.DefaultTransport
|
||||
if proxyURL != "" {
|
||||
if transport := buildProxyTransport(proxyURL); transport != nil {
|
||||
standardTransport = transport
|
||||
}
|
||||
} else if ctxRoundTripper != nil {
|
||||
utlsRT = ctxRoundTripper
|
||||
standardTransport = ctxRoundTripper
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
|
||||
45
internal/runtime/executor/helps/utls_client_test.go
Normal file
45
internal/runtime/executor/helps/utls_client_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package helps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type utlsClientRoundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f utlsClientRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func TestNewUtlsHTTPClientUsesContextRoundTripperForProtectedHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
called := false
|
||||
ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", utlsClientRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
called = true
|
||||
if req.URL.Hostname() != "chatgpt.com" {
|
||||
t.Fatalf("hostname = %q, want chatgpt.com", req.URL.Hostname())
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("{}")),
|
||||
Request: req,
|
||||
}, nil
|
||||
}))
|
||||
|
||||
client := NewUtlsHTTPClient(ctx, nil, nil, 0)
|
||||
resp, err := client.Get("https://chatgpt.com/backend-api/codex/responses")
|
||||
if err != nil {
|
||||
t.Fatalf("client.Get returned error: %v", err)
|
||||
}
|
||||
if errClose := resp.Body.Close(); errClose != nil {
|
||||
t.Fatalf("response body close returned error: %v", errClose)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("expected context RoundTripper to handle protected host request")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user