|
|
var url = require("url");var URL = url.URL;var http = require("http");var https = require("https");var Writable = require("stream").Writable;var assert = require("assert");var debug = require("./debug");
// Preventive platform detection
// istanbul ignore next
(function detectUnsupportedEnvironment() { var looksLikeNode = typeof process !== "undefined"; var looksLikeBrowser = typeof window !== "undefined" && typeof document !== "undefined"; var looksLikeV8 = isFunction(Error.captureStackTrace); if (!looksLikeNode && (looksLikeBrowser || !looksLikeV8)) { console.warn("The follow-redirects package should be excluded from browser builds."); }}());
// Whether to use the native URL object or the legacy url module
var useNativeURL = false;try { assert(new URL(""));}catch (error) { useNativeURL = error.code === "ERR_INVALID_URL";}
// URL fields to preserve in copy operations
var preservedUrlFields = [ "auth", "host", "hostname", "href", "path", "pathname", "port", "protocol", "query", "search", "hash",];
// Create handlers that pass events from native requests
var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];var eventHandlers = Object.create(null);events.forEach(function (event) { eventHandlers[event] = function (arg1, arg2, arg3) { this._redirectable.emit(event, arg1, arg2, arg3); };});
// Error types with codes
var InvalidUrlError = createErrorType( "ERR_INVALID_URL", "Invalid URL", TypeError);var RedirectionError = createErrorType( "ERR_FR_REDIRECTION_FAILURE", "Redirected request failed");var TooManyRedirectsError = createErrorType( "ERR_FR_TOO_MANY_REDIRECTS", "Maximum number of redirects exceeded", RedirectionError);var MaxBodyLengthExceededError = createErrorType( "ERR_FR_MAX_BODY_LENGTH_EXCEEDED", "Request body larger than maxBodyLength limit");var WriteAfterEndError = createErrorType( "ERR_STREAM_WRITE_AFTER_END", "write after end");
// istanbul ignore next
var destroy = Writable.prototype.destroy || noop;
// An HTTP(S) request that can be redirected
function RedirectableRequest(options, responseCallback) { // Initialize the request
Writable.call(this); this._sanitizeOptions(options); this._options = options; this._ended = false; this._ending = false; this._redirectCount = 0; this._redirects = []; this._requestBodyLength = 0; this._requestBodyBuffers = [];
// Attach a callback if passed
if (responseCallback) { this.on("response", responseCallback); }
// React to responses of native requests
var self = this; this._onNativeResponse = function (response) { try { self._processResponse(response); } catch (cause) { self.emit("error", cause instanceof RedirectionError ? cause : new RedirectionError({ cause: cause })); } };
// Perform the first request
this._performRequest();}RedirectableRequest.prototype = Object.create(Writable.prototype);
RedirectableRequest.prototype.abort = function () { destroyRequest(this._currentRequest); this._currentRequest.abort(); this.emit("abort");};
RedirectableRequest.prototype.destroy = function (error) { destroyRequest(this._currentRequest, error); destroy.call(this, error); return this;};
// Writes buffered data to the current native request
RedirectableRequest.prototype.write = function (data, encoding, callback) { // Writing is not allowed if end has been called
if (this._ending) { throw new WriteAfterEndError(); }
// Validate input and shift parameters if necessary
if (!isString(data) && !isBuffer(data)) { throw new TypeError("data should be a string, Buffer or Uint8Array"); } if (isFunction(encoding)) { callback = encoding; encoding = null; }
// Ignore empty buffers, since writing them doesn't invoke the callback
// https://github.com/nodejs/node/issues/22066
if (data.length === 0) { if (callback) { callback(); } return; } // Only write when we don't exceed the maximum body length
if (this._requestBodyLength + data.length <= this._options.maxBodyLength) { this._requestBodyLength += data.length; this._requestBodyBuffers.push({ data: data, encoding: encoding }); this._currentRequest.write(data, encoding, callback); } // Error when we exceed the maximum body length
else { this.emit("error", new MaxBodyLengthExceededError()); this.abort(); }};
// Ends the current native request
RedirectableRequest.prototype.end = function (data, encoding, callback) { // Shift parameters if necessary
if (isFunction(data)) { callback = data; data = encoding = null; } else if (isFunction(encoding)) { callback = encoding; encoding = null; }
// Write data if needed and end
if (!data) { this._ended = this._ending = true; this._currentRequest.end(null, null, callback); } else { var self = this; var currentRequest = this._currentRequest; this.write(data, encoding, function () { self._ended = true; currentRequest.end(null, null, callback); }); this._ending = true; }};
// Sets a header value on the current native request
RedirectableRequest.prototype.setHeader = function (name, value) { this._options.headers[name] = value; this._currentRequest.setHeader(name, value);};
// Clears a header value on the current native request
RedirectableRequest.prototype.removeHeader = function (name) { delete this._options.headers[name]; this._currentRequest.removeHeader(name);};
// Global timeout for all underlying requests
RedirectableRequest.prototype.setTimeout = function (msecs, callback) { var self = this;
// Destroys the socket on timeout
function destroyOnTimeout(socket) { socket.setTimeout(msecs); socket.removeListener("timeout", socket.destroy); socket.addListener("timeout", socket.destroy); }
// Sets up a timer to trigger a timeout event
function startTimer(socket) { if (self._timeout) { clearTimeout(self._timeout); } self._timeout = setTimeout(function () { self.emit("timeout"); clearTimer(); }, msecs); destroyOnTimeout(socket); }
// Stops a timeout from triggering
function clearTimer() { // Clear the timeout
if (self._timeout) { clearTimeout(self._timeout); self._timeout = null; }
// Clean up all attached listeners
self.removeListener("abort", clearTimer); self.removeListener("error", clearTimer); self.removeListener("response", clearTimer); self.removeListener("close", clearTimer); if (callback) { self.removeListener("timeout", callback); } if (!self.socket) { self._currentRequest.removeListener("socket", startTimer); } }
// Attach callback if passed
if (callback) { this.on("timeout", callback); }
// Start the timer if or when the socket is opened
if (this.socket) { startTimer(this.socket); } else { this._currentRequest.once("socket", startTimer); }
// Clean up on events
this.on("socket", destroyOnTimeout); this.on("abort", clearTimer); this.on("error", clearTimer); this.on("response", clearTimer); this.on("close", clearTimer);
return this;};
// Proxy all other public ClientRequest methods
[ "flushHeaders", "getHeader", "setNoDelay", "setSocketKeepAlive",].forEach(function (method) { RedirectableRequest.prototype[method] = function (a, b) { return this._currentRequest[method](a, b); };});
// Proxy all public ClientRequest properties
["aborted", "connection", "socket"].forEach(function (property) { Object.defineProperty(RedirectableRequest.prototype, property, { get: function () { return this._currentRequest[property]; }, });});
RedirectableRequest.prototype._sanitizeOptions = function (options) { // Ensure headers are always present
if (!options.headers) { options.headers = {}; }
// Since http.request treats host as an alias of hostname,
// but the url module interprets host as hostname plus port,
// eliminate the host property to avoid confusion.
if (options.host) { // Use hostname if set, because it has precedence
if (!options.hostname) { options.hostname = options.host; } delete options.host; }
// 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); } }};
// Executes the next native request (initial or redirect)
RedirectableRequest.prototype._performRequest = function () { // Load the native protocol
var protocol = this._options.protocol; var nativeProtocol = this._options.nativeProtocols[protocol]; if (!nativeProtocol) { throw new TypeError("Unsupported protocol " + protocol); }
// If specified, use the agent corresponding to the protocol
// (HTTP and HTTPS use different types of agents)
if (this._options.agents) { var scheme = protocol.slice(0, -1); this._options.agent = this._options.agents[scheme]; }
// Create the native request and set up its event handlers
var request = this._currentRequest = nativeProtocol.request(this._options, this._onNativeResponse); request._redirectable = this; for (var event of events) { request.on(event, eventHandlers[event]); }
// RFC7230§5.3.1: When making a request directly to an origin server, […]
// a client MUST send only the absolute path […] as the request-target.
this._currentUrl = /^\//.test(this._options.path) ? url.format(this._options) : // When making a request to a proxy, […]
// a client MUST send the target URI in absolute-form […].
this._options.path;
// End a redirected request
// (The first request must be ended explicitly with RedirectableRequest#end)
if (this._isRedirect) { // Write the request entity and end
var i = 0; var self = this; var buffers = this._requestBodyBuffers; (function writeNext(error) { // Only write if this request has not been redirected yet
// istanbul ignore else
if (request === self._currentRequest) { // Report any write errors
// istanbul ignore if
if (error) { self.emit("error", error); } // Write the next buffer if there are still left
else if (i < buffers.length) { var buffer = buffers[i++]; // istanbul ignore else
if (!request.finished) { request.write(buffer.data, buffer.encoding, writeNext); } } // End the request if `end` has been called on us
else if (self._ended) { request.end(); } } }()); }};
// Processes a response from the current native request
RedirectableRequest.prototype._processResponse = function (response) { // Store the redirected response
var statusCode = response.statusCode; if (this._options.trackRedirects) { this._redirects.push({ url: this._currentUrl, headers: response.headers, statusCode: statusCode, }); }
// 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.
// If the response is not a redirect; return it as-is
var location = response.headers.location; if (!location || this._options.followRedirects === false || statusCode < 300 || statusCode >= 400) { response.responseUrl = this._currentUrl; response.redirects = this._redirects; this.emit("response", response);
// Clean up
this._requestBodyBuffers = []; return; }
// The response is a redirect, so abort the current request
destroyRequest(this._currentRequest); // Discard the remainder of the response to avoid waiting for data
response.destroy();
// RFC7231§6.4: A client SHOULD detect and intervene
// in cyclical redirections (i.e., "infinite" redirection loops).
if (++this._redirectCount > this._options.maxRedirects) { throw new TooManyRedirectsError(); }
// Store the request headers if applicable
var requestHeaders; var beforeRedirect = this._options.beforeRedirect; if (beforeRedirect) { requestHeaders = Object.assign({ // The Host header was set by nativeProtocol.request
Host: response.req.getHeader("host"), }, this._options.headers); }
// RFC7231§6.4: Automatic redirection needs to done with
// care for methods not known to be safe, […]
// RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
// the request method from POST to GET for the subsequent request.
var method = this._options.method; if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" || // RFC7231§6.4.4: The 303 (See Other) status code indicates that
// the server is redirecting the user agent to a different resource […]
// A user agent can perform a retrieval request targeting that URI
// (a GET or HEAD request if using HTTP) […]
(statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) { this._options.method = "GET"; // Drop a possible entity and headers related to it
this._requestBodyBuffers = []; removeMatchingHeaders(/^content-/i, this._options.headers); }
// Drop the Host header, as the redirect might lead to a different host
var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
// If the redirect is relative, carry over the host of the last request
var currentUrlParts = parseUrl(this._currentUrl); var currentHost = currentHostHeader || currentUrlParts.host; var currentUrl = /^\w+:/.test(location) ? this._currentUrl : url.format(Object.assign(currentUrlParts, { host: currentHost }));
// Create the redirected request
var redirectUrl = resolveUrl(location, currentUrl); debug("redirecting to", redirectUrl.href); this._isRedirect = true; spreadUrlObject(redirectUrl, this._options);
// Drop confidential headers when redirecting to a less secure protocol
// or to a different domain that is not a superdomain
if (redirectUrl.protocol !== currentUrlParts.protocol && redirectUrl.protocol !== "https:" || redirectUrl.host !== currentHost && !isSubdomain(redirectUrl.host, currentHost)) { removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers); }
// Evaluate the beforeRedirect callback
if (isFunction(beforeRedirect)) { var responseDetails = { headers: response.headers, statusCode: statusCode, }; var requestDetails = { url: currentUrl, method: method, headers: requestHeaders, }; beforeRedirect(this._options, responseDetails, requestDetails); this._sanitizeOptions(this._options); }
// Perform the redirected request
this._performRequest();};
// Wraps the key/value object of protocols with redirect functionality
function wrap(protocols) { // Default settings
var exports = { maxRedirects: 21, maxBodyLength: 10 * 1024 * 1024, };
// Wrap each protocol
var nativeProtocols = {}; Object.keys(protocols).forEach(function (scheme) { var protocol = scheme + ":"; var nativeProtocol = nativeProtocols[protocol] = protocols[scheme]; var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
// Executes a request, following redirects
function request(input, options, callback) { // Parse parameters, ensuring that input is an object
if (isURL(input)) { input = spreadUrlObject(input); } else if (isString(input)) { input = spreadUrlObject(parseUrl(input)); } else { callback = options; options = validateUrl(input); input = { protocol: protocol }; } if (isFunction(options)) { callback = options; options = null; }
// Set defaults
options = Object.assign({ maxRedirects: exports.maxRedirects, maxBodyLength: exports.maxBodyLength, }, input, options); options.nativeProtocols = nativeProtocols; if (!isString(options.host) && !isString(options.hostname)) { options.hostname = "::1"; }
assert.equal(options.protocol, protocol, "protocol mismatch"); debug("options", options); return new RedirectableRequest(options, callback); }
// Executes a GET request, following redirects
function get(input, options, callback) { var wrappedRequest = wrappedProtocol.request(input, options, callback); wrappedRequest.end(); return wrappedRequest; }
// Expose the properties on the wrapped protocol
Object.defineProperties(wrappedProtocol, { request: { value: request, configurable: true, enumerable: true, writable: true }, get: { value: get, configurable: true, enumerable: true, writable: true }, }); }); return exports;}
function noop() { /* empty */ }
function parseUrl(input) { var parsed; // istanbul ignore else
if (useNativeURL) { parsed = new URL(input); } else { // Ensure the URL is valid and absolute
parsed = validateUrl(url.parse(input)); if (!isString(parsed.protocol)) { throw new InvalidUrlError({ input }); } } return parsed;}
function resolveUrl(relative, base) { // istanbul ignore next
return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative));}
function validateUrl(input) { if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) { throw new InvalidUrlError({ input: input.href || input }); } if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) { throw new InvalidUrlError({ input: input.href || input }); } return input;}
function spreadUrlObject(urlObject, target) { var spread = target || {}; for (var key of preservedUrlFields) { spread[key] = urlObject[key]; }
// Fix IPv6 hostname
if (spread.hostname.startsWith("[")) { spread.hostname = spread.hostname.slice(1, -1); } // Ensure port is a number
if (spread.port !== "") { spread.port = Number(spread.port); } // Concatenate path
spread.path = spread.search ? spread.pathname + spread.search : spread.pathname;
return spread;}
function removeMatchingHeaders(regex, headers) { var lastValue; for (var header in headers) { if (regex.test(header)) { lastValue = headers[header]; delete headers[header]; } } return (lastValue === null || typeof lastValue === "undefined") ? undefined : String(lastValue).trim();}
function createErrorType(code, message, baseClass) { // Create constructor
function CustomError(properties) { // istanbul ignore else
if (isFunction(Error.captureStackTrace)) { Error.captureStackTrace(this, this.constructor); } Object.assign(this, properties || {}); this.code = code; this.message = this.cause ? message + ": " + this.cause.message : message; }
// Attach constructor and set default properties
CustomError.prototype = new (baseClass || Error)(); Object.defineProperties(CustomError.prototype, { constructor: { value: CustomError, enumerable: false, }, name: { value: "Error [" + code + "]", enumerable: false, }, }); return CustomError;}
function destroyRequest(request, error) { for (var event of events) { request.removeListener(event, eventHandlers[event]); } request.on("error", noop); request.destroy(error);}
function isSubdomain(subdomain, domain) { assert(isString(subdomain) && isString(domain)); var dot = subdomain.length - domain.length - 1; return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);}
function isString(value) { return typeof value === "string" || value instanceof String;}
function isFunction(value) { return typeof value === "function";}
function isBuffer(value) { return typeof value === "object" && ("length" in value);}
function isURL(value) { return URL && value instanceof URL;}
// Exports
module.exports = wrap({ http: http, https: https });module.exports.wrap = wrap;
|