2025-12-31 08:50:12 +08:00
|
|
|
|
// Package httpclient 提供共享 HTTP 客户端池
|
|
|
|
|
|
//
|
|
|
|
|
|
// 性能优化说明:
|
|
|
|
|
|
// 原实现在多个服务中重复创建 http.Client:
|
|
|
|
|
|
// 1. proxy_probe_service.go: 每次探测创建新客户端
|
|
|
|
|
|
// 2. pricing_service.go: 每次请求创建新客户端
|
|
|
|
|
|
// 3. turnstile_service.go: 每次验证创建新客户端
|
|
|
|
|
|
// 4. github_release_service.go: 每次请求创建新客户端
|
|
|
|
|
|
// 5. claude_usage_service.go: 每次请求创建新客户端
|
|
|
|
|
|
//
|
|
|
|
|
|
// 新实现使用统一的客户端池:
|
|
|
|
|
|
// 1. 相同配置复用同一 http.Client 实例
|
|
|
|
|
|
// 2. 复用 Transport 连接池,减少 TCP/TLS 握手开销
|
2026-01-04 11:43:58 +08:00
|
|
|
|
// 3. 支持 HTTP/HTTPS/SOCKS5/SOCKS5H 代理
|
|
|
|
|
|
// 4. 代理配置失败时直接返回错误,不会回退到直连(避免 IP 关联风险)
|
2025-12-31 08:50:12 +08:00
|
|
|
|
package httpclient
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/url"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-01-04 11:43:58 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyutil"
|
2026-01-03 10:52:24 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
|
2025-12-31 08:50:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Transport 连接池默认配置
|
|
|
|
|
|
const (
|
|
|
|
|
|
defaultMaxIdleConns = 100 // 最大空闲连接数
|
|
|
|
|
|
defaultMaxIdleConnsPerHost = 10 // 每个主机最大空闲连接数
|
2026-01-04 19:49:59 +08:00
|
|
|
|
defaultIdleConnTimeout = 90 * time.Second // 空闲连接超时时间(建议小于上游 LB 超时)
|
2025-12-31 08:50:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Options 定义共享 HTTP 客户端的构建参数
|
|
|
|
|
|
type Options struct {
|
2026-01-04 11:43:58 +08:00
|
|
|
|
ProxyURL string // 代理 URL(支持 http/https/socks5/socks5h)
|
2025-12-31 08:50:12 +08:00
|
|
|
|
Timeout time.Duration // 请求总超时时间
|
|
|
|
|
|
ResponseHeaderTimeout time.Duration // 等待响应头超时时间
|
2026-01-06 11:36:38 +08:00
|
|
|
|
InsecureSkipVerify bool // 是否跳过 TLS 证书验证(已禁用,不允许设置为 true)
|
2025-12-31 08:50:12 +08:00
|
|
|
|
ProxyStrict bool // 严格代理模式:代理失败时返回错误而非回退
|
2026-01-03 10:52:24 +08:00
|
|
|
|
ValidateResolvedIP bool // 是否校验解析后的 IP(防止 DNS Rebinding)
|
|
|
|
|
|
AllowPrivateHosts bool // 允许私有地址解析(与 ValidateResolvedIP 一起使用)
|
2025-12-31 08:50:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 可选的连接池参数(不设置则使用默认值)
|
|
|
|
|
|
MaxIdleConns int // 最大空闲连接总数(默认 100)
|
|
|
|
|
|
MaxIdleConnsPerHost int // 每主机最大空闲连接(默认 10)
|
|
|
|
|
|
MaxConnsPerHost int // 每主机最大连接数(默认 0 无限制)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sharedClients 存储按配置参数缓存的 http.Client 实例
|
|
|
|
|
|
var sharedClients sync.Map
|
|
|
|
|
|
|
|
|
|
|
|
// GetClient 返回共享的 HTTP 客户端实例
|
|
|
|
|
|
// 性能优化:相同配置复用同一客户端,避免重复创建 Transport
|
2026-01-04 11:43:58 +08:00
|
|
|
|
// 安全说明:代理配置失败时直接返回错误,不会回退到直连,避免 IP 关联风险
|
2025-12-31 08:50:12 +08:00
|
|
|
|
func GetClient(opts Options) (*http.Client, error) {
|
|
|
|
|
|
key := buildClientKey(opts)
|
|
|
|
|
|
if cached, ok := sharedClients.Load(key); ok {
|
2025-12-31 14:51:58 +08:00
|
|
|
|
if client, ok := cached.(*http.Client); ok {
|
|
|
|
|
|
return client, nil
|
|
|
|
|
|
}
|
2025-12-31 08:50:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client, err := buildClient(opts)
|
|
|
|
|
|
if err != nil {
|
2026-01-04 11:43:58 +08:00
|
|
|
|
return nil, err
|
2025-12-31 08:50:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actual, _ := sharedClients.LoadOrStore(key, client)
|
2025-12-31 14:51:58 +08:00
|
|
|
|
if c, ok := actual.(*http.Client); ok {
|
|
|
|
|
|
return c, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return client, nil
|
2025-12-31 08:50:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildClient(opts Options) (*http.Client, error) {
|
|
|
|
|
|
transport, err := buildTransport(opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 10:52:24 +08:00
|
|
|
|
var rt http.RoundTripper = transport
|
|
|
|
|
|
if opts.ValidateResolvedIP && !opts.AllowPrivateHosts {
|
|
|
|
|
|
rt = &validatedTransport{base: transport}
|
|
|
|
|
|
}
|
2025-12-31 08:50:12 +08:00
|
|
|
|
return &http.Client{
|
2026-01-03 10:52:24 +08:00
|
|
|
|
Transport: rt,
|
2025-12-31 08:50:12 +08:00
|
|
|
|
Timeout: opts.Timeout,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildTransport(opts Options) (*http.Transport, error) {
|
|
|
|
|
|
// 使用自定义值或默认值
|
|
|
|
|
|
maxIdleConns := opts.MaxIdleConns
|
|
|
|
|
|
if maxIdleConns <= 0 {
|
|
|
|
|
|
maxIdleConns = defaultMaxIdleConns
|
|
|
|
|
|
}
|
|
|
|
|
|
maxIdleConnsPerHost := opts.MaxIdleConnsPerHost
|
|
|
|
|
|
if maxIdleConnsPerHost <= 0 {
|
|
|
|
|
|
maxIdleConnsPerHost = defaultMaxIdleConnsPerHost
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
transport := &http.Transport{
|
|
|
|
|
|
MaxIdleConns: maxIdleConns,
|
|
|
|
|
|
MaxIdleConnsPerHost: maxIdleConnsPerHost,
|
|
|
|
|
|
MaxConnsPerHost: opts.MaxConnsPerHost, // 0 表示无限制
|
|
|
|
|
|
IdleConnTimeout: defaultIdleConnTimeout,
|
|
|
|
|
|
ResponseHeaderTimeout: opts.ResponseHeaderTimeout,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if opts.InsecureSkipVerify {
|
2026-01-06 11:36:38 +08:00
|
|
|
|
// 安全要求:禁止跳过证书验证,避免中间人攻击。
|
|
|
|
|
|
return nil, fmt.Errorf("insecure_skip_verify is not allowed; install a trusted certificate instead")
|
2025-12-31 08:50:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
proxyURL := strings.TrimSpace(opts.ProxyURL)
|
|
|
|
|
|
if proxyURL == "" {
|
|
|
|
|
|
return transport, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parsed, err := url.Parse(proxyURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 11:43:58 +08:00
|
|
|
|
if err := proxyutil.ConfigureTransportProxy(transport, parsed); err != nil {
|
|
|
|
|
|
return nil, err
|
2025-12-31 08:50:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return transport, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildClientKey(opts Options) string {
|
2026-01-03 10:52:24 +08:00
|
|
|
|
return fmt.Sprintf("%s|%s|%s|%t|%t|%t|%t|%d|%d|%d",
|
2025-12-31 08:50:12 +08:00
|
|
|
|
strings.TrimSpace(opts.ProxyURL),
|
|
|
|
|
|
opts.Timeout.String(),
|
|
|
|
|
|
opts.ResponseHeaderTimeout.String(),
|
|
|
|
|
|
opts.InsecureSkipVerify,
|
|
|
|
|
|
opts.ProxyStrict,
|
2026-01-03 10:52:24 +08:00
|
|
|
|
opts.ValidateResolvedIP,
|
|
|
|
|
|
opts.AllowPrivateHosts,
|
2025-12-31 08:50:12 +08:00
|
|
|
|
opts.MaxIdleConns,
|
|
|
|
|
|
opts.MaxIdleConnsPerHost,
|
|
|
|
|
|
opts.MaxConnsPerHost,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-01-03 10:52:24 +08:00
|
|
|
|
|
|
|
|
|
|
type validatedTransport struct {
|
|
|
|
|
|
base http.RoundTripper
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (t *validatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
|
|
if req != nil && req.URL != nil {
|
|
|
|
|
|
host := strings.TrimSpace(req.URL.Hostname())
|
|
|
|
|
|
if host != "" {
|
|
|
|
|
|
if err := urlvalidator.ValidateResolvedIP(host); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return t.base.RoundTrip(req)
|
|
|
|
|
|
}
|