blob: c942c67db70265514bced65a94513f119154c328 [file] [log] [blame]
'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;
};
});