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