Files
clawgo/pkg/providers/http_proxy.go
2026-03-11 19:00:19 +08:00

157 lines
4.2 KiB
Go

package providers
import (
"bufio"
"context"
stdtls "crypto/tls"
"encoding/base64"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
xproxy "golang.org/x/net/proxy"
)
func normalizeOptionalProxyURL(raw string) (string, error) {
value := strings.TrimSpace(raw)
if value == "" {
return "", nil
}
if !strings.Contains(value, "://") {
value = "http://" + value
}
parsed, err := url.Parse(value)
if err != nil {
return "", fmt.Errorf("invalid network proxy: %w", err)
}
if parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("invalid network proxy: host is required")
}
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "http", "https", "socks5", "socks5h":
return parsed.String(), nil
default:
return "", fmt.Errorf("invalid network proxy: unsupported scheme %q", parsed.Scheme)
}
}
func maskedProxyURL(raw string) string {
normalized, err := normalizeOptionalProxyURL(raw)
if err != nil || normalized == "" {
return ""
}
parsed, err := url.Parse(normalized)
if err != nil {
return ""
}
if parsed.User != nil {
username := parsed.User.Username()
if username != "" {
parsed.User = url.UserPassword(username, "***")
} else {
parsed.User = url.User("***")
}
}
return parsed.String()
}
func proxyDialContext(proxyRaw string) (func(context.Context, string, string) (net.Conn, error), error) {
normalized, err := normalizeOptionalProxyURL(proxyRaw)
if err != nil {
return nil, err
}
if normalized == "" {
dialer := &net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}
return dialer.DialContext, nil
}
parsed, err := url.Parse(normalized)
if err != nil {
return nil, err
}
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
case "socks5", "socks5h":
base := &net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}
dialer, err := xproxy.FromURL(parsed, base)
if err != nil {
return nil, fmt.Errorf("configure socks proxy failed: %w", err)
}
if ctxDialer, ok := dialer.(xproxy.ContextDialer); ok {
return ctxDialer.DialContext, nil
}
return func(ctx context.Context, network, addr string) (net.Conn, error) {
type dialResult struct {
conn net.Conn
err error
}
ch := make(chan dialResult, 1)
go func() {
conn, err := dialer.Dial(network, addr)
ch <- dialResult{conn: conn, err: err}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case res := <-ch:
return res.conn, res.err
}
}, nil
case "http", "https":
base := &net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}
return func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := base.DialContext(ctx, "tcp", parsed.Host)
if err != nil {
return nil, err
}
if strings.EqualFold(parsed.Scheme, "https") {
tlsConn := stdtls.Client(conn, &stdtls.Config{ServerName: parsed.Hostname()})
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
return nil, err
}
conn = tlsConn
}
connectReq := buildProxyConnectRequest(parsed, addr)
if _, err := conn.Write([]byte(connectReq)); err != nil {
_ = conn.Close()
return nil, err
}
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, &http.Request{Method: http.MethodConnect})
if err != nil {
_ = conn.Close()
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
_ = conn.Close()
return nil, fmt.Errorf("proxy connect failed: status=%d", resp.StatusCode)
}
return conn, nil
}, nil
default:
return nil, fmt.Errorf("invalid network proxy: unsupported scheme %q", parsed.Scheme)
}
}
func buildProxyConnectRequest(proxyURL *url.URL, targetAddr string) string {
var b strings.Builder
b.WriteString("CONNECT ")
b.WriteString(targetAddr)
b.WriteString(" HTTP/1.1\r\nHost: ")
b.WriteString(targetAddr)
b.WriteString("\r\n")
if proxyURL != nil && proxyURL.User != nil {
username := proxyURL.User.Username()
password, _ := proxyURL.User.Password()
encoded := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
b.WriteString("Proxy-Authorization: Basic ")
b.WriteString(encoded)
b.WriteString("\r\n")
}
b.WriteString("\r\n")
return b.String()
}