186 lines
6.3 KiB
JavaScript
Vendored
186 lines
6.3 KiB
JavaScript
Vendored
'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;
|
|
};
|
|
});
|