refactor: consolidate duplicate UA and header scrubbing into shared misc functions

This commit is contained in:
maplelove
2026-02-27 10:57:13 +08:00
parent f3c164d345
commit fc0257d6d9
5 changed files with 72 additions and 83 deletions

View File

@@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -75,36 +76,9 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi
req.Header.Del("Authorization") req.Header.Del("Authorization")
req.Header.Del("X-Api-Key") req.Header.Del("X-Api-Key")
req.Header.Del("X-Goog-Api-Key") req.Header.Del("X-Goog-Api-Key")
// Remove proxy tracing headers to avoid upstream detection
req.Header.Del("X-Forwarded-For")
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Proto")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Real-IP")
req.Header.Del("Forwarded")
req.Header.Del("Via")
// Remove client identity headers that reveal third-party clients // Remove proxy, client identity, and browser fingerprint headers
req.Header.Del("X-Title") misc.ScrubProxyAndFingerprintHeaders(req)
req.Header.Del("X-Stainless-Lang")
req.Header.Del("X-Stainless-Package-Version")
req.Header.Del("X-Stainless-Os")
req.Header.Del("X-Stainless-Arch")
req.Header.Del("X-Stainless-Runtime")
req.Header.Del("X-Stainless-Runtime-Version")
req.Header.Del("Http-Referer")
req.Header.Del("Referer")
// Remove browser / Chromium fingerprint headers
req.Header.Del("Sec-Ch-Ua")
req.Header.Del("Sec-Ch-Ua-Mobile")
req.Header.Del("Sec-Ch-Ua-Platform")
req.Header.Del("Sec-Fetch-Mode")
req.Header.Del("Sec-Fetch-Site")
req.Header.Del("Sec-Fetch-Dest")
req.Header.Del("Priority")
req.Header.Del("Accept-Encoding")
// Remove query-based credentials if they match the authenticated client API key. // Remove query-based credentials if they match the authenticated client API key.
// This prevents leaking client auth material to the Amp upstream while avoiding // This prevents leaking client auth material to the Amp upstream while avoiding

View File

@@ -13,7 +13,6 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -21,6 +20,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -33,7 +33,7 @@ const (
) )
func getGeminiCLIUserAgent() string { func getGeminiCLIUserAgent() string {
return fmt.Sprintf("GeminiCLI/1.0.0/unknown (%s; %s)", runtime.GOOS, runtime.GOARCH) return misc.GeminiCLIUserAgent("")
} }
type projectSelectionRequiredError struct{} type projectSelectionRequiredError struct{}

View File

@@ -4,10 +4,68 @@
package misc package misc
import ( import (
"fmt"
"net/http" "net/http"
"runtime"
"strings" "strings"
) )
// GeminiCLIUserAgent returns a User-Agent string that matches the Gemini CLI format.
// The model parameter is included in the UA; pass "" or "unknown" when the model is not applicable.
func GeminiCLIUserAgent(model string) string {
if model == "" {
model = "unknown"
}
return fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH)
}
// ScrubProxyAndFingerprintHeaders removes all headers that could reveal
// proxy infrastructure, client identity, or browser fingerprints from an
// outgoing request. This ensures requests to upstream services look like they
// originate directly from a native client rather than a third-party client
// behind a reverse proxy.
func ScrubProxyAndFingerprintHeaders(req *http.Request) {
if req == nil {
return
}
// --- Proxy tracing headers ---
req.Header.Del("X-Forwarded-For")
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Proto")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Real-IP")
req.Header.Del("Forwarded")
req.Header.Del("Via")
// --- Client identity headers ---
req.Header.Del("X-Title")
req.Header.Del("X-Stainless-Lang")
req.Header.Del("X-Stainless-Package-Version")
req.Header.Del("X-Stainless-Os")
req.Header.Del("X-Stainless-Arch")
req.Header.Del("X-Stainless-Runtime")
req.Header.Del("X-Stainless-Runtime-Version")
req.Header.Del("Http-Referer")
req.Header.Del("Referer")
// --- Browser / Chromium fingerprint headers ---
// These are sent by Electron-based clients (e.g. CherryStudio) using the
// Fetch API, but NOT by Node.js https module (which Antigravity uses).
req.Header.Del("Sec-Ch-Ua")
req.Header.Del("Sec-Ch-Ua-Mobile")
req.Header.Del("Sec-Ch-Ua-Platform")
req.Header.Del("Sec-Fetch-Mode")
req.Header.Del("Sec-Fetch-Site")
req.Header.Del("Sec-Fetch-Dest")
req.Header.Del("Priority")
// --- Encoding negotiation ---
// Antigravity (Node.js) sends "gzip, deflate, br" by default;
// Electron-based clients may add "zstd" which is a fingerprint mismatch.
req.Header.Del("Accept-Encoding")
}
// EnsureHeader ensures that a header exists in the target header map by checking // EnsureHeader ensures that a header exists in the target header map by checking
// multiple sources in order of priority: source headers, existing target headers, // multiple sources in order of priority: source headers, existing target headers,
// and finally the default value. It only sets the header if it's not already present // and finally the default value. It only sets the header if it's not already present
@@ -35,3 +93,4 @@ func EnsureHeader(target http.Header, source http.Header, key, defaultValue stri
target.Set(key, val) target.Set(key, val)
} }
} }

