mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 23:27:30 +08:00
157 lines
4.2 KiB
Go
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()
|
|
}
|