whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
463 lines (446 loc) • 12.4 kB
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
);
};