| '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; | |
| 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, 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, | |
| headers: { | |
| host: options.host + ':' + options.port | |
| } | |
| }); | |
| if (options.localAddress) { | |
| connectOptions.localAddress = options.localAddress; | |
| } | |
| 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) { | |
| debug('tunneling socket could not be established, statusCode=%d', | |
| res.statusCode); | |
| socket.destroy(); | |
| var error = new Error('tunneling socket could not be established, ' + | |
| 'statusCode=' + res.statusCode); | |
| error.code = 'ECONNRESET'; | |
| options.request.emit('error', error); | |
| self.removeSocket(placeholder); | |
| return; | |
| } | |
| if (head.length > 0) { | |
| debug('got illegal response body from proxy'); | |
| socket.destroy(); | |
| var error = new Error('got illegal response body from proxy'); | |
| error.code = 'ECONNRESET'; | |
| options.request.emit('error', error); | |
| self.removeSocket(placeholder); | |
| return; | |
| } | |
| debug('tunneling connection has established'); | |
| self.sockets[self.sockets.indexOf(placeholder)] = socket; | |
| return cb(socket); | |
| } | |
| 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 |