2019-07-11 22:52:31 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var net = require('net');
|
|
|
|
var tls = require('tls');
|
|
|
|
var http = require('http');
|
|
|
|
var https = require('https');
|
|
|
|
var events = require('events');
|
|
|
|
var assert = require('assert');
|
|
|
|
var util = require('util');
|
|
|
|
|
|
|
|
|
|
|
|
exports.httpOverHttp = httpOverHttp;
|
|
|
|
exports.httpsOverHttp = httpsOverHttp;
|
|
|
|
exports.httpOverHttps = httpOverHttps;
|
|
|
|
exports.httpsOverHttps = httpsOverHttps;
|
|
|
|
|
|
|
|
|
|
|
|
function httpOverHttp(options) {
|
|
|
|
var agent = new TunnelingAgent(options);
|
|
|
|
agent.request = http.request;
|
|
|
|
return agent;
|
|
|
|
}
|
|
|
|
|
|
|
|
function httpsOverHttp(options) {
|
|
|
|
var agent = new TunnelingAgent(options);
|
|
|
|
agent.request = http.request;
|
|
|
|
agent.createSocket = createSecureSocket;
|
|
|
|
return agent;
|
|
|
|
}
|
|
|
|
|
|
|
|
function httpOverHttps(options) {
|
|
|
|
var agent = new TunnelingAgent(options);
|
|
|
|
agent.request = https.request;
|
|
|
|
return agent;
|
|
|
|
}
|
|
|
|
|
|
|
|
function httpsOverHttps(options) {
|
|
|
|
var agent = new TunnelingAgent(options);
|
|
|
|
agent.request = https.request;
|
|
|
|
agent.createSocket = createSecureSocket;
|
|
|
|
return agent;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function TunnelingAgent(options) {
|
|
|
|
var self = this;
|
|
|
|
self.options = options || {};
|
|
|
|
self.proxyOptions = self.options.proxy || {};
|
|
|
|
self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets;
|
|
|
|
self.requests = [];
|
|
|
|
self.sockets = [];
|
|
|
|
|
|
|
|
self.on('free', function onFree(socket, host, port, localAddress) {
|
|
|
|
var options = toOptions(host, port, localAddress);
|
|
|
|
for (var i = 0, len = self.requests.length; i < len; ++i) {
|
|
|
|
var pending = self.requests[i];
|
|
|
|
if (pending.host === options.host && pending.port === options.port) {
|
|
|
|
// Detect the request to connect same origin server,
|
|
|
|
// reuse the connection.
|
|
|
|
self.requests.splice(i, 1);
|
|
|
|
pending.request.onSocket(socket);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
socket.destroy();
|
|
|
|
self.removeSocket(socket);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
util.inherits(TunnelingAgent, events.EventEmitter);
|
|
|
|
|
|
|
|
TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) {
|
|
|
|
var self = this;
|
|
|
|
var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress));
|
|
|
|
|
|
|
|
if (self.sockets.length >= this.maxSockets) {
|
|
|
|
// We are over limit so we'll add it to the queue.
|
|
|
|
self.requests.push(options);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are under maxSockets create a new one.
|
|
|
|
self.createSocket(options, function(socket) {
|
|
|
|
socket.on('free', onFree);
|
|
|
|
socket.on('close', onCloseOrRemove);
|
|
|
|
socket.on('agentRemove', onCloseOrRemove);
|
|
|
|
req.onSocket(socket);
|
|
|
|
|
|
|
|
function onFree() {
|
|
|
|
self.emit('free', socket, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
function onCloseOrRemove(err) {
|
|
|
|
self.removeSocket(socket);
|
|
|
|
socket.removeListener('free', onFree);
|
|
|
|
socket.removeListener('close', onCloseOrRemove);
|
|
|
|
socket.removeListener('agentRemove', onCloseOrRemove);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
TunnelingAgent.prototype.createSocket = function createSocket(options, cb) {
|
|
|
|
var self = this;
|
|
|
|
var placeholder = {};
|
|
|
|
self.sockets.push(placeholder);
|
|
|
|
|
|
|
|
var connectOptions = mergeOptions({}, self.proxyOptions, {
|
|
|
|
method: 'CONNECT',
|
|
|
|
path: options.host + ':' + options.port,
|
|
|
|
agent: false
|
|
|
|
});
|
|
|
|
if (connectOptions.proxyAuth) {
|
|
|
|
connectOptions.headers = connectOptions.headers || {};
|
|
|
|
connectOptions.headers['Proxy-Authorization'] = 'Basic ' +
|
|
|
|
new Buffer(connectOptions.proxyAuth).toString('base64');
|
|
|
|
}
|
|
|
|
|
|
|
|
debug('making CONNECT request');
|
|
|
|
var connectReq = self.request(connectOptions);
|
|
|
|
connectReq.useChunkedEncodingByDefault = false; // for v0.6
|
|
|
|
connectReq.once('response', onResponse); // for v0.6
|
|
|
|
connectReq.once('upgrade', onUpgrade); // for v0.6
|
|
|
|
connectReq.once('connect', onConnect); // for v0.7 or later
|
|
|
|
connectReq.once('error', onError);
|
|
|
|
connectReq.end();
|
|
|
|
|
|
|
|
function onResponse(res) {
|
|
|
|
// Very hacky. This is necessary to avoid http-parser leaks.
|
|
|
|
res.upgrade = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function onUpgrade(res, socket, head) {
|
|
|
|
// Hacky.
|
|
|
|
process.nextTick(function() {
|
|
|
|
onConnect(res, socket, head);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function onConnect(res, socket, head) {
|
|
|
|
connectReq.removeAllListeners();
|
|
|
|
socket.removeAllListeners();
|
|
|
|
|
|
|
|
if (res.statusCode === 200) {
|
|
|
|
assert.equal(head.length, 0);
|
|
|
|
debug('tunneling connection has established');
|
|
|
|
self.sockets[self.sockets.indexOf(placeholder)] = socket;
|
|
|
|
cb(socket);
|
|
|
|
} else {
|
|
|
|
debug('tunneling socket could not be established, statusCode=%d',
|
|
|
|
res.statusCode);
|
|
|
|
var error = new Error('tunneling socket could not be established, ' +
|
|
|
|
'statusCode=' + res.statusCode);
|
|
|
|
error.code = 'ECONNRESET';
|
|
|
|
options.request.emit('error', error);
|
|
|
|
self.removeSocket(placeholder);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onError(cause) {
|
|
|
|
connectReq.removeAllListeners();
|
|
|
|
|
|
|
|
debug('tunneling socket could not be established, cause=%s\n',
|
|
|
|
cause.message, cause.stack);
|
|
|
|
var error = new Error('tunneling socket could not be established, ' +
|
|
|
|
'cause=' + cause.message);
|
|
|
|
error.code = 'ECONNRESET';
|
|
|
|
options.request.emit('error', error);
|
|
|
|
self.removeSocket(placeholder);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
TunnelingAgent.prototype.removeSocket = function removeSocket(socket) {
|
|
|
|
var pos = this.sockets.indexOf(socket)
|
|
|
|
if (pos === -1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.sockets.splice(pos, 1);
|
|
|
|
|
|
|
|
var pending = this.requests.shift();
|
|
|
|
if (pending) {
|
|
|
|
// If we have pending requests and a socket gets closed a new one
|
|
|
|
// needs to be created to take over in the pool for the one that closed.
|
|
|
|
this.createSocket(pending, function(socket) {
|
|
|
|
pending.request.onSocket(socket);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function createSecureSocket(options, cb) {
|
|
|
|
var self = this;
|
|
|
|
TunnelingAgent.prototype.createSocket.call(self, options, function(socket) {
|
|
|
|
var hostHeader = options.request.getHeader('host');
|
|
|
|
var tlsOptions = mergeOptions({}, self.options, {
|
|
|
|
socket: socket,
|
|
|
|
servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host
|
|
|
|
});
|
|
|
|
|
|
|
|
// 0 is dummy port for v0.6
|
|
|
|
var secureSocket = tls.connect(0, tlsOptions);
|
|
|
|
self.sockets[self.sockets.indexOf(socket)] = secureSocket;
|
|
|
|
cb(secureSocket);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toOptions(host, port, localAddress) {
|
|
|
|
if (typeof host === 'string') { // since v0.10
|
|
|
|
return {
|
|
|
|
host: host,
|
|
|
|
port: port,
|
|
|
|
localAddress: localAddress
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return host; // for v0.11 or later
|
|
|
|
}
|
|
|
|
|
|
|
|
function mergeOptions(target) {
|
|
|
|
for (var i = 1, len = arguments.length; i < len; ++i) {
|
|
|
|
var overrides = arguments[i];
|
|
|
|
if (typeof overrides === 'object') {
|
|
|
|
var keys = Object.keys(overrides);
|
|
|
|
for (var j = 0, keyLen = keys.length; j < keyLen; ++j) {
|
|
|
|
var k = keys[j];
|
|
|
|
if (overrides[k] !== undefined) {
|
|
|
|
target[k] = overrides[k];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var debug;
|
|
|
|
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
|
|
|
debug = function() {
|
|
|
|
var args = Array.prototype.slice.call(arguments);
|
|
|
|
if (typeof args[0] === 'string') {
|
|
|
|
args[0] = 'TUNNEL: ' + args[0];
|
|
|
|
} else {
|
|
|
|
args.unshift('TUNNEL:');
|
|
|
|
}
|
|
|
|
console.error.apply(console, args);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
debug = function() {};
|
|
|
|
}
|
|
|
|
exports.debug = debug; // for test
|