source: frontend/node_modules/follow-redirects/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 20.7 KB
Line 
1var url = require("url");
2var URL = url.URL;
3var http = require("http");
4var https = require("https");
5var Writable = require("stream").Writable;
6var assert = require("assert");
7var debug = require("./debug");
8
9// Preventive platform detection
10// istanbul ignore next
11(function detectUnsupportedEnvironment() {
12 var looksLikeNode = typeof process !== "undefined";
13 var looksLikeBrowser = typeof window !== "undefined" && typeof document !== "undefined";
14 var looksLikeV8 = isFunction(Error.captureStackTrace);
15 if (!looksLikeNode && (looksLikeBrowser || !looksLikeV8)) {
16 console.warn("The follow-redirects package should be excluded from browser builds.");
17 }
18}());
19
20// Whether to use the native URL object or the legacy url module
21var useNativeURL = false;
22try {
23 assert(new URL(""));
24}
25catch (error) {
26 useNativeURL = error.code === "ERR_INVALID_URL";
27}
28
29// HTTP headers to drop across HTTP/HTTPS and domain boundaries
30var sensitiveHeaders = [
31 "Authorization",
32 "Proxy-Authorization",
33 "Cookie",
34];
35
36// URL fields to preserve in copy operations
37var preservedUrlFields = [
38 "auth",
39 "host",
40 "hostname",
41 "href",
42 "path",
43 "pathname",
44 "port",
45 "protocol",
46 "query",
47 "search",
48 "hash",
49];
50
51// Create handlers that pass events from native requests
52var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];
53var eventHandlers = Object.create(null);
54events.forEach(function (event) {
55 eventHandlers[event] = function (arg1, arg2, arg3) {
56 this._redirectable.emit(event, arg1, arg2, arg3);
57 };
58});
59
60// Error types with codes
61var InvalidUrlError = createErrorType(
62 "ERR_INVALID_URL",
63 "Invalid URL",
64 TypeError
65);
66var RedirectionError = createErrorType(
67 "ERR_FR_REDIRECTION_FAILURE",
68 "Redirected request failed"
69);
70var TooManyRedirectsError = createErrorType(
71 "ERR_FR_TOO_MANY_REDIRECTS",
72 "Maximum number of redirects exceeded",
73 RedirectionError
74);
75var MaxBodyLengthExceededError = createErrorType(
76 "ERR_FR_MAX_BODY_LENGTH_EXCEEDED",
77 "Request body larger than maxBodyLength limit"
78);
79var WriteAfterEndError = createErrorType(
80 "ERR_STREAM_WRITE_AFTER_END",
81 "write after end"
82);
83
84// istanbul ignore next
85var destroy = Writable.prototype.destroy || noop;
86
87// An HTTP(S) request that can be redirected
88function RedirectableRequest(options, responseCallback) {
89 // Initialize the request
90 Writable.call(this);
91 this._sanitizeOptions(options);
92 this._options = options;
93 this._ended = false;
94 this._ending = false;
95 this._redirectCount = 0;
96 this._redirects = [];
97 this._requestBodyLength = 0;
98 this._requestBodyBuffers = [];
99
100 // Attach a callback if passed
101 if (responseCallback) {
102 this.on("response", responseCallback);
103 }
104
105 // React to responses of native requests
106 var self = this;
107 this._onNativeResponse = function (response) {
108 try {
109 self._processResponse(response);
110 }
111 catch (cause) {
112 self.emit("error", cause instanceof RedirectionError ?
113 cause : new RedirectionError({ cause: cause }));
114 }
115 };
116
117 // Create filter for sensitive HTTP headers
118 this._headerFilter = new RegExp("^(?:" +
119 sensitiveHeaders.concat(options.sensitiveHeaders).map(escapeRegex).join("|") +
120 ")$", "i");
121
122 // Perform the first request
123 this._performRequest();
124}
125RedirectableRequest.prototype = Object.create(Writable.prototype);
126
127RedirectableRequest.prototype.abort = function () {
128 destroyRequest(this._currentRequest);
129 this._currentRequest.abort();
130 this.emit("abort");
131};
132
133RedirectableRequest.prototype.destroy = function (error) {
134 destroyRequest(this._currentRequest, error);
135 destroy.call(this, error);
136 return this;
137};
138
139// Writes buffered data to the current native request
140RedirectableRequest.prototype.write = function (data, encoding, callback) {
141 // Writing is not allowed if end has been called
142 if (this._ending) {
143 throw new WriteAfterEndError();
144 }
145
146 // Validate input and shift parameters if necessary
147 if (!isString(data) && !isBuffer(data)) {
148 throw new TypeError("data should be a string, Buffer or Uint8Array");
149 }
150 if (isFunction(encoding)) {
151 callback = encoding;
152 encoding = null;
153 }
154
155 // Ignore empty buffers, since writing them doesn't invoke the callback
156 // https://github.com/nodejs/node/issues/22066
157 if (data.length === 0) {
158 if (callback) {
159 callback();
160 }
161 return;
162 }
163 // Only write when we don't exceed the maximum body length
164 if (this._requestBodyLength + data.length <= this._options.maxBodyLength) {
165 this._requestBodyLength += data.length;
166 this._requestBodyBuffers.push({ data: data, encoding: encoding });
167 this._currentRequest.write(data, encoding, callback);
168 }
169 // Error when we exceed the maximum body length
170 else {
171 this.emit("error", new MaxBodyLengthExceededError());
172 this.abort();
173 }
174};
175
176// Ends the current native request
177RedirectableRequest.prototype.end = function (data, encoding, callback) {
178 // Shift parameters if necessary
179 if (isFunction(data)) {
180 callback = data;
181 data = encoding = null;
182 }
183 else if (isFunction(encoding)) {
184 callback = encoding;
185 encoding = null;
186 }
187
188 // Write data if needed and end
189 if (!data) {
190 this._ended = this._ending = true;
191 this._currentRequest.end(null, null, callback);
192 }
193 else {
194 var self = this;
195 var currentRequest = this._currentRequest;
196 this.write(data, encoding, function () {
197 self._ended = true;
198 currentRequest.end(null, null, callback);
199 });
200 this._ending = true;
201 }
202};
203
204// Sets a header value on the current native request
205RedirectableRequest.prototype.setHeader = function (name, value) {
206 this._options.headers[name] = value;
207 this._currentRequest.setHeader(name, value);
208};
209
210// Clears a header value on the current native request
211RedirectableRequest.prototype.removeHeader = function (name) {
212 delete this._options.headers[name];
213 this._currentRequest.removeHeader(name);
214};
215
216// Global timeout for all underlying requests
217RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
218 var self = this;
219
220 // Destroys the socket on timeout
221 function destroyOnTimeout(socket) {
222 socket.setTimeout(msecs);
223 socket.removeListener("timeout", socket.destroy);
224 socket.addListener("timeout", socket.destroy);
225 }
226
227 // Sets up a timer to trigger a timeout event
228 function startTimer(socket) {
229 if (self._timeout) {
230 clearTimeout(self._timeout);
231 }
232 self._timeout = setTimeout(function () {
233 self.emit("timeout");
234 clearTimer();
235 }, msecs);
236 destroyOnTimeout(socket);
237 }
238
239 // Stops a timeout from triggering
240 function clearTimer() {
241 // Clear the timeout
242 if (self._timeout) {
243 clearTimeout(self._timeout);
244 self._timeout = null;
245 }
246
247 // Clean up all attached listeners
248 self.removeListener("abort", clearTimer);
249 self.removeListener("error", clearTimer);
250 self.removeListener("response", clearTimer);
251 self.removeListener("close", clearTimer);
252 if (callback) {
253 self.removeListener("timeout", callback);
254 }
255 if (!self.socket) {
256 self._currentRequest.removeListener("socket", startTimer);
257 }
258 }
259
260 // Attach callback if passed
261 if (callback) {
262 this.on("timeout", callback);
263 }
264
265 // Start the timer if or when the socket is opened
266 if (this.socket) {
267 startTimer(this.socket);
268 }
269 else {
270 this._currentRequest.once("socket", startTimer);
271 }
272
273 // Clean up on events
274 this.on("socket", destroyOnTimeout);
275 this.on("abort", clearTimer);
276 this.on("error", clearTimer);
277 this.on("response", clearTimer);
278 this.on("close", clearTimer);
279
280 return this;
281};
282
283// Proxy all other public ClientRequest methods
284[
285 "flushHeaders", "getHeader",
286 "setNoDelay", "setSocketKeepAlive",
287].forEach(function (method) {
288 RedirectableRequest.prototype[method] = function (a, b) {
289 return this._currentRequest[method](a, b);
290 };
291});
292
293// Proxy all public ClientRequest properties
294["aborted", "connection", "socket"].forEach(function (property) {
295 Object.defineProperty(RedirectableRequest.prototype, property, {
296 get: function () { return this._currentRequest[property]; },
297 });
298});
299
300RedirectableRequest.prototype._sanitizeOptions = function (options) {
301 // Ensure headers are always present
302 if (!options.headers) {
303 options.headers = {};
304 }
305 if (!isArray(options.sensitiveHeaders)) {
306 options.sensitiveHeaders = [];
307 }
308
309 // Since http.request treats host as an alias of hostname,
310 // but the url module interprets host as hostname plus port,
311 // eliminate the host property to avoid confusion.
312 if (options.host) {
313 // Use hostname if set, because it has precedence
314 if (!options.hostname) {
315 options.hostname = options.host;
316 }
317 delete options.host;
318 }
319
320 // Complete the URL object when necessary
321 if (!options.pathname && options.path) {
322 var searchPos = options.path.indexOf("?");
323 if (searchPos < 0) {
324 options.pathname = options.path;
325 }
326 else {
327 options.pathname = options.path.substring(0, searchPos);
328 options.search = options.path.substring(searchPos);
329 }
330 }
331};
332
333
334// Executes the next native request (initial or redirect)
335RedirectableRequest.prototype._performRequest = function () {
336 // Load the native protocol
337 var protocol = this._options.protocol;
338 var nativeProtocol = this._options.nativeProtocols[protocol];
339 if (!nativeProtocol) {
340 throw new TypeError("Unsupported protocol " + protocol);
341 }
342
343 // If specified, use the agent corresponding to the protocol
344 // (HTTP and HTTPS use different types of agents)
345 if (this._options.agents) {
346 var scheme = protocol.slice(0, -1);
347 this._options.agent = this._options.agents[scheme];
348 }
349
350 // Create the native request and set up its event handlers
351 var request = this._currentRequest =
352 nativeProtocol.request(this._options, this._onNativeResponse);
353 request._redirectable = this;
354 for (var event of events) {
355 request.on(event, eventHandlers[event]);
356 }
357
358 // RFC7230§5.3.1: When making a request directly to an origin server, […]
359 // a client MUST send only the absolute path […] as the request-target.
360 this._currentUrl = /^\//.test(this._options.path) ?
361 url.format(this._options) :
362 // When making a request to a proxy, […]
363 // a client MUST send the target URI in absolute-form […].
364 this._options.path;
365
366 // End a redirected request
367 // (The first request must be ended explicitly with RedirectableRequest#end)
368 if (this._isRedirect) {
369 // Write the request entity and end
370 var i = 0;
371 var self = this;
372 var buffers = this._requestBodyBuffers;
373 (function writeNext(error) {
374 // Only write if this request has not been redirected yet
375 // istanbul ignore else
376 if (request === self._currentRequest) {
377 // Report any write errors
378 // istanbul ignore if
379 if (error) {
380 self.emit("error", error);
381 }
382 // Write the next buffer if there are still left
383 else if (i < buffers.length) {
384 var buffer = buffers[i++];
385 // istanbul ignore else
386 if (!request.finished) {
387 request.write(buffer.data, buffer.encoding, writeNext);
388 }
389 }
390 // End the request if `end` has been called on us
391 else if (self._ended) {
392 request.end();
393 }
394 }
395 }());
396 }
397};
398
399// Processes a response from the current native request
400RedirectableRequest.prototype._processResponse = function (response) {
401 // Store the redirected response
402 var statusCode = response.statusCode;
403 if (this._options.trackRedirects) {
404 this._redirects.push({
405 url: this._currentUrl,
406 headers: response.headers,
407 statusCode: statusCode,
408 });
409 }
410
411 // RFC7231§6.4: The 3xx (Redirection) class of status code indicates
412 // that further action needs to be taken by the user agent in order to
413 // fulfill the request. If a Location header field is provided,
414 // the user agent MAY automatically redirect its request to the URI
415 // referenced by the Location field value,
416 // even if the specific status code is not understood.
417
418 // If the response is not a redirect; return it as-is
419 var location = response.headers.location;
420 if (!location || this._options.followRedirects === false ||
421 statusCode < 300 || statusCode >= 400) {
422 response.responseUrl = this._currentUrl;
423 response.redirects = this._redirects;
424 this.emit("response", response);
425
426 // Clean up
427 this._requestBodyBuffers = [];
428 return;
429 }
430
431 // The response is a redirect, so abort the current request
432 destroyRequest(this._currentRequest);
433 // Discard the remainder of the response to avoid waiting for data
434 response.destroy();
435
436 // RFC7231§6.4: A client SHOULD detect and intervene
437 // in cyclical redirections (i.e., "infinite" redirection loops).
438 if (++this._redirectCount > this._options.maxRedirects) {
439 throw new TooManyRedirectsError();
440 }
441
442 // Store the request headers if applicable
443 var requestHeaders;
444 var beforeRedirect = this._options.beforeRedirect;
445 if (beforeRedirect) {
446 requestHeaders = Object.assign({
447 // The Host header was set by nativeProtocol.request
448 Host: response.req.getHeader("host"),
449 }, this._options.headers);
450 }
451
452 // RFC7231§6.4: Automatic redirection needs to done with
453 // care for methods not known to be safe, […]
454 // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
455 // the request method from POST to GET for the subsequent request.
456 var method = this._options.method;
457 if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
458 // RFC7231§6.4.4: The 303 (See Other) status code indicates that
459 // the server is redirecting the user agent to a different resource […]
460 // A user agent can perform a retrieval request targeting that URI
461 // (a GET or HEAD request if using HTTP) […]
462 (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
463 this._options.method = "GET";
464 // Drop a possible entity and headers related to it
465 this._requestBodyBuffers = [];
466 removeMatchingHeaders(/^content-/i, this._options.headers);
467 }
468
469 // Drop the Host header, as the redirect might lead to a different host
470 var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
471
472 // If the redirect is relative, carry over the host of the last request
473 var currentUrlParts = parseUrl(this._currentUrl);
474 var currentHost = currentHostHeader || currentUrlParts.host;
475 var currentUrl = /^\w+:/.test(location) ? this._currentUrl :
476 url.format(Object.assign(currentUrlParts, { host: currentHost }));
477
478 // Create the redirected request
479 var redirectUrl = resolveUrl(location, currentUrl);
480 debug("redirecting to", redirectUrl.href);
481 this._isRedirect = true;
482 spreadUrlObject(redirectUrl, this._options);
483
484 // Drop confidential headers when redirecting to a less secure protocol
485 // or to a different domain that is not a superdomain
486 if (redirectUrl.protocol !== currentUrlParts.protocol &&
487 redirectUrl.protocol !== "https:" ||
488 redirectUrl.host !== currentHost &&
489 !isSubdomain(redirectUrl.host, currentHost)) {
490 removeMatchingHeaders(this._headerFilter, this._options.headers);
491 }
492
493 // Evaluate the beforeRedirect callback
494 if (isFunction(beforeRedirect)) {
495 var responseDetails = {
496 headers: response.headers,
497 statusCode: statusCode,
498 };
499 var requestDetails = {
500 url: currentUrl,
501 method: method,
502 headers: requestHeaders,
503 };
504 beforeRedirect(this._options, responseDetails, requestDetails);
505 this._sanitizeOptions(this._options);
506 }
507
508 // Perform the redirected request
509 this._performRequest();
510};
511
512// Wraps the key/value object of protocols with redirect functionality
513function wrap(protocols) {
514 // Default settings
515 var exports = {
516 maxRedirects: 21,
517 maxBodyLength: 10 * 1024 * 1024,
518 };
519
520 // Wrap each protocol
521 var nativeProtocols = {};
522 Object.keys(protocols).forEach(function (scheme) {
523 var protocol = scheme + ":";
524 var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
525 var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
526
527 // Executes a request, following redirects
528 function request(input, options, callback) {
529 // Parse parameters, ensuring that input is an object
530 if (isURL(input)) {
531 input = spreadUrlObject(input);
532 }
533 else if (isString(input)) {
534 input = spreadUrlObject(parseUrl(input));
535 }
536 else {
537 callback = options;
538 options = validateUrl(input);
539 input = { protocol: protocol };
540 }
541 if (isFunction(options)) {
542 callback = options;
543 options = null;
544 }
545
546 // Set defaults
547 options = Object.assign({
548 maxRedirects: exports.maxRedirects,
549 maxBodyLength: exports.maxBodyLength,
550 }, input, options);
551 options.nativeProtocols = nativeProtocols;
552 if (!isString(options.host) && !isString(options.hostname)) {
553 options.hostname = "::1";
554 }
555
556 assert.equal(options.protocol, protocol, "protocol mismatch");
557 debug("options", options);
558 return new RedirectableRequest(options, callback);
559 }
560
561 // Executes a GET request, following redirects
562 function get(input, options, callback) {
563 var wrappedRequest = wrappedProtocol.request(input, options, callback);
564 wrappedRequest.end();
565 return wrappedRequest;
566 }
567
568 // Expose the properties on the wrapped protocol
569 Object.defineProperties(wrappedProtocol, {
570 request: { value: request, configurable: true, enumerable: true, writable: true },
571 get: { value: get, configurable: true, enumerable: true, writable: true },
572 });
573 });
574 return exports;
575}
576
577function noop() { /* empty */ }
578
579function parseUrl(input) {
580 var parsed;
581 // istanbul ignore else
582 if (useNativeURL) {
583 parsed = new URL(input);
584 }
585 else {
586 // Ensure the URL is valid and absolute
587 parsed = validateUrl(url.parse(input));
588 if (!isString(parsed.protocol)) {
589 throw new InvalidUrlError({ input });
590 }
591 }
592 return parsed;
593}
594
595function resolveUrl(relative, base) {
596 // istanbul ignore next
597 return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative));
598}
599
600function validateUrl(input) {
601 if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) {
602 throw new InvalidUrlError({ input: input.href || input });
603 }
604 if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) {
605 throw new InvalidUrlError({ input: input.href || input });
606 }
607 return input;
608}
609
610function spreadUrlObject(urlObject, target) {
611 var spread = target || {};
612 for (var key of preservedUrlFields) {
613 spread[key] = urlObject[key];
614 }
615
616 // Fix IPv6 hostname
617 if (spread.hostname.startsWith("[")) {
618 spread.hostname = spread.hostname.slice(1, -1);
619 }
620 // Ensure port is a number
621 if (spread.port !== "") {
622 spread.port = Number(spread.port);
623 }
624 // Concatenate path
625 spread.path = spread.search ? spread.pathname + spread.search : spread.pathname;
626
627 return spread;
628}
629
630function removeMatchingHeaders(regex, headers) {
631 var lastValue;
632 for (var header in headers) {
633 if (regex.test(header)) {
634 lastValue = headers[header];
635 delete headers[header];
636 }
637 }
638 return (lastValue === null || typeof lastValue === "undefined") ?
639 undefined : String(lastValue).trim();
640}
641
642function createErrorType(code, message, baseClass) {
643 // Create constructor
644 function CustomError(properties) {
645 // istanbul ignore else
646 if (isFunction(Error.captureStackTrace)) {
647 Error.captureStackTrace(this, this.constructor);
648 }
649 Object.assign(this, properties || {});
650 this.code = code;
651 this.message = this.cause ? message + ": " + this.cause.message : message;
652 }
653
654 // Attach constructor and set default properties
655 CustomError.prototype = new (baseClass || Error)();
656 Object.defineProperties(CustomError.prototype, {
657 constructor: {
658 value: CustomError,
659 enumerable: false,
660 },
661 name: {
662 value: "Error [" + code + "]",
663 enumerable: false,
664 },
665 });
666 return CustomError;
667}
668
669function destroyRequest(request, error) {
670 for (var event of events) {
671 request.removeListener(event, eventHandlers[event]);
672 }
673 request.on("error", noop);
674 request.destroy(error);
675}
676
677function isSubdomain(subdomain, domain) {
678 assert(isString(subdomain) && isString(domain));
679 var dot = subdomain.length - domain.length - 1;
680 return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);
681}
682
683function isArray(value) {
684 return value instanceof Array;
685}
686
687function isString(value) {
688 return typeof value === "string" || value instanceof String;
689}
690
691function isFunction(value) {
692 return typeof value === "function";
693}
694
695function isBuffer(value) {
696 return typeof value === "object" && ("length" in value);
697}
698
699function isURL(value) {
700 return URL && value instanceof URL;
701}
702
703function escapeRegex(regex) {
704 return regex.replace(/[\]\\/()*+?.$]/g, "\\$&");
705}
706
707// Exports
708module.exports = wrap({ http: http, https: https });
709module.exports.wrap = wrap;
Note: See TracBrowser for help on using the repository browser.