mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
v0.3.8
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user