• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3var 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
13exports.httpOverHttp = httpOverHttp
14exports.httpsOverHttp = httpsOverHttp
15exports.httpOverHttps = httpOverHttps
16exports.httpsOverHttps = httpsOverHttps
17
18
19function httpOverHttp(options) {
20  var agent = new TunnelingAgent(options)
21  agent.request = http.request
22  return agent
23}
24
25function 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
33function httpOverHttps(options) {
34  var agent = new TunnelingAgent(options)
35  agent.request = https.request
36  return agent
37}
38
39function 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
48function 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}
71util.inherits(TunnelingAgent, events.EventEmitter)
72
73TunnelingAgent.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
95TunnelingAgent.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
117TunnelingAgent.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
184TunnelingAgent.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
198function 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
213function 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
230var debug
231if (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}
244exports.debug = debug // for test
245