fix: 去掉 H2/ALPN 复杂度,回到纯 https.request + 动态主机 + 响应日志
Some checks failed
CI / test (push) Failing after 1m24s
CI / golangci-lint (push) Failing after 4s
Security Scan / backend-security (push) Failing after 4s
Security Scan / frontend-security (push) Failing after 4s

This commit is contained in:
win 2026-03-22 02:03:19 +08:00
parent 47066d4111
commit 4ea945bb56

View File

@ -2,8 +2,6 @@
const http = require('http');
const https = require('https');
const http2 = require('http2');
const tls = require('tls');
const net = require('net');
// ─── 配置 ───────────────────────────────────────────────
@ -22,34 +20,6 @@ const log = (level, msg, extra = {}) => {
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) => {
@ -101,94 +71,14 @@ function connectViaProxy(proxyUrl, targetHost, targetPort) {
});
}
// ─── TLS + ALPN 探测:判断上游支持 h2 还是 http/1.1 ─────────
const alpnCache = new Map();
// ─── 代理请求 ───────────────────────────────────────────
async function proxyRequest(req, res) {
// 动态确定上游主机
const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST;
function probeALPN(host) {
const cached = alpnCache.get(host);
if (cached) return Promise.resolve(cached);
log('info', 'proxy_request', { host: targetHost, method: req.method, path: req.url });
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'];
@ -205,6 +95,7 @@ async function proxyViaH1(targetHost, req, res) {
headers,
servername: targetHost,
timeout: CONNECT_TIMEOUT,
// 不设置任何自定义 TLS 选项 → Node.js 默认 TLS stack → JA3/JA4 天然匹配
};
let proxyReq;
@ -227,11 +118,17 @@ async function proxyViaH1(targetHost, req, res) {
proxyReq = https.request(opts);
}
// 上游响应
proxyReq.on('response', (proxyRes) => {
const responseHeaders = { ...proxyRes.headers };
delete responseHeaders['connection'];
delete responseHeaders['keep-alive'];
delete responseHeaders['transfer-encoding'];
log('info', 'proxy_response', {
host: targetHost,
status: proxyRes.statusCode,
path: req.url,
});
res.writeHead(proxyRes.statusCode, responseHeaders);
proxyRes.pipe(res, { end: true });
@ -242,8 +139,14 @@ async function proxyViaH1(targetHost, req, res) {
});
});
// 上游连接错误
proxyReq.on('error', (err) => {
log('error', 'h1 upstream error', { error: err.message, host: targetHost, path: req.url, method: req.method });
log('error', 'upstream request 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 }));
@ -262,22 +165,6 @@ async function proxyViaH1(targetHost, req, res) {
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) {
@ -288,8 +175,6 @@ const server = http.createServer((req, res) => {
node: process.version,
openssl: process.versions.openssl,
uptime: process.uptime(),
h2Sessions: h2Sessions.size,
alpnCache: Object.fromEntries(alpnCache),
}));
return;
}
@ -314,7 +199,6 @@ server.listen(LISTEN_PORT, LISTEN_HOST, () => {
proxy: UPSTREAM_PROXY || '(direct)',
node: process.version,
openssl: process.versions.openssl,
features: ['dynamic-host', 'h2-auto', 'alpn-probe'],
});
});
@ -324,11 +208,6 @@ 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);