fix: 去掉 H2/ALPN 复杂度,回到纯 https.request + 动态主机 + 响应日志
This commit is contained in:
parent
47066d4111
commit
4ea945bb56
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user