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