tough-cookie 0.13.0 → 2.0.0

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.
package/lib/cookie.js CHANGED
@@ -33,6 +33,9 @@ var net = require('net');
33
33
  var urlParse = require('url').parse;
34
34
  var pubsuffix = require('./pubsuffix');
35
35
  var Store = require('./store').Store;
36
+ var MemoryCookieStore = require('./memstore').MemoryCookieStore;
37
+ var pathMatch = require('./pathMatch').pathMatch;
38
+ var VERSION = require('../package.json').version;
36
39
 
37
40
  var punycode;
38
41
  try {
@@ -43,17 +46,11 @@ try {
43
46
 
44
47
  var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
45
48
 
46
- // From RFC2616 S2.2:
47
- var TOKEN = /[\x21\x23-\x26\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/;
48
-
49
49
  // From RFC6265 S4.1.1
50
50
  // note that it excludes \x3B ";"
51
51
  var COOKIE_OCTET = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/;
52
52
  var COOKIE_OCTETS = new RegExp('^'+COOKIE_OCTET.source+'$');
53
53
 
54
- // The name/key cannot be empty but the value can (S5.2):
55
- var COOKIE_PAIR_STRICT = new RegExp('^('+TOKEN.source+'+)=("?)('+COOKIE_OCTET.source+'*)\\2$');
56
-
57
54
  // Double quotes are part of the value (see: S4.1.1).
58
55
  // '\r', '\n' and '\0' should be treated as a terminator in the "relaxed" mode
59
56
  // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60)
@@ -61,11 +58,9 @@ var COOKIE_PAIR_STRICT = new RegExp('^('+TOKEN.source+'+)=("?)('+COOKIE_OCTET.so
61
58
  // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L64)
62
59
  var COOKIE_PAIR = /^([^=;]+)\s*=\s*(("?)[^\n\r\0]*\3)/;
63
60
 
64
- // RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"'
61
+ // RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"'
65
62
  // Note ';' is \x3B
66
- var NON_CTL_SEMICOLON = /[\x20-\x3A\x3C-\x7E]+/;
67
- var EXTENSION_AV = NON_CTL_SEMICOLON;
68
- var PATH_VALUE = NON_CTL_SEMICOLON;
63
+ var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/;
69
64
 
70
65
  // Used for checking whether or not there is a trailing semi-colon
71
66
  var TRAILING_SEMICOLON = /;+$/;
@@ -90,11 +85,9 @@ var YEAR = /^(\d{2}|\d{4})$/; // 2 to 4 digits
90
85
  var MAX_TIME = 2147483647000; // 31-bit max
91
86
  var MIN_TIME = 0; // 31-bit min
92
87
 
93
- var cookiesCreated = 0; // Number of cookies created in runtime
94
-
95
88
 
96
89
  // RFC6265 S5.1.1 date parser:
97
- function parseDate(str,strict) {
90
+ function parseDate(str) {
98
91
  if (!str) {
99
92
  return;
100
93
  }
@@ -115,9 +108,6 @@ function parseDate(str,strict) {
115
108
  var month = null;
116
109
  var year = null;
117
110
 
118
- var date = new Date();
119
- date.setMilliseconds(0);
120
-
121
111
  for (var i=0; i<tokens.length; i++) {
122
112
  var token = tokens[i].trim();
123
113
  if (!token.length) {
@@ -208,7 +198,6 @@ function parseDate(str,strict) {
208
198
  if (year < 1601) {
209
199
  return; // 5. ... the year-value is less than 1601
210
200
  }
211
- continue;
212
201
  }
213
202
  }
214
203
  }
@@ -325,52 +314,19 @@ function defaultPath(path) {
325
314
  return path.slice(0, rightSlash);
326
315
  }
327
316
 
328
- /*
329
- * "A request-path path-matches a given cookie-path if at least one of the
330
- * following conditions holds:"
331
- */
332
- function pathMatch(reqPath,cookiePath) {
333
- // "o The cookie-path and the request-path are identical."
334
- if (cookiePath === reqPath) {
335
- return true;
336
- }
337
-
338
- var idx = reqPath.indexOf(cookiePath);
339
- if (idx === 0) {
340
- // "o The cookie-path is a prefix of the request-path, and the last
341
- // character of the cookie-path is %x2F ("/")."
342
- if (cookiePath.substr(-1) === "/") {
343
- return true;
344
- }
345
-
346
- // " o The cookie-path is a prefix of the request-path, and the first
347
- // character of the request-path that is not included in the cookie- path
348
- // is a %x2F ("/") character."
349
- if (reqPath.substr(cookiePath.length,1) === "/") {
350
- return true;
351
- }
352
- }
353
-
354
- return false;
355
- }
356
317
 
357
- function parse(str, strict) {
318
+ function parse(str) {
358
319
  str = str.trim();
359
320
 
360
321
  // S4.1.1 Trailing semi-colons are not part of the specification.
361
- // If we are not in strict mode we remove the trailing semi-colons.
362
322
  var semiColonCheck = TRAILING_SEMICOLON.exec(str);
363
323
  if (semiColonCheck) {
364
- if (strict) {
365
- return;
366
- }
367
324
  str = str.slice(0, semiColonCheck.index);
368
325
  }
369
326
 
370
327
  // We use a regex to parse the "name-value-pair" part of S5.2
371
328
  var firstSemi = str.indexOf(';'); // S5.2 step 1
372
- var pairRx = strict ? COOKIE_PAIR_STRICT : COOKIE_PAIR;
373
- var result = pairRx.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
329
+ var result = COOKIE_PAIR.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
374
330
 
375
331
  // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
376
332
  // constraints as well as trimming any whitespace.
@@ -379,10 +335,8 @@ function parse(str, strict) {
379
335
  }
380
336
 
381
337
  var c = new Cookie();
382
- c.key = result[1]; // the regexp should trim() already
383
- c.value = strict ? result[3] : result[2]; // [2] is quotes or empty-string in strict mode
384
- c.key = c.key.trim();
385
- c.value = c.value.trim();
338
+ c.key = result[1].trim();
339
+ c.value = result[2].trim();
386
340
 
387
341
  if (firstSemi === -1) {
388
342
  return c;
@@ -410,13 +364,9 @@ function parse(str, strict) {
410
364
  var cookie_avs = unparsed.split(/\s*;\s*/);
411
365
  while (cookie_avs.length) {
412
366
  var av = cookie_avs.shift();
413
-
414
- if (strict && !EXTENSION_AV.test(av)) {
415
- return;
416
- }
417
-
418
367
  var av_sep = av.indexOf('=');
419
368
  var av_key, av_value;
369
+
420
370
  if (av_sep === -1) {
421
371
  av_key = av;
422
372
  av_value = null;
@@ -426,47 +376,51 @@ function parse(str, strict) {
426
376
  }
427
377
 
428
378
  av_key = av_key.trim().toLowerCase();
379
+
429
380
  if (av_value) {
430
381
  av_value = av_value.trim();
431
382
  }
432
383
 
433
384
  switch(av_key) {
434
385
  case 'expires': // S5.2.1
435
- if (!av_value) {if(strict){return;}else{break;} }
436
- var exp = parseDate(av_value,strict);
437
- // "If the attribute-value failed to parse as a cookie date, ignore the
438
- // cookie-av."
439
- if (exp == null) { if(strict){return;}else{break;} }
440
- c.expires = exp;
441
- // over and underflow not realistically a concern: V8's getTime() seems to
442
- // store something larger than a 32-bit time_t (even with 32-bit node)
386
+ if (av_value) {
387
+ var exp = parseDate(av_value);
388
+ // "If the attribute-value failed to parse as a cookie date, ignore the
389
+ // cookie-av."
390
+ if (exp) {
391
+ // over and underflow not realistically a concern: V8's getTime() seems to
392
+ // store something larger than a 32-bit time_t (even with 32-bit node)
393
+ c.expires = exp;
394
+ }
395
+ }
443
396
  break;
444
397
 
445
398
  case 'max-age': // S5.2.2
446
- if (!av_value) { if(strict){return;}else{break;} }
447
- // "If the first character of the attribute-value is not a DIGIT or a "-"
448
- // character ...[or]... If the remainder of attribute-value contains a
449
- // non-DIGIT character, ignore the cookie-av."
450
- if (!/^-?[0-9]+$/.test(av_value)) { if(strict){return;}else{break;} }
451
- var delta = parseInt(av_value,10);
452
- if (strict && delta <= 0) {
453
- return; // S4.1.1
399
+ if (av_value) {
400
+ // "If the first character of the attribute-value is not a DIGIT or a "-"
401
+ // character ...[or]... If the remainder of attribute-value contains a
402
+ // non-DIGIT character, ignore the cookie-av."
403
+ if (/^-?[0-9]+$/.test(av_value)) {
404
+ var delta = parseInt(av_value, 10);
405
+ // "If delta-seconds is less than or equal to zero (0), let expiry-time
406
+ // be the earliest representable date and time."
407
+ c.setMaxAge(delta);
408
+ }
454
409
  }
455
- // "If delta-seconds is less than or equal to zero (0), let expiry-time
456
- // be the earliest representable date and time."
457
- c.setMaxAge(delta);
458
410
  break;
459
411
 
460
412
  case 'domain': // S5.2.3
461
413
  // "If the attribute-value is empty, the behavior is undefined. However,
462
414
  // the user agent SHOULD ignore the cookie-av entirely."
463
- if (!av_value) { if(strict){return;}else{break;} }
464
- // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
465
- // (".") character."
466
- var domain = av_value.trim().replace(/^\./,'');
467
- if (!domain) { if(strict){return;}else{break;} } // see "is empty" above
468
- // "Convert the cookie-domain to lower case."
469
- c.domain = domain.toLowerCase();
415
+ if (av_value) {
416
+ // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
417
+ // (".") character."
418
+ var domain = av_value.trim().replace(/^\./, '');
419
+ if (domain) {
420
+ // "Convert the cookie-domain to lower case."
421
+ c.domain = domain.toLowerCase();
422
+ }
423
+ }
470
424
  break;
471
425
 
472
426
  case 'path': // S5.2.4
@@ -480,11 +434,7 @@ function parse(str, strict) {
480
434
  * We'll represent the default-path as null since it depends on the
481
435
  * context of the parsing.
482
436
  */
483
- if (!av_value || av_value.substr(0,1) != "/") {
484
- c.path = null;
485
- if(strict){return;}else{break;}
486
- }
487
- c.path = av_value;
437
+ c.path = av_value && av_value[0] === "/" ? av_value : null;
488
438
  break;
489
439
 
490
440
  case 'secure': // S5.2.5
@@ -493,12 +443,10 @@ function parse(str, strict) {
493
443
  * the user agent MUST append an attribute to the cookie-attribute-list
494
444
  * with an attribute-name of Secure and an empty attribute-value."
495
445
  */
496
- if (av_value != null) { if(strict){return;} }
497
446
  c.secure = true;
498
447
  break;
499
448
 
500
449
  case 'httponly': // S5.2.6 -- effectively the same as 'secure'
501
- if (av_value != null) { if(strict){return;} }
502
450
  c.httpOnly = true;
503
451
  break;
504
452
 
@@ -509,47 +457,60 @@ function parse(str, strict) {
509
457
  }
510
458
  }
511
459
 
512
- // ensure a default date for sorting:
513
- c.creation = new Date();
514
- //NOTE: add runtime index for the cookieCompare() to resolve the situation when Date's precision is not enough .
515
- //Store initial UTC time as well, so we will be able to determine if we need to fallback to the Date object.
516
- c._creationRuntimeIdx = ++cookiesCreated;
517
- c._initialCreationTime = c.creation.getTime();
518
460
  return c;
519
461
  }
520
462
 
463
+ // avoid the V8 deoptimization monster!
464
+ function jsonParse(str) {
465
+ var obj;
466
+ try {
467
+ obj = JSON.parse(str);
468
+ } catch (e) {
469
+ return e;
470
+ }
471
+ return obj;
472
+ }
473
+
521
474
  function fromJSON(str) {
522
475
  if (!str) {
523
476
  return null;
524
477
  }
525
478
 
526
479
  var obj;
527
- try {
528
- obj = JSON.parse(str);
529
- } catch (e) {
530
- return null;
480
+ if (typeof str === 'string') {
481
+ obj = jsonParse(str);
482
+ if (obj instanceof Error) {
483
+ return null;
484
+ }
485
+ } else {
486
+ // assume it's an Object
487
+ obj = str;
531
488
  }
532
489
 
533
490
  var c = new Cookie();
534
- for (var i=0; i<numCookieProperties; i++) {
535
- var prop = cookieProperties[i];
536
- if (obj[prop] == null) {
537
- continue;
491
+ for (var i=0; i<Cookie.serializableProperties.length; i++) {
492
+ var prop = Cookie.serializableProperties[i];
493
+ if (obj[prop] === undefined ||
494
+ obj[prop] === Cookie.prototype[prop])
495
+ {
496
+ continue; // leave as prototype default
538
497
  }
498
+
539
499
  if (prop === 'expires' ||
540
500
  prop === 'creation' ||
541
501
  prop === 'lastAccessed')
542
502
  {
543
- c[prop] = obj[prop] == "Infinity" ? "Infinity" : new Date(obj[prop]);
503
+ if (obj[prop] === null) {
504
+ c[prop] = null;
505
+ } else {
506
+ c[prop] = obj[prop] == "Infinity" ?
507
+ "Infinity" : new Date(obj[prop]);
508
+ }
544
509
  } else {
545
510
  c[prop] = obj[prop];
546
511
  }
547
512
  }
548
513
 
549
-
550
- // ensure a default date for sorting:
551
- c.creation = c.creation || new Date();
552
-
553
514
  return c;
554
515
  }
555
516
 
@@ -563,45 +524,28 @@ function fromJSON(str) {
563
524
  */
564
525
 
565
526
  function cookieCompare(a,b) {
527
+ var cmp = 0;
528
+
566
529
  // descending for length: b CMP a
567
- var deltaLen = (b.path ? b.path.length : 0) - (a.path ? a.path.length : 0);
568
- if (deltaLen !== 0) {
569
- return deltaLen;
530
+ var aPathLen = a.path ? a.path.length : 0;
531
+ var bPathLen = b.path ? b.path.length : 0;
532
+ cmp = bPathLen - aPathLen;
533
+ if (cmp !== 0) {
534
+ return cmp;
570
535
  }
571
536
 
537
+ // ascending for time: a CMP b
572
538
  var aTime = a.creation ? a.creation.getTime() : MAX_TIME;
573
539
  var bTime = b.creation ? b.creation.getTime() : MAX_TIME;
574
-
575
- // NOTE: if creation dates are equal and they were not modified from the outside,
576
- // then use _creationRuntimeIdx for the comparison.
577
- if(aTime === bTime && aTime === a._initialCreationTime && bTime === b._initialCreationTime) {
578
- return a._creationRuntimeIdx - b._creationRuntimeIdx;
540
+ cmp = aTime - bTime;
541
+ if (cmp !== 0) {
542
+ return cmp;
579
543
  }
580
544
 
581
- // ascending for time: a CMP b
582
- return aTime - bTime;
583
- }
584
-
585
- // Gives the permutation of all possible domainMatch()es of a given domain. The
586
- // array is in shortest-to-longest order. Handy for indexing.
587
- function permuteDomain(domain) {
588
- var pubSuf = pubsuffix.getPublicSuffix(domain);
589
- if (!pubSuf) {
590
- return null;
591
- }
592
- if (pubSuf == domain) {
593
- return [domain];
594
- }
545
+ // break ties for the same millisecond (precision of JavaScript's clock)
546
+ cmp = a.creationIndex - b.creationIndex;
595
547
 
596
- var prefix = domain.slice(0,-(pubSuf.length+1)); // ".example.com"
597
- var parts = prefix.split('.').reverse();
598
- var cur = pubSuf;
599
- var permutations = [cur];
600
- while (parts.length) {
601
- cur = parts.shift()+'.'+cur;
602
- permutations.push(cur);
603
- }
604
- return permutations;
548
+ return cmp;
605
549
  }
606
550
 
607
551
  // Gives the permutation of all possible pathMatch()es of a given path. The
@@ -627,20 +571,46 @@ function permutePath(path) {
627
571
  }
628
572
 
629
573
  function getCookieContext(url) {
630
- return (url instanceof Object) ? url : urlParse(decodeURI(url));
574
+ if (url instanceof Object) {
575
+ return url;
576
+ }
577
+ // NOTE: decodeURI will throw on malformed URIs (see GH-32).
578
+ // Therefore, we will just skip decoding for such URIs.
579
+ try {
580
+ url = decodeURI(url);
581
+ }
582
+ catch(err) {
583
+ // Silently swallow error
584
+ }
585
+
586
+ return urlParse(url);
631
587
  }
632
588
 
633
- function Cookie (opts) {
634
- if (typeof opts !== "object") {
635
- return;
636
- }
637
- Object.keys(opts).forEach(function (key) {
638
- if (Cookie.prototype.hasOwnProperty(key)) {
639
- this[key] = opts[key] || Cookie.prototype[key];
589
+ function Cookie(opts) {
590
+ opts = opts || {};
591
+
592
+ Object.keys(opts).forEach(function(prop) {
593
+ if (Cookie.prototype.hasOwnProperty(prop) &&
594
+ Cookie.prototype[prop] !== opts[prop] &&
595
+ prop.substr(0,1) !== '_')
596
+ {
597
+ this[prop] = opts[prop];
640
598
  }
641
- }.bind(this));
599
+ }, this);
600
+
601
+ this.creation = this.creation || new Date();
602
+
603
+ // used to break creation ties in cookieCompare():
604
+ Object.defineProperty(this, 'creationIndex', {
605
+ configurable: false,
606
+ enumerable: false, // important for assert.deepEqual checks
607
+ writable: true,
608
+ value: ++Cookie.cookiesCreated
609
+ });
642
610
  }
643
611
 
612
+ Cookie.cookiesCreated = 0; // incremented each time a cookie is created
613
+
644
614
  Cookie.parse = parse;
645
615
  Cookie.fromJSON = fromJSON;
646
616
 
@@ -660,17 +630,22 @@ Cookie.prototype.extensions = null;
660
630
  Cookie.prototype.hostOnly = null; // boolean when set
661
631
  Cookie.prototype.pathIsDefault = null; // boolean when set
662
632
  Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse
663
- Cookie.prototype._initialCreationTime = null; // Used to determine if cookie.creation was modified
664
- Cookie.prototype._creationRuntimeIdx = null; // Runtime index of the created cookie, used in cookieCompare()
665
633
  Cookie.prototype.lastAccessed = null; // Date when set
634
+ Object.defineProperty(Cookie.prototype, 'creationIndex', {
635
+ configurable: true,
636
+ enumerable: false,
637
+ writable: true,
638
+ value: 0
639
+ });
666
640
 
667
- var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function(p) {
668
- if (p instanceof Function) {
669
- return;
670
- }
671
- return p;
672
- }));
673
- var numCookieProperties = cookieProperties.length;
641
+ Cookie.serializableProperties = Object.keys(Cookie.prototype)
642
+ .filter(function(prop) {
643
+ return !(
644
+ Cookie.prototype[prop] instanceof Function ||
645
+ prop === 'creationIndex' ||
646
+ prop.substr(0,1) === '_'
647
+ );
648
+ });
674
649
 
675
650
  Cookie.prototype.inspect = function inspect() {
676
651
  var now = Date.now();
@@ -681,11 +656,51 @@ Cookie.prototype.inspect = function inspect() {
681
656
  '"';
682
657
  };
683
658
 
659
+ Cookie.prototype.toJSON = function() {
660
+ var obj = {};
661
+
662
+ var props = Cookie.serializableProperties;
663
+ for (var i=0; i<props.length; i++) {
664
+ var prop = props[i];
665
+ if (this[prop] === Cookie.prototype[prop]) {
666
+ continue; // leave as prototype default
667
+ }
668
+
669
+ if (prop === 'expires' ||
670
+ prop === 'creation' ||
671
+ prop === 'lastAccessed')
672
+ {
673
+ if (this[prop] === null) {
674
+ obj[prop] = null;
675
+ } else {
676
+ obj[prop] = this[prop] == "Infinity" ? // intentionally not ===
677
+ "Infinity" : this[prop].toISOString();
678
+ }
679
+ } else if (prop === 'maxAge') {
680
+ if (this[prop] !== null) {
681
+ // again, intentionally not ===
682
+ obj[prop] = (this[prop] == Infinity || this[prop] == -Infinity) ?
683
+ this[prop].toString() : this[prop];
684
+ }
685
+ } else {
686
+ if (this[prop] !== Cookie.prototype[prop]) {
687
+ obj[prop] = this[prop];
688
+ }
689
+ }
690
+ }
691
+
692
+ return obj;
693
+ };
694
+
695
+ Cookie.prototype.clone = function() {
696
+ return fromJSON(this.toJSON());
697
+ };
698
+
684
699
  Cookie.prototype.validate = function validate() {
685
700
  if (!COOKIE_OCTETS.test(this.value)) {
686
701
  return false;
687
702
  }
688
- if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires,true)) {
703
+ if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires)) {
689
704
  return false;
690
705
  }
691
706
  if (this.maxAge != null && this.maxAge <= 0) {
@@ -805,7 +820,7 @@ Cookie.prototype.TTL = function TTL(now) {
805
820
  // elsewhere)
806
821
  Cookie.prototype.expiryTime = function expiryTime(now) {
807
822
  if (this.maxAge != null) {
808
- var relativeTo = this.creation || now || new Date();
823
+ var relativeTo = now || this.creation || new Date();
809
824
  var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000;
810
825
  return relativeTo.getTime() + age;
811
826
  }
@@ -843,16 +858,13 @@ Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() {
843
858
  return canonicalDomain(this.domain);
844
859
  };
845
860
 
846
-
847
- var memstore;
848
861
  function CookieJar(store, rejectPublicSuffixes) {
849
862
  if (rejectPublicSuffixes != null) {
850
863
  this.rejectPublicSuffixes = rejectPublicSuffixes;
851
864
  }
852
865
 
853
866
  if (!store) {
854
- memstore = memstore || require('./memstore');
855
- store = new memstore.MemoryCookieStore();
867
+ store = new MemoryCookieStore();
856
868
  }
857
869
  this.store = store;
858
870
  }
@@ -873,7 +885,7 @@ CookieJar.prototype.setCookie = function(cookie, url, options, cb) {
873
885
 
874
886
  // S5.3 step 1
875
887
  if (!(cookie instanceof Cookie)) {
876
- cookie = Cookie.parse(cookie, options.strict === true);
888
+ cookie = Cookie.parse(cookie);
877
889
  }
878
890
  if (!cookie) {
879
891
  err = new Error("Cookie failed to parse");
@@ -958,6 +970,7 @@ CookieJar.prototype.setCookie = function(cookie, url, options, cb) {
958
970
  return cb(options.ignoreError ? null : err);
959
971
  }
960
972
  cookie.creation = oldCookie.creation; // step 11.3
973
+ cookie.creationIndex = oldCookie.creationIndex; // preserve tie-breaker
961
974
  cookie.lastAccessed = now;
962
975
  // Step 11.4 (delete cookie) is implied by just setting the new one:
963
976
  store.updateCookie(oldCookie, cookie, next); // step 12
@@ -1104,6 +1117,151 @@ CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) {
1104
1117
  this.getCookies.apply(this,args);
1105
1118
  };
1106
1119
 
1120
+ CAN_BE_SYNC.push('serialize');
1121
+ CookieJar.prototype.serialize = function(cb) {
1122
+ var type = this.store.constructor.name;
1123
+ if (type === 'Object') {
1124
+ type = null;
1125
+ }
1126
+
1127
+ // update README.md "Serialization Format" if you change this, please!
1128
+ var serialized = {
1129
+ // The version of tough-cookie that serialized this jar. Generally a good
1130
+ // practice since future versions can make data import decisions based on
1131
+ // known past behavior. When/if this matters, use `semver`.
1132
+ version: 'tough-cookie@'+VERSION,
1133
+
1134
+ // add the store type, to make humans happy:
1135
+ storeType: type,
1136
+
1137
+ // CookieJar configuration:
1138
+ rejectPublicSuffixes: !!this.rejectPublicSuffixes,
1139
+
1140
+ // this gets filled from getAllCookies:
1141
+ cookies: []
1142
+ };
1143
+
1144
+ if (!(this.store.getAllCookies &&
1145
+ typeof this.store.getAllCookies === 'function'))
1146
+ {
1147
+ return cb(new Error('store does not support getAllCookies and cannot be serialized'));
1148
+ }
1149
+
1150
+ this.store.getAllCookies(function(err,cookies) {
1151
+ if (err) {
1152
+ return cb(err);
1153
+ }
1154
+
1155
+ serialized.cookies = cookies.map(function(cookie) {
1156
+ // convert to serialized 'raw' cookies
1157
+ cookie = (cookie instanceof Cookie) ? cookie.toJSON() : cookie;
1158
+
1159
+ // Remove the index so new ones get assigned during deserialization
1160
+ delete cookie.creationIndex;
1161
+
1162
+ return cookie;
1163
+ });
1164
+
1165
+ return cb(null, serialized);
1166
+ });
1167
+ };
1168
+
1169
+ // well-known name that JSON.stringify calls
1170
+ CookieJar.prototype.toJSON = function() {
1171
+ return this.serializeSync();
1172
+ };
1173
+
1174
+ // use the class method CookieJar.deserialize instead of calling this directly
1175
+ CAN_BE_SYNC.push('_importCookies');
1176
+ CookieJar.prototype._importCookies = function(serialized, cb) {
1177
+ var jar = this;
1178
+ var cookies = serialized.cookies;
1179
+ if (!cookies || !Array.isArray(cookies)) {
1180
+ return cb(new Error('serialized jar has no cookies array'));
1181
+ }
1182
+
1183
+ function putNext(err) {
1184
+ if (err) {
1185
+ return cb(err);
1186
+ }
1187
+
1188
+ if (!cookies.length) {
1189
+ return cb(err, jar);
1190
+ }
1191
+
1192
+ var cookie;
1193
+ try {
1194
+ cookie = fromJSON(cookies.shift());
1195
+ } catch (e) {
1196
+ return cb(e);
1197
+ }
1198
+
1199
+ if (cookie === null) {
1200
+ return putNext(null); // skip this cookie
1201
+ }
1202
+
1203
+ jar.store.putCookie(cookie, putNext);
1204
+ }
1205
+
1206
+ putNext();
1207
+ };
1208
+
1209
+ CookieJar.deserialize = function(strOrObj, store, cb) {
1210
+ if (arguments.length !== 3) {
1211
+ // store is optional
1212
+ cb = store;
1213
+ store = null;
1214
+ }
1215
+
1216
+ var serialized;
1217
+ if (typeof strOrObj === 'string') {
1218
+ serialized = jsonParse(strOrObj);
1219
+ if (serialized instanceof Error) {
1220
+ return cb(serialized);
1221
+ }
1222
+ } else {
1223
+ serialized = strOrObj;
1224
+ }
1225
+
1226
+ var jar = new CookieJar(store, serialized.rejectPublicSuffixes);
1227
+ jar._importCookies(serialized, function(err) {
1228
+ if (err) {
1229
+ return cb(err);
1230
+ }
1231
+ cb(null, jar);
1232
+ });
1233
+ };
1234
+
1235
+ CookieJar.fromJSON = CookieJar.deserializeSync;
1236
+ CookieJar.deserializeSync = function(strOrObj, store) {
1237
+ var serialized = typeof strOrObj === 'string' ?
1238
+ JSON.parse(strOrObj) : strOrObj;
1239
+ var jar = new CookieJar(store, serialized.rejectPublicSuffixes);
1240
+
1241
+ // catch this mistake early:
1242
+ if (!jar.store.synchronous) {
1243
+ throw new Error('CookieJar store is not synchronous; use async API instead.');
1244
+ }
1245
+
1246
+ jar._importCookiesSync(serialized);
1247
+ return jar;
1248
+ };
1249
+
1250
+ CAN_BE_SYNC.push('clone');
1251
+ CookieJar.prototype.clone = function(newStore, cb) {
1252
+ if (arguments.length === 1) {
1253
+ cb = newStore;
1254
+ newStore = null;
1255
+ }
1256
+
1257
+ this.serialize(function(err,serialized) {
1258
+ if (err) {
1259
+ return cb(err);
1260
+ }
1261
+ CookieJar.deserialize(newStore, serialized, cb);
1262
+ });
1263
+ };
1264
+
1107
1265
  // Use a closure to provide a true imperative API for synchronous stores.
1108
1266
  function syncWrap(method) {
1109
1267
  return function() {
@@ -1135,6 +1293,7 @@ module.exports = {
1135
1293
  CookieJar: CookieJar,
1136
1294
  Cookie: Cookie,
1137
1295
  Store: Store,
1296
+ MemoryCookieStore: MemoryCookieStore,
1138
1297
  parseDate: parseDate,
1139
1298
  formatDate: formatDate,
1140
1299
  parse: parse,
@@ -1144,7 +1303,7 @@ module.exports = {
1144
1303
  pathMatch: pathMatch,
1145
1304
  getPublicSuffix: pubsuffix.getPublicSuffix,
1146
1305
  cookieCompare: cookieCompare,
1147
- permuteDomain: permuteDomain,
1306
+ permuteDomain: require('./permuteDomain').permuteDomain,
1148
1307
  permutePath: permutePath,
1149
- canonicalDomain: canonicalDomain,
1308
+ canonicalDomain: canonicalDomain
1150
1309
  };