source: trip-planner-front/node_modules/follow-redirects/index.js@ 6c1585f

Last change on this file since 6c1585f was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 16.1 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// Create handlers that pass events from native requests
10var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];
11var eventHandlers = Object.create(null);
12events.forEach(function (event) {
13 eventHandlers[event] = function (arg1, arg2, arg3) {
14 this._redirectable.emit(event, arg1, arg2, arg3);
15 };
16});
17
18// Error types with codes
19var RedirectionError = createErrorType(
20 "ERR_FR_REDIRECTION_FAILURE",
21 ""
22);
23var TooManyRedirectsError = createErrorType(
24 "ERR_FR_TOO_MANY_REDIRECTS",
25 "Maximum number of redirects exceeded"
26);
27var MaxBodyLengthExceededError = createErrorType(
28 "ERR_FR_MAX_BODY_LENGTH_EXCEEDED",
29 "Request body larger than maxBodyLength limit"
30);
31var WriteAfterEndError = createErrorType(
32 "ERR_STREAM_WRITE_AFTER_END",
33 "write after end"
34);
35
36// An HTTP(S) request that can be redirected
37function RedirectableRequest(options, responseCallback) {
38 // Initialize the request
39 Writable.call(this);
40 this._sanitizeOptions(options);
41 this._options = options;
42 this._ended = false;
43 this._ending = false;
44 this._redirectCount = 0;
45 this._redirects = [];
46 this._requestBodyLength = 0;
47 this._requestBodyBuffers = [];
48
49 // Attach a callback if passed
50 if (responseCallback) {
51 this.on("response", responseCallback);
52 }
53
54 // React to responses of native requests
55 var self = this;
56 this._onNativeResponse = function (response) {
57 self._processResponse(response);
58 };
59
60 // Perform the first request
61 this._performRequest();
62}
63RedirectableRequest.prototype = Object.create(Writable.prototype);
64
65RedirectableRequest.prototype.abort = function () {
66 abortRequest(this._currentRequest);
67 this.emit("abort");
68};
69
70// Writes buffered data to the current native request
71RedirectableRequest.prototype.write = function (data, encoding, callback) {
72 // Writing is not allowed if end has been called
73 if (this._ending) {
74 throw new WriteAfterEndError();
75 }
76
77 // Validate input and shift parameters if necessary
78 if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) {
79 throw new TypeError("data should be a string, Buffer or Uint8Array");
80 }
81 if (typeof encoding === "function") {
82 callback = encoding;
83 encoding = null;
84 }
85
86 // Ignore empty buffers, since writing them doesn't invoke the callback
87 // https://github.com/nodejs/node/issues/22066
88 if (data.length === 0) {
89 if (callback) {
90 callback();
91 }
92 return;
93 }
94 // Only write when we don't exceed the maximum body length
95 if (this._requestBodyLength + data.length <= this._options.maxBodyLength) {
96 this._requestBodyLength += data.length;
97 this._requestBodyBuffers.push({ data: data, encoding: encoding });
98 this._currentRequest.write(data, encoding, callback);
99 }
100 // Error when we exceed the maximum body length
101 else {
102 this.emit("error", new MaxBodyLengthExceededError());
103 this.abort();
104 }
105};
106
107// Ends the current native request
108RedirectableRequest.prototype.end = function (data, encoding, callback) {
109 // Shift parameters if necessary
110 if (typeof data === "function") {
111 callback = data;
112 data = encoding = null;
113 }
114 else if (typeof encoding === "function") {
115 callback = encoding;
116 encoding = null;
117 }
118
119 // Write data if needed and end
120 if (!data) {
121 this._ended = this._ending = true;
122 this._currentRequest.end(null, null, callback);
123 }
124 else {
125 var self = this;
126 var currentRequest = this._currentRequest;
127 this.write(data, encoding, function () {
128 self._ended = true;
129 currentRequest.end(null, null, callback);
130 });
131 this._ending = true;
132 }
133};
134
135// Sets a header value on the current native request
136RedirectableRequest.prototype.setHeader = function (name, value) {
137 this._options.headers[name] = value;
138 this._currentRequest.setHeader(name, value);
139};
140
141// Clears a header value on the current native request
142RedirectableRequest.prototype.removeHeader = function (name) {
143 delete this._options.headers[name];
144 this._currentRequest.removeHeader(name);
145};
146
147// Global timeout for all underlying requests
148RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
149 var self = this;
150
151 // Destroys the socket on timeout
152 function destroyOnTimeout(socket) {
153 socket.setTimeout(msecs);
154 socket.removeListener("timeout", socket.destroy);
155 socket.addListener("timeout", socket.destroy);
156 }
157
158 // Sets up a timer to trigger a timeout event
159 function startTimer(socket) {
160 if (self._timeout) {
161 clearTimeout(self._timeout);
162 }
163 self._timeout = setTimeout(function () {
164 self.emit("timeout");
165 clearTimer();
166 }, msecs);
167 destroyOnTimeout(socket);
168 }
169
170 // Stops a timeout from triggering
171 function clearTimer() {
172 if (self._timeout) {
173 clearTimeout(self._timeout);
174 self._timeout = null;
175 }
176 if (callback) {
177 self.removeListener("timeout", callback);
178 }
179 if (!self.socket) {
180 self._currentRequest.removeListener("socket", startTimer);
181 }
182 }
183
184 // Attach callback if passed
185 if (callback) {
186 this.on("timeout", callback);
187 }
188
189 // Start the timer if or when the socket is opened
190 if (this.socket) {
191 startTimer(this.socket);
192 }
193 else {
194 this._currentRequest.once("socket", startTimer);
195 }
196
197 // Clean up on events
198 this.on("socket", destroyOnTimeout);
199 this.once("response", clearTimer);
200 this.once("error", clearTimer);
201
202 return this;
203};
204
205// Proxy all other public ClientRequest methods
206[
207 "flushHeaders", "getHeader",
208 "setNoDelay", "setSocketKeepAlive",
209].forEach(function (method) {
210 RedirectableRequest.prototype[method] = function (a, b) {
211 return this._currentRequest[method](a, b);
212 };
213});
214
215// Proxy all public ClientRequest properties
216["aborted", "connection", "socket"].forEach(function (property) {
217 Object.defineProperty(RedirectableRequest.prototype, property, {
218 get: function () { return this._currentRequest[property]; },
219 });
220});
221
222RedirectableRequest.prototype._sanitizeOptions = function (options) {
223 // Ensure headers are always present
224 if (!options.headers) {
225 options.headers = {};
226 }
227
228 // Since http.request treats host as an alias of hostname,
229 // but the url module interprets host as hostname plus port,
230 // eliminate the host property to avoid confusion.
231 if (options.host) {
232 // Use hostname if set, because it has precedence
233 if (!options.hostname) {
234 options.hostname = options.host;
235 }
236 delete options.host;
237 }
238
239 // Complete the URL object when necessary
240 if (!options.pathname && options.path) {
241 var searchPos = options.path.indexOf("?");
242 if (searchPos < 0) {
243 options.pathname = options.path;
244 }
245 else {
246 options.pathname = options.path.substring(0, searchPos);
247 options.search = options.path.substring(searchPos);
248 }
249 }
250};
251
252
253// Executes the next native request (initial or redirect)
254RedirectableRequest.prototype._performRequest = function () {
255 // Load the native protocol
256 var protocol = this._options.protocol;
257 var nativeProtocol = this._options.nativeProtocols[protocol];
258 if (!nativeProtocol) {
259 this.emit("error", new TypeError("Unsupported protocol " + protocol));
260 return;
261 }
262
263 // If specified, use the agent corresponding to the protocol
264 // (HTTP and HTTPS use different types of agents)
265 if (this._options.agents) {
266 var scheme = protocol.substr(0, protocol.length - 1);
267 this._options.agent = this._options.agents[scheme];
268 }
269
270 // Create the native request
271 var request = this._currentRequest =
272 nativeProtocol.request(this._options, this._onNativeResponse);
273 this._currentUrl = url.format(this._options);
274
275 // Set up event handlers
276 request._redirectable = this;
277 for (var e = 0; e < events.length; e++) {
278 request.on(events[e], eventHandlers[events[e]]);
279 }
280
281 // End a redirected request
282 // (The first request must be ended explicitly with RedirectableRequest#end)
283 if (this._isRedirect) {
284 // Write the request entity and end.
285 var i = 0;
286 var self = this;
287 var buffers = this._requestBodyBuffers;
288 (function writeNext(error) {
289 // Only write if this request has not been redirected yet
290 /* istanbul ignore else */
291 if (request === self._currentRequest) {
292 // Report any write errors
293 /* istanbul ignore if */
294 if (error) {
295 self.emit("error", error);
296 }
297 // Write the next buffer if there are still left
298 else if (i < buffers.length) {
299 var buffer = buffers[i++];
300 /* istanbul ignore else */
301 if (!request.finished) {
302 request.write(buffer.data, buffer.encoding, writeNext);
303 }
304 }
305 // End the request if `end` has been called on us
306 else if (self._ended) {
307 request.end();
308 }
309 }
310 }());
311 }
312};
313
314// Processes a response from the current native request
315RedirectableRequest.prototype._processResponse = function (response) {
316 // Store the redirected response
317 var statusCode = response.statusCode;
318 if (this._options.trackRedirects) {
319 this._redirects.push({
320 url: this._currentUrl,
321 headers: response.headers,
322 statusCode: statusCode,
323 });
324 }
325
326 // RFC7231§6.4: The 3xx (Redirection) class of status code indicates
327 // that further action needs to be taken by the user agent in order to
328 // fulfill the request. If a Location header field is provided,
329 // the user agent MAY automatically redirect its request to the URI
330 // referenced by the Location field value,
331 // even if the specific status code is not understood.
332 var location = response.headers.location;
333 if (location && this._options.followRedirects !== false &&
334 statusCode >= 300 && statusCode < 400) {
335 // Abort the current request
336 abortRequest(this._currentRequest);
337 // Discard the remainder of the response to avoid waiting for data
338 response.destroy();
339
340 // RFC7231§6.4: A client SHOULD detect and intervene
341 // in cyclical redirections (i.e., "infinite" redirection loops).
342 if (++this._redirectCount > this._options.maxRedirects) {
343 this.emit("error", new TooManyRedirectsError());
344 return;
345 }
346
347 // RFC7231§6.4: Automatic redirection needs to done with
348 // care for methods not known to be safe, […]
349 // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
350 // the request method from POST to GET for the subsequent request.
351 if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
352 // RFC7231§6.4.4: The 303 (See Other) status code indicates that
353 // the server is redirecting the user agent to a different resource […]
354 // A user agent can perform a retrieval request targeting that URI
355 // (a GET or HEAD request if using HTTP) […]
356 (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
357 this._options.method = "GET";
358 // Drop a possible entity and headers related to it
359 this._requestBodyBuffers = [];
360 removeMatchingHeaders(/^content-/i, this._options.headers);
361 }
362
363 // Drop the Host header, as the redirect might lead to a different host
364 var previousHostName = removeMatchingHeaders(/^host$/i, this._options.headers) ||
365 url.parse(this._currentUrl).hostname;
366
367 // Create the redirected request
368 var redirectUrl = url.resolve(this._currentUrl, location);
369 debug("redirecting to", redirectUrl);
370 this._isRedirect = true;
371 var redirectUrlParts = url.parse(redirectUrl);
372 Object.assign(this._options, redirectUrlParts);
373
374 // Drop the Authorization header if redirecting to another host
375 if (redirectUrlParts.hostname !== previousHostName) {
376 removeMatchingHeaders(/^authorization$/i, this._options.headers);
377 }
378
379 // Evaluate the beforeRedirect callback
380 if (typeof this._options.beforeRedirect === "function") {
381 var responseDetails = { headers: response.headers };
382 try {
383 this._options.beforeRedirect.call(null, this._options, responseDetails);
384 }
385 catch (err) {
386 this.emit("error", err);
387 return;
388 }
389 this._sanitizeOptions(this._options);
390 }
391
392 // Perform the redirected request
393 try {
394 this._performRequest();
395 }
396 catch (cause) {
397 var error = new RedirectionError("Redirected request failed: " + cause.message);
398 error.cause = cause;
399 this.emit("error", error);
400 }
401 }
402 else {
403 // The response is not a redirect; return it as-is
404 response.responseUrl = this._currentUrl;
405 response.redirects = this._redirects;
406 this.emit("response", response);
407
408 // Clean up
409 this._requestBodyBuffers = [];
410 }
411};
412
413// Wraps the key/value object of protocols with redirect functionality
414function wrap(protocols) {
415 // Default settings
416 var exports = {
417 maxRedirects: 21,
418 maxBodyLength: 10 * 1024 * 1024,
419 };
420
421 // Wrap each protocol
422 var nativeProtocols = {};
423 Object.keys(protocols).forEach(function (scheme) {
424 var protocol = scheme + ":";
425 var nativeProtocol = nativeProtocols[protocol] = protocols[scheme];
426 var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol);
427
428 // Executes a request, following redirects
429 function request(input, options, callback) {
430 // Parse parameters
431 if (typeof input === "string") {
432 var urlStr = input;
433 try {
434 input = urlToOptions(new URL(urlStr));
435 }
436 catch (err) {
437 /* istanbul ignore next */
438 input = url.parse(urlStr);
439 }
440 }
441 else if (URL && (input instanceof URL)) {
442 input = urlToOptions(input);
443 }
444 else {
445 callback = options;
446 options = input;
447 input = { protocol: protocol };
448 }
449 if (typeof options === "function") {
450 callback = options;
451 options = null;
452 }
453
454 // Set defaults
455 options = Object.assign({
456 maxRedirects: exports.maxRedirects,
457 maxBodyLength: exports.maxBodyLength,
458 }, input, options);
459 options.nativeProtocols = nativeProtocols;
460
461 assert.equal(options.protocol, protocol, "protocol mismatch");
462 debug("options", options);
463 return new RedirectableRequest(options, callback);
464 }
465
466 // Executes a GET request, following redirects
467 function get(input, options, callback) {
468 var wrappedRequest = wrappedProtocol.request(input, options, callback);
469 wrappedRequest.end();
470 return wrappedRequest;
471 }
472
473 // Expose the properties on the wrapped protocol
474 Object.defineProperties(wrappedProtocol, {
475 request: { value: request, configurable: true, enumerable: true, writable: true },
476 get: { value: get, configurable: true, enumerable: true, writable: true },
477 });
478 });
479 return exports;
480}
481
482/* istanbul ignore next */
483function noop() { /* empty */ }
484
485// from https://github.com/nodejs/node/blob/master/lib/internal/url.js
486function urlToOptions(urlObject) {
487 var options = {
488 protocol: urlObject.protocol,
489 hostname: urlObject.hostname.startsWith("[") ?
490 /* istanbul ignore next */
491 urlObject.hostname.slice(1, -1) :
492 urlObject.hostname,
493 hash: urlObject.hash,
494 search: urlObject.search,
495 pathname: urlObject.pathname,
496 path: urlObject.pathname + urlObject.search,
497 href: urlObject.href,
498 };
499 if (urlObject.port !== "") {
500 options.port = Number(urlObject.port);
501 }
502 return options;
503}
504
505function removeMatchingHeaders(regex, headers) {
506 var lastValue;
507 for (var header in headers) {
508 if (regex.test(header)) {
509 lastValue = headers[header];
510 delete headers[header];
511 }
512 }
513 return lastValue;
514}
515
516function createErrorType(code, defaultMessage) {
517 function CustomError(message) {
518 Error.captureStackTrace(this, this.constructor);
519 this.message = message || defaultMessage;
520 }
521 CustomError.prototype = new Error();
522 CustomError.prototype.constructor = CustomError;
523 CustomError.prototype.name = "Error [" + code + "]";
524 CustomError.prototype.code = code;
525 return CustomError;
526}
527
528function abortRequest(request) {
529 for (var e = 0; e < events.length; e++) {
530 request.removeListener(events[e], eventHandlers[events[e]]);
531 }
532 request.on("error", noop);
533 request.abort();
534}
535
536// Exports
537module.exports = wrap({ http: http, https: https });
538module.exports.wrap = wrap;
Note: See TracBrowser for help on using the repository browser.