120 lines
3.8 KiB
Go
120 lines
3.8 KiB
Go
package oci
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/proxy"
|
|
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
)
|
|
|
|
// ProxySpec 是租户关联的出站代理;密码已解密,仅在进程内传递。
|
|
type ProxySpec struct {
|
|
Type string // "socks5" / "http" / "https"
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
// proxyClientTimeout 与 SDK 默认 HTTPClient 超时保持一致。
|
|
const proxyClientTimeout = 60 * time.Second
|
|
|
|
// 阶段超时对齐 SDK 直连 Transport 模板(transport_template_provider):连不上的
|
|
// 代理快速失败,而不是拖满总超时;responses 直通去掉总超时后这是建立阶段的兜底之一。
|
|
const (
|
|
proxyDialTimeout = 30 * time.Second
|
|
proxyTLSHandshakeTimeout = 10 * time.Second
|
|
)
|
|
|
|
// 连接池参数:PerHost 须 > 批量删除并发数(service 层 16)——并发拨号竞速时
|
|
// Transport 会投机新建连接,池上限过紧会让这些连接用一次即被丢弃(churn);
|
|
// IdleConnTimeout 显式关闭空闲连接(零值=永不关,只能等远端 ~65s 断开,连接会堆积)。
|
|
const (
|
|
proxyMaxIdleConns = 128
|
|
proxyMaxIdleConnsPerHost = 32
|
|
proxyIdleConnTimeout = 90 * time.Second
|
|
)
|
|
|
|
// proxyClients 按代理配置复用 http.Client。SDK client 每次操作新建,
|
|
// 若 Transport 也随之新建则连接零复用:每个请求都要付完整的
|
|
// TCP+SOCKS/CONNECT+TLS 握手(经代理 3+ 个 RTT),批量操作被拖到分钟级。
|
|
var proxyClients sync.Map // ProxySpec(值) → *http.Client
|
|
|
|
// applyProxy 在 SDK client 构造后统一挂出站代理;未关联代理时不动默认配置。
|
|
// 所有 New*ClientWithConfigurationProvider 调用点构造成功后都必须经过这里。
|
|
func applyProxy(base *common.BaseClient, cred Credentials) {
|
|
if hc := proxyHTTPClient(cred.Proxy); hc != nil {
|
|
base.HTTPClient = hc
|
|
}
|
|
}
|
|
|
|
// proxyHTTPClient 按代理配置取共享 http.Client;nil 或非法配置返回 nil(走直连)。
|
|
// 同配置复用同一实例(连接池随之复用);调用方只可包装、不得改写其字段。
|
|
func proxyHTTPClient(p *ProxySpec) *http.Client {
|
|
if p == nil || p.Host == "" || p.Port <= 0 {
|
|
return nil
|
|
}
|
|
if v, ok := proxyClients.Load(*p); ok {
|
|
return v.(*http.Client)
|
|
}
|
|
tr := transportFor(p)
|
|
if tr == nil {
|
|
return nil
|
|
}
|
|
v, _ := proxyClients.LoadOrStore(*p, &http.Client{Transport: tr, Timeout: proxyClientTimeout})
|
|
return v.(*http.Client)
|
|
}
|
|
|
|
// HTTPClientFor 供面板自身请求(如代理出口地理探测)复用与租户 SDK
|
|
// 完全一致的代理链路;nil 或非法配置返回 nil。
|
|
func HTTPClientFor(p *ProxySpec) *http.Client {
|
|
return proxyHTTPClient(p)
|
|
}
|
|
|
|
// pooledTransport 连接池参数统一的 Transport 骨架;阶段超时见常量注释。
|
|
func pooledTransport() *http.Transport {
|
|
return &http.Transport{
|
|
TLSHandshakeTimeout: proxyTLSHandshakeTimeout,
|
|
MaxIdleConns: proxyMaxIdleConns,
|
|
MaxIdleConnsPerHost: proxyMaxIdleConnsPerHost,
|
|
IdleConnTimeout: proxyIdleConnTimeout,
|
|
}
|
|
}
|
|
|
|
// transportFor 构造代理 Transport:http / https 走 CONNECT,socks5 走拨号器。
|
|
func transportFor(p *ProxySpec) *http.Transport {
|
|
addr := fmt.Sprintf("%s:%d", p.Host, p.Port)
|
|
dialer := &net.Dialer{Timeout: proxyDialTimeout}
|
|
if p.Type == "http" || p.Type == "https" {
|
|
u := &url.URL{Scheme: p.Type, Host: addr}
|
|
if p.Username != "" {
|
|
u.User = url.UserPassword(p.Username, p.Password)
|
|
}
|
|
tr := pooledTransport()
|
|
tr.Proxy = http.ProxyURL(u)
|
|
tr.DialContext = dialer.DialContext
|
|
return tr
|
|
}
|
|
var auth *proxy.Auth
|
|
if p.Username != "" {
|
|
auth = &proxy.Auth{User: p.Username, Password: p.Password}
|
|
}
|
|
d, err := proxy.SOCKS5("tcp", addr, auth, dialer)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
cd, ok := d.(proxy.ContextDialer)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
tr := pooledTransport()
|
|
tr.DialContext = cd.DialContext
|
|
return tr
|
|
}
|