View File

@@ -12,7 +12,6 @@ import (
"io" "io"
"net/http" "net/http"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -745,12 +744,7 @@ func applyGeminiCLIHeaders(r *http.Request, model string) {
ginHeaders = ginCtx.Request.Header ginHeaders = ginCtx.Request.Header
} }
if model == "" { misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", misc.GeminiCLIUserAgent(model))
model = "unknown"
}
userAgent := fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH)
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", userAgent)
} }
// cliPreviewFallbackOrder returns preview model candidates for a base model. // cliPreviewFallbackOrder returns preview model candidates for a base model.

View File

@@ -1,50 +1,12 @@
package executor package executor
import "net/http" import (
"net/http"
// scrubProxyAndFingerprintHeaders removes all headers that could reveal "github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
// proxy infrastructure, client identity, or browser fingerprints from an )
// outgoing request. This ensures requests to Google look like they
// originate directly from the Antigravity IDE (Node.js) rather than // scrubProxyAndFingerprintHeaders delegates to the shared utility in internal/misc.
// a third-party client behind a reverse proxy.
func scrubProxyAndFingerprintHeaders(req *http.Request) { func scrubProxyAndFingerprintHeaders(req *http.Request) {
if req == nil { misc.ScrubProxyAndFingerprintHeaders(req)
return
}
// --- Proxy tracing headers ---
req.Header.Del("X-Forwarded-For")
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Proto")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Real-IP")
req.Header.Del("Forwarded")
req.Header.Del("Via")
// --- Client identity headers ---
req.Header.Del("X-Title")
req.Header.Del("X-Stainless-Lang")
req.Header.Del("X-Stainless-Package-Version")
req.Header.Del("X-Stainless-Os")
req.Header.Del("X-Stainless-Arch")
req.Header.Del("X-Stainless-Runtime")
req.Header.Del("X-Stainless-Runtime-Version")
req.Header.Del("Http-Referer")
req.Header.Del("Referer")
// --- Browser / Chromium fingerprint headers ---
// These are sent by Electron-based clients (e.g. CherryStudio) using the
// Fetch API, but NOT by Node.js https module (which Antigravity uses).
req.Header.Del("Sec-Ch-Ua")
req.Header.Del("Sec-Ch-Ua-Mobile")
req.Header.Del("Sec-Ch-Ua-Platform")
req.Header.Del("Sec-Fetch-Mode")
req.Header.Del("Sec-Fetch-Site")
req.Header.Del("Sec-Fetch-Dest")
req.Header.Del("Priority")
// --- Encoding negotiation ---
// Antigravity (Node.js) sends "gzip, deflate, br" by default;
// Electron-based clients may add "zstd" which is a fingerprint mismatch.
req.Header.Del("Accept-Encoding")
} }