Files
2023-12-24 23:44:17 +08:00

440 lines
11 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 http2 = config.enableH2 ? require('http2') : null;
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');
function onClose(stream, callback) {
stream.on('error', callback);
stream.once('close', function() {
callback(CLOSED_ERR);
});
}
setInterval(function () {
var now = Date.now();
Object.keys(clients).forEach(function (name) {
var client = clients[name];
if (now - client._updateTime > CACHE_TIMEOUT) {
client.close();
delete clients[name];
}
});
}, INTERVAL);
function getKey(options) {
var proxyOpts = options._proxyOptions;
var proxyType = '';
if (proxyOpts) {
var auth =
(proxyOpts.headers && proxyOpts.headers['proxy-authorization']) || '';
proxyType = [
proxyOpts.proxyType,
proxyOpts.proxyHost,
proxyOpts.proxyPort,
proxyOpts.headers.host,
proxyOpts.proxyTunnelPath || '',
auth
].join(':');
}
return [
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) {
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 opts;
}
function getProxySocket(options, callback, ciphers, isHttp) {
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 {
socket = tls.connect(
addCert(
{
servername: options.servername,
socket: socket,
rejectUnauthorized: config.rejectUnauthorized,
ALPNProtocols: SUPPORTED_PROTOS,
NPNProtocols: SUPPORTED_PROTOS,
ciphers: ciphers
},
options
),
handleCallback
);
socket.on('error', function (e) {
if (!ciphers && util.isCiphersError(e)) {
return getProxySocket(
options,
callback,
util.getCipher(options._rules)
);
} 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, isHttp) {
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)
: 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
),
handleCallback
);
}
function getClient(req, socket, name, callback) {
var origin = (req.useHttpH2 ? 'http' : 'https') + '://' + req.headers.host;
var handleCallback = function(err, clt) {
if (callback) {
clearTimeout(timer);
if (err || !clt) {
delete clients[name];
socket.destroy();
client.close();
}
callback(clt);
callback = timer = null;
}
};
var timer = setTimeout(handleCallback, REQ_TIMEOUT);
var client = http2.connect(
origin, {
settings: H2_SETTINGS,
maxSessionMemory: 128,
rejectUnauthorized: config.rejectUnauthorized,
createConnection: function () {
return socket;
}
}, function() {
handleCallback(null, client);
});
clients[name] = client;
client._updateTime = Date.now();
onClose(client, handleCallback);
onClose(socket, handleCallback);
return client;
}
function requestH2(client, req, res, callback) {
if (req._hasError) {
return;
}
var headers = util.formatH2Headers(req.headers);
delete req.headers.connection;
delete req.headers['keep-alive'];
delete req.headers['http2-settings'];
delete req.headers['proxy-connection'];
delete req.headers['transfer-encoding'];
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);
if (req.noReqBody) {
onClose(h2Session, function() {
if (!responsed) {
responsed = true;
h2Session.destroy();
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[config.ADDITIONAL_HEAD] = additionalHeaders;
}
svrRes.on('trailers', function (trailers) {
svrRes.trailers = trailers;
});
svrRes.statusCode = statusCode;
// HTTP2 对响应内容格式要求太严格(NGHTTP2_PROTOCOL_ERROR)
if (!util.hasBody(svrRes, req)) {
h2Session.destroy();
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) {
!responsed && callback();
responsed = true;
}
}
function bindListner(server, listener) {
server.timeout = config.timeout;
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 = 128;
options.settings = H2_SVR_SETTINGS;
server = http2.createSecureServer(options);
} else {
server = https.createServer(options);
}
return bindListner(server, listener);
};
exports.getHttpServer = function (_, listener) {
var server = http2.createServer({
maxSessionMemory: 128,
settings: H2_SVR_SETTINGS,
allowHTTP1: true
});
return bindListner(server, listener);
};
function checkTlsError(err) {
if (!err) {
return true;
}
var code = err.code;
if (typeof code !== 'string') {
return false;
}
return code.indexOf('ERR_TLS_') === 0 || code.indexOf('ERR_SSL_') === 0;
}
exports.request = function (req, res, callback) {
var options = req.useH2 && req.options;
if (!options || options.isPlugin || req._isInternalProxy) {
return callback();
}
var key = getKey(options);
var name = req.clientIp + '\n' + key;
var client = clients[name];
if (client) {
if (client.curIndex) {
name = name + '\n' + client.curIndex;
client.curIndex = ++client.curIndex % CONCURRENT;
client = clients[name];
} else {
client.curIndex = 1;
}
if (client) {
client._updateTime = Date.now();
return requestH2(client, req, res, callback);
}
}
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
}
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.useHttpH2
);
};