Files
ds2api/internal/deepseek/transport/transport.go
Jason.li 8ae2ea10c8 feat(proxy): add proxy IP management and account routing
Add admin CRUD and connectivity checks for SOCKS5/SOCKS5H proxy nodes.

Allow accounts to bind to a proxy, route DeepSeek requests through the selected node, and expose proxy management in the admin UI.
2026-04-07 14:16:13 +08:00

114 lines
3.1 KiB
Go

package transport
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
)
type Doer interface {
Do(req *http.Request) (*http.Response, error)
}
type DialContextFunc func(ctx context.Context, network, addr string) (net.Conn, error)
type Client struct {
http *http.Client
}
func New(timeout time.Duration) *Client {
return NewWithDialContext(timeout, nil)
}
func NewWithDialContext(timeout time.Duration, dialContext DialContextFunc) *Client {
useEnvProxy := dialContext == nil
if dialContext == nil {
dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
}
base := &http.Transport{
ForceAttemptHTTP2: false,
MaxIdleConns: 200,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DialContext: dialContext,
DialTLSContext: safariTLSDialer(dialContext),
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
}
if useEnvProxy {
base.Proxy = http.ProxyFromEnvironment
}
return &Client{http: &http.Client{Timeout: timeout, Transport: base}}
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.http.Do(req)
}
func NewFallbackClient(timeout time.Duration, dialContext DialContextFunc) *http.Client {
useEnvProxy := dialContext == nil
if dialContext == nil {
dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
}
base := &http.Transport{
ForceAttemptHTTP2: false,
MaxIdleConns: 200,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DialContext: dialContext,
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
}
if useEnvProxy {
base.Proxy = http.ProxyFromEnvironment
}
return &http.Client{Timeout: timeout, Transport: base}
}
func safariTLSDialer(dialContext DialContextFunc) func(ctx context.Context, network, addr string) (net.Conn, error) {
if dialContext == nil {
dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
}
return func(ctx context.Context, network, addr string) (net.Conn, error) {
plainConn, err := dialContext(ctx, network, addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
uCfg := &utls.Config{ServerName: host}
uConn := utls.UClient(plainConn, uCfg, utls.HelloSafari_Auto)
if err := forceHTTP11ALPN(uConn); err != nil {
_ = plainConn.Close()
return nil, err
}
err = uConn.HandshakeContext(ctx)
if err != nil {
_ = plainConn.Close()
return nil, err
}
if negotiated := uConn.ConnectionState().NegotiatedProtocol; negotiated != "" && negotiated != "http/1.1" {
_ = uConn.Close()
return nil, fmt.Errorf("unexpected ALPN protocol negotiated: %s", negotiated)
}
return uConn, nil
}
}
func forceHTTP11ALPN(uConn *utls.UConn) error {
if err := uConn.BuildHandshakeState(); err != nil {
return err
}
for _, ext := range uConn.Extensions {
alpnExt, ok := ext.(*utls.ALPNExtension)
if !ok {
continue
}
alpnExt.AlpnProtocols = []string{"http/1.1"}
return nil
}
return nil
}