| 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"); |
|
|
| |
| |
| (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."); |
| } |
| }()); |
|
|
| |
| var useNativeURL = false; |
| try { |
| assert(new URL("")); |
| } |
| catch (error) { |
| useNativeURL = error.code === "ERR_INVALID_URL"; |
| } |
|
|
| |
| var preservedUrlFields = [ |
| "auth", |
| "host", |
| "hostname", |
| "href", |
| "path", |
| "pathname", |
| "port", |
| "protocol", |
| "query", |
| "search", |
| "hash", |
| ]; |
|
|
| |
| 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); |
| }; |
| }); |
|
|
| |
| 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" |
| ); |
|
|
| |
| var destroy = Writable.prototype.destroy || noop; |
|
|
| |
| function RedirectableRequest(options, responseCallback) { |
| |
| 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 = []; |
|
|
| |
| if (responseCallback) { |
| this.on("response", responseCallback); |
| } |
|
|
| |
| var self = this; |
| this._onNativeResponse = function (response) { |
| try { |
| self._processResponse(response); |
| } |
| catch (cause) { |
| self.emit("error", cause instanceof RedirectionError ? |
| cause : new RedirectionError({ cause: cause })); |
| } |
| }; |
|
|
| |
| 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; |
| }; |
|
|
| |
| RedirectableRequest.prototype.write = function (data, encoding, callback) { |
| |
| if (this._ending) { |
| throw new WriteAfterEndError(); |
| } |
|
|
| |
| if (!isString(data) && !isBuffer(data)) { |
| throw new TypeError("data should be a string, Buffer or Uint8Array"); |
| } |
| if (isFunction(encoding)) { |
| callback = encoding; |
| encoding = null; |
| } |
|
|
| |
| |
| if (data.length === 0) { |
| if (callback) { |
| callback(); |
| } |
| return; |
| } |
| |
| 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); |
| } |
| |
| else { |
| this.emit("error", new MaxBodyLengthExceededError()); |
| this.abort(); |
| } |
| }; |
|
|
| |
| RedirectableRequest.prototype.end = function (data, encoding, callback) { |
| |
| if (isFunction(data)) { |
| callback = data; |
| data = encoding = null; |
| } |
| else if (isFunction(encoding)) { |
| callback = encoding; |
| encoding = null; |
| } |
|
|
| |
| 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; |
| } |
| }; |
|
|
| |
| RedirectableRequest.prototype.setHeader = function (name, value) { |
| this._options.headers[name] = value; |
| this._currentRequest.setHeader(name, value); |
| }; |
|
|
| |
| RedirectableRequest.prototype.removeHeader = function (name) { |
| delete this._options.headers[name]; |
| this._currentRequest.removeHeader(name); |
| }; |
|
|
| |
| RedirectableRequest.prototype.setTimeout = function (msecs, callback) { |
| var self = this; |
|
|
| |
| function destroyOnTimeout(socket) { |
| socket.setTimeout(msecs); |
| socket.removeListener("timeout", socket.destroy); |
| socket.addListener("timeout", socket.destroy); |
| } |
|
|
| |
| function startTimer(socket) { |
| if (self._timeout) { |
| clearTimeout(self._timeout); |
| } |
| self._timeout = setTimeout(function () { |
| self.emit("timeout"); |
| clearTimer(); |
| }, msecs); |
| destroyOnTimeout(socket); |
| } |
|
|
| |
| function clearTimer() { |
| |
| if (self._timeout) { |
| clearTimeout(self._timeout); |
| self._timeout = null; |
| } |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| if (callback) { |
| this.on("timeout", callback); |
| } |
|
|
| |
| if (this.socket) { |
| startTimer(this.socket); |
| } |
| else { |
| this._currentRequest.once("socket", startTimer); |
| } |
|
|
| |
| this.on("socket", destroyOnTimeout); |
| this.on("abort", clearTimer); |
| this.on("error", clearTimer); |
| this.on("response", clearTimer); |
| this.on("close", clearTimer); |
|
|
| return this; |
| }; |
|
|
| |
| [ |
| "flushHeaders", "getHeader", |
| "setNoDelay", "setSocketKeepAlive", |
| ].forEach(function (method) { |
| RedirectableRequest.prototype[method] = function (a, b) { |
| return this._currentRequest[method](a, b); |
| }; |
| }); |
|
|
| |
| ["aborted", "connection", "socket"].forEach(function (property) { |
| Object.defineProperty(RedirectableRequest.prototype, property, { |
| get: function () { return this._currentRequest[property]; }, |
| }); |
| }); |
|
|
| RedirectableRequest.prototype._sanitizeOptions = function (options) { |
| |
| if (!options.headers) { |
| options.headers = {}; |
| } |
|
|
| |
| |
| |
| if (options.host) { |
| |
| if (!options.hostname) { |
| options.hostname = options.host; |
| } |
| delete options.host; |
| } |
|
|
| |
| 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); |
| } |
| } |
| }; |
|
|
|
|
| |
| RedirectableRequest.prototype._performRequest = function () { |
| |
| var protocol = this._options.protocol; |
| var nativeProtocol = this._options.nativeProtocols[protocol]; |
| if (!nativeProtocol) { |
| throw new TypeError("Unsupported protocol " + protocol); |
| } |
|
|
| |
| |
| if (this._options.agents) { |
| var scheme = protocol.slice(0, -1); |
| this._options.agent = this._options.agents[scheme]; |
| } |
|
|
| |
| var request = this._currentRequest = |
| nativeProtocol.request(this._options, this._onNativeResponse); |
| request._redirectable = this; |
| for (var event of events) { |
| request.on(event, eventHandlers[event]); |
| } |
|
|
| |
| |
| this._currentUrl = /^\//.test(this._options.path) ? |
| url.format(this._options) : |
| |
| |
| this._options.path; |
|
|
| |
| |
| if (this._isRedirect) { |
| |
| var i = 0; |
| var self = this; |
| var buffers = this._requestBodyBuffers; |
| (function writeNext(error) { |
| |
| |
| if (request === self._currentRequest) { |
| |
| |
| if (error) { |
| self.emit("error", error); |
| } |
| |
| else if (i < buffers.length) { |
| var buffer = buffers[i++]; |
| |
| if (!request.finished) { |
| request.write(buffer.data, buffer.encoding, writeNext); |
| } |
| } |
| |
| else if (self._ended) { |
| request.end(); |
| } |
| } |
| }()); |
| } |
| }; |
|
|
| |
| RedirectableRequest.prototype._processResponse = function (response) { |
| |
| var statusCode = response.statusCode; |
| if (this._options.trackRedirects) { |
| this._redirects.push({ |
| url: this._currentUrl, |
| headers: response.headers, |
| statusCode: statusCode, |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| 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); |
|
|
| |
| this._requestBodyBuffers = []; |
| return; |
| } |
|
|
| |
| destroyRequest(this._currentRequest); |
| |
| response.destroy(); |
|
|
| |
| |
| if (++this._redirectCount > this._options.maxRedirects) { |
| throw new TooManyRedirectsError(); |
| } |
|
|
| |
| var requestHeaders; |
| var beforeRedirect = this._options.beforeRedirect; |
| if (beforeRedirect) { |
| requestHeaders = Object.assign({ |
| |
| Host: response.req.getHeader("host"), |
| }, this._options.headers); |
| } |
|
|
| |
| |
| |
| |
| var method = this._options.method; |
| if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" || |
| |
| |
| |
| |
| (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) { |
| this._options.method = "GET"; |
| |
| this._requestBodyBuffers = []; |
| removeMatchingHeaders(/^content-/i, this._options.headers); |
| } |
|
|
| |
| var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers); |
|
|
| |
| var currentUrlParts = parseUrl(this._currentUrl); |
| var currentHost = currentHostHeader || currentUrlParts.host; |
| var currentUrl = /^\w+:/.test(location) ? this._currentUrl : |
| url.format(Object.assign(currentUrlParts, { host: currentHost })); |
|
|
| |
| var redirectUrl = resolveUrl(location, currentUrl); |
| debug("redirecting to", redirectUrl.href); |
| this._isRedirect = true; |
| spreadUrlObject(redirectUrl, this._options); |
|
|
| |
| |
| if (redirectUrl.protocol !== currentUrlParts.protocol && |
| redirectUrl.protocol !== "https:" || |
| redirectUrl.host !== currentHost && |
| !isSubdomain(redirectUrl.host, currentHost)) { |
| removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| this._performRequest(); |
| }; |
|
|
| |
| function wrap(protocols) { |
| |
| var exports = { |
| maxRedirects: 21, |
| maxBodyLength: 10 * 1024 * 1024, |
| }; |
|
|
| |
| var nativeProtocols = {}; |
| Object.keys(protocols).forEach(function (scheme) { |
| var protocol = scheme + ":"; |
| var nativeProtocol = nativeProtocols[protocol] = protocols[scheme]; |
| var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); |
|
|
| |
| function request(input, options, callback) { |
| |
| 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; |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| function get(input, options, callback) { |
| var wrappedRequest = wrappedProtocol.request(input, options, callback); |
| wrappedRequest.end(); |
| return wrappedRequest; |
| } |
|
|
| |
| 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() { } |
|
|
| function parseUrl(input) { |
| var parsed; |
| |
| if (useNativeURL) { |
| parsed = new URL(input); |
| } |
| else { |
| |
| parsed = validateUrl(url.parse(input)); |
| if (!isString(parsed.protocol)) { |
| throw new InvalidUrlError({ input }); |
| } |
| } |
| return parsed; |
| } |
|
|
| function resolveUrl(relative, base) { |
| |
| 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]; |
| } |
|
|
| |
| if (spread.hostname.startsWith("[")) { |
| spread.hostname = spread.hostname.slice(1, -1); |
| } |
| |
| if (spread.port !== "") { |
| spread.port = Number(spread.port); |
| } |
| |
| 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) { |
| |
| function CustomError(properties) { |
| |
| 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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| module.exports = wrap({ http: http, https: https }); |
| module.exports.wrap = wrap; |
|
|