win 5de1618e08 fix: Node.js TLS 代理动态识别上游主机
- Go: 通过 X-Forwarded-Host 传递原始目标主机给 Node.js 代理
- Node.js: 读取 X-Forwarded-Host 动态连接到正确的上游主机
- 所有 HTTPS 上游请求统一走代理,不再固定绑定 api.anthropic.com
- Gemini/Sora 等不同上游自动识别,无需手动配置
2026-03-25 11:37:27 +08:00

250 lines
8.3 KiB
JavaScript
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.

'use strict';
const http = require('http');
const https = require('https');
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,
};
process.stderr.write(JSON.stringify(entry) + '\n');
};
// ─── 健康检查路径 ─────────────────────────────────────────
const HEALTH_PATH = '/__health';
// ─── 通过 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); // 清除超时
// 如果头之后有残余数据,先 unshift 回去
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);
});
}
// ─── 构建上游请求选项 ──────────────────────────────────────
function buildUpstreamOptions(req) {
// 动态确定上游主机:优先使用 X-Forwarded-Host回退到 UPSTREAM_HOST 配置
const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST;
// 复制头,重写 host 为实际目标
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 {
hostname: targetHost,
port: 443,
path: req.url,
method: req.method,
headers,
// 关键:不设置任何自定义 TLS 选项
// 让 Node.js 使用默认 TLS stack → JA3/JA4 天然匹配
servername: targetHost, // SNI
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);
opts.socket = socket;
opts.agent = false; // 使用自定义 socket不走连接池
proxyReq = https.request(opts);
} catch (err) {
log('error', 'proxy tunnel failed', { error: err.message });
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) => {
// 过滤 hop-by-hop 头
const responseHeaders = { ...proxyRes.headers };
delete responseHeaders['connection'];
delete responseHeaders['keep-alive'];
delete responseHeaders['transfer-encoding']; // Node.js 会自动处理
res.writeHead(proxyRes.statusCode, responseHeaders);
// SSE / 流式透传:逐块 pipe
proxyRes.pipe(res, { end: true });
proxyRes.on('error', (err) => {
log('error', 'upstream response error', { error: err.message });
res.end();
});
});
// 上游连接错误
proxyReq.on('error', (err) => {
log('error', 'upstream request error', {
error: err.message,
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 });
proxyReq.destroy(new Error('upstream timeout'));
});
// 客户端断开时中止上游请求
req.on('close', () => {
if (!proxyReq.destroyed) {
proxyReq.destroy();
}
});
// 将客户端请求体 pipe 到上游
req.pipe(proxyReq, { end: true });
}
// ─── 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(),
}));
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' }));
}
});
});
// SSE 长连接:禁用 server 级超时(由上游控制)
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`);
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) });
});