Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 68 additions & 8 deletions internal/api/handlers/management/api_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,27 @@ import (
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
)

const defaultAPICallTimeout = 60 * time.Second

// chromeAPICallUserAgent matches the ChatGPT web client used by the proven
// subscriptions fetch path. Applied only when the caller omitted User-Agent
// for chatgpt.com so Go's default "Go-http-client" UA is not sent.
const chromeAPICallUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

// newAPICallFingerprintTransport builds the shared Anthropic/ChatGPT
// fingerprint RoundTripper. Host routing and ClientHello live in helps so they
// evolve with Codex/Claude instead of a private management copy. Tests replace
// this to avoid live TLS handshakes.
var newAPICallFingerprintTransport = func(proxyURL string, fallback http.RoundTripper) http.RoundTripper {
return helps.NewFingerprintRoundTripper(proxyURL, fallback)
}

const (
antigravityOAuthClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
antigravityOAuthClientSecret = "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"
Expand Down Expand Up @@ -82,6 +96,12 @@ type apiCallResponse struct {
// 3. Global config proxy-url
// 4. Direct connect (environment proxies are not used)
//
// ChatGPT (https://chatgpt.com) and Anthropic HTTPS origins reuse the shared
// helps fingerprint RoundTripper (same routing as Codex/Claude). Callers may
// send ChatGPT web headers (User-Agent, oai-language, x-openai-target-path /
// x-openai-target-route); missing ones are filled only for ChatGPT. Other hosts
// keep the standard transport.
//
// Response JSON (returned with HTTP 200 when the APICall itself succeeds):
// - status_code: Upstream HTTP status code.
// - header: Upstream response headers.
Expand Down Expand Up @@ -207,11 +227,12 @@ func (h *Handler) APICall(c *gin.Context) {
if hostOverride != "" {
req.Host = hostOverride
}
applyChatGPTAPICallHeaderDefaults(req)

httpClient := &http.Client{
Timeout: defaultAPICallTimeout,
Timeout: defaultAPICallTimeout,
Transport: h.apiCallClientTransport(auth, requestProxyURL),
}
httpClient.Transport = h.apiCallTransport(auth, requestProxyURL)

resp, errDo := httpClient.Do(req)
if errDo != nil {
Expand Down Expand Up @@ -593,14 +614,29 @@ func (h *Handler) authByIndex(authIndex string) *coreauth.Auth {
return nil
}

func (h *Handler) apiCallTransport(auth *coreauth.Auth, requestProxyURL string) http.RoundTripper {
func (h *Handler) apiCallClientTransport(auth *coreauth.Auth, requestProxyURL string) http.RoundTripper {
proxyURL := h.apiCallProxyURL(auth, requestProxyURL)
fallback := h.apiCallTransport(auth, requestProxyURL)
return newAPICallFingerprintTransport(proxyURL, fallback)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve SOCKS5H proxy routing for fingerprinted calls

When the selected request, credential, or global proxy is socks5h://..., this newly routes ChatGPT requests into newUtlsRoundTripper, which passes that raw URL to proxyutil.BuildDialer. Its proxy.FromURL path does not recognize the socks5h scheme, logs the setup error, and retains proxy.Direct; consequently these ChatGPT management calls bypass the configured proxy (whereas the existing apiCallTransport explicitly supports SOCKS5H). Normalize or handle socks5h before constructing the fingerprint transport.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in proxyutil.BuildDialer (socks5/socks5h via SOCKS5 dialer, same as BuildHTTPTransport) + live proof on this branch.

TestFingerprintRoundTripperSOCKS5HUsesProxy: NewFingerprintRoundTripper("socks5h://127.0.0.1:<port>", fallback) GET https://chatgpt.com/ — local SOCKS5 listener logged domain CONNECT chatgpt.com:443 (remote DNS), not Direct.

Contrast TestFingerprintRoundTripperDirectMissesSOCKS: no CONNECT on the proxy.

Harness: internal/runtime/executor/helps/fingerprint_socks5h_live_test.go
Commit: 525f518

(403 without credentials is expected; the assert is proxy routing.)

}

func (h *Handler) apiCallProxyURL(auth *coreauth.Auth, requestProxyURL string) string {
if proxyStr := strings.TrimSpace(requestProxyURL); proxyStr != "" {
if transport := buildProxyTransport(proxyStr); transport != nil {
return transport
if buildProxyTransport(proxyStr) != nil {
return proxyStr
}
return "direct"
}

for _, proxyStr := range h.apiCallProxyCandidates(auth) {
if buildProxyTransport(proxyStr) != nil {
return proxyStr
}
return directAPICallTransport()
}
return ""
}

func (h *Handler) apiCallProxyCandidates(auth *coreauth.Auth) []string {
var proxyCandidates []string
if auth != nil {
if proxyStr := strings.TrimSpace(auth.ProxyURL); proxyStr != "" {
Expand All @@ -617,16 +653,40 @@ func (h *Handler) apiCallTransport(auth *coreauth.Auth, requestProxyURL string)
proxyCandidates = append(proxyCandidates, proxyStr)
}
}
return proxyCandidates
}

for _, proxyStr := range proxyCandidates {
func (h *Handler) apiCallTransport(auth *coreauth.Auth, requestProxyURL string) http.RoundTripper {
if proxyStr := h.apiCallProxyURL(auth, requestProxyURL); proxyStr != "" {
if transport := buildProxyTransport(proxyStr); transport != nil {
return transport
}
}

return directAPICallTransport()
}

func applyChatGPTAPICallHeaderDefaults(req *http.Request) {
if req == nil || !helps.IsChatGPTUpstreamURL(req.URL) {
return
}
setHeaderDefault := func(key, value string) {
if strings.TrimSpace(req.Header.Get(key)) == "" {
req.Header.Set(key, value)
}
}
path := req.URL.EscapedPath()
if path == "" {
path = "/"
}
setHeaderDefault("Accept", "*/*")
setHeaderDefault("Accept-Language", "en-US,en;q=0.9")
setHeaderDefault("OAI-Language", "en-US")
setHeaderDefault("Referer", "https://chatgpt.com/")
setHeaderDefault("User-Agent", chromeAPICallUserAgent)
setHeaderDefault("X-OpenAI-Target-Path", path)
setHeaderDefault("X-OpenAI-Target-Route", path)
}

func directAPICallTransport() http.RoundTripper {
transport, ok := http.DefaultTransport.(*http.Transport)
if !ok || transport == nil {
Expand Down
Loading
Loading