| 'use strict'; |
| var url = require('url'); |
| var assert = require('assert'); |
| var http = require('http'); |
| var https = require('https'); |
| var Writable = require('stream').Writable; |
| var debug = require('debug')('follow-redirects'); |
| |
| var nativeProtocols = {'http:': http, 'https:': https}; |
| var schemes = {}; |
| var exports = module.exports = { |
| maxRedirects: 21 |
| }; |
| // RFC7231§4.2.1: Of the request methods defined by this specification, |
| // the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. |
| var safeMethods = {GET: true, HEAD: true, OPTIONS: true, TRACE: true}; |
| |
| // Create handlers that pass events from native requests |
| var eventHandlers = Object.create(null); |
| ['abort', 'aborted', 'error'].forEach(function (event) { |
| eventHandlers[event] = function (arg) { |
| this._redirectable.emit(event, arg); |
| }; |
| }); |
| |
| // An HTTP(S) request that can be redirected |
| function RedirectableRequest(options, responseCallback) { |
| // Initialize the request |
| Writable.call(this); |
| this._options = options; |
| this._redirectCount = 0; |
| |
| // Attach a callback if passed |
| if (responseCallback) { |
| this.on('response', responseCallback); |
| } |
| |
| // React to responses of native requests |
| var self = this; |
| this._onNativeResponse = function (response) { |
| self._processResponse(response); |
| }; |
| |
| // Perform the first request |
| this._performRequest(); |
| } |
| RedirectableRequest.prototype = Object.create(Writable.prototype); |
| |
| // Executes the next native request (initial or redirect) |
| RedirectableRequest.prototype._performRequest = function () { |
| // If specified, use the agent corresponding to the protocol |
| // (HTTP and HTTPS use different types of agents) |
| var protocol = this._options.protocol; |
| if (this._options.agents) { |
| this._options.agent = this._options.agents[schemes[protocol]]; |
| } |
| |
| // Create the native request |
| var nativeProtocol = nativeProtocols[this._options.protocol]; |
| var request = this._currentRequest = |
| nativeProtocol.request(this._options, this._onNativeResponse); |
| this._currentUrl = url.format(this._options); |
| |
| // Set up event handlers |
| request._redirectable = this; |
| for (var event in eventHandlers) { |
| if (event) { |
| request.on(event, eventHandlers[event]); |
| } |
| } |
| |
| // The first request is explicitly ended in RedirectableRequest#end |
| if (this._currentResponse) { |
| request.end(); |
| } |
| }; |
| |
| // Processes a response from the current native request |
| RedirectableRequest.prototype._processResponse = function (response) { |
| // RFC7231§6.4: The 3xx (Redirection) class of status code indicates |
| // that further action needs to be taken by the user agent in order to |
| // fulfill the request. If a Location header field is provided, |
| // the user agent MAY automatically redirect its request to the URI |
| // referenced by the Location field value, |
| // even if the specific status code is not understood. |
| var location = response.headers.location; |
| if (location && this._options.followRedirects !== false && |
| response.statusCode >= 300 && response.statusCode < 400) { |
| // RFC7231§6.4: A client SHOULD detect and intervene |
| // in cyclical redirections (i.e., "infinite" redirection loops). |
| if (++this._redirectCount > this._options.maxRedirects) { |
| return this.emit('error', new Error('Max redirects exceeded.')); |
| } |
| |
| // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates |
| // that the target resource resides temporarily under a different URI |
| // and the user agent MUST NOT change the request method |
| // if it performs an automatic redirection to that URI. |
| if (response.statusCode !== 307) { |
| // RFC7231§6.4: Automatic redirection needs to done with |
| // care for methods not known to be safe […], |
| // since the user might not wish to redirect an unsafe request. |
| if (!(this._options.method in safeMethods)) { |
| this._options.method = 'GET'; |
| } |
| } |
| |
| // Perform the redirected request |
| var redirectUrl = url.resolve(this._currentUrl, location); |
| debug('redirecting to', redirectUrl); |
| Object.assign(this._options, url.parse(redirectUrl)); |
| this._currentResponse = response; |
| this._performRequest(); |
| } else { |
| // The response is not a redirect; return it as-is |
| response.responseUrl = this._currentUrl; |
| return this.emit('response', response); |
| } |
| }; |
| |
| // Aborts the current native request |
| RedirectableRequest.prototype.abort = function () { |
| this._currentRequest.abort(); |
| }; |
| |
| // Ends the current native request |
| RedirectableRequest.prototype.end = function (data, encoding, callback) { |
| this._currentRequest.end(data, encoding, callback); |
| }; |
| |
| // Flushes the headers of the current native request |
| RedirectableRequest.prototype.flushHeaders = function () { |
| this._currentRequest.flushHeaders(); |
| }; |
| |
| // Sets the noDelay option of the current native request |
| RedirectableRequest.prototype.setNoDelay = function (noDelay) { |
| this._currentRequest.setNoDelay(noDelay); |
| }; |
| |
| // Sets the socketKeepAlive option of the current native request |
| RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) { |
| this._currentRequest.setSocketKeepAlive(enable, initialDelay); |
| }; |
| |
| // Sets the timeout option of the current native request |
| RedirectableRequest.prototype.setTimeout = function (timeout, callback) { |
| this._currentRequest.setTimeout(timeout, callback); |
| }; |
| |
| // Writes buffered data to the current native request |
| RedirectableRequest.prototype._write = function (chunk, encoding, callback) { |
| this._currentRequest.write(chunk, encoding, callback); |
| }; |
| |
| // Export a redirecting wrapper for each native protocol |
| Object.keys(nativeProtocols).forEach(function (protocol) { |
| var scheme = schemes[protocol] = protocol.substr(0, protocol.length - 1); |
| var nativeProtocol = nativeProtocols[protocol]; |
| var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); |
| |
| // Executes an HTTP request, following redirects |
| wrappedProtocol.request = function (options, callback) { |
| if (typeof options === 'string') { |
| options = url.parse(options); |
| options.maxRedirects = exports.maxRedirects; |
| } else { |
| options = Object.assign({ |
| maxRedirects: exports.maxRedirects, |
| protocol: protocol |
| }, options); |
| } |
| assert.equal(options.protocol, protocol, 'protocol mismatch'); |
| debug('options', options); |
| |
| return new RedirectableRequest(options, callback); |
| }; |
| |
| // Executes a GET request, following redirects |
| wrappedProtocol.get = function (options, callback) { |
| var request = wrappedProtocol.request(options, callback); |
| request.end(); |
| return request; |
| }; |
| }); |