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