mirror of
https://github.com/avwo/whistle.git
synced 2026-10-18 22:51:32 +08:00
463 lines
12 KiB
JavaScript
463 lines
12 KiB
JavaScript
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 = 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');
|
||
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);
|
||
});
|
||
}
|
||
|
||
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, 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 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, 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();
|
||
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, 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) {
|
||
!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);
|
||
}
|
||
|
||
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 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];
|
||
} 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
|
||
}
|
||
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
|
||
);
|
||
};
|