'use strict'; 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'; const UPSTREAM_PROXY = process.env.UPSTREAM_PROXY || ''; const CONNECT_TIMEOUT = parseInt(process.env.CONNECT_TIMEOUT || '30000', 10); const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '600000', 10); // ─── 日志 ─────────────────────────────────────────────── const log = (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) => { const proxy = new URL(proxyUrl); const proxyPort = parseInt(proxy.port || '80', 10); const conn = net.connect(proxyPort, proxy.hostname, () => { const connectReq = `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 || '')}` ).toString('base64')}\r\n` : ''; conn.write(connectReq + auth + '\r\n'); }); conn.once('error', reject); conn.setTimeout(CONNECT_TIMEOUT, () => { 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; 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); const remainder = buf.slice(idx + 4); if (remainder.length > 0) { conn.unshift(Buffer.from(remainder)); } resolve(conn); } else { conn.destroy(); reject(new Error(`proxy CONNECT failed: ${statusLine}`)); } }; conn.on('data', onData); }); } // ─── TLS + ALPN 探测:判断上游支持 h2 还是 http/1.1 ───────── const alpnCache = new Map(); 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; delete headers['x-forwarded-host']; delete headers['connection']; delete headers['keep-alive']; delete headers['proxy-connection']; delete headers['transfer-encoding']; const opts = { hostname: targetHost, port: 443, path: req.url, method: req.method, headers, servername: targetHost, timeout: CONNECT_TIMEOUT, }; let proxyReq; if (UPSTREAM_PROXY) { try { const socket = await connectViaProxy(UPSTREAM_PROXY, targetHost, 443); opts.socket = socket; opts.agent = false; proxyReq = https.request(opts); } catch (err) { 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 })); } return; } } else { proxyReq = https.request(opts); } proxyReq.on('response', (proxyRes) => { const responseHeaders = { ...proxyRes.headers }; delete responseHeaders['connection']; delete responseHeaders['keep-alive']; delete responseHeaders['transfer-encoding']; res.writeHead(proxyRes.statusCode, responseHeaders); proxyRes.pipe(res, { end: true }); proxyRes.on('error', (err) => { log('error', 'upstream response error', { error: err.message, host: targetHost }); res.end(); }); }); proxyReq.on('error', (err) => { 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', { host: targetHost, path: req.url }); proxyReq.destroy(new Error('upstream timeout')); }); req.on('close', () => { if (!proxyReq.destroyed) proxyReq.destroy(); }); 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({ status: 'ok', upstream: UPSTREAM_HOST, node: process.version, openssl: process.versions.openssl, uptime: process.uptime(), h2Sessions: h2Sessions.size, alpnCache: Object.fromEntries(alpnCache), })); return; } proxyRequest(req, res).catch((err) => { log('error', 'unhandled proxy error', { error: err.message }); if (!res.headersSent) { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'internal_error' })); } }); }); server.timeout = 0; server.keepAliveTimeout = IDLE_TIMEOUT; server.headersTimeout = 60000; server.listen(LISTEN_PORT, LISTEN_HOST, () => { log('info', 'node-tls-proxy started', { listen: `${LISTEN_HOST}:${LISTEN_PORT}`, upstream: `${UPSTREAM_HOST}:443`, proxy: UPSTREAM_PROXY || '(direct)', node: process.version, openssl: process.versions.openssl, features: ['dynamic-host', 'h2-auto', 'alpn-probe'], }); }); // ─── 优雅关闭 ───────────────────────────────────────────── let shuttingDown = false; 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 }); }); process.on('unhandledRejection', (reason) => { log('error', 'unhandled rejection', { error: String(reason) }); });