index.js (6884B)
1 'use strict' 2 3 var net = require('net') 4 , tls = require('tls') 5 , http = require('http') 6 , https = require('https') 7 , events = require('events') 8 , assert = require('assert') 9 , util = require('util') 10 , Buffer = require('safe-buffer').Buffer 11 ; 12 13 exports.httpOverHttp = httpOverHttp 14 exports.httpsOverHttp = httpsOverHttp 15 exports.httpOverHttps = httpOverHttps 16 exports.httpsOverHttps = httpsOverHttps 17 18 19 function httpOverHttp(options) { 20 var agent = new TunnelingAgent(options) 21 agent.request = http.request 22 return agent 23 } 24 25 function httpsOverHttp(options) { 26 var agent = new TunnelingAgent(options) 27 agent.request = http.request 28 agent.createSocket = createSecureSocket 29 agent.defaultPort = 443 30 return agent 31 } 32 33 function httpOverHttps(options) { 34 var agent = new TunnelingAgent(options) 35 agent.request = https.request 36 return agent 37 } 38 39 function httpsOverHttps(options) { 40 var agent = new TunnelingAgent(options) 41 agent.request = https.request 42 agent.createSocket = createSecureSocket 43 agent.defaultPort = 443 44 return agent 45 } 46 47 48 function TunnelingAgent(options) { 49 var self = this 50 self.options = options || {} 51 self.proxyOptions = self.options.proxy || {} 52 self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets 53 self.requests = [] 54 self.sockets = [] 55 56 self.on('free', function onFree(socket, host, port) { 57 for (var i = 0, len = self.requests.length; i < len; ++i) { 58 var pending = self.requests[i] 59 if (pending.host === host && pending.port === port) { 60 // Detect the request to connect same origin server, 61 // reuse the connection. 62 self.requests.splice(i, 1) 63 pending.request.onSocket(socket) 64 return 65 } 66 } 67 socket.destroy() 68 self.removeSocket(socket) 69 }) 70 } 71 util.inherits(TunnelingAgent, events.EventEmitter) 72 73 TunnelingAgent.prototype.addRequest = function addRequest(req, options) { 74 var self = this 75 76 // Legacy API: addRequest(req, host, port, path) 77 if (typeof options === 'string') { 78 options = { 79 host: options, 80 port: arguments[2], 81 path: arguments[3] 82 }; 83 } 84 85 if (self.sockets.length >= this.maxSockets) { 86 // We are over limit so we'll add it to the queue. 87 self.requests.push({host: options.host, port: options.port, request: req}) 88 return 89 } 90 91 // If we are under maxSockets create a new one. 92 self.createConnection({host: options.host, port: options.port, request: req}) 93 } 94 95 TunnelingAgent.prototype.createConnection = function createConnection(pending) { 96 var self = this 97 98 self.createSocket(pending, function(socket) { 99 socket.on('free', onFree) 100 socket.on('close', onCloseOrRemove) 101 socket.on('agentRemove', onCloseOrRemove) 102 pending.request.onSocket(socket) 103 104 function onFree() { 105 self.emit('free', socket, pending.host, pending.port) 106 } 107 108 function onCloseOrRemove(err) { 109 self.removeSocket(socket) 110 socket.removeListener('free', onFree) 111 socket.removeListener('close', onCloseOrRemove) 112 socket.removeListener('agentRemove', onCloseOrRemove) 113 } 114 }) 115 } 116 117 TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { 118 var self = this 119 var placeholder = {} 120 self.sockets.push(placeholder) 121 122 var connectOptions = mergeOptions({}, self.proxyOptions, 123 { method: 'CONNECT' 124 , path: options.host + ':' + options.port 125 , agent: false 126 } 127 ) 128 if (connectOptions.proxyAuth) { 129 connectOptions.headers = connectOptions.headers || {} 130 connectOptions.headers['Proxy-Authorization'] = 'Basic ' + 131 Buffer.from(connectOptions.proxyAuth).toString('base64') 132 } 133 134 debug('making CONNECT request') 135 var connectReq = self.request(connectOptions) 136 connectReq.useChunkedEncodingByDefault = false // for v0.6 137 connectReq.once('response', onResponse) // for v0.6 138 connectReq.once('upgrade', onUpgrade) // for v0.6 139 connectReq.once('connect', onConnect) // for v0.7 or later 140 connectReq.once('error', onError) 141 connectReq.end() 142 143 function onResponse(res) { 144 // Very hacky. This is necessary to avoid http-parser leaks. 145 res.upgrade = true 146 } 147 148 function onUpgrade(res, socket, head) { 149 // Hacky. 150 process.nextTick(function() { 151 onConnect(res, socket, head) 152 }) 153 } 154 155 function onConnect(res, socket, head) { 156 connectReq.removeAllListeners() 157 socket.removeAllListeners() 158 159 if (res.statusCode === 200) { 160 assert.equal(head.length, 0) 161 debug('tunneling connection has established') 162 self.sockets[self.sockets.indexOf(placeholder)] = socket 163 cb(socket) 164 } else { 165 debug('tunneling socket could not be established, statusCode=%d', res.statusCode) 166 var error = new Error('tunneling socket could not be established, ' + 'statusCode=' + res.statusCode) 167 error.code = 'ECONNRESET' 168 options.request.emit('error', error) 169 self.removeSocket(placeholder) 170 } 171 } 172 173 function onError(cause) { 174 connectReq.removeAllListeners() 175 176 debug('tunneling socket could not be established, cause=%s\n', cause.message, cause.stack) 177 var error = new Error('tunneling socket could not be established, ' + 'cause=' + cause.message) 178 error.code = 'ECONNRESET' 179 options.request.emit('error', error) 180 self.removeSocket(placeholder) 181 } 182 } 183 184 TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { 185 var pos = this.sockets.indexOf(socket) 186 if (pos === -1) return 187 188 this.sockets.splice(pos, 1) 189 190 var pending = this.requests.shift() 191 if (pending) { 192 // If we have pending requests and a socket gets closed a new one 193 // needs to be created to take over in the pool for the one that closed. 194 this.createConnection(pending) 195 } 196 } 197 198 function createSecureSocket(options, cb) { 199 var self = this 200 TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { 201 // 0 is dummy port for v0.6 202 var secureSocket = tls.connect(0, mergeOptions({}, self.options, 203 { servername: options.host 204 , socket: socket 205 } 206 )) 207 self.sockets[self.sockets.indexOf(socket)] = secureSocket 208 cb(secureSocket) 209 }) 210 } 211 212 213 function mergeOptions(target) { 214 for (var i = 1, len = arguments.length; i < len; ++i) { 215 var overrides = arguments[i] 216 if (typeof overrides === 'object') { 217 var keys = Object.keys(overrides) 218 for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { 219 var k = keys[j] 220 if (overrides[k] !== undefined) { 221 target[k] = overrides[k] 222 } 223 } 224 } 225 } 226 return target 227 } 228 229 230 var debug 231 if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { 232 debug = function() { 233 var args = Array.prototype.slice.call(arguments) 234 if (typeof args[0] === 'string') { 235 args[0] = 'TUNNEL: ' + args[0] 236 } else { 237 args.unshift('TUNNEL:') 238 } 239 console.error.apply(console, args) 240 } 241 } else { 242 debug = function() {} 243 } 244 exports.debug = debug // for test