mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
471 lines
12 KiB
Go
471 lines
12 KiB
Go
package netproxy
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"cursor/internal/logger"
|
|
|
|
"golang.org/x/net/http/httpproxy"
|
|
)
|
|
|
|
const (
|
|
proxyCacheTTL = time.Second
|
|
alwaysNoProxyList = "localhost,127.0.0.1,::1"
|
|
)
|
|
|
|
var (
|
|
installOnce sync.Once
|
|
initialDefaultTransport = cloneDefaultTransport()
|
|
defaultResolver = &proxyResolver{}
|
|
proxyTransports sync.Map
|
|
)
|
|
|
|
// InstallDefaultTransport makes clients with a nil Transport use the same
|
|
// proxy resolution as clients created through this package.
|
|
func InstallDefaultTransport() {
|
|
installOnce.Do(func() {
|
|
http.DefaultTransport = NewTransport(nil)
|
|
defaultResolver.logCurrentSnapshot("default transport installed")
|
|
})
|
|
}
|
|
|
|
// NewHTTPClient creates an HTTP client that follows environment and OS proxy
|
|
// settings while preserving the caller's timeout choice.
|
|
func NewHTTPClient(timeout time.Duration) *http.Client {
|
|
return &http.Client{
|
|
Transport: NewTransport(nil),
|
|
Timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// NewTransport clones the given transport and installs proxy resolution on it.
|
|
// When base is nil, it clones Go's original default transport.
|
|
func NewTransport(base *http.Transport) *http.Transport {
|
|
transport := initialDefaultTransport.Clone()
|
|
if base != nil {
|
|
transport = base.Clone()
|
|
}
|
|
transport.Proxy = ProxyForRequest
|
|
proxyTransports.Store(transport, struct{}{})
|
|
return transport
|
|
}
|
|
|
|
// ProxyForRequest resolves the proxy URL for a single request.
|
|
func ProxyForRequest(req *http.Request) (*url.URL, error) {
|
|
if req == nil || req.URL == nil {
|
|
return nil, nil
|
|
}
|
|
return defaultResolver.proxyForURL(req.URL)
|
|
}
|
|
|
|
// CurrentStatus returns the latest proxy resolver snapshot without exposing
|
|
// proxy credentials.
|
|
func CurrentStatus() Status {
|
|
return statusFromSnapshot(defaultResolver.currentSnapshot())
|
|
}
|
|
|
|
type proxyResolver struct {
|
|
mu sync.Mutex
|
|
snapshot proxySnapshot
|
|
}
|
|
|
|
type proxySnapshot struct {
|
|
expiresAt time.Time
|
|
source string
|
|
active bool
|
|
description string
|
|
key string
|
|
httpProxy string
|
|
httpsProxy string
|
|
proxyFunc func(*url.URL) (*url.URL, error)
|
|
systemBypass []string
|
|
excludeSimple bool
|
|
pacIgnored bool
|
|
loadErrMessage string
|
|
}
|
|
|
|
// Status is a sanitized summary of the proxy resolver's current decision.
|
|
type Status struct {
|
|
Source string `json:"source"`
|
|
Active bool `json:"active"`
|
|
UsingSystemProxy bool `json:"usingSystemProxy"`
|
|
UsingEnvProxy bool `json:"usingEnvProxy"`
|
|
HTTPProxy string `json:"httpProxy"`
|
|
HTTPSProxy string `json:"httpsProxy"`
|
|
Description string `json:"description"`
|
|
PACIgnored bool `json:"pacIgnored"`
|
|
LoadError string `json:"loadError"`
|
|
}
|
|
|
|
type systemProxyConfig struct {
|
|
Source string
|
|
HTTPProxy string
|
|
HTTPSProxy string
|
|
SOCKSProxy string
|
|
Bypass []string
|
|
ExcludeSimple bool
|
|
PACEnabled bool
|
|
PACURL string
|
|
LoadErrMessage string
|
|
}
|
|
|
|
func (resolver *proxyResolver) proxyForURL(reqURL *url.URL) (*url.URL, error) {
|
|
snapshot := resolver.currentSnapshot()
|
|
if snapshot.proxyFunc == nil {
|
|
return nil, nil
|
|
}
|
|
if shouldBypassSystemProxy(reqURL, snapshot.systemBypass, snapshot.excludeSimple) {
|
|
return nil, nil
|
|
}
|
|
return snapshot.proxyFunc(reqURL)
|
|
}
|
|
|
|
func (resolver *proxyResolver) currentSnapshot() proxySnapshot {
|
|
now := time.Now()
|
|
resolver.mu.Lock()
|
|
defer resolver.mu.Unlock()
|
|
|
|
if !resolver.snapshot.expiresAt.IsZero() && now.Before(resolver.snapshot.expiresAt) {
|
|
return resolver.snapshot
|
|
}
|
|
next := buildProxySnapshot(now)
|
|
if next.key != resolver.snapshot.key {
|
|
logProxySnapshot(next)
|
|
closeIdleProxyConnections()
|
|
}
|
|
resolver.snapshot = next
|
|
return next
|
|
}
|
|
|
|
func (resolver *proxyResolver) logCurrentSnapshot(prefix string) {
|
|
resolver.mu.Lock()
|
|
next := buildProxySnapshot(time.Now())
|
|
if prefix != "" {
|
|
next.description = strings.TrimSpace(prefix + "; " + next.description)
|
|
}
|
|
logProxySnapshot(next)
|
|
resolver.snapshot = next
|
|
closeIdleProxyConnections()
|
|
resolver.mu.Unlock()
|
|
}
|
|
|
|
func buildProxySnapshot(now time.Time) proxySnapshot {
|
|
if cfg, ok := envProxyConfig(); ok {
|
|
return snapshotFromConfig(now, "env", cfg, nil, false, false, "")
|
|
}
|
|
|
|
systemConfig := loadSystemProxyConfig()
|
|
httpProxy := systemConfig.HTTPProxy
|
|
httpsProxy := systemConfig.HTTPSProxy
|
|
if systemConfig.SOCKSProxy != "" {
|
|
if httpProxy == "" {
|
|
httpProxy = systemConfig.SOCKSProxy
|
|
}
|
|
if httpsProxy == "" {
|
|
httpsProxy = systemConfig.SOCKSProxy
|
|
}
|
|
}
|
|
if httpProxy == "" && httpsProxy == "" {
|
|
return proxySnapshot{
|
|
expiresAt: now.Add(proxyCacheTTL),
|
|
source: directSource(systemConfig),
|
|
description: directDescription(systemConfig),
|
|
key: directKey(systemConfig),
|
|
pacIgnored: systemConfig.PACEnabled,
|
|
loadErrMessage: systemConfig.LoadErrMessage,
|
|
}
|
|
}
|
|
|
|
cfg := httpproxy.Config{
|
|
HTTPProxy: httpProxy,
|
|
HTTPSProxy: httpsProxy,
|
|
NoProxy: joinNoProxy(alwaysNoProxyList, envNoProxy(), strings.Join(systemConfig.Bypass, ",")),
|
|
}
|
|
return snapshotFromConfig(now, "system", cfg, systemConfig.Bypass, systemConfig.ExcludeSimple, systemConfig.PACEnabled, systemConfig.LoadErrMessage)
|
|
}
|
|
|
|
func snapshotFromConfig(now time.Time, source string, cfg httpproxy.Config, bypass []string, excludeSimple bool, pacIgnored bool, loadErr string) proxySnapshot {
|
|
proxyFunc := cfg.ProxyFunc()
|
|
httpDesc := sanitizeProxyValue(cfg.HTTPProxy)
|
|
httpsDesc := sanitizeProxyValue(cfg.HTTPSProxy)
|
|
active := httpDesc != "" || httpsDesc != ""
|
|
description := fmt.Sprintf("source=%s http=%s https=%s", source, displayProxyDesc(httpDesc), displayProxyDesc(httpsDesc))
|
|
if cfg.NoProxy != "" {
|
|
description += " no_proxy=configured"
|
|
}
|
|
if excludeSimple {
|
|
description += " exclude_simple=true"
|
|
}
|
|
if pacIgnored {
|
|
description += " pac=ignored"
|
|
}
|
|
if loadErr != "" {
|
|
description += " load_error=" + loadErr
|
|
}
|
|
return proxySnapshot{
|
|
expiresAt: now.Add(proxyCacheTTL),
|
|
source: source,
|
|
active: active,
|
|
description: description,
|
|
key: strings.Join([]string{source, httpDesc, httpsDesc, cfg.NoProxy, fmt.Sprint(excludeSimple), fmt.Sprint(pacIgnored), loadErr}, "|"),
|
|
httpProxy: httpDesc,
|
|
httpsProxy: httpsDesc,
|
|
proxyFunc: proxyFunc,
|
|
systemBypass: cleanBypassRules(bypass),
|
|
excludeSimple: excludeSimple,
|
|
pacIgnored: pacIgnored,
|
|
loadErrMessage: loadErr,
|
|
}
|
|
}
|
|
|
|
func envProxyConfig() (httpproxy.Config, bool) {
|
|
allProxy := firstEnv("ALL_PROXY", "all_proxy")
|
|
httpProxy := firstEnv("HTTP_PROXY", "http_proxy")
|
|
httpsProxy := firstEnv("HTTPS_PROXY", "https_proxy")
|
|
if httpProxy == "" {
|
|
httpProxy = allProxy
|
|
}
|
|
if httpsProxy == "" {
|
|
httpsProxy = allProxy
|
|
}
|
|
hasProxy := httpProxy != "" || httpsProxy != "" || allProxy != ""
|
|
return httpproxy.Config{
|
|
HTTPProxy: httpProxy,
|
|
HTTPSProxy: httpsProxy,
|
|
NoProxy: joinNoProxy(alwaysNoProxyList, envNoProxy()),
|
|
CGI: os.Getenv("REQUEST_METHOD") != "",
|
|
}, hasProxy
|
|
}
|
|
|
|
func envNoProxy() string {
|
|
return firstEnv("NO_PROXY", "no_proxy")
|
|
}
|
|
|
|
func firstEnv(names ...string) string {
|
|
for _, name := range names {
|
|
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func directSource(cfg systemProxyConfig) string {
|
|
if cfg.PACEnabled {
|
|
return "system-pac-ignored"
|
|
}
|
|
if cfg.LoadErrMessage != "" {
|
|
return "direct"
|
|
}
|
|
return "none"
|
|
}
|
|
|
|
func directDescription(cfg systemProxyConfig) string {
|
|
parts := []string{"source=direct"}
|
|
if cfg.PACEnabled {
|
|
parts = append(parts, "pac=ignored")
|
|
}
|
|
if cfg.LoadErrMessage != "" {
|
|
parts = append(parts, "load_error="+cfg.LoadErrMessage)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func directKey(cfg systemProxyConfig) string {
|
|
return strings.Join([]string{"direct", fmt.Sprint(cfg.PACEnabled), cfg.PACURL, cfg.LoadErrMessage}, "|")
|
|
}
|
|
|
|
func logProxySnapshot(snapshot proxySnapshot) {
|
|
if snapshot.description == "" {
|
|
snapshot.description = "source=direct"
|
|
}
|
|
logger.Infof("net proxy: %s", snapshot.description)
|
|
}
|
|
|
|
func statusFromSnapshot(snapshot proxySnapshot) Status {
|
|
source := strings.TrimSpace(snapshot.source)
|
|
if source == "" || source == "none" || source == "system-pac-ignored" {
|
|
source = "direct"
|
|
}
|
|
return Status{
|
|
Source: source,
|
|
Active: snapshot.active,
|
|
UsingSystemProxy: snapshot.active && snapshot.source == "system",
|
|
UsingEnvProxy: snapshot.active && snapshot.source == "env",
|
|
HTTPProxy: snapshot.httpProxy,
|
|
HTTPSProxy: snapshot.httpsProxy,
|
|
Description: snapshot.description,
|
|
PACIgnored: snapshot.pacIgnored,
|
|
LoadError: snapshot.loadErrMessage,
|
|
}
|
|
}
|
|
|
|
func closeIdleProxyConnections() {
|
|
proxyTransports.Range(func(key any, _ any) bool {
|
|
if transport, ok := key.(*http.Transport); ok && transport != nil {
|
|
transport.CloseIdleConnections()
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func cloneDefaultTransport() *http.Transport {
|
|
if transport, ok := http.DefaultTransport.(*http.Transport); ok {
|
|
clone := transport.Clone()
|
|
clone.Proxy = nil
|
|
return clone
|
|
}
|
|
return &http.Transport{
|
|
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
}
|
|
|
|
func joinNoProxy(parts ...string) string {
|
|
items := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
for _, value := range strings.FieldsFunc(part, func(r rune) bool {
|
|
return r == ',' || r == ';'
|
|
}) {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || strings.EqualFold(value, "<local>") {
|
|
continue
|
|
}
|
|
items = append(items, value)
|
|
}
|
|
}
|
|
return strings.Join(items, ",")
|
|
}
|
|
|
|
func cleanBypassRules(rules []string) []string {
|
|
cleaned := make([]string, 0, len(rules))
|
|
for _, rule := range rules {
|
|
for _, value := range strings.FieldsFunc(rule, func(r rune) bool {
|
|
return r == ',' || r == ';'
|
|
}) {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, value)
|
|
}
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
func shouldBypassSystemProxy(reqURL *url.URL, rules []string, excludeSimple bool) bool {
|
|
if reqURL == nil {
|
|
return false
|
|
}
|
|
host := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(reqURL.Hostname()), "."))
|
|
if host == "" {
|
|
return false
|
|
}
|
|
if excludeSimple && isSimpleHostname(host) {
|
|
return true
|
|
}
|
|
for _, rule := range rules {
|
|
if ruleMatchesHost(rule, host) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isSimpleHostname(host string) bool {
|
|
if host == "" || strings.Contains(host, ".") {
|
|
return false
|
|
}
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return false
|
|
}
|
|
return !strings.Contains(host, ":")
|
|
}
|
|
|
|
func ruleMatchesHost(rule string, host string) bool {
|
|
rule = strings.TrimSpace(strings.TrimSuffix(rule, "."))
|
|
if rule == "" {
|
|
return false
|
|
}
|
|
if rule == "*" {
|
|
return true
|
|
}
|
|
if strings.EqualFold(rule, "<local>") {
|
|
return isSimpleHostname(host)
|
|
}
|
|
if strings.Contains(rule, "://") {
|
|
if parsed, err := url.Parse(rule); err == nil {
|
|
rule = strings.TrimSpace(parsed.Hostname())
|
|
}
|
|
}
|
|
if strings.Contains(rule, ":") {
|
|
if ruleHost, _, err := net.SplitHostPort(rule); err == nil {
|
|
rule = ruleHost
|
|
}
|
|
}
|
|
rule = strings.ToLower(strings.TrimSuffix(rule, "."))
|
|
if strings.ContainsAny(rule, "*?") {
|
|
matched, err := path.Match(rule, host)
|
|
return err == nil && matched
|
|
}
|
|
if strings.HasPrefix(rule, ".") {
|
|
return strings.HasSuffix(host, rule)
|
|
}
|
|
if strings.HasPrefix(rule, "*.") {
|
|
return strings.HasSuffix(host, strings.TrimPrefix(rule, "*"))
|
|
}
|
|
return host == rule || strings.HasSuffix(host, "."+rule)
|
|
}
|
|
|
|
func sanitizeProxyValue(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
parsed, err = url.Parse("http://" + raw)
|
|
}
|
|
if err != nil || parsed == nil || parsed.Host == "" {
|
|
return "invalid"
|
|
}
|
|
scheme := strings.TrimSpace(parsed.Scheme)
|
|
if scheme == "" {
|
|
scheme = "http"
|
|
}
|
|
return scheme + "://" + parsed.Host
|
|
}
|
|
|
|
func displayProxyDesc(value string) string {
|
|
if value == "" {
|
|
return "none"
|
|
}
|
|
return value
|
|
}
|
|
|
|
func normalizeProxyAddress(defaultScheme string, raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
if strings.Contains(raw, "://") {
|
|
return raw
|
|
}
|
|
if defaultScheme == "" {
|
|
defaultScheme = "http"
|
|
}
|
|
return defaultScheme + "://" + raw
|
|
}
|