| '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 |