248 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			248 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | '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
 |