follow-redirects 1.14.4 → 1.15.4

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