| 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"); | 
|   | 
| // 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); | 
|   }; | 
| }); | 
|   | 
| var InvalidUrlError = createErrorType( | 
|   "ERR_INVALID_URL", | 
|   "Invalid URL", | 
|   TypeError | 
| ); | 
| // Error types with codes | 
| var RedirectionError = createErrorType( | 
|   "ERR_FR_REDIRECTION_FAILURE", | 
|   "Redirected request failed" | 
| ); | 
| var TooManyRedirectsError = createErrorType( | 
|   "ERR_FR_TOO_MANY_REDIRECTS", | 
|   "Maximum number of redirects exceeded" | 
| ); | 
| 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" | 
| ); | 
|   | 
| // 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) { | 
|     self._processResponse(response); | 
|   }; | 
|   | 
|   // Perform the first request | 
|   this._performRequest(); | 
| } | 
| RedirectableRequest.prototype = Object.create(Writable.prototype); | 
|   | 
| RedirectableRequest.prototype.abort = function () { | 
|   abortRequest(this._currentRequest); | 
|   this.emit("abort"); | 
| }; | 
|   | 
| // 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); | 
|     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); | 
|   | 
|   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) { | 
|     this.emit("error", new TypeError("Unsupported protocol " + protocol)); | 
|     return; | 
|   } | 
|   | 
|   // 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 | 
|   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: 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 (isFunction(beforeRedirect)) { | 
|     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: cause })); | 
|   } | 
| }; | 
|   | 
| // 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 | 
|       if (isString(input)) { | 
|         var parsed; | 
|         try { | 
|           parsed = urlToOptions(new URL(input)); | 
|         } | 
|         catch (err) { | 
|           /* istanbul ignore next */ | 
|           parsed = url.parse(input); | 
|         } | 
|         if (!isString(parsed.protocol)) { | 
|           throw new InvalidUrlError({ input }); | 
|         } | 
|         input = parsed; | 
|       } | 
|       else if (URL && (input instanceof URL)) { | 
|         input = urlToOptions(input); | 
|       } | 
|       else { | 
|         callback = options; | 
|         options = 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; | 
| } | 
|   | 
| /* istanbul ignore next */ | 
| function noop() { /* empty */ } | 
|   | 
| // from https://github.com/nodejs/node/blob/master/lib/internal/url.js | 
| function urlToOptions(urlObject) { | 
|   var options = { | 
|     protocol: urlObject.protocol, | 
|     hostname: urlObject.hostname.startsWith("[") ? | 
|       /* istanbul ignore next */ | 
|       urlObject.hostname.slice(1, -1) : | 
|       urlObject.hostname, | 
|     hash: urlObject.hash, | 
|     search: urlObject.search, | 
|     pathname: urlObject.pathname, | 
|     path: urlObject.pathname + urlObject.search, | 
|     href: urlObject.href, | 
|   }; | 
|   if (urlObject.port !== "") { | 
|     options.port = Number(urlObject.port); | 
|   } | 
|   return options; | 
| } | 
|   | 
| 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) { | 
|     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)(); | 
|   CustomError.prototype.constructor = CustomError; | 
|   CustomError.prototype.name = "Error [" + code + "]"; | 
|   return CustomError; | 
| } | 
|   | 
| function abortRequest(request) { | 
|   for (var event of events) { | 
|     request.removeListener(event, eventHandlers[event]); | 
|   } | 
|   request.on("error", noop); | 
|   request.abort(); | 
| } | 
|   | 
| 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); | 
| } | 
|   | 
| // Exports | 
| module.exports = wrap({ http: http, https: https }); | 
| module.exports.wrap = wrap; |