'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', 'socket'].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; this._bufferedWrites = []; // 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); }; // Complete the URL object when necessary if (!options.pathname && options.path) { var searchPos = options.path.indexOf('?'); if (searchPos < 0) { options.pathname = options.path; } else { options.pathname = options.path.substring(0, searchPos); options.search = options.path.substring(searchPos); } } // 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[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) { /* istanbul ignore else */ if (event) { request.on(event, eventHandlers[event]); } } // End a redirected request // (The first request must be ended explicitly with RedirectableRequest#end) if (this._isRedirect) { // If the request doesn't have en entity, end directly. var bufferedWrites = this._bufferedWrites; if (bufferedWrites.length === 0) { request.end(); // Otherwise, write the request entity and end afterwards. } else { var i = 0; (function writeNext() { if (i < bufferedWrites.length) { var bufferedWrite = bufferedWrites[i++]; request.write(bufferedWrite.data, bufferedWrite.encoding, writeNext); } else { 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: 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. // 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. var header; var headers = this._options.headers; if (response.statusCode !== 307 && !(this._options.method in safeMethods)) { this._options.method = 'GET'; // Drop a possible entity and headers related to it this._bufferedWrites = []; for (header in headers) { if (/^content-/i.test(header)) { delete headers[header]; } } } // Drop the Host header, as the redirect might lead to a different host if (!this._isRedirect) { for (header in headers) { if (/^host$/i.test(header)) { delete headers[header]; } } } // Perform the redirected request var redirectUrl = url.resolve(this._currentUrl, location); debug('redirecting to', redirectUrl); Object.assign(this._options, url.parse(redirectUrl)); this._isRedirect = true; this._performRequest(); } else { // The response is not a redirect; return it as-is response.responseUrl = this._currentUrl; this.emit('response', response); // Clean up delete this._options; delete this._bufferedWrites; } }; // Aborts the current native request RedirectableRequest.prototype.abort = function () { this._currentRequest.abort(); }; // 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 (data, encoding, callback) { this._currentRequest.write(data, encoding, callback); this._bufferedWrites.push({data: data, encoding: encoding}); }; // Ends the current native request RedirectableRequest.prototype.end = function (data, encoding, callback) { this._currentRequest.end(data, encoding, callback); if (data) { this._bufferedWrites.push({data: data, encoding: encoding}); } }; // 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; }; });