| var url = require("url"); |
| var URL = url.URL; |
| var http = require("http"); |
| var https = require("https"); |
| var assert = require("assert"); |
| var Writable = require("stream").Writable; |
| var debug = require("debug")("follow-redirects"); |
| |
| // 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 SAFE_METHODS = { GET: true, HEAD: true, OPTIONS: true, TRACE: true }; |
| |
| // Create handlers that pass events from native requests |
| var eventHandlers = Object.create(null); |
| ["abort", "aborted", "connect", "error", "socket", "timeout"].forEach(function (event) { |
| eventHandlers[event] = function (arg1, arg2, arg3) { |
| this._redirectable.emit(event, arg1, arg2, arg3); |
| }; |
| }); |
| |
| // 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); |
| |
| // 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 Error("write after end"); |
| } |
| |
| // Validate input and shift parameters if necessary |
| if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) { |
| throw new Error("data should be a string, Buffer or Uint8Array"); |
| } |
| if (typeof encoding === "function") { |
| 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 Error("Request body larger than maxBodyLength limit")); |
| this.abort(); |
| } |
| }; |
| |
| // Ends the current native request |
| RedirectableRequest.prototype.end = function (data, encoding, callback) { |
| // Shift parameters if necessary |
| if (typeof data === "function") { |
| callback = data; |
| data = encoding = null; |
| } |
| else if (typeof encoding === "function") { |
| 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) { |
| if (callback) { |
| this.once("timeout", callback); |
| } |
| |
| if (this.socket) { |
| startTimer(this, msecs); |
| } |
| else { |
| var self = this; |
| this._currentRequest.once("socket", function () { |
| startTimer(self, msecs); |
| }); |
| } |
| |
| this.once("response", clearTimer); |
| this.once("error", clearTimer); |
| |
| return this; |
| }; |
| |
| function startTimer(request, msecs) { |
| clearTimeout(request._timeout); |
| request._timeout = setTimeout(function () { |
| request.emit("timeout"); |
| }, msecs); |
| } |
| |
| function clearTimer() { |
| clearTimeout(this._timeout); |
| } |
| |
| // Proxy all other public ClientRequest methods |
| [ |
| "abort", "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 Error("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.substr(0, protocol.length - 1); |
| this._options.agent = this._options.agents[scheme]; |
| } |
| |
| // Create the native request |
| 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) { |
| /* istanbul ignore else */ |
| if (event) { |
| request.on(event, eventHandlers[event]); |
| } |
| } |
| |
| // 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. |
| var location = response.headers.location; |
| if (location && this._options.followRedirects !== false && |
| statusCode >= 300 && statusCode < 400) { |
| // Abort the current request |
| this._currentRequest.removeAllListeners(); |
| this._currentRequest.on("error", noop); |
| this._currentRequest.abort(); |
| // 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 Error("Max redirects exceeded.")); |
| return; |
| } |
| |
| // 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. |
| // 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. |
| var header; |
| var headers = this._options.headers; |
| if (statusCode !== 307 && !(this._options.method in SAFE_METHODS)) { |
| this._options.method = "GET"; |
| // Drop a possible entity and headers related to it |
| this._requestBodyBuffers = []; |
| for (header in headers) { |
| if (/^content-/i.test(header)) { |
| delete headers[header]; |
| } |
| } |
| } |
| |
| // Drop the Host header, as the redirect might lead to a different host |
| if (!this._isRedirect) { |
| for (header in headers) { |
| if (/^host$/i.test(header)) { |
| delete headers[header]; |
| } |
| } |
| } |
| |
| // Perform the redirected request |
| var redirectUrl = url.resolve(this._currentUrl, location); |
| debug("redirecting to", redirectUrl); |
| Object.assign(this._options, url.parse(redirectUrl)); |
| if (typeof this._options.beforeRedirect === "function") { |
| try { |
| this._options.beforeRedirect.call(null, this._options); |
| } |
| catch (err) { |
| this.emit("error", err); |
| return; |
| } |
| this._sanitizeOptions(this._options); |
| } |
| this._isRedirect = true; |
| this._performRequest(); |
| } |
| else { |
| // The response is not a redirect; return it as-is |
| response.responseUrl = this._currentUrl; |
| response.redirects = this._redirects; |
| this.emit("response", response); |
| |
| // Clean up |
| this._requestBodyBuffers = []; |
| } |
| }; |
| |
| // 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 |
| wrappedProtocol.request = function (input, options, callback) { |
| // Parse parameters |
| if (typeof input === "string") { |
| var urlStr = input; |
| try { |
| input = urlToOptions(new URL(urlStr)); |
| } |
| catch (err) { |
| /* istanbul ignore next */ |
| input = url.parse(urlStr); |
| } |
| } |
| else if (URL && (input instanceof URL)) { |
| input = urlToOptions(input); |
| } |
| else { |
| callback = options; |
| options = input; |
| input = { protocol: protocol }; |
| } |
| if (typeof options === "function") { |
| callback = options; |
| options = null; |
| } |
| |
| // Set defaults |
| options = Object.assign({ |
| maxRedirects: exports.maxRedirects, |
| maxBodyLength: exports.maxBodyLength, |
| }, input, options); |
| options.nativeProtocols = nativeProtocols; |
| |
| assert.equal(options.protocol, protocol, "protocol mismatch"); |
| debug("options", options); |
| return new RedirectableRequest(options, callback); |
| }; |
| |
| // Executes a GET request, following redirects |
| wrappedProtocol.get = function (input, options, callback) { |
| var request = wrappedProtocol.request(input, options, callback); |
| request.end(); |
| return request; |
| }; |
| }); |
| 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; |
| } |
| |
| // Exports |
| module.exports = wrap({ http: http, https: https }); |
| module.exports.wrap = wrap; |