| | |
| | | // Error types with codes |
| | | var RedirectionError = createErrorType( |
| | | "ERR_FR_REDIRECTION_FAILURE", |
| | | "" |
| | | "Redirected request failed" |
| | | ); |
| | | var TooManyRedirectsError = createErrorType( |
| | | "ERR_FR_TOO_MANY_REDIRECTS", |
| | |
| | | |
| | | // 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); |
| | | if (callback) { |
| | | self.removeListener("timeout", callback); |
| | | } |
| | |
| | | |
| | | // Clean up on events |
| | | this.on("socket", destroyOnTimeout); |
| | | this.once("response", clearTimer); |
| | | this.once("error", clearTimer); |
| | | this.on("abort", clearTimer); |
| | | this.on("error", clearTimer); |
| | | this.on("response", clearTimer); |
| | | |
| | | return this; |
| | | }; |
| | |
| | | // If specified, use the agent corresponding to the protocol |
| | | // (HTTP and HTTPS use different types of agents) |
| | | if (this._options.agents) { |
| | | var scheme = protocol.substr(0, protocol.length - 1); |
| | | var scheme = protocol.slice(0, -1); |
| | | this._options.agent = this._options.agents[scheme]; |
| | | } |
| | | |
| | |
| | | // 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) { |
| | | // Abort the current request |
| | | abortRequest(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) { |
| | | this.emit("error", new TooManyRedirectsError()); |
| | | return; |
| | | } |
| | | |
| | | // 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. |
| | | 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 previousHostName = removeMatchingHeaders(/^host$/i, this._options.headers) || |
| | | url.parse(this._currentUrl).hostname; |
| | | |
| | | // Create the redirected request |
| | | var redirectUrl = url.resolve(this._currentUrl, location); |
| | | debug("redirecting to", redirectUrl); |
| | | this._isRedirect = true; |
| | | var redirectUrlParts = url.parse(redirectUrl); |
| | | Object.assign(this._options, redirectUrlParts); |
| | | |
| | | // Drop the Authorization header if redirecting to another host |
| | | if (redirectUrlParts.hostname !== previousHostName) { |
| | | removeMatchingHeaders(/^authorization$/i, this._options.headers); |
| | | } |
| | | |
| | | // Evaluate the beforeRedirect callback |
| | | if (typeof this._options.beforeRedirect === "function") { |
| | | var responseDetails = { headers: response.headers }; |
| | | try { |
| | | this._options.beforeRedirect.call(null, this._options, responseDetails); |
| | | } |
| | | catch (err) { |
| | | this.emit("error", err); |
| | | return; |
| | | } |
| | | this._sanitizeOptions(this._options); |
| | | } |
| | | |
| | | // Perform the redirected request |
| | | try { |
| | | this._performRequest(); |
| | | } |
| | | catch (cause) { |
| | | var error = new RedirectionError("Redirected request failed: " + cause.message); |
| | | error.cause = cause; |
| | | this.emit("error", error); |
| | | } |
| | | } |
| | | else { |
| | | // The response is not a redirect; return it as-is |
| | | 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 |
| | | abortRequest(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) { |
| | | this.emit("error", new TooManyRedirectsError()); |
| | | return; |
| | | } |
| | | |
| | | // 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 = url.parse(this._currentUrl); |
| | | var currentHost = currentHostHeader || currentUrlParts.host; |
| | | var currentUrl = /^\w+:/.test(location) ? this._currentUrl : |
| | | url.format(Object.assign(currentUrlParts, { host: currentHost })); |
| | | |
| | | // Determine the URL of the redirection |
| | | var redirectUrl; |
| | | try { |
| | | redirectUrl = url.resolve(currentUrl, location); |
| | | } |
| | | catch (cause) { |
| | | this.emit("error", new RedirectionError(cause)); |
| | | return; |
| | | } |
| | | |
| | | // Create the redirected request |
| | | debug("redirecting to", redirectUrl); |
| | | this._isRedirect = true; |
| | | var redirectUrlParts = url.parse(redirectUrl); |
| | | Object.assign(this._options, redirectUrlParts); |
| | | |
| | | // Drop confidential headers when redirecting to a less secure protocol |
| | | // or to a different domain that is not a superdomain |
| | | if (redirectUrlParts.protocol !== currentUrlParts.protocol && |
| | | redirectUrlParts.protocol !== "https:" || |
| | | redirectUrlParts.host !== currentHost && |
| | | !isSubdomain(redirectUrlParts.host, currentHost)) { |
| | | removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); |
| | | } |
| | | |
| | | // Evaluate the beforeRedirect callback |
| | | if (typeof beforeRedirect === "function") { |
| | | var responseDetails = { |
| | | headers: response.headers, |
| | | statusCode: statusCode, |
| | | }; |
| | | var requestDetails = { |
| | | url: currentUrl, |
| | | method: method, |
| | | headers: requestHeaders, |
| | | }; |
| | | try { |
| | | beforeRedirect(this._options, responseDetails, requestDetails); |
| | | } |
| | | catch (err) { |
| | | this.emit("error", err); |
| | | return; |
| | | } |
| | | this._sanitizeOptions(this._options); |
| | | } |
| | | |
| | | // Perform the redirected request |
| | | try { |
| | | this._performRequest(); |
| | | } |
| | | catch (cause) { |
| | | this.emit("error", new RedirectionError(cause)); |
| | | } |
| | | }; |
| | | |
| | |
| | | delete headers[header]; |
| | | } |
| | | } |
| | | return lastValue; |
| | | return (lastValue === null || typeof lastValue === "undefined") ? |
| | | undefined : String(lastValue).trim(); |
| | | } |
| | | |
| | | function createErrorType(code, defaultMessage) { |
| | | function CustomError(message) { |
| | | function CustomError(cause) { |
| | | Error.captureStackTrace(this, this.constructor); |
| | | this.message = message || defaultMessage; |
| | | if (!cause) { |
| | | this.message = defaultMessage; |
| | | } |
| | | else { |
| | | this.message = defaultMessage + ": " + cause.message; |
| | | this.cause = cause; |
| | | } |
| | | } |
| | | CustomError.prototype = new Error(); |
| | | CustomError.prototype.constructor = CustomError; |
| | |
| | | request.abort(); |
| | | } |
| | | |
| | | function isSubdomain(subdomain, domain) { |
| | | const dot = subdomain.length - domain.length - 1; |
| | | return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain); |
| | | } |
| | | |
| | | // Exports |
| | | module.exports = wrap({ http: http, https: https }); |
| | | module.exports.wrap = wrap; |