Files
2026-10-03 12:21:15 +08:00

482 lines
12 KiB
JavaScript
Raw Permalink 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.
var https = require('https');
var LRU = require('lru-cache');
var tls = require('tls');
var sockx = require('sockx');
var util = require('../util');
var config = require('../config');
var extend = require('extend');
var http2 = require('http2');
var SUPPORTED_PROTOS = ['h2', 'http/1.1', 'http/1.0'];
var H2_SETTINGS = { enablePush: false };
var H2_SVR_SETTINGS = { enablePush: false, enableConnectProtocol: false };
var CACHE_TIMEOUT = 1000 * 60;
var INTERVAL = 1000 * 60;
var clients = {};
var notH2 = new LRU({ max: 2560 });
var pendingH2 = {};
var pendingList = {};
var TIMEOUT = 36000;
var REQ_TIMEOUT = 16000;
var CONCURRENT = 3;
var CLOSED_ERR = new Error('closed');
var MSM = 1200;
var PMCS = 3600;
var RESET_BURST = 100000;
var RESET_RATE = 33;
var REQ_OPTS = { endStream: true };
function onClose(stream, callback) {
stream.on('error', callback);
stream.once('close', function() {
callback(CLOSED_ERR);
});
}
function closeClose(client, name) {
try {
client.close();
} catch (e) {}
delete clients[name];
}
setInterval(function () {
var now = Date.now();
Object.keys(clients).forEach(function (name) {
var client = clients[name];
if (isClosed(client) || now - client._updateTime > CACHE_TIMEOUT) {
closeClose(client, name);
}
});
}, INTERVAL);
function getOrigin(req) {
return (req.useHttpH2 ? 'http' : 'https') + '://' + req.headers.host;
}
function getKey(options, req) {
var proxyOpts = options._proxyOptions;
var proxyType = '';
if (proxyOpts) {
var headers = proxyOpts.headers || {};
proxyType = [
proxyOpts.proxyType,
proxyOpts.proxyHost,
proxyOpts.proxyPort,
headers.host,
proxyOpts.proxyTunnelPath || '',
headers['proxy-authorization'] || ''
].join(':');
}
return [
getOrigin(req),
options.servername,
options.host,
options.port || '',
proxyType,
options.cacheKey || ''
].join('/');
}
function getSocksSocket(options, callback) {
var done;
var handleCallback = function (err, socket) {
if (!done) {
done = true;
callback(err, socket);
}
};
var proxyOpts = options._proxyOptions;
var client = sockx.connect(
{
localDNS: false,
proxyHost: proxyOpts.proxyHost,
proxyPort: proxyOpts.proxyPort,
auths: config.getAuths(proxyOpts),
host: options.host,
port: options.port || 443
},
function (socket) {
handleCallback(null, socket);
}
);
onClose(client, handleCallback);
}
function getTunnelSocket(options, callback) {
var done;
var handleCallback = function (err, socket) {
if (!done) {
done = true;
callback(err, socket);
}
};
var connReq = config.connect(options._proxyOptions, function (socket) {
handleCallback(null, socket);
});
onClose(connReq, handleCallback);
}
function addCert(opts, options, disabled) {
if (options.cert) {
opts.key = options.key;
opts.cert = options.cert;
} else if (options.pfx) {
opts.pfx = options.pfx;
if (options.passphrase) {
opts.passphrase = options.passphrase;
}
}
return disabled ? opts : util.setSecureOptions(opts);
}
function getProxySocket(options, callback, tlsOpts, isHttp, disabled) {
var handleConnect = function (err, socket) {
if (err) {
return callback(err);
}
if (isHttp) {
return callback(null, socket);
}
var timer = setTimeout(function () {
if (timer) {
handleCallback(util.TIMEOUT_ERR);
socket.destroy();
}
}, REQ_TIMEOUT);
var handleCallback = function (err) {
if (timer) {
clearTimeout(timer);
timer = null;
callback(err, err ? null : socket);
}
};
try {
var opts = {
servername: options.servername,
socket: socket,
rejectUnauthorized: config.rejectUnauthorized,
ALPNProtocols: SUPPORTED_PROTOS,
NPNProtocols: SUPPORTED_PROTOS
};
if (tlsOpts) {
extend(opts, tlsOpts);
}
socket = tls.connect(
addCert(
opts,
options,
disabled
),
handleCallback
);
socket.on('error', function (e) {
if (!tlsOpts && util.isCiphersError(e)) {
return getProxySocket(
options,
callback,
util.getTlsOptions(options._rules),
false,
disabled
);
} else {
handleCallback(e);
}
});
socket.on('close', function() {
handleCallback(CLOSED_ERR);
});
} catch (e) {
handleCallback(e);
}
};
var proxyOpts = options._proxyOptions;
proxyOpts.proxyType === 'socks'
? getSocksSocket(options, handleConnect)
: getTunnelSocket(options, handleConnect);
}
function getSocket(options, callback, req) {
var isHttp = req.useHttpH2;
var disabled = req.disable.secureOptions;
var handleCallback = function (err, socket) {
if (err) {
return callback(false, null, err);
}
var proto = socket.alpnProtocol || socket.npnProtocol;
callback(isHttp || proto === 'h2', socket);
};
options._proxyOptions
? getProxySocket(options, handleCallback, null, isHttp, disabled)
: util.connect(
isHttp
? {
host: options.host,
port: options.port || 80
}
: addCert(
{
servername: options.servername,
host: options.host,
port: options.port || 443,
rejectUnauthorized: config.rejectUnauthorized,
ALPNProtocols: SUPPORTED_PROTOS,
NPNProtocols: SUPPORTED_PROTOS,
_rules: options._rules
},
options,
disabled
),
handleCallback
);
}
function getClient(req, socket, name, callback) {
var handleCallback = function(err, clt) {
if (callback) {
clearTimeout(timer);
if (err || !clt) {
closeClose(client, name);
socket.destroy();
}
callback(clt);
callback = timer = null;
}
};
var timer = setTimeout(handleCallback, REQ_TIMEOUT);
var client = http2.connect(getOrigin(req), util.setRejectUnauthorized(req, {
settings: H2_SETTINGS,
maxSessionMemory: MSM,
peerMaxConcurrentStreams: PMCS,
streamResetBurst: RESET_BURST,
streamResetRate: RESET_RATE,
createConnection: function () {
return socket;
}
}), function() {
handleCallback(null, client);
});
clients[name] = client;
client._updateTime = Date.now();
client.once('goaway', function() {
closeClose(client, name);
});
onClose(client, handleCallback);
onClose(socket, handleCallback);
return client;
}
function requestH2(client, req, res, callback) {
if (req._hasError) {
return;
}
var headers = util.formatH2Headers(req.headers);
var options = req.options;
var responsed;
headers[':path'] = options.path;
headers[':method'] = options.method;
headers[':authority'] = req.headers.host;
try {
var h2Session = client.request(headers, req.noReqBody ? REQ_OPTS : undefined);
onClose(h2Session, function() {
if (!responsed) {
responsed = true;
h2Session.destroy();
req.noReqBody && callback();
}
});
var additionalHeaders;
h2Session.once('headers', function(headers) {
try {
additionalHeaders = JSON.stringify(headers);
} catch (e) {}
});
h2Session.on('response', function (h2Headers) {
if (responsed) {
return;
}
client._updateTime = Date.now();
responsed = true;
var newHeaders = {};
var statusCode = h2Headers[':status'];
var svrRes = h2Session;
if (additionalHeaders) {
newHeaders[util.ADDITIONAL_HEAD] = additionalHeaders;
}
svrRes.on('trailers', function (trailers) {
svrRes.trailers = trailers;
});
svrRes.statusCode = statusCode;
// HTTP2 对响应内容格式要求太严格(NGHTTP2_PROTOCOL_ERROR)
if (!util.hasBody(svrRes, req)) {
h2Session.on('data', util.noop);
svrRes = util.createTransform();
svrRes.statusCode = statusCode;
svrRes.push(null);
}
svrRes.httpVersion = '1.1';
svrRes.headers = newHeaders;
Object.keys(h2Headers).forEach(function (name) {
if (
name[0] !== ':' &&
name !== 'content-length' &&
name !== 'transfer-encoding'
) {
newHeaders[name] = h2Headers[name];
}
});
if (req.isPluginReq && !req._isProxyReq) {
newHeaders[config.PROXY_ID_HEADER] = 'h2';
}
res.response(svrRes);
});
req.pipe(h2Session);
} catch (e) {
if (!responsed) {
callback();
responsed = true;
}
}
}
function bindListner(server, listener) {
if (typeof listener === 'function') {
server.on('request', listener);
} else if (listener) {
Object.keys(listener).forEach(function (name) {
server.on(name, listener[name]);
});
}
return server;
}
exports.getServer = function (options, listener) {
var server;
if (options.allowHTTP1 && http2) {
options.maxSessionMemory = MSM;
options.peerMaxConcurrentStreams = PMCS;
options.streamResetBurst = RESET_BURST;
options.streamResetRate = RESET_RATE;
options.settings = H2_SVR_SETTINGS;
server = http2.createSecureServer(options);
} else {
server = https.createServer(options);
}
server.requestTimeout = 0;
return bindListner(server, listener);
};
exports.getHttpServer = function (_, listener) {
var server = http2.createServer({
maxSessionMemory: MSM,
peerMaxConcurrentStreams: PMCS,
streamResetBurst: RESET_BURST,
streamResetRate: RESET_RATE,
settings: H2_SVR_SETTINGS,
allowHTTP1: true
});
server.requestTimeout = 0;
return bindListner(server, listener);
};
function checkTlsError(err) {
if (!err) {
return true;
}
var code = err.code;
return typeof code === 'string' && (code.indexOf('ERR_TLS_') === 0 || code.indexOf('ERR_SSL_') === 0);
}
function isClosed(client) {
return client.destroyed || client.closed;
}
exports.request = function (req, res, callback) {
var options = req.useH2 && req.options;
if (!options || options.isPlugin || req._isInternalProxy) {
return callback();
}
var key = getKey(options, req);
var reqId = req.disable.keepH2Session ? '' : req._h2ReqId;
var name = (reqId || req.clientIp) + '\n' + key;
var client = clients[name];
if (client) {
if (!reqId) {
if (client.curIndex) {
name = name + '\n' + client.curIndex;
client.curIndex = ++client.curIndex % CONCURRENT;
client = clients[name];
if (client && isClosed(client)) {
delete clients[name];
client = null;
}
} else {
client.curIndex = 1;
}
}
if (client) {
if (!isClosed(client)) {
client._updateTime = Date.now();
return requestH2(client, req, res, callback);
}
delete clients[name];
client = null;
}
}
var time = notH2.peek(key);
if (time && (Date.now() - time < TIMEOUT || pendingH2[key])) {
return callback();
}
pendingH2[key] = 1;
var pendingItem = pendingList[name];
if (pendingItem) {
return pendingItem.push([req, res, callback]);
}
pendingItem = [[req, res, callback]];
pendingList[name] = pendingItem;
options._rules = req.rules;
var proxyOpts = options._proxyOptions;
if (proxyOpts) {
proxyOpts.enableIntercept = true;
proxyOpts.proxyTunnelPath = util.getProxyTunnelPath(req, true);
}
getSocket(
options,
function (isH2, socket, err) {
if (socket) {
socket.secureConnecting = false; // fix: node bug
}
req._connectTime = Date.now();
var handleH2 = function (clt) {
delete pendingList[name];
delete pendingH2[key];
if (clt) {
notH2.del(key);
pendingItem.forEach(function (list) {
requestH2(clt, list[0], list[1], list[2]);
});
} else {
checkTlsError(err) && notH2.set(key, Date.now());
if (req.useHttpH2) {
socket = null;
}
pendingItem.forEach(function (list) {
list[2](socket);
socket = null;
});
}
};
if (err || !socket || !isH2) {
return handleH2();
}
getClient(req, socket, name, function(clt) {
if (!clt) {
socket = null;
}
handleH2(clt);
});
},
req
);
};