Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
10 kB · 364 lines
Go
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365package proxyutil
import ( "bufio" "context" "crypto/tls" "encoding/base64" "errors" "fmt" "net" "net/http" "net/url" "strings" "time"
"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") } if parsedURL.Scheme == "" || parsedURL.Host == "" { setting.Mode = ModeInvalid return setting, fmt.Errorf("proxy URL missing scheme/host") }
switch parsedURL.Scheme { case "socks5", "socks5h", "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) }}
func cloneDefaultTransport() *http.Transport { if transport, ok := http.DefaultTransport.(*http.Transport); ok && transport != nil { return transport.Clone() } return &http.Transport{}}
// NewDirectTransport returns a transport that bypasses environment proxies.func NewDirectTransport() *http.Transport { clone := cloneDefaultTransport() clone.Proxy = nil return clone}
// 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" || setting.URL.Scheme == "socks5h" { 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) } transport := cloneDefaultTransport() transport.Proxy = nil transport.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) { return dialer.Dial(network, addr) } return transport, setting.Mode, nil } if setting.URL.Scheme == "https" { transport := cloneDefaultTransport() transport.Proxy = http.ProxyURL(setting.URL) transport.DialTLSContext = buildHTTPSProxyDialTLSContext(setting.URL, nil, transport.TLSHandshakeTimeout, transport.DialContext) return transport, setting.Mode, nil } transport := cloneDefaultTransport() transport.Proxy = http.ProxyURL(setting.URL) return transport, setting.Mode, nil default: return nil, setting.Mode, nil }}
func buildHTTPSProxyDialTLSContext( proxyURL *url.URL, baseTLS *tls.Config, handshakeTimeout time.Duration, baseDialContext func(ctx context.Context, network, addr string) (net.Conn, error),) func(ctx context.Context, network, addr string) (net.Conn, error) { proxyHostname := proxyURL.Hostname() var privateTLS *tls.Config if baseTLS != nil { privateTLS = baseTLS.Clone() } dialContext := baseDialContext if dialContext == nil { dialContext = (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext } return func(ctx context.Context, network, addr string) (net.Conn, error) { rawConn, errDial := dialContext(ctx, network, addr) if errDial != nil { return nil, errDial } var tlsConfig *tls.Config if privateTLS != nil { tlsConfig = privateTLS.Clone() } else { tlsConfig = &tls.Config{} } if tlsConfig.ServerName == "" { tlsConfig.ServerName = proxyHostname } tlsConfig.NextProtos = []string{"http/1.1"}
tlsConn := tls.Client(rawConn, tlsConfig) handshakeCtx := ctx if handshakeTimeout > 0 { var cancelHandshake context.CancelFunc handshakeCtx, cancelHandshake = context.WithTimeout(ctx, handshakeTimeout) defer cancelHandshake() } if errHandshake := tlsConn.HandshakeContext(handshakeCtx); errHandshake != nil { if errClose := rawConn.Close(); errClose != nil { return nil, fmt.Errorf("HTTPS proxy TLS handshake failed: %w; close failed: %v", errHandshake, errClose) } return nil, fmt.Errorf("HTTPS proxy TLS handshake failed: %w", errHandshake) } return tlsConn, 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: if setting.URL.Scheme == "http" || setting.URL.Scheme == "https" { return &httpConnectDialer{proxyURL: setting.URL, dialer: proxy.Direct}, setting.Mode, nil } 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 }}
type httpConnectDialer struct { proxyURL *url.URL dialer proxy.Dialer tlsConfig *tls.Config}
func (d *httpConnectDialer) Dial(network, addr string) (net.Conn, error) { return d.DialContext(context.Background(), network, addr)}
func (d *httpConnectDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { if ctx == nil { ctx = context.Background() } contextDialer, ok := d.dialer.(proxy.ContextDialer) if !ok { return nil, errors.New("HTTP proxy base dialer does not support context cancellation") } proxyConn, errDial := contextDialer.DialContext(ctx, network, proxyDialAddr(d.proxyURL)) if errDial != nil { return nil, fmt.Errorf("dial HTTP proxy failed: %w", errDial) }
conn := proxyConn cancelDone := make(chan struct{}) stopCancel := context.AfterFunc(ctx, func() { _ = proxyConn.Close() close(cancelDone) }) defer func() { if !stopCancel() { <-cancelDone } }() if d.proxyURL.Scheme == "https" { var tlsConfig *tls.Config if d.tlsConfig != nil { tlsConfig = d.tlsConfig.Clone() } else { tlsConfig = &tls.Config{} } if tlsConfig.ServerName == "" { tlsConfig.ServerName = d.proxyURL.Hostname() } tlsConfig.NextProtos = []string{"http/1.1"}
tlsConn := tls.Client(conn, tlsConfig) if errHandshake := tlsConn.HandshakeContext(ctx); errHandshake != nil { if errClose := conn.Close(); errClose != nil { return nil, fmt.Errorf("HTTPS proxy TLS handshake failed: %w; close failed: %v", errHandshake, errClose) } return nil, fmt.Errorf("HTTPS proxy TLS handshake failed: %w", errHandshake) } conn = tlsConn }
req := (&http.Request{ Method: http.MethodConnect, URL: &url.URL{Host: addr}, Host: addr, Header: make(http.Header), }).WithContext(ctx) if d.proxyURL.User != nil { req.Header.Set("Proxy-Authorization", proxyAuthorization(d.proxyURL.User)) } if errWrite := req.Write(conn); errWrite != nil { if errClose := conn.Close(); errClose != nil { return nil, fmt.Errorf("write CONNECT request failed: %w; close failed: %v", errWrite, errClose) } return nil, fmt.Errorf("write CONNECT request failed: %w", errWrite) }
reader := bufio.NewReader(conn) resp, errRead := http.ReadResponse(reader, req) if errRead != nil { if errClose := conn.Close(); errClose != nil { return nil, fmt.Errorf("read CONNECT response failed: %w; close failed: %v", errRead, errClose) } return nil, fmt.Errorf("read CONNECT response failed: %w", errRead) } if resp.StatusCode != http.StatusOK { if resp.Body != nil { _ = resp.Body.Close() } if errClose := conn.Close(); errClose != nil { return nil, fmt.Errorf("proxy CONNECT returned status %s; close failed: %v", resp.Status, errClose) } return nil, fmt.Errorf("proxy CONNECT returned status %s", resp.Status) }
if errContext := ctx.Err(); errContext != nil { if errClose := conn.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) { return nil, fmt.Errorf("HTTP proxy context ended: %w; close failed: %v", errContext, errClose) } return nil, errContext } if reader.Buffered() > 0 { return &bufferedConn{Conn: conn, reader: reader}, nil } return conn, nil}
func proxyDialAddr(proxyURL *url.URL) string { port := proxyURL.Port() if port == "" { port = "80" if proxyURL.Scheme == "https" { port = "443" } } return net.JoinHostPort(proxyURL.Hostname(), port)}
func proxyAuthorization(user *url.Userinfo) string { username := user.Username() password, _ := user.Password() encoded := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) return "Basic " + encoded}
// Redact returns a log-safe proxy URL with credentials and path-like data removed.func Redact(raw string) string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return "" }
parsedURL, errParse := url.Parse(trimmed) if errParse != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { return "<invalid proxy URL>" }
redacted := &url.URL{ Scheme: parsedURL.Scheme, Host: parsedURL.Host, } if parsedURL.User != nil { redacted.User = url.User("redacted") } return redacted.String()}
type bufferedConn struct { net.Conn reader *bufio.Reader}
func (c *bufferedConn) Read(p []byte) (int, error) { if c.reader.Buffered() > 0 { return c.reader.Read(p) } return c.Conn.Read(p)}