feat(proxy): 统一代理配置并支持 SOCKS5H 协议
- 新增 proxyutil 包,统一 HTTP/HTTPS/SOCKS5/SOCKS5H 代理配置逻辑 - SOCKS5H 支持服务端 DNS 解析,避免本地 DNS 泄露 - 移除 ProxyStrict 宽松模式,代理失败直接返回错误不回退直连 - 前端代理管理页面支持 SOCKS5H 协议的添加/编辑/批量导入 - 补充 IPv6 地址和特殊字符密码的边界测试
This commit is contained in:
@@ -11,22 +11,20 @@
|
||||
// 新实现使用统一的客户端池:
|
||||
// 1. 相同配置复用同一 http.Client 实例
|
||||
// 2. 复用 Transport 连接池,减少 TCP/TLS 握手开销
|
||||
// 3. 支持 HTTP/HTTPS/SOCKS5 代理
|
||||
// 4. 支持严格代理模式(代理失败则返回错误)
|
||||
// 3. 支持 HTTP/HTTPS/SOCKS5/SOCKS5H 代理
|
||||
// 4. 代理配置失败时直接返回错误,不会回退到直连(避免 IP 关联风险)
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyutil"
|
||||
)
|
||||
|
||||
// Transport 连接池默认配置
|
||||
@@ -38,11 +36,10 @@ const (
|
||||
|
||||
// Options 定义共享 HTTP 客户端的构建参数
|
||||
type Options struct {
|
||||
ProxyURL string // 代理 URL(支持 http/https/socks5)
|
||||
ProxyURL string // 代理 URL(支持 http/https/socks5/socks5h)
|
||||
Timeout time.Duration // 请求总超时时间
|
||||
ResponseHeaderTimeout time.Duration // 等待响应头超时时间
|
||||
InsecureSkipVerify bool // 是否跳过 TLS 证书验证
|
||||
ProxyStrict bool // 严格代理模式:代理失败时返回错误而非回退
|
||||
|
||||
// 可选的连接池参数(不设置则使用默认值)
|
||||
MaxIdleConns int // 最大空闲连接总数(默认 100)
|
||||
@@ -55,6 +52,7 @@ var sharedClients sync.Map
|
||||
|
||||
// GetClient 返回共享的 HTTP 客户端实例
|
||||
// 性能优化:相同配置复用同一客户端,避免重复创建 Transport
|
||||
// 安全说明:代理配置失败时直接返回错误,不会回退到直连,避免 IP 关联风险
|
||||
func GetClient(opts Options) (*http.Client, error) {
|
||||
key := buildClientKey(opts)
|
||||
if cached, ok := sharedClients.Load(key); ok {
|
||||
@@ -65,12 +63,7 @@ func GetClient(opts Options) (*http.Client, error) {
|
||||
|
||||
client, err := buildClient(opts)
|
||||
if err != nil {
|
||||
if opts.ProxyStrict {
|
||||
return nil, err
|
||||
}
|
||||
fallback := opts
|
||||
fallback.ProxyURL = ""
|
||||
client, _ = buildClient(fallback)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actual, _ := sharedClients.LoadOrStore(key, client)
|
||||
@@ -125,31 +118,19 @@ func buildTransport(opts Options) (*http.Transport, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "http", "https":
|
||||
transport.Proxy = http.ProxyURL(parsed)
|
||||
case "socks5", "socks5h":
|
||||
dialer, err := proxy.FromURL(parsed, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.Dial(network, addr)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy protocol: %s", parsed.Scheme)
|
||||
if err := proxyutil.ConfigureTransportProxy(transport, parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
func buildClientKey(opts Options) string {
|
||||
return fmt.Sprintf("%s|%s|%s|%t|%t|%d|%d|%d",
|
||||
return fmt.Sprintf("%s|%s|%s|%t|%d|%d|%d",
|
||||
strings.TrimSpace(opts.ProxyURL),
|
||||
opts.Timeout.String(),
|
||||
opts.ResponseHeaderTimeout.String(),
|
||||
opts.InsecureSkipVerify,
|
||||
opts.ProxyStrict,
|
||||
opts.MaxIdleConns,
|
||||
opts.MaxIdleConnsPerHost,
|
||||
opts.MaxConnsPerHost,
|
||||
|
||||
Reference in New Issue
Block a user