Files
whistle/lib/https/h2.js
T
2019-10-22 22:39:36 +08:00

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;
});
}
});
};