'use strict'; const http = require('http'); const https = require('https'); const http2 = require('http2'); 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'; // ─── 协议缓存:记录哪些主机需要 H2 ────────────────────── // 首次请求用 H1,如果秒挂(socket hang up < 2s)自动切 H2 并缓存 const h2Hosts = new Set(); // ─── H2 会话池 ────────────────────────────────────────── 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}`); 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; } // ─── CONNECT 隧道 ──────────────────────────────────────── function connectViaProxy(proxyUrl, targetHost, targetPort) { return new Promise((resolve, reject) => { const proxy = new URL(proxyUrl); const conn = net.connect(parseInt(proxy.port || '80', 10), proxy.hostname, () => { const auth = proxy.username ? `Proxy-Authorization: Basic ${Buffer.from( `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password || '')}` ).toString('base64')}\r\n` : ''; conn.write( `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\n` + `Host: ${targetHost}:${targetPort}\r\n` + 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 code = parseInt(buf.split(' ')[1], 10); if (code === 200) { conn.setTimeout(0); const rest = buf.slice(idx + 4); if (rest.length > 0) conn.unshift(Buffer.from(rest)); resolve(conn); } else { conn.destroy(); reject(new Error(`CONNECT failed: ${code}`)); } }; conn.on('data', onData); }); } // ─── H1 代理 ───────────────────────────────────────────── function proxyViaH1(targetHost, req, res) { return new Promise((resolve) => { 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, }; const startTime = Date.now(); let proxyReq; const doRequest = (requestOpts) => { proxyReq = https.request(requestOpts); proxyReq.on('response', (proxyRes) => { log('info', 'proxy_response', { host: targetHost, status: proxyRes.statusCode, path: req.url, proto: 'h1' }); const rh = { ...proxyRes.headers }; delete rh['connection']; delete rh['keep-alive']; res.writeHead(proxyRes.statusCode, rh); proxyRes.pipe(res, { end: true }); proxyRes.on('error', (e) => { log('error', 'h1_response_error', { error: e.message }); res.end(); }); resolve('ok'); }); proxyReq.on('error', (err) => { const elapsed = Date.now() - startTime; // socket hang up < 2 秒 = 服务器拒绝 H1,切换到 H2 if (err.message === 'socket hang up' && elapsed < 2000) { log('info', 'h1_rejected_switching_to_h2', { host: targetHost, elapsed }); h2Hosts.add(targetHost); proxyViaH2(targetHost, req, res); resolve('h2_fallback'); return; } log('error', 'h1_upstream_error', { error: err.message, host: targetHost, path: req.url }); if (!res.headersSent) { res.writeHead(502, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'upstream_error', message: err.message })); } resolve('error'); }); proxyReq.on('timeout', () => proxyReq.destroy(new Error('upstream timeout'))); req.on('close', () => { if (!proxyReq.destroyed) proxyReq.destroy(); }); req.pipe(proxyReq, { end: true }); }; if (UPSTREAM_PROXY) { connectViaProxy(UPSTREAM_PROXY, targetHost, 443) .then((socket) => { opts.socket = socket; opts.agent = false; doRequest(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' })); } resolve('error'); }); } else { doRequest(opts); } }); } // ─── H2 代理 ───────────────────────────────────────────── function proxyViaH2(targetHost, req, res) { try { const session = getH2Session(targetHost); const headers = {}; // 只拷贝合法的 H2 头(跳过 H1 专用头和连接头) const skipHeaders = new Set([ 'host', 'connection', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'upgrade', 'x-forwarded-host', 'http2-settings', ]); for (const [k, v] of Object.entries(req.headers)) { if (!skipHeaders.has(k.toLowerCase())) { headers[k] = v; } } // H2 伪头 headers[':method'] = req.method; headers[':path'] = req.url; headers[':authority'] = targetHost; headers[':scheme'] = 'https'; const h2Stream = session.request(headers); let responded = false; h2Stream.on('response', (h2Headers) => { responded = true; const status = h2Headers[':status'] || 502; const respHeaders = {}; for (const [k, v] of Object.entries(h2Headers)) { if (!k.startsWith(':')) respHeaders[k] = v; } log('info', 'proxy_response', { host: targetHost, status, path: req.url, proto: 'h2' }); res.writeHead(status, respHeaders); h2Stream.pipe(res, { end: true }); }); h2Stream.on('error', (err) => { log('error', 'h2_stream_error', { error: err.message, host: targetHost, path: req.url }); h2Sessions.delete(targetHost); // 清理坏 session if (!responded && !res.headersSent) { res.writeHead(502, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'h2_error', message: err.message })); } }); h2Stream.on('close', () => { if (!responded && !res.headersSent) { log('warn', 'h2_stream_closed_no_response', { host: targetHost, path: req.url }); res.writeHead(502, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'h2_no_response' })); } }); // 超时 h2Stream.setTimeout(CONNECT_TIMEOUT, () => { log('warn', 'h2_timeout', { host: targetHost, path: req.url }); h2Stream.close(); }); req.on('close', () => { if (!h2Stream.destroyed) h2Stream.close(); }); // pipe 请求体 req.pipe(h2Stream, { end: true }); } catch (err) { log('error', 'h2_proxy_exception', { error: err.message, host: targetHost }); h2Sessions.delete(targetHost); if (!res.headersSent) { res.writeHead(502, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'h2_exception', message: err.message })); } } } // ─── 请求入口 ───────────────────────────────────────────── 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 的主机直接走 H2 if (h2Hosts.has(targetHost)) { proxyViaH2(targetHost, req, res); return; } // 首次请求走 H1,如果秒挂自动切 H2 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(), h2Hosts: [...h2Hosts], })); return; } proxyRequest(req, res).catch((err) => { log('error', 'unhandled_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, }); }); let shuttingDown = false; function shutdown(signal) { if (shuttingDown) return; shuttingDown = true; log('info', `received ${signal}, shutting down`); for (const s of h2Sessions.values()) s.close(); h2Sessions.clear(); server.close(() => 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', { error: err.message, stack: err.stack })); process.on('unhandledRejection', (r) => log('error', 'unhandled_rejection', { error: String(r) }));