'use strict'; var net = require('net'), tls = require('tls'), http = require('http'), https = require('https'), events = require('events'), assert = require('assert'), util = require('util'), Buffer = require('safe-buffer').Buffer; 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; agent.defaultPort = 443; 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; agent.defaultPort = 443; 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) { for (var i = 0, len = self.requests.length; i < len; ++i) { var pending = self.requests[i]; if (pending.host === host && pending.port === 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, options) { var self = this; // Legacy API: addRequest(req, host, port, path) if (typeof options === 'string') { options = { host: options, port: arguments[2], path: arguments[3], }; } if (self.sockets.length >= this.maxSockets) { // We are over limit so we'll add it to the queue. self.requests.push({ host: options.host, port: options.port, request: req, }); return; } // If we are under maxSockets create a new one. self.createConnection({ host: options.host, port: options.port, request: req, }); }; TunnelingAgent.prototype.createConnection = function createConnection(pending) { var self = this; self.createSocket(pending, function (socket) { socket.on('free', onFree); socket.on('close', onCloseOrRemove); socket.on('agentRemove', onCloseOrRemove); pending.request.onSocket(socket); function onFree() { self.emit('free', socket, pending.host, pending.port); } 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 ' + Buffer.from(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.createConnection(pending); } }; function createSecureSocket(options, cb) { var self = this; TunnelingAgent.prototype.createSocket.call(self, options, function (socket) { // 0 is dummy port for v0.6 var secureSocket = tls.connect( 0, mergeOptions({}, self.options, { servername: options.host, socket: socket, }) ); self.sockets[self.sockets.indexOf(socket)] = secureSocket; cb(secureSocket); }); } 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