1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | var http = require('http')
|
16 | , https = false
|
17 | , tls = false
|
18 | , url = require('url')
|
19 | , util = require('util')
|
20 | , stream = require('stream')
|
21 | , qs = require('querystring')
|
22 | , mimetypes = require('./mimetypes')
|
23 | ;
|
24 |
|
25 | try {
|
26 | https = require('https')
|
27 | } catch (e) {}
|
28 |
|
29 | try {
|
30 | tls = require('tls')
|
31 | } catch (e) {}
|
32 |
|
33 | function toBase64 (str) {
|
34 | return (new Buffer(str || "", "ascii")).toString("base64")
|
35 | }
|
36 |
|
37 |
|
38 | if (https && !https.Agent) {
|
39 | https.Agent = function (options) {
|
40 | http.Agent.call(this, options)
|
41 | }
|
42 | util.inherits(https.Agent, http.Agent)
|
43 | https.Agent.prototype._getConnection = function(host, port, cb) {
|
44 | var s = tls.connect(port, host, this.options, function() {
|
45 |
|
46 | if (cb) cb()
|
47 | })
|
48 | return s
|
49 | }
|
50 | }
|
51 |
|
52 | function isReadStream (rs) {
|
53 | if (rs.readable && rs.path && rs.mode) {
|
54 | return true
|
55 | }
|
56 | }
|
57 |
|
58 | function copy (obj) {
|
59 | var o = {}
|
60 | for (var i in obj) o[i] = obj[i]
|
61 | return o
|
62 | }
|
63 |
|
64 | var isUrl = /^https?:/
|
65 |
|
66 | var globalPool = {}
|
67 |
|
68 | function Request (options) {
|
69 | stream.Stream.call(this)
|
70 | this.readable = true
|
71 | this.writable = true
|
72 |
|
73 | if (typeof options === 'string') {
|
74 | options = {uri:options}
|
75 | }
|
76 |
|
77 | for (var i in options) {
|
78 | this[i] = options[i]
|
79 | }
|
80 | if (!this.pool) this.pool = globalPool
|
81 | this.dests = []
|
82 | this.__isRequestRequest = true
|
83 | }
|
84 | util.inherits(Request, stream.Stream)
|
85 | Request.prototype.getAgent = function (host, port) {
|
86 | if (!this.pool[host+':'+port]) {
|
87 | this.pool[host+':'+port] = new this.httpModule.Agent({host:host, port:port})
|
88 | }
|
89 | return this.pool[host+':'+port]
|
90 | }
|
91 | Request.prototype.request = function () {
|
92 | var self = this
|
93 |
|
94 |
|
95 | if (!self._callback && self.callback) {
|
96 | self._callback = self.callback
|
97 | self.callback = function () {
|
98 | if (self._callbackCalled) return
|
99 | self._callback.apply(self, arguments)
|
100 | self._callbackCalled = true
|
101 | }
|
102 | }
|
103 |
|
104 | if (self.url) {
|
105 |
|
106 | self.uri = self.url
|
107 | delete self.url
|
108 | }
|
109 |
|
110 | if (!self.uri) {
|
111 | throw new Error("options.uri is a required argument")
|
112 | } else {
|
113 | if (typeof self.uri == "string") self.uri = url.parse(self.uri)
|
114 | }
|
115 | if (self.proxy) {
|
116 | if (typeof self.proxy == 'string') self.proxy = url.parse(self.proxy)
|
117 | }
|
118 |
|
119 | self._redirectsFollowed = self._redirectsFollowed || 0
|
120 | self.maxRedirects = (self.maxRedirects !== undefined) ? self.maxRedirects : 10
|
121 | self.followRedirect = (self.followRedirect !== undefined) ? self.followRedirect : true
|
122 | if (self.followRedirect)
|
123 | self.redirects = self.redirects || []
|
124 |
|
125 | self.headers = self.headers ? copy(self.headers) : {}
|
126 |
|
127 | var setHost = false
|
128 | if (!self.headers.host) {
|
129 | self.headers.host = self.uri.hostname
|
130 | if (self.uri.port) {
|
131 | if ( !(self.uri.port === 80 && self.uri.protocol === 'http:') &&
|
132 | !(self.uri.port === 443 && self.uri.protocol === 'https:') )
|
133 | self.headers.host += (':'+self.uri.port)
|
134 | }
|
135 | setHost = true
|
136 | }
|
137 |
|
138 | if (!self.uri.pathname) {self.uri.pathname = '/'}
|
139 | if (!self.uri.port) {
|
140 | if (self.uri.protocol == 'http:') {self.uri.port = 80}
|
141 | else if (self.uri.protocol == 'https:') {self.uri.port = 443}
|
142 | }
|
143 |
|
144 | if (self.proxy) {
|
145 | self.port = self.proxy.port
|
146 | self.host = self.proxy.hostname
|
147 | } else {
|
148 | self.port = self.uri.port
|
149 | self.host = self.uri.hostname
|
150 | }
|
151 |
|
152 | if (self.onResponse === true) {
|
153 | self.onResponse = self.callback
|
154 | delete self.callback
|
155 | }
|
156 |
|
157 | var clientErrorHandler = function (error) {
|
158 | if (setHost) delete self.headers.host
|
159 | if (self.timeout && self.timeoutTimer) clearTimeout(self.timeoutTimer)
|
160 | self.emit('error', error)
|
161 | }
|
162 | if (self.onResponse) self.on('error', function (e) {self.onResponse(e)})
|
163 | if (self.callback) self.on('error', function (e) {self.callback(e)})
|
164 |
|
165 |
|
166 | if (self.uri.auth && !self.headers.authorization) {
|
167 | self.headers.authorization = "Basic " + toBase64(self.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
|
168 | }
|
169 | if (self.proxy && self.proxy.auth && !self.headers['proxy-authorization']) {
|
170 | self.headers['proxy-authorization'] = "Basic " + toBase64(self.proxy.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
|
171 | }
|
172 |
|
173 | if (self.uri.path) {
|
174 | self.path = self.uri.path
|
175 | } else {
|
176 | self.path = self.uri.pathname + (self.uri.search || "")
|
177 | }
|
178 |
|
179 | if (self.path.length === 0) self.path = '/'
|
180 |
|
181 | if (self.proxy) self.path = (self.uri.protocol + '//' + self.uri.host + self.path)
|
182 |
|
183 | if (self.json) {
|
184 | self.headers['content-type'] = 'application/json'
|
185 | if (typeof self.json === 'boolean') {
|
186 | if (typeof self.body === 'object') self.body = JSON.stringify(self.body)
|
187 | } else {
|
188 | self.body = JSON.stringify(self.json)
|
189 | }
|
190 |
|
191 | } else if (self.multipart) {
|
192 | self.body = ''
|
193 | self.headers['content-type'] = 'multipart/related;boundary="frontier"'
|
194 | if (!self.multipart.forEach) throw new Error('Argument error, options.multipart.')
|
195 |
|
196 | self.multipart.forEach(function (part) {
|
197 | var body = part.body
|
198 | if(!body) throw Error('Body attribute missing in multipart.')
|
199 | delete part.body
|
200 | self.body += '--frontier\r\n'
|
201 | Object.keys(part).forEach(function(key){
|
202 | self.body += key + ': ' + part[key] + '\r\n'
|
203 | })
|
204 | self.body += '\r\n' + body + '\r\n'
|
205 | })
|
206 | self.body += '--frontier--'
|
207 | }
|
208 |
|
209 | if (self.body) {
|
210 | if (!Buffer.isBuffer(self.body)) {
|
211 | self.body = new Buffer(self.body)
|
212 | }
|
213 | if (self.body.length) {
|
214 | self.headers['content-length'] = self.body.length
|
215 | } else {
|
216 | throw new Error('Argument error, options.body.')
|
217 | }
|
218 | }
|
219 |
|
220 | self.httpModule =
|
221 | {"http:":http, "https:":https}[self.proxy ? self.proxy.protocol : self.uri.protocol]
|
222 |
|
223 | if (!self.httpModule) throw new Error("Invalid protocol")
|
224 |
|
225 | if (self.pool === false) {
|
226 | self.agent = false
|
227 | } else {
|
228 | if (self.maxSockets) {
|
229 |
|
230 | self.agent = self.httpModule.globalAgent || self.getAgent(self.host, self.port)
|
231 | self.agent.maxSockets = self.maxSockets
|
232 | }
|
233 | if (self.pool.maxSockets) {
|
234 |
|
235 | self.agent = self.httpModule.globalAgent || self.getAgent(self.host, self.port)
|
236 | self.agent.maxSockets = self.pool.maxSockets
|
237 | }
|
238 | }
|
239 |
|
240 | self.start = function () {
|
241 | self._started = true
|
242 | self.method = self.method || 'GET'
|
243 |
|
244 | self.req = self.httpModule.request(self, function (response) {
|
245 | self.response = response
|
246 | response.request = self
|
247 |
|
248 | if (self.httpModule === https &&
|
249 | self.strictSSL &&
|
250 | !response.client.authorized) {
|
251 | var sslErr = response.client.authorizationError
|
252 | self.emit('error', new Error('SSL Error: '+ sslErr))
|
253 | return
|
254 | }
|
255 |
|
256 | if (setHost) delete self.headers.host
|
257 | if (self.timeout && self.timeoutTimer) clearTimeout(self.timeoutTimer)
|
258 |
|
259 | if (response.statusCode >= 300 &&
|
260 | response.statusCode < 400 &&
|
261 | self.followRedirect &&
|
262 | self.method !== 'PUT' &&
|
263 | self.method !== 'POST' &&
|
264 | response.headers.location) {
|
265 | if (self._redirectsFollowed >= self.maxRedirects) {
|
266 | self.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop."))
|
267 | return
|
268 | }
|
269 | self._redirectsFollowed += 1
|
270 |
|
271 | if (!isUrl.test(response.headers.location)) {
|
272 | response.headers.location = url.resolve(self.uri.href, response.headers.location)
|
273 | }
|
274 | self.uri = response.headers.location
|
275 | self.redirects.push( { statusCode : response.statusCode,
|
276 | redirectUri: response.headers.location })
|
277 | delete self.req
|
278 | delete self.agent
|
279 | delete self._started
|
280 | if (self.headers) {
|
281 | delete self.headers.host
|
282 | }
|
283 | request(self, self.callback)
|
284 | return
|
285 | } else {
|
286 | self._redirectsFollowed = self._redirectsFollowed || 0
|
287 |
|
288 |
|
289 | response.on('close', function () {
|
290 | if (!self._ended) self.response.emit('end')
|
291 | })
|
292 |
|
293 | if (self.encoding) {
|
294 | if (self.dests.length !== 0) {
|
295 | console.error("Ingoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.")
|
296 | } else {
|
297 | response.setEncoding(self.encoding)
|
298 | }
|
299 | }
|
300 |
|
301 | self.pipeDest = function (dest) {
|
302 | if (dest.headers) {
|
303 | dest.headers['content-type'] = response.headers['content-type']
|
304 | if (response.headers['content-length']) {
|
305 | dest.headers['content-length'] = response.headers['content-length']
|
306 | }
|
307 | }
|
308 | if (dest.setHeader) {
|
309 | for (var i in response.headers) {
|
310 | dest.setHeader(i, response.headers[i])
|
311 | }
|
312 | dest.statusCode = response.statusCode
|
313 | }
|
314 | if (self.pipefilter) self.pipefilter(response, dest)
|
315 | }
|
316 |
|
317 | self.dests.forEach(function (dest) {
|
318 | self.pipeDest(dest)
|
319 | })
|
320 |
|
321 | response.on("data", function (chunk) {
|
322 | self._destdata = true
|
323 | self.emit("data", chunk)
|
324 | })
|
325 | response.on("end", function (chunk) {
|
326 | self._ended = true
|
327 | self.emit("end", chunk)
|
328 | })
|
329 | response.on("close", function () {self.emit("close")})
|
330 |
|
331 | self.emit('response', response)
|
332 |
|
333 | if (self.onResponse) {
|
334 | self.onResponse(null, response)
|
335 | }
|
336 | if (self.callback) {
|
337 | var buffer = []
|
338 | var bodyLen = 0
|
339 | self.on("data", function (chunk) {
|
340 | buffer.push(chunk)
|
341 | bodyLen += chunk.length
|
342 | })
|
343 | self.on("end", function () {
|
344 | if (buffer.length && Buffer.isBuffer(buffer[0])) {
|
345 | var body = new Buffer(bodyLen)
|
346 | var i = 0
|
347 | buffer.forEach(function (chunk) {
|
348 | chunk.copy(body, i, 0, chunk.length)
|
349 | i += chunk.length
|
350 | })
|
351 | response.body = body.toString()
|
352 | } else if (buffer.length) {
|
353 | response.body = buffer.join('')
|
354 | }
|
355 |
|
356 | if (self.json) {
|
357 | try {
|
358 | response.body = JSON.parse(response.body)
|
359 | } catch (e) {}
|
360 | }
|
361 | self.callback(null, response, response.body)
|
362 | })
|
363 | }
|
364 | }
|
365 | })
|
366 |
|
367 | if (self.timeout) {
|
368 | self.timeoutTimer = setTimeout(function() {
|
369 | self.req.abort()
|
370 | var e = new Error("ETIMEDOUT")
|
371 | e.code = "ETIMEDOUT"
|
372 | self.emit("error", e)
|
373 | }, self.timeout)
|
374 | }
|
375 |
|
376 | self.req.on('error', clientErrorHandler)
|
377 | }
|
378 |
|
379 | self.once('pipe', function (src) {
|
380 | if (self.ntick) throw new Error("You cannot pipe to this stream after the first nextTick() after creation of the request stream.")
|
381 | self.src = src
|
382 | if (isReadStream(src)) {
|
383 | if (!self.headers['content-type'] && !self.headers['Content-Type'])
|
384 | self.headers['content-type'] = mimetypes.lookup(src.path.slice(src.path.lastIndexOf('.')+1))
|
385 | } else {
|
386 | if (src.headers) {
|
387 | for (var i in src.headers) {
|
388 | if (!self.headers[i]) {
|
389 | self.headers[i] = src.headers[i]
|
390 | }
|
391 | }
|
392 | }
|
393 | if (src.method && !self.method) {
|
394 | self.method = src.method
|
395 | }
|
396 | }
|
397 |
|
398 | self.on('pipe', function () {
|
399 | console.error("You have already piped to this stream. Pipeing twice is likely to break the request.")
|
400 | })
|
401 | })
|
402 |
|
403 | process.nextTick(function () {
|
404 | if (self.body) {
|
405 | self.write(self.body)
|
406 | self.end()
|
407 | } else if (self.requestBodyStream) {
|
408 | console.warn("options.requestBodyStream is deprecated, please pass the request object to stream.pipe.")
|
409 | self.requestBodyStream.pipe(self)
|
410 | } else if (!self.src) {
|
411 | self.headers['content-length'] = 0
|
412 | self.end()
|
413 | }
|
414 | self.ntick = true
|
415 | })
|
416 | }
|
417 | Request.prototype.pipe = function (dest) {
|
418 | if (this.response) {
|
419 | if (this._destdata) {
|
420 | throw new Error("You cannot pipe after data has been emitted from the response.")
|
421 | } else if (this._ended) {
|
422 | throw new Error("You cannot pipe after the response has been ended.")
|
423 | } else {
|
424 | stream.Stream.prototype.pipe.call(this, dest)
|
425 | this.pipeDest(dest)
|
426 | return dest
|
427 | }
|
428 | } else {
|
429 | this.dests.push(dest)
|
430 | stream.Stream.prototype.pipe.call(this, dest)
|
431 | return dest
|
432 | }
|
433 | }
|
434 | Request.prototype.write = function () {
|
435 | if (!this._started) this.start()
|
436 | if (!this.req) throw new Error("This request has been piped before http.request() was called.")
|
437 | this.req.write.apply(this.req, arguments)
|
438 | }
|
439 | Request.prototype.end = function () {
|
440 | if (!this._started) this.start()
|
441 | if (!this.req) throw new Error("This request has been piped before http.request() was called.")
|
442 | this.req.end.apply(this.req, arguments)
|
443 | }
|
444 | Request.prototype.pause = function () {
|
445 | if (!this.response) throw new Error("This request has been piped before http.request() was called.")
|
446 | this.response.pause.apply(this.response, arguments)
|
447 | }
|
448 | Request.prototype.resume = function () {
|
449 | if (!this.response) throw new Error("This request has been piped before http.request() was called.")
|
450 | this.response.resume.apply(this.response, arguments)
|
451 | }
|
452 |
|
453 | function request (options, callback) {
|
454 | if (typeof options === 'string') options = {uri:options}
|
455 | if (callback) options.callback = callback
|
456 | var r = new Request(options)
|
457 | r.request()
|
458 | return r
|
459 | }
|
460 |
|
461 | module.exports = request
|
462 |
|
463 | request.defaults = function (options) {
|
464 | var def = function (method) {
|
465 | var d = function (opts, callback) {
|
466 | if (typeof opts === 'string') opts = {uri:opts}
|
467 | for (var i in options) {
|
468 | if (opts[i] === undefined) opts[i] = options[i]
|
469 | }
|
470 | return method(opts, callback)
|
471 | }
|
472 | return d
|
473 | }
|
474 | var de = def(request)
|
475 | de.get = def(request.get)
|
476 | de.post = def(request.post)
|
477 | de.put = def(request.put)
|
478 | de.head = def(request.head)
|
479 | de.del = def(request.del)
|
480 | return de
|
481 | }
|
482 |
|
483 | request.get = request
|
484 | request.post = function (options, callback) {
|
485 | if (typeof options === 'string') options = {uri:options}
|
486 | options.method = 'POST'
|
487 | return request(options, callback)
|
488 | }
|
489 | request.put = function (options, callback) {
|
490 | if (typeof options === 'string') options = {uri:options}
|
491 | options.method = 'PUT'
|
492 | return request(options, callback)
|
493 | }
|
494 | request.head = function (options, callback) {
|
495 | if (typeof options === 'string') options = {uri:options}
|
496 | options.method = 'HEAD'
|
497 | if (options.body || options.requestBodyStream || options.json || options.multipart) {
|
498 | throw new Error("HTTP HEAD requests MUST NOT include a request body.")
|
499 | }
|
500 | return request(options, callback)
|
501 | }
|
502 | request.del = function (options, callback) {
|
503 | if (typeof options === 'string') options = {uri:options}
|
504 | options.method = 'DELETE'
|
505 | return request(options, callback)
|
506 | }
|