mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 19:37:31 +08:00
release: v0.2.0
This commit is contained in:
156
pkg/providers/http_proxy.go
Normal file
156
pkg/providers/http_proxy.go
Normal file
@@ -0,0 +1,156 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user