fix: 去掉 H2/ALPN 复杂度,回到纯 https.request + 动态主机 + 响应日志
This commit is contained in:
parent
47066d4111
commit
4ea945bb56
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const http2 = require('http2');
|
|
||||||
const tls = require('tls');
|
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
|
|
||||||
// ─── 配置 ───────────────────────────────────────────────
|
// ─── 配置 ───────────────────────────────────────────────
|
||||||
@ -22,34 +20,6 @@ const log = (level, msg, extra = {}) => {
|
|||||||
|
|
||||||
const HEALTH_PATH = '/__health';
|
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 隧道 ──────────────────────
|
// ─── 通过 HTTP 代理建立 CONNECT 隧道 ──────────────────────
|
||||||
function connectViaProxy(proxyUrl, targetHost, targetPort) {
|
function connectViaProxy(proxyUrl, targetHost, targetPort) {
|
||||||
return new Promise((resolve, reject) => {
|
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) {
|
log('info', 'proxy_request', { host: targetHost, method: req.method, path: req.url });
|
||||||
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 };
|
const headers = { ...req.headers };
|
||||||
headers.host = targetHost;
|
headers.host = targetHost;
|
||||||
delete headers['x-forwarded-host'];
|
delete headers['x-forwarded-host'];
|
||||||
@ -205,6 +95,7 @@ async function proxyViaH1(targetHost, req, res) {
|
|||||||
headers,
|
headers,
|
||||||
servername: targetHost,
|
servername: targetHost,
|
||||||
timeout: CONNECT_TIMEOUT,
|
timeout: CONNECT_TIMEOUT,
|
||||||
|
// 不设置任何自定义 TLS 选项 → Node.js 默认 TLS stack → JA3/JA4 天然匹配
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxyReq;
|
let proxyReq;
|
||||||
@ -227,11 +118,17 @@ async function proxyViaH1(targetHost, req, res) {
|
|||||||
proxyReq = https.request(opts);
|
proxyReq = https.request(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上游响应
|
||||||
proxyReq.on('response', (proxyRes) => {
|
proxyReq.on('response', (proxyRes) => {
|
||||||
const responseHeaders = { ...proxyRes.headers };
|
const responseHeaders = { ...proxyRes.headers };
|
||||||
delete responseHeaders['connection'];
|
delete responseHeaders['connection'];
|
||||||
delete responseHeaders['keep-alive'];
|
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);
|
res.writeHead(proxyRes.statusCode, responseHeaders);
|
||||||
proxyRes.pipe(res, { end: true });
|
proxyRes.pipe(res, { end: true });
|
||||||
@ -242,8 +139,14 @@ async function proxyViaH1(targetHost, req, res) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 上游连接错误
|
||||||
proxyReq.on('error', (err) => {
|
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) {
|
if (!res.headersSent) {
|
||||||
res.writeHead(502, { 'content-type': 'application/json' });
|
res.writeHead(502, { 'content-type': 'application/json' });
|
||||||
res.end(JSON.stringify({ error: 'upstream_error', message: err.message }));
|
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 });
|
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 服务器 ─────────────────────────────────────────
|
// ─── HTTP 服务器 ─────────────────────────────────────────
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.url === HEALTH_PATH) {
|
if (req.url === HEALTH_PATH) {
|
||||||
@ -288,8 +175,6 @@ const server = http.createServer((req, res) => {
|
|||||||
node: process.version,
|
node: process.version,
|
||||||
openssl: process.versions.openssl,
|
openssl: process.versions.openssl,
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
h2Sessions: h2Sessions.size,
|
|
||||||
alpnCache: Object.fromEntries(alpnCache),
|
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -314,7 +199,6 @@ server.listen(LISTEN_PORT, LISTEN_HOST, () => {
|
|||||||
proxy: UPSTREAM_PROXY || '(direct)',
|
proxy: UPSTREAM_PROXY || '(direct)',
|
||||||
node: process.version,
|
node: process.version,
|
||||||
openssl: process.versions.openssl,
|
openssl: process.versions.openssl,
|
||||||
features: ['dynamic-host', 'h2-auto', 'alpn-probe'],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -324,11 +208,6 @@ function shutdown(signal) {
|
|||||||
if (shuttingDown) return;
|
if (shuttingDown) return;
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
log('info', `received ${signal}, shutting down`);
|
log('info', `received ${signal}, shutting down`);
|
||||||
// 关闭所有 h2 session
|
|
||||||
for (const [host, session] of h2Sessions) {
|
|
||||||
session.close();
|
|
||||||
}
|
|
||||||
h2Sessions.clear();
|
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
log('info', 'server closed');
|
log('info', 'server closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user