follow-redirects 1.14.4 → 1.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of follow-redirects might be problematic. Click here for more details.

Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/index.js +267 -133
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -63,10 +63,17 @@ const { http, https } = require('follow-redirects');
63
63
 
64
64
  const options = url.parse('http://bit.ly/900913');
65
65
  options.maxRedirects = 10;
66
- options.beforeRedirect = (options, { headers }) => {
66
+ options.beforeRedirect = (options, response, request) => {
67
67
  // Use this to adjust the request options upon redirecting,
68
68
  // to inspect the latest response headers,
69
69
  // or to cancel the request by throwing an error
70
+
71
+ // response.headers = the redirect response headers
72
+ // response.statusCode = the redirect response code (eg. 301, 307, etc.)
73
+
74
+ // request.url = the requested URL that resulted in a redirect
75
+ // request.headers = the headers in the request that resulted in a redirect
76
+ // request.method = the method of the request that resulted in a redirect
70
77
  if (options.hostname === "example.com") {
71
78
  options.auth = "user:password";
72
79
  }
package/index.js CHANGED
@@ -6,6 +6,30 @@ var Writable = require("stream").Writable;
6
6
  var assert = require("assert");
7
7
  var debug = require("./debug");
8
8
 
9
+ // Whether to use the native URL object or the legacy url module
10
+ var useNativeURL = false;
11
+ try {
12
+ assert(new URL());
13
+ }
14
+ catch (error) {
15
+ useNativeURL = error.code === "ERR_INVALID_URL";
16
+ }
17
+
18
+ // URL fields to preserve in copy operations
19
+ var preservedUrlFields = [
20
+ "auth",
21
+ "host",
22
+ "hostname",
23
+ "href",
24
+ "path",
25
+ "pathname",
26
+ "port",
27
+ "protocol",
28
+ "query",
29
+ "search",
30
+ "hash",
31
+ ];
32
+
9
33
  // Create handlers that pass events from native requests
10
34
  var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];
11
35
  var eventHandlers = Object.create(null);
@@ -16,13 +40,19 @@ events.forEach(function (event) {
16
40
  });
17
41
 
18
42
  // Error types with codes
43
+ var InvalidUrlError = createErrorType(
44
+ "ERR_INVALID_URL",
45
+ "Invalid URL",
46
+ TypeError
47
+ );
19
48
  var RedirectionError = createErrorType(
20
49
  "ERR_FR_REDIRECTION_FAILURE",
21
- ""
50
+ "Redirected request failed"
22
51
  );
23
52
  var TooManyRedirectsError = createErrorType(
24
53
  "ERR_FR_TOO_MANY_REDIRECTS",
25
- "Maximum number of redirects exceeded"
54
+ "Maximum number of redirects exceeded",
55
+ RedirectionError
26
56
  );
27
57
  var MaxBodyLengthExceededError = createErrorType(
28
58
  "ERR_FR_MAX_BODY_LENGTH_EXCEEDED",
@@ -33,6 +63,9 @@ var WriteAfterEndError = createErrorType(
33
63
  "write after end"
34
64
  );
35
65
 
66
+ // istanbul ignore next
67
+ var destroy = Writable.prototype.destroy || noop;
68
+
36
69
  // An HTTP(S) request that can be redirected
37
70
  function RedirectableRequest(options, responseCallback) {
38
71
  // Initialize the request
@@ -54,7 +87,13 @@ function RedirectableRequest(options, responseCallback) {
54
87
  // React to responses of native requests
55
88
  var self = this;
56
89
  this._onNativeResponse = function (response) {
57
- self._processResponse(response);
90
+ try {
91
+ self._processResponse(response);
92
+ }
93
+ catch (cause) {
94
+ self.emit("error", cause instanceof RedirectionError ?
95
+ cause : new RedirectionError({ cause: cause }));
96
+ }
58
97
  };
59
98
 
60
99
  // Perform the first request
@@ -63,10 +102,17 @@ function RedirectableRequest(options, responseCallback) {
63
102
  RedirectableRequest.prototype = Object.create(Writable.prototype);
64
103
 
65
104
  RedirectableRequest.prototype.abort = function () {
66
- abortRequest(this._currentRequest);
105
+ destroyRequest(this._currentRequest);
106
+ this._currentRequest.abort();
67
107
  this.emit("abort");
68
108
  };
69
109
 
110
+ RedirectableRequest.prototype.destroy = function (error) {
111
+ destroyRequest(this._currentRequest, error);
112
+ destroy.call(this, error);
113
+ return this;
114
+ };
115
+
70
116
  // Writes buffered data to the current native request
71
117
  RedirectableRequest.prototype.write = function (data, encoding, callback) {
72
118
  // Writing is not allowed if end has been called
@@ -75,10 +121,10 @@ RedirectableRequest.prototype.write = function (data, encoding, callback) {
75
121
  }
76
122
 
77
123
  // Validate input and shift parameters if necessary
78
- if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) {
124
+ if (!isString(data) && !isBuffer(data)) {
79
125
  throw new TypeError("data should be a string, Buffer or Uint8Array");
80
126
  }
81
- if (typeof encoding === "function") {
127
+ if (isFunction(encoding)) {
82
128
  callback = encoding;
83
129
  encoding = null;
84
130
  }
@@ -107,11 +153,11 @@ RedirectableRequest.prototype.write = function (data, encoding, callback) {
107
153
  // Ends the current native request
108
154
  RedirectableRequest.prototype.end = function (data, encoding, callback) {
109
155
  // Shift parameters if necessary
110
- if (typeof data === "function") {
156
+ if (isFunction(data)) {
111
157
  callback = data;
112
158
  data = encoding = null;
113
159
  }
114
- else if (typeof encoding === "function") {
160
+ else if (isFunction(encoding)) {
115
161
  callback = encoding;
116
162
  encoding = null;
117
163
  }
@@ -169,10 +215,17 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
169
215
 
170
216
  // Stops a timeout from triggering
171
217
  function clearTimer() {
218
+ // Clear the timeout
172
219
  if (self._timeout) {
173
220
  clearTimeout(self._timeout);
174
221
  self._timeout = null;
175
222
  }
223
+
224
+ // Clean up all attached listeners
225
+ self.removeListener("abort", clearTimer);
226
+ self.removeListener("error", clearTimer);
227
+ self.removeListener("response", clearTimer);
228
+ self.removeListener("close", clearTimer);
176
229
  if (callback) {
177
230
  self.removeListener("timeout", callback);
178
231
  }
@@ -196,8 +249,10 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
196
249
 
197
250
  // Clean up on events
198
251
  this.on("socket", destroyOnTimeout);
199
- this.once("response", clearTimer);
200
- this.once("error", clearTimer);
252
+ this.on("abort", clearTimer);
253
+ this.on("error", clearTimer);
254
+ this.on("response", clearTimer);
255
+ this.on("close", clearTimer);
201
256
 
202
257
  return this;
203
258
  };
@@ -256,32 +311,36 @@ RedirectableRequest.prototype._performRequest = function () {
256
311
  var protocol = this._options.protocol;
257
312
  var nativeProtocol = this._options.nativeProtocols[protocol];
258
313
  if (!nativeProtocol) {
259
- this.emit("error", new TypeError("Unsupported protocol " + protocol));
260
- return;
314
+ throw new TypeError("Unsupported protocol " + protocol);
261
315
  }
262
316
 
263
317
  // If specified, use the agent corresponding to the protocol
264
318
  // (HTTP and HTTPS use different types of agents)
265
319
  if (this._options.agents) {
266
- var scheme = protocol.substr(0, protocol.length - 1);
320
+ var scheme = protocol.slice(0, -1);
267
321
  this._options.agent = this._options.agents[scheme];
268
322
  }
269
323
 
270
- // Create the native request
324
+ // Create the native request and set up its event handlers
271
325
  var request = this._currentRequest =
272
326
  nativeProtocol.request(this._options, this._onNativeResponse);
273
- this._currentUrl = url.format(this._options);
274
-
275
- // Set up event handlers
276
327
  request._redirectable = this;
277
- for (var e = 0; e < events.length; e++) {
278
- request.on(events[e], eventHandlers[events[e]]);
328
+ for (var event of events) {
329
+ request.on(event, eventHandlers[event]);
279
330
  }
280
331
 
332
+ // RFC7230§5.3.1: When making a request directly to an origin server, […]
333
+ // a client MUST send only the absolute path […] as the request-target.
334
+ this._currentUrl = /^\//.test(this._options.path) ?
335
+ url.format(this._options) :
336
+ // When making a request to a proxy, […]
337
+ // a client MUST send the target URI in absolute-form […].
338
+ this._options.path;
339
+
281
340
  // End a redirected request
282
341
  // (The first request must be ended explicitly with RedirectableRequest#end)
283
342
  if (this._isRedirect) {
284
- // Write the request entity and end.
343
+ // Write the request entity and end
285
344
  var i = 0;
286
345
  var self = this;
287
346
  var buffers = this._requestBodyBuffers;
@@ -329,85 +388,99 @@ RedirectableRequest.prototype._processResponse = function (response) {
329
388
  // the user agent MAY automatically redirect its request to the URI
330
389
  // referenced by the Location field value,
331
390
  // even if the specific status code is not understood.
391
+
392
+ // If the response is not a redirect; return it as-is
332
393
  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
- }
394
+ if (!location || this._options.followRedirects === false ||
395
+ statusCode < 300 || statusCode >= 400) {
396
+ response.responseUrl = this._currentUrl;
397
+ response.redirects = this._redirects;
398
+ this.emit("response", response);
346
399
 
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
- }
400
+ // Clean up
401
+ this._requestBodyBuffers = [];
402
+ return;
403
+ }
362
404
 
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;
405
+ // The response is a redirect, so abort the current request
406
+ destroyRequest(this._currentRequest);
407
+ // Discard the remainder of the response to avoid waiting for data
408
+ response.destroy();
366
409
 
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);
410
+ // RFC7231§6.4: A client SHOULD detect and intervene
411
+ // in cyclical redirections (i.e., "infinite" redirection loops).
412
+ if (++this._redirectCount > this._options.maxRedirects) {
413
+ throw new TooManyRedirectsError();
414
+ }
373
415
 
374
- // Drop the Authorization header if redirecting to another host
375
- if (redirectUrlParts.hostname !== previousHostName) {
376
- removeMatchingHeaders(/^authorization$/i, this._options.headers);
377
- }
416
+ // Store the request headers if applicable
417
+ var requestHeaders;
418
+ var beforeRedirect = this._options.beforeRedirect;
419
+ if (beforeRedirect) {
420
+ requestHeaders = Object.assign({
421
+ // The Host header was set by nativeProtocol.request
422
+ Host: response.req.getHeader("host"),
423
+ }, this._options.headers);
424
+ }
378
425
 
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
- }
426
+ // RFC7231§6.4: Automatic redirection needs to done with
427
+ // care for methods not known to be safe, […]
428
+ // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
429
+ // the request method from POST to GET for the subsequent request.
430
+ var method = this._options.method;
431
+ if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
432
+ // RFC7231§6.4.4: The 303 (See Other) status code indicates that
433
+ // the server is redirecting the user agent to a different resource […]
434
+ // A user agent can perform a retrieval request targeting that URI
435
+ // (a GET or HEAD request if using HTTP) […]
436
+ (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
437
+ this._options.method = "GET";
438
+ // Drop a possible entity and headers related to it
439
+ this._requestBodyBuffers = [];
440
+ removeMatchingHeaders(/^content-/i, this._options.headers);
441
+ }
391
442
 
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
- }
443
+ // Drop the Host header, as the redirect might lead to a different host
444
+ var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
445
+
446
+ // If the redirect is relative, carry over the host of the last request
447
+ var currentUrlParts = parseUrl(this._currentUrl);
448
+ var currentHost = currentHostHeader || currentUrlParts.host;
449
+ var currentUrl = /^\w+:/.test(location) ? this._currentUrl :
450
+ url.format(Object.assign(currentUrlParts, { host: currentHost }));
451
+
452
+ // Create the redirected request
453
+ var redirectUrl = resolveUrl(location, currentUrl);
454
+ debug("redirecting to", redirectUrl.href);
455
+ this._isRedirect = true;
456
+ spreadUrlObject(redirectUrl, this._options);
457
+
458
+ // Drop confidential headers when redirecting to a less secure protocol
459
+ // or to a different domain that is not a superdomain
460
+ if (redirectUrl.protocol !== currentUrlParts.protocol &&
461
+ redirectUrl.protocol !== "https:" ||
462
+ redirectUrl.host !== currentHost &&
463
+ !isSubdomain(redirectUrl.host, currentHost)) {
464
+ removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers);
401
465
  }
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
466
 
408
- // Clean up
409
- this._requestBodyBuffers = [];
467
+ // Evaluate the beforeRedirect callback
468
+ if (isFunction(beforeRedirect)) {
469
+ var responseDetails = {
470
+ headers: response.headers,
471
+ statusCode: statusCode,
472
+ };
473
+ var requestDetails = {
474
+ url: currentUrl,
475
+ method: method,
476
+ headers: requestHeaders,
477
+ };
478
+ beforeRedirect(this._options, responseDetails, requestDetails);
479
+ this._sanitizeOptions(this._options);
410
480
  }
481
+
482
+ // Perform the redirected request
483
+ this._performRequest();
411
484
  };
412
485
 
413
486
  // Wraps the key/value object of protocols with redirect functionality
@@ -427,26 +500,19 @@ function wrap(protocols) {
427
500
 
428
501
  // Executes a request, following redirects
429
502
  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
- }
503
+ // Parse parameters, ensuring that input is an object
504
+ if (isURL(input)) {
505
+ input = spreadUrlObject(input);
440
506
  }
441
- else if (URL && (input instanceof URL)) {
442
- input = urlToOptions(input);
507
+ else if (isString(input)) {
508
+ input = spreadUrlObject(parseUrl(input));
443
509
  }
444
510
  else {
445
511
  callback = options;
446
- options = input;
512
+ options = validateUrl(input);
447
513
  input = { protocol: protocol };
448
514
  }
449
- if (typeof options === "function") {
515
+ if (isFunction(options)) {
450
516
  callback = options;
451
517
  options = null;
452
518
  }
@@ -457,6 +523,9 @@ function wrap(protocols) {
457
523
  maxBodyLength: exports.maxBodyLength,
458
524
  }, input, options);
459
525
  options.nativeProtocols = nativeProtocols;
526
+ if (!isString(options.host) && !isString(options.hostname)) {
527
+ options.hostname = "::1";
528
+ }
460
529
 
461
530
  assert.equal(options.protocol, protocol, "protocol mismatch");
462
531
  debug("options", options);
@@ -479,27 +548,57 @@ function wrap(protocols) {
479
548
  return exports;
480
549
  }
481
550
 
482
- /* istanbul ignore next */
483
551
  function noop() { /* empty */ }
484
552
 
485
- // from https://github.com/nodejs/node/blob/master/lib/internal/url.js
486
- function 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);
553
+ function parseUrl(input) {
554
+ var parsed;
555
+ /* istanbul ignore else */
556
+ if (useNativeURL) {
557
+ parsed = new URL(input);
558
+ }
559
+ else {
560
+ // Ensure the URL is valid and absolute
561
+ parsed = validateUrl(url.parse(input));
562
+ if (!isString(parsed.protocol)) {
563
+ throw new InvalidUrlError({ input });
564
+ }
565
+ }
566
+ return parsed;
567
+ }
568
+
569
+ function resolveUrl(relative, base) {
570
+ /* istanbul ignore next */
571
+ return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative));
572
+ }
573
+
574
+ function validateUrl(input) {
575
+ if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) {
576
+ throw new InvalidUrlError({ input: input.href || input });
501
577
  }
502
- return options;
578
+ if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) {
579
+ throw new InvalidUrlError({ input: input.href || input });
580
+ }
581
+ return input;
582
+ }
583
+
584
+ function spreadUrlObject(urlObject, target) {
585
+ var spread = target || {};
586
+ for (var key of preservedUrlFields) {
587
+ spread[key] = urlObject[key];
588
+ }
589
+
590
+ // Fix IPv6 hostname
591
+ if (spread.hostname.startsWith("[")) {
592
+ spread.hostname = spread.hostname.slice(1, -1);
593
+ }
594
+ // Ensure port is a number
595
+ if (spread.port !== "") {
596
+ spread.port = Number(spread.port);
597
+ }
598
+ // Concatenate path
599
+ spread.path = spread.search ? spread.pathname + spread.search : spread.pathname;
600
+
601
+ return spread;
503
602
  }
504
603
 
505
604
  function removeMatchingHeaders(regex, headers) {
@@ -510,27 +609,62 @@ function removeMatchingHeaders(regex, headers) {
510
609
  delete headers[header];
511
610
  }
512
611
  }
513
- return lastValue;
612
+ return (lastValue === null || typeof lastValue === "undefined") ?
613
+ undefined : String(lastValue).trim();
514
614
  }
515
615
 
516
- function createErrorType(code, defaultMessage) {
517
- function CustomError(message) {
616
+ function createErrorType(code, message, baseClass) {
617
+ // Create constructor
618
+ function CustomError(properties) {
518
619
  Error.captureStackTrace(this, this.constructor);
519
- this.message = message || defaultMessage;
620
+ Object.assign(this, properties || {});
621
+ this.code = code;
622
+ this.message = this.cause ? message + ": " + this.cause.message : message;
520
623
  }
521
- CustomError.prototype = new Error();
522
- CustomError.prototype.constructor = CustomError;
523
- CustomError.prototype.name = "Error [" + code + "]";
524
- CustomError.prototype.code = code;
624
+
625
+ // Attach constructor and set default properties
626
+ CustomError.prototype = new (baseClass || Error)();
627
+ Object.defineProperties(CustomError.prototype, {
628
+ constructor: {
629
+ value: CustomError,
630
+ enumerable: false,
631
+ },
632
+ name: {
633
+ value: "Error [" + code + "]",
634
+ enumerable: false,
635
+ },
636
+ });
525
637
  return CustomError;
526
638
  }
527
639
 
528
- function abortRequest(request) {
529
- for (var e = 0; e < events.length; e++) {
530
- request.removeListener(events[e], eventHandlers[events[e]]);
640
+ function destroyRequest(request, error) {
641
+ for (var event of events) {
642
+ request.removeListener(event, eventHandlers[event]);
531
643
  }
532
644
  request.on("error", noop);
533
- request.abort();
645
+ request.destroy(error);
646
+ }
647
+
648
+ function isSubdomain(subdomain, domain) {
649
+ assert(isString(subdomain) && isString(domain));
650
+ var dot = subdomain.length - domain.length - 1;
651
+ return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);
652
+ }
653
+
654
+ function isString(value) {
655
+ return typeof value === "string" || value instanceof String;
656
+ }
657
+
658
+ function isFunction(value) {
659
+ return typeof value === "function";
660
+ }
661
+
662
+ function isBuffer(value) {
663
+ return typeof value === "object" && ("length" in value);
664
+ }
665
+
666
+ function isURL(value) {
667
+ return URL && value instanceof URL;
534
668
  }
535
669
 
536
670
  // Exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "follow-redirects",
3
- "version": "1.14.4",
3
+ "version": "1.15.5",
4
4
  "description": "HTTP and HTTPS modules that follow redirects.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -11,9 +11,8 @@
11
11
  "node": ">=4.0"
12
12
  },
13
13
  "scripts": {
14
- "test": "npm run lint && npm run mocha",
15
14
  "lint": "eslint *.js test",
16
- "mocha": "nyc mocha"
15
+ "test": "nyc mocha"
17
16
  },
18
17
  "repository": {
19
18
  "type": "git",