AIClient-2-API/tls-sidecar/main.go
hex2077 3989add60b chore: 更新版本至2.10.4并移除iFlow协议支持
- 更新VERSION文件至2.10.4
- 在tls-sidecar中强制日志输出到stdout,避免Node.js误判
- 更新程序时排除tls-sidecar目录
- 增强GrokConverter的模型列表转换逻辑,支持更多数据结构
2026-03-03 01:15:43 +08:00

424 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
"golang.org/x/net/proxy"
)
// ──────────────────────────────────────────────
// TLS Sidecar — Go uTLS reverse proxy
//
// Node.js 发请求到 http://127.0.0.1:<port>
// 通过以下自定义 Header 传递目标信息:
// X-Target-Url: 实际目标 URL必填
// X-Proxy-Url: 上游代理(可选,支持 http/socks5
//
// 所有其他 Header 原样转发给目标服务器。
// 响应(包括 SSE 流式)透传回 Node.js。
//
// uTLS 使用 Chrome 最新指纹ALPN 协商 h2/http1.1
// 根据服务器返回的 ALPN 自动选择 HTTP/2 或 HTTP/1.1 传输。
// ──────────────────────────────────────────────
const (
defaultPort = 9090
headerTarget = "X-Target-Url"
headerProxy = "X-Proxy-Url"
readTimeout = 30 * time.Second
writeTimeout = 0 // SSE 流式响应不设写超时(仅监听 localhost安全
idleTimeout = 120 * time.Second
)
// 全局 RoundTripper 缓存(按 proxyURL 分组,复用 H2 连接)
var (
rtCacheMu sync.Mutex
rtCache = make(map[string]*utlsRoundTripper)
)
func getOrCreateRT(proxyURL string) *utlsRoundTripper {
rtCacheMu.Lock()
defer rtCacheMu.Unlock()
if rt, ok := rtCache[proxyURL]; ok {
return rt
}
rt := newUTLSRoundTripper(proxyURL)
rtCache[proxyURL] = rt
return rt
}
// ──────────────── uTLS RoundTripper ────────────────
// 根据 ALPN 协商结果自动选择 H2 或 H1 传输
type utlsRoundTripper struct {
proxyURL string
mu sync.Mutex
h2Conns map[string]*http2.ClientConn // H2 连接缓存 (per host)
}
func newUTLSRoundTripper(proxyURL string) *utlsRoundTripper {
return &utlsRoundTripper{
proxyURL: proxyURL,
h2Conns: make(map[string]*http2.ClientConn),
}
}
func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
addr := req.URL.Host
if !strings.Contains(addr, ":") {
if req.URL.Scheme == "https" {
addr += ":443"
} else {
addr += ":80"
}
}
// 尝试复用已有的 H2 连接
rt.mu.Lock()
if cc, ok := rt.h2Conns[addr]; ok {
rt.mu.Unlock()
if cc.CanTakeNewRequest() {
resp, err := cc.RoundTrip(req)
if err == nil {
return resp, nil
}
// H2 连接已失效,清除缓存重建
log.Printf("[TLS-Sidecar] Cached H2 conn failed for %s: %v, reconnecting", addr, err)
}
rt.mu.Lock()
delete(rt.h2Conns, addr)
rt.mu.Unlock()
} else {
rt.mu.Unlock()
}
// 建立新的 uTLS 连接
conn, err := dialUTLS(req.Context(), "tcp", addr, rt.proxyURL)
if err != nil {
return nil, err
}
// 根据 ALPN 协商结果决定走 H2 还是 H1
alpn := conn.ConnectionState().NegotiatedProtocol
log.Printf("[TLS-Sidecar] Connected to %s, ALPN: %q", addr, alpn)
if alpn == "h2" {
// HTTP/2: 创建 H2 ClientConn
t2 := &http2.Transport{
StrictMaxConcurrentStreams: true,
AllowHTTP: false,
}
cc, err := t2.NewClientConn(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("h2 client conn: %w", err)
}
rt.mu.Lock()
rt.h2Conns[addr] = cc
rt.mu.Unlock()
return cc.RoundTrip(req)
}
// HTTP/1.1: 通过一次性 Transport 使用已建立的 TLS 连接
// DialTLSContext 返回已完成 TLS 握手的 connhttp.Transport 不会重复握手
used := false
t1 := &http.Transport{
DialTLSContext: func(ctx context.Context, network, a string) (net.Conn, error) {
if !used {
used = true
return conn, nil
}
// 后续连接走正常 uTLS dial
return dialUTLS(ctx, network, a, rt.proxyURL)
},
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 90 * time.Second,
}
resp, err := t1.RoundTrip(req)
if err != nil {
conn.Close()
t1.CloseIdleConnections()
}
return resp, err
}
func (rt *utlsRoundTripper) CloseIdleConnections() {
rt.mu.Lock()
defer rt.mu.Unlock()
for k, cc := range rt.h2Conns {
cc.Close()
delete(rt.h2Conns, k)
}
}
// ──────────────── Main ────────────────
func main() {
// 强制将日志输出到 Stdout避免 Node.js 侧将其误判为 Error
log.SetOutput(os.Stdout)
port := defaultPort
if p := os.Getenv("TLS_SIDECAR_PORT"); p != "" {
if v, err := strconv.Atoi(p); err == nil {
port = v
}
}
mux := http.NewServeMux()
mux.HandleFunc("/health", handleHealth)
mux.HandleFunc("/", handleProxy)
srv := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", port),
Handler: mux,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("[TLS-Sidecar] Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
log.Printf("[TLS-Sidecar] Listening on 127.0.0.1:%d (Chrome uTLS, H2+H1 auto)\n", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("[TLS-Sidecar] Fatal: %v", err)
}
}
// ──────────────── Health ────────────────
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, `{"status":"ok","tls":"utls-chrome-auto","protocols":"h2,http/1.1"}`)
}
// ──────────────── Proxy Handler ────────────────
func handleProxy(w http.ResponseWriter, r *http.Request) {
targetURL := r.Header.Get(headerTarget)
if targetURL == "" {
http.Error(w, `{"error":"missing X-Target-Url header"}`, http.StatusBadRequest)
return
}
proxyURL := r.Header.Get(headerProxy)
// Parse target
parsed, err := url.Parse(targetURL)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error":"invalid target url: %s"}`, err), http.StatusBadRequest)
return
}
// Build outgoing request
outReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error":"failed to create request: %s"}`, err), http.StatusInternalServerError)
return
}
// Copy headers (skip internal + hop-by-hop)
// 403 关键修复:彻底清理所有非浏览器标头,严格保持小写
for key, vals := range r.Header {
lk := strings.ToLower(key)
if lk == strings.ToLower(headerTarget) || lk == strings.ToLower(headerProxy) {
continue
}
// 移除所有代理、本地网络特征标头,防止 Cloudflare 识别
if lk == "connection" || lk == "keep-alive" || lk == "transfer-encoding" ||
lk == "te" || lk == "trailer" || lk == "upgrade" || lk == "host" ||
lk == "x-forwarded-for" || lk == "x-real-ip" || lk == "x-forwarded-proto" ||
lk == "x-forwarded-host" || lk == "via" || lk == "proxy-connection" ||
lk == "cf-connecting-ip" || lk == "true-client-ip" {
continue
}
// 直接通过 map 赋值,确保 Go 的 http2 栈能识别并以原始(小写)形式发出
outReq.Header[key] = vals
}
outReq.Host = parsed.Host
// 针对 Grok 的特殊处理:如果 Accept-Encoding 包含 br 且环境可能存在压缩协商问题
// 强制设置为标准的浏览器组合
if ae := outReq.Header["Accept-Encoding"]; len(ae) > 0 {
outReq.Header["Accept-Encoding"] = []string{"gzip, deflate, br, zstd"}
}
// Execute via uTLS RoundTripper
rt := getOrCreateRT(proxyURL)
resp, err := rt.RoundTrip(outReq)
if err != nil {
log.Printf("[TLS-Sidecar] RoundTrip error → %s: %v", parsed.Host, err)
http.Error(w, fmt.Sprintf(`{"error":"upstream request failed: %s"}`, err), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// Copy response headers
for key, vals := range resp.Header {
for _, v := range vals {
w.Header().Add(key, v)
}
}
w.WriteHeader(resp.StatusCode)
// Stream body (SSE-friendly: flush after every read)
flusher, canFlush := w.(http.Flusher)
buf := make([]byte, 32*1024)
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
if _, writeErr := w.Write(buf[:n]); writeErr != nil {
log.Printf("[TLS-Sidecar] Write error: %v", writeErr)
return
}
if canFlush {
flusher.Flush()
}
}
if readErr != nil {
if readErr != io.EOF {
log.Printf("[TLS-Sidecar] Read error: %v", readErr)
}
return
}
}
}
// ──────────────── uTLS Dial ────────────────
func dialUTLS(ctx context.Context, network, addr string, proxyURL string) (*utls.UConn, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
// TCP 连接(可能经过代理)
var rawConn net.Conn
if proxyURL != "" {
rawConn, err = dialViaProxy(ctx, network, addr, proxyURL)
} else {
var d net.Dialer
rawConn, err = d.DialContext(ctx, network, addr)
}
if err != nil {
return nil, fmt.Errorf("tcp dial failed: %w", err)
}
// uTLS 握手 — 使用 Chrome 最新自动指纹
// 403 错误通过保持标头小写和清理转发标头来解决
tlsConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
NextProtos: []string{"h2", "http/1.1"},
}, utls.HelloChrome_Auto)
// 握手超时
if deadline, ok := ctx.Deadline(); ok {
tlsConn.SetDeadline(deadline)
} else {
tlsConn.SetDeadline(time.Now().Add(15 * time.Second))
}
if err := tlsConn.Handshake(); err != nil {
rawConn.Close()
return nil, fmt.Errorf("utls handshake failed: %w", err)
}
// 握手完成,清除超时
tlsConn.SetDeadline(time.Time{})
return tlsConn, nil
}
// ──────────────── Proxy Dialer ────────────────
func dialViaProxy(ctx context.Context, network, addr string, proxyURL string) (net.Conn, error) {
parsed, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy url: %w", err)
}
switch strings.ToLower(parsed.Scheme) {
case "socks5", "socks5h", "socks4", "socks":
var auth *proxy.Auth
if parsed.User != nil {
auth = &proxy.Auth{
User: parsed.User.Username(),
}
auth.Password, _ = parsed.User.Password()
}
dialer, err := proxy.SOCKS5("tcp", parsed.Host, auth, &net.Dialer{
Timeout: 15 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("socks5 dialer: %w", err)
}
if ctxDialer, ok := dialer.(proxy.ContextDialer); ok {
return ctxDialer.DialContext(ctx, network, addr)
}
return dialer.Dial(network, addr)
case "http", "https":
proxyConn, err := net.DialTimeout("tcp", parsed.Host, 15*time.Second)
if err != nil {
return nil, fmt.Errorf("connect to http proxy: %w", err)
}
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", addr, addr)
if parsed.User != nil {
username := parsed.User.Username()
password, _ := parsed.User.Password()
cred := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", cred)
}
connectReq += "\r\n"
if _, err = proxyConn.Write([]byte(connectReq)); err != nil {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT write: %w", err)
}
buf := make([]byte, 4096)
n, err := proxyConn.Read(buf)
if err != nil {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT read: %w", err)
}
if !strings.Contains(string(buf[:n]), "200") {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT rejected: %s", strings.TrimSpace(string(buf[:n])))
}
return proxyConn, nil
default:
return nil, fmt.Errorf("unsupported proxy scheme: %s", parsed.Scheme)
}
}