package proxyutil import ( "context" "fmt" "net" "net/http" "net/url" "strings" "golang.org/x/net/proxy" ) // Mode describes how a proxy setting should be interpreted. type Mode int const ( // ModeInherit means no explicit proxy behavior was configured. ModeInherit Mode = iota // ModeDirect means outbound requests must bypass proxies explicitly. ModeDirect // ModeProxy means a concrete proxy URL was configured. ModeProxy // ModeInvalid means the proxy setting is present but malformed or unsupported. ModeInvalid ) // Setting is the normalized interpretation of a proxy configuration value. type Setting struct { Raw string Mode Mode URL *url.URL } // Parse normalizes a proxy configuration value into inherit, direct, or proxy modes. func Parse(raw string) (Setting, error) { trimmed := strings.TrimSpace(raw) setting := Setting{Raw: trimmed} if trimmed == "" { setting.Mode = ModeInherit return setting, nil } if strings.EqualFold(trimmed, "direct") || strings.EqualFold(trimmed, "none") { setting.Mode = ModeDirect return setting, nil } parsedURL, errParse := url.Parse(trimmed) if errParse != nil { setting.Mode = ModeInvalid return setting, fmt.Errorf("parse proxy URL failed: %w", errParse) } if parsedURL.Scheme == "" || parsedURL.Host == "" { setting.Mode = ModeInvalid return setting, fmt.Errorf("proxy URL missing scheme/host") } switch parsedURL.Scheme { case "socks5", "http", "https": setting.Mode = ModeProxy setting.URL = parsedURL return setting, nil default: setting.Mode = ModeInvalid return setting, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme) } } // NewDirectTransport returns a transport that bypasses environment proxies. func NewDirectTransport() *http.Transport { if transport, ok := http.DefaultTransport.(*http.Transport); ok && transport != nil { clone := transport.Clone() clone.Proxy = nil return clone } return &http.Transport{Proxy: nil} } // BuildHTTPTransport constructs an HTTP transport for the provided proxy setting. func BuildHTTPTransport(raw string) (*http.Transport, Mode, error) { setting, errParse := Parse(raw) if errParse != nil { return nil, setting.Mode, errParse } switch setting.Mode { case ModeInherit: return nil, setting.Mode, nil case ModeDirect: return NewDirectTransport(), setting.Mode, nil case ModeProxy: if setting.URL.Scheme == "socks5" { var proxyAuth *proxy.Auth if setting.URL.User != nil { username := setting.URL.User.Username() password, _ := setting.URL.User.Password() proxyAuth = &proxy.Auth{User: username, Password: password} } dialer, errSOCKS5 := proxy.SOCKS5("tcp", setting.URL.Host, proxyAuth, proxy.Direct) if errSOCKS5 != nil { return nil, setting.Mode, fmt.Errorf("create SOCKS5 dialer failed: %w", errSOCKS5) } return &http.Transport{ Proxy: nil, DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { return dialer.Dial(network, addr) }, }, setting.Mode, nil } return &http.Transport{Proxy: http.ProxyURL(setting.URL)}, setting.Mode, nil default: return nil, setting.Mode, nil } } // BuildDialer constructs a proxy dialer for settings that operate at the connection layer. func BuildDialer(raw string) (proxy.Dialer, Mode, error) { setting, errParse := Parse(raw) if errParse != nil { return nil, setting.Mode, errParse } switch setting.Mode { case ModeInherit: return nil, setting.Mode, nil case ModeDirect: return proxy.Direct, setting.Mode, nil case ModeProxy: dialer, errDialer := proxy.FromURL(setting.URL, proxy.Direct) if errDialer != nil { return nil, setting.Mode, fmt.Errorf("create proxy dialer failed: %w", errDialer) } return dialer, setting.Mode, nil default: return nil, setting.Mode, nil } }