mirror of
https://github.com/avwo/whistle.git
synced 2026-10-19 15:21:49 +08:00
252 lines
6.6 KiB
JavaScript
252 lines
6.6 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 http2 = config.enableH2 ? require('http2') : null;
|
|
|
|
var SUPPORTED_PROTOS = ['h2', 'http/1.1', 'http/1.0'];
|
|
var H2_SETTINGS = { enablePush: false , enableConnectProtocol: false };
|
|
var clients = {};
|
|
var notH2 = new LRU({max: 2560});
|
|
var pendingH2 = {};
|
|
var pendingList = {};
|
|
var TIMEOUT = 36000;
|
|
|
|
function getKey(options) {
|
|
var proxyOpts = options._proxyOptions;
|
|
var proxyType = '';
|
|
if (proxyOpts) {
|
|
proxyType = [proxyOpts.proxyType, proxyOpts.proxyHost, proxyOpts.proxyPort, proxyOpts.headers.host].join(':');
|
|
}
|
|
return [options.servername, options.host, options.port || '', proxyType].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);
|
|
});
|
|
client.on('error', 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);
|
|
});
|
|
connReq.on('error', handleCallback);
|
|
}
|
|
|
|
function getProxySocket(options, callback) {
|
|
var handleConnect = function(err, socket) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
var timer = setTimeout(function() {
|
|
if (timer) {
|
|
timer = null;
|
|
socket.destroy();
|
|
}
|
|
}, config.CONN_TIMEOUT);
|
|
var handleCallback = function(err) {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
callback(err, err ? null : socket);
|
|
}
|
|
};
|
|
socket = tls.connect({
|
|
servername: options.servername,
|
|
socket: socket,
|
|
rejectUnauthorized: false,
|
|
ALPNProtocols: SUPPORTED_PROTOS,
|
|
NPNProtocols: SUPPORTED_PROTOS
|
|
}, handleCallback);
|
|
socket.on('error', handleCallback);
|
|
};
|
|
var proxyOpts = options._proxyOptions;
|
|
proxyOpts.proxyType === 'socks' ?
|
|
getSocksSocket(options, handleConnect) :
|
|
getTunnelSocket(options, handleConnect);
|
|
}
|
|
|
|
function getSocket(options, callback) {
|
|
if (!options) {
|
|
return callback();
|
|
}
|
|
var handleCallback = function(err, socket) {
|
|
if (err) {
|
|
return callback(false, null, err);
|
|
}
|
|
var proto = socket.alpnProtocol || socket.npnProtocol;
|
|
callback(proto === 'h2', socket);
|
|
};
|
|
options._proxyOptions ?
|
|
getProxySocket(options, handleCallback) :
|
|
util.connect({
|
|
servername: options.servername,
|
|
host: options.host,
|
|
port: options.port || 443,
|
|
rejectUnauthorized: false,
|
|
ALPNProtocols: SUPPORTED_PROTOS,
|
|
NPNProtocols: SUPPORTED_PROTOS
|
|
}, handleCallback);
|
|
}
|
|
|
|
function getClient(req, socket, name) {
|
|
var client = http2.connect('https://' + req.headers.host, {
|
|
setttings: H2_SETTINGS,
|
|
rejectUnauthorized: false,
|
|
createConnection: function() {
|
|
return socket;
|
|
}
|
|
});
|
|
clients[name] = client;
|
|
var closed;
|
|
var handleClose = function() {
|
|
if (!closed) {
|
|
closed = true;
|
|
delete clients[name];
|
|
client.close();
|
|
socket.destroy();
|
|
}
|
|
};
|
|
socket.on('error', handleClose);
|
|
socket.on('close', handleClose);
|
|
client.on('error', handleClose);
|
|
return client;
|
|
}
|
|
|
|
function requestH2(client, req, res) {
|
|
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;
|
|
headers[':path'] = options.path;
|
|
headers[':method'] = options.method;
|
|
headers[':authority'] = req.headers.host;
|
|
try {
|
|
var h2Session = client.request(headers);
|
|
h2Session.on('error', util.noop);
|
|
h2Session.on('response', function(h2Headers) {
|
|
var newHeaders = {};
|
|
h2Session.statusCode = h2Headers[':status'];
|
|
h2Session.httpVersion = '1.1';
|
|
h2Session.headers = newHeaders;
|
|
Object.keys(h2Headers).forEach(function(name) {
|
|
if (name[0] !== ':') {
|
|
newHeaders[name] = h2Headers[name];
|
|
}
|
|
});
|
|
res.response(h2Session);
|
|
});
|
|
req.pipe(h2Session);
|
|
} catch (e) {
|
|
client.emit('error', e);
|
|
}
|
|
}
|
|
|
|
exports.getServer = function(options, listener) {
|
|
var createServer;
|
|
if (options.allowHTTP1 && http2) {
|
|
createServer = http2.createSecureServer;
|
|
options.setttings = H2_SETTINGS;
|
|
} else {
|
|
createServer = https.createServer;
|
|
}
|
|
var server = createServer(options);
|
|
if (typeof listener === 'function') {
|
|
server.on('request', listener);
|
|
} else if (listener) {
|
|
Object.keys(listener).forEach(function(name) {
|
|
server.on(name, listener[name]);
|
|
});
|
|
}
|
|
return server;
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* TODO(v2.0): 遗留两种需要处理的H2请求
|
|
* 1. 设置代理后的请求
|
|
* 2. 插件转发回来的请求
|
|
*/
|
|
exports.request = function(req, res, callback) {
|
|
var options = req.useH2 && req.options;
|
|
if (!options) {
|
|
return callback();
|
|
}
|
|
var key = getKey(options);
|
|
var name = req.clientIp + '\n' + key;
|
|
var client = clients[name];
|
|
if (client) {
|
|
return requestH2(client, req, res);
|
|
}
|
|
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;
|
|
getSocket(options, function(isH2, socket, err) {
|
|
client =isH2 && getClient(req, socket, name);
|
|
delete pendingList[name];
|
|
delete pendingH2[key];
|
|
if (client) {
|
|
notH2.del(key);
|
|
pendingItem.forEach(function(list) {
|
|
requestH2(client, list[0], list[1]);
|
|
});
|
|
} else {
|
|
checkTlsError(err) && notH2.set(key, Date.now());
|
|
pendingItem.forEach(function(list) {
|
|
list[2](socket);
|
|
socket = null;
|
|
});
|
|
}
|
|
});
|
|
};
|