diff --git a/tools/node-tls-proxy/proxy.js b/tools/node-tls-proxy/proxy.js index 341a9e8d..5d365275 100644 --- a/tools/node-tls-proxy/proxy.js +++ b/tools/node-tls-proxy/proxy.js @@ -2,33 +2,54 @@ const http = require('http'); const https = require('https'); +const http2 = require('http2'); +const tls = require('tls'); const net = require('net'); // ─── 配置 ─────────────────────────────────────────────── const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'api.anthropic.com'; const LISTEN_PORT = parseInt(process.env.PROXY_PORT || '3456', 10); const LISTEN_HOST = process.env.PROXY_HOST || '127.0.0.1'; -// 可选:上游 HTTPS 代理(仅支持 HTTP CONNECT 隧道) const UPSTREAM_PROXY = process.env.UPSTREAM_PROXY || ''; -// 连接超时(ms) const CONNECT_TIMEOUT = parseInt(process.env.CONNECT_TIMEOUT || '30000', 10); -// 空闲超时(ms)— 对 SSE 长连接要足够大 const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '600000', 10); // ─── 日志 ─────────────────────────────────────────────── const log = (level, msg, extra = {}) => { - const entry = { - time: new Date().toISOString(), - level, - msg, - ...extra, - }; + const entry = { time: new Date().toISOString(), level, msg, ...extra }; process.stderr.write(JSON.stringify(entry) + '\n'); }; -// ─── 健康检查路径 ───────────────────────────────────────── const HEALTH_PATH = '/__health'; +// ─── HTTP/2 会话缓存 ───────────────────────────────────── +// 按 host 缓存 h2 session,避免每个请求都建新连接 +const h2Sessions = new Map(); + +function getH2Session(host) { + const existing = h2Sessions.get(host); + if (existing && !existing.closed && !existing.destroyed) { + return existing; + } + const session = http2.connect(`https://${host}`, { + // 不设置自定义 TLS 选项 → 用 Node.js 默认 TLS stack + }); + session.on('error', (err) => { + log('warn', 'h2 session error', { host, error: err.message }); + h2Sessions.delete(host); + }); + session.on('close', () => { + h2Sessions.delete(host); + }); + // 空闲超时自动关闭 + session.setTimeout(IDLE_TIMEOUT, () => { + session.close(); + h2Sessions.delete(host); + }); + h2Sessions.set(host, session); + return session; +} + // ─── 通过 HTTP 代理建立 CONNECT 隧道 ────────────────────── function connectViaProxy(proxyUrl, targetHost, targetPort) { return new Promise((resolve, reject) => { @@ -40,7 +61,6 @@ function connectViaProxy(proxyUrl, targetHost, targetPort) { `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\n` + `Host: ${targetHost}:${targetPort}\r\n`; - // 代理认证 const auth = proxy.username ? `Proxy-Authorization: Basic ${Buffer.from( `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password || '')}` @@ -55,20 +75,18 @@ function connectViaProxy(proxyUrl, targetHost, targetPort) { conn.destroy(new Error('proxy CONNECT timeout')); }); - // 读取代理响应 let buf = ''; const onData = (chunk) => { buf += chunk.toString(); const idx = buf.indexOf('\r\n\r\n'); - if (idx === -1) return; // 头还没完整 + if (idx === -1) return; conn.removeListener('data', onData); const statusLine = buf.slice(0, buf.indexOf('\r\n')); const statusCode = parseInt(statusLine.split(' ')[1], 10); if (statusCode === 200) { - conn.setTimeout(0); // 清除超时 - // 如果头之后有残余数据,先 unshift 回去 + conn.setTimeout(0); const remainder = buf.slice(idx + 4); if (remainder.length > 0) { conn.unshift(Buffer.from(remainder)); @@ -83,49 +101,122 @@ function connectViaProxy(proxyUrl, targetHost, targetPort) { }); } -// ─── 构建上游请求选项 ────────────────────────────────────── -function buildUpstreamOptions(req) { - // 动态确定上游主机:优先使用 X-Forwarded-Host,回退到 UPSTREAM_HOST 配置 - const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST; +// ─── TLS + ALPN 探测:判断上游支持 h2 还是 http/1.1 ───────── +const alpnCache = new Map(); - // 复制头,重写 host 为实际目标 +function probeALPN(host) { + const cached = alpnCache.get(host); + if (cached) return Promise.resolve(cached); + + return new Promise((resolve) => { + const socket = tls.connect(443, host, { + ALPNProtocols: ['h2', 'http/1.1'], + servername: host, + timeout: 5000, + }); + socket.once('secureConnect', () => { + const proto = socket.alpnProtocol || 'http/1.1'; + alpnCache.set(host, proto); + socket.destroy(); + resolve(proto); + }); + socket.once('error', () => { + alpnCache.set(host, 'http/1.1'); + socket.destroy(); + resolve('http/1.1'); + }); + socket.once('timeout', () => { + alpnCache.set(host, 'http/1.1'); + socket.destroy(); + resolve('http/1.1'); + }); + }); +} + +// ─── HTTP/2 代理请求 ───────────────────────────────────── +function proxyViaH2(targetHost, req, res) { + const session = getH2Session(targetHost); + + // 构建 h2 请求头 + const headers = { ...req.headers }; + headers[':method'] = req.method; + headers[':path'] = req.url; + headers[':authority'] = targetHost; + headers[':scheme'] = 'https'; + // 移除 HTTP/1.1 专用头 + delete headers['host']; + delete headers['connection']; + delete headers['keep-alive']; + delete headers['proxy-connection']; + delete headers['transfer-encoding']; + delete headers['x-forwarded-host']; + + const h2Req = session.request(headers); + + h2Req.on('response', (h2Headers) => { + const status = h2Headers[':status'] || 502; + // 过滤 h2 伪头 + const respHeaders = {}; + for (const [k, v] of Object.entries(h2Headers)) { + if (!k.startsWith(':')) { + respHeaders[k] = v; + } + } + res.writeHead(status, respHeaders); + h2Req.pipe(res, { end: true }); + }); + + h2Req.on('error', (err) => { + log('error', 'h2 upstream error', { error: err.message, host: targetHost, path: req.url }); + // h2 session 可能坏了,清理缓存 + h2Sessions.delete(targetHost); + if (!res.headersSent) { + res.writeHead(502, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ error: 'h2_upstream_error', message: err.message })); + } + }); + + h2Req.setTimeout(CONNECT_TIMEOUT, () => { + h2Req.close(); + }); + + req.on('close', () => { + if (!h2Req.destroyed) h2Req.close(); + }); + + req.pipe(h2Req, { end: true }); +} + +// ─── HTTP/1.1 代理请求 ──────────────────────────────────── +async function proxyViaH1(targetHost, req, res) { const headers = { ...req.headers }; headers.host = targetHost; - // 移除内部头和 hop-by-hop 头 delete headers['x-forwarded-host']; delete headers['connection']; delete headers['keep-alive']; delete headers['proxy-connection']; delete headers['transfer-encoding']; - return { + const opts = { hostname: targetHost, port: 443, path: req.url, method: req.method, headers, - // 关键:不设置任何自定义 TLS 选项 - // 让 Node.js 使用默认 TLS stack → JA3/JA4 天然匹配 - servername: targetHost, // SNI + servername: targetHost, timeout: CONNECT_TIMEOUT, }; -} - -// ─── 代理请求 ─────────────────────────────────────────── -async function proxyRequest(req, res) { - const opts = buildUpstreamOptions(req); let proxyReq; if (UPSTREAM_PROXY) { - // 通过代理建立隧道,然后 TLS 握手 try { - const socket = await connectViaProxy(UPSTREAM_PROXY, UPSTREAM_HOST, 443); + const socket = await connectViaProxy(UPSTREAM_PROXY, targetHost, 443); opts.socket = socket; - opts.agent = false; // 使用自定义 socket,不走连接池 + opts.agent = false; proxyReq = https.request(opts); } catch (err) { - log('error', 'proxy tunnel failed', { error: err.message }); + log('error', 'proxy tunnel failed', { error: err.message, host: targetHost }); if (!res.headersSent) { res.writeHead(502, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'proxy_tunnel_error', message: err.message })); @@ -136,58 +227,59 @@ async function proxyRequest(req, res) { proxyReq = https.request(opts); } - // 上游响应 proxyReq.on('response', (proxyRes) => { - // 过滤 hop-by-hop 头 const responseHeaders = { ...proxyRes.headers }; delete responseHeaders['connection']; delete responseHeaders['keep-alive']; - delete responseHeaders['transfer-encoding']; // Node.js 会自动处理 + delete responseHeaders['transfer-encoding']; res.writeHead(proxyRes.statusCode, responseHeaders); - - // SSE / 流式透传:逐块 pipe proxyRes.pipe(res, { end: true }); proxyRes.on('error', (err) => { - log('error', 'upstream response error', { error: err.message }); + log('error', 'upstream response error', { error: err.message, host: targetHost }); res.end(); }); }); - // 上游连接错误 proxyReq.on('error', (err) => { - log('error', 'upstream request error', { - error: err.message, - path: req.url, - method: req.method, - }); + log('error', 'h1 upstream error', { error: err.message, host: targetHost, path: req.url, method: req.method }); if (!res.headersSent) { res.writeHead(502, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'upstream_error', message: err.message })); } }); - // 超时 proxyReq.on('timeout', () => { - log('warn', 'upstream request timeout', { path: req.url }); + log('warn', 'upstream request timeout', { host: targetHost, path: req.url }); proxyReq.destroy(new Error('upstream timeout')); }); - // 客户端断开时中止上游请求 req.on('close', () => { - if (!proxyReq.destroyed) { - proxyReq.destroy(); - } + if (!proxyReq.destroyed) proxyReq.destroy(); }); - // 将客户端请求体 pipe 到上游 req.pipe(proxyReq, { end: true }); } +// ─── 代理请求入口 ───────────────────────────────────────── +async function proxyRequest(req, res) { + const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST; + + log('info', 'proxy_request', { host: targetHost, method: req.method, path: req.url }); + + // 探测上游支持的协议,选择 h2 或 h1 + const proto = await probeALPN(targetHost); + + if (proto === 'h2') { + proxyViaH2(targetHost, req, res); + } else { + await proxyViaH1(targetHost, req, res); + } +} + // ─── HTTP 服务器 ───────────────────────────────────────── const server = http.createServer((req, res) => { - // 健康检查 if (req.url === HEALTH_PATH) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ @@ -196,6 +288,8 @@ const server = http.createServer((req, res) => { node: process.version, openssl: process.versions.openssl, uptime: process.uptime(), + h2Sessions: h2Sessions.size, + alpnCache: Object.fromEntries(alpnCache), })); return; } @@ -209,7 +303,6 @@ const server = http.createServer((req, res) => { }); }); -// SSE 长连接:禁用 server 级超时(由上游控制) server.timeout = 0; server.keepAliveTimeout = IDLE_TIMEOUT; server.headersTimeout = 60000; @@ -221,6 +314,7 @@ server.listen(LISTEN_PORT, LISTEN_HOST, () => { proxy: UPSTREAM_PROXY || '(direct)', node: process.version, openssl: process.versions.openssl, + features: ['dynamic-host', 'h2-auto', 'alpn-probe'], }); }); @@ -230,17 +324,20 @@ function shutdown(signal) { if (shuttingDown) return; shuttingDown = true; log('info', `received ${signal}, shutting down`); + // 关闭所有 h2 session + for (const [host, session] of h2Sessions) { + session.close(); + } + h2Sessions.clear(); server.close(() => { log('info', 'server closed'); process.exit(0); }); - // 强制退出兜底 setTimeout(() => process.exit(1), 5000); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); -// 未捕获异常不要崩进程 process.on('uncaughtException', (err) => { log('error', 'uncaught exception', { error: err.message, stack: err.stack }); });