tough-cookie 0.10.0 → 0.13.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
@@ -1,28 +1,38 @@
1
- /*
2
- * Copyright GoInstant, Inc. and other contributors. All rights reserved.
3
- * Permission is hereby granted, free of charge, to any person obtaining a copy
4
- * of this software and associated documentation files (the "Software"), to
5
- * deal in the Software without restriction, including without limitation the
6
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
- * sell copies of the Software, and to permit persons to whom the Software is
8
- * furnished to do so, subject to the following conditions:
1
+ /*!
2
+ * Copyright (c) 2015, Salesforce.com, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * Redistribution and use in source and binary forms, with or without
6
+ * modification, are permitted provided that the following conditions are met:
7
+ *
8
+ * 1. Redistributions of source code must retain the above copyright notice,
9
+ * this list of conditions and the following disclaimer.
10
+ *
11
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ * this list of conditions and the following disclaimer in the documentation
13
+ * and/or other materials provided with the distribution.
9
14
  *
10
- * The above copyright notice and this permission notice shall be included in
11
- * all copies or substantial portions of the Software.
15
+ * 3. Neither the name of Salesforce.com nor the names of its contributors may
16
+ * be used to endorse or promote products derived from this software without
17
+ * specific prior written permission.
12
18
  *
13
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19
- * IN THE SOFTWARE.
19
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ * POSSIBILITY OF SUCH DAMAGE.
20
30
  */
21
-
22
- /*jshint regexp:false */
31
+ 'use strict';
23
32
  var net = require('net');
24
33
  var urlParse = require('url').parse;
25
34
  var pubsuffix = require('./pubsuffix');
35
+ var Store = require('./store').Store;
26
36
 
27
37
  var punycode;
28
38
  try {
@@ -43,7 +53,13 @@ var COOKIE_OCTETS = new RegExp('^'+COOKIE_OCTET.source+'$');
43
53
 
44
54
  // The name/key cannot be empty but the value can (S5.2):
45
55
  var COOKIE_PAIR_STRICT = new RegExp('^('+TOKEN.source+'+)=("?)('+COOKIE_OCTET.source+'*)\\2$');
46
- var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2\s*$/;
56
+
57
+ // Double quotes are part of the value (see: S4.1.1).
58
+ // '\r', '\n' and '\0' should be treated as a terminator in the "relaxed" mode
59
+ // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60)
60
+ // '=' and ';' are attribute/values separators
61
+ // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L64)
62
+ var COOKIE_PAIR = /^([^=;]+)\s*=\s*(("?)[^\n\r\0]*\3)/;
47
63
 
48
64
  // RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"'
49
65
  // Note ';' is \x3B
@@ -54,23 +70,13 @@ var PATH_VALUE = NON_CTL_SEMICOLON;
54
70
  // Used for checking whether or not there is a trailing semi-colon
55
71
  var TRAILING_SEMICOLON = /;+$/;
56
72
 
57
- /* RFC6265 S5.1.1.5:
58
- * [fail if] the day-of-month-value is less than 1 or greater than 31
59
- */
60
- var DAY_OF_MONTH = /^(0?[1-9]|[12][0-9]|3[01])$/;
73
+ var DAY_OF_MONTH = /^(\d{1,2})[^\d]*$/;
74
+ var TIME = /^(\d{1,2})[^\d]*:(\d{1,2})[^\d]*:(\d{1,2})[^\d]*$/;
75
+ var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i;
61
76
 
62
- /* RFC6265 S5.1.1.5:
63
- * [fail if]
64
- * * the hour-value is greater than 23,
65
- * * the minute-value is greater than 59, or
66
- * * the second-value is greater than 59.
67
- */
68
- var TIME = /(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/;
69
- var STRICT_TIME = /^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
70
-
71
- var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/i;
72
77
  var MONTH_TO_NUM = {
73
- jan:0, feb:1, mar:2, apr:3, may:4, jun:5, jul:6, aug:7, sep:8, oct:9, nov:10, dec:11
78
+ jan:0, feb:1, mar:2, apr:3, may:4, jun:5,
79
+ jul:6, aug:7, sep:8, oct:9, nov:10, dec:11
74
80
  };
75
81
  var NUM_TO_MONTH = [
76
82
  'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'
@@ -79,32 +85,44 @@ var NUM_TO_DAY = [
79
85
  'Sun','Mon','Tue','Wed','Thu','Fri','Sat'
80
86
  ];
81
87
 
82
- var YEAR = /^([1-9][0-9]{1,3})$/; // 2 to 4 digits (will check range when parsing)
88
+ var YEAR = /^(\d{2}|\d{4})$/; // 2 to 4 digits
83
89
 
84
90
  var MAX_TIME = 2147483647000; // 31-bit max
85
- var MAX_DATE = new Date(CookieJar.MAX_TIME); // 31-bit max
86
91
  var MIN_TIME = 0; // 31-bit min
87
- var MIN_DATE = new Date(CookieJar.MIN_TIME); // 31-bit min
92
+
93
+ var cookiesCreated = 0; // Number of cookies created in runtime
88
94
 
89
95
 
90
96
  // RFC6265 S5.1.1 date parser:
91
97
  function parseDate(str,strict) {
92
- if (!str) return;
93
- var found_time, found_dom, found_month, found_year;
98
+ if (!str) {
99
+ return;
100
+ }
94
101
 
95
102
  /* RFC6265 S5.1.1:
96
103
  * 2. Process each date-token sequentially in the order the date-tokens
97
104
  * appear in the cookie-date
98
105
  */
99
106
  var tokens = str.split(DATE_DELIM);
100
- if (!tokens) return;
107
+ if (!tokens) {
108
+ return;
109
+ }
110
+
111
+ var hour = null;
112
+ var minutes = null;
113
+ var seconds = null;
114
+ var day = null;
115
+ var month = null;
116
+ var year = null;
101
117
 
102
118
  var date = new Date();
103
119
  date.setMilliseconds(0);
104
120
 
105
121
  for (var i=0; i<tokens.length; i++) {
106
122
  var token = tokens[i].trim();
107
- if (!token.length) continue;
123
+ if (!token.length) {
124
+ continue;
125
+ }
108
126
 
109
127
  var result;
110
128
 
@@ -114,13 +132,22 @@ function parseDate(str,strict) {
114
132
  * the date-token, respectively. Skip the remaining sub-steps and continue
115
133
  * to the next date-token.
116
134
  */
117
- if (!found_time) {
118
- result = (strict ? STRICT_TIME : TIME).exec(token);
135
+ if (seconds === null) {
136
+ result = TIME.exec(token);
119
137
  if (result) {
120
- found_time = true;
121
- date.setUTCHours(result[1]);
122
- date.setUTCMinutes(result[2]);
123
- date.setUTCSeconds(result[3]);
138
+ hour = parseInt(result[1], 10);
139
+ minutes = parseInt(result[2], 10);
140
+ seconds = parseInt(result[3], 10);
141
+ /* RFC6265 S5.1.1.5:
142
+ * [fail if]
143
+ * * the hour-value is greater than 23,
144
+ * * the minute-value is greater than 59, or
145
+ * * the second-value is greater than 59.
146
+ */
147
+ if(hour > 23 || minutes > 59 || seconds > 59) {
148
+ return;
149
+ }
150
+
124
151
  continue;
125
152
  }
126
153
  }
@@ -130,11 +157,16 @@ function parseDate(str,strict) {
130
157
  * the day-of-month-value to the number denoted by the date-token. Skip
131
158
  * the remaining sub-steps and continue to the next date-token.
132
159
  */
133
- if (!found_dom) {
160
+ if (day === null) {
134
161
  result = DAY_OF_MONTH.exec(token);
135
162
  if (result) {
136
- found_dom = true;
137
- date.setUTCDate(result[1]);
163
+ day = parseInt(result, 10);
164
+ /* RFC6265 S5.1.1.5:
165
+ * [fail if] the day-of-month-value is less than 1 or greater than 31
166
+ */
167
+ if(day < 1 || day > 31) {
168
+ return;
169
+ }
138
170
  continue;
139
171
  }
140
172
  }
@@ -144,11 +176,10 @@ function parseDate(str,strict) {
144
176
  * the month denoted by the date-token. Skip the remaining sub-steps and
145
177
  * continue to the next date-token.
146
178
  */
147
- if (!found_month) {
179
+ if (month === null) {
148
180
  result = MONTH.exec(token);
149
181
  if (result) {
150
- found_month = true;
151
- date.setUTCMonth(MONTH_TO_NUM[result[1].toLowerCase()]);
182
+ month = MONTH_TO_NUM[result[1].toLowerCase()];
152
183
  continue;
153
184
  }
154
185
  }
@@ -158,37 +189,36 @@ function parseDate(str,strict) {
158
189
  * denoted by the date-token. Skip the remaining sub-steps and continue to
159
190
  * the next date-token.
160
191
  */
161
- if (!found_year) {
192
+ if (year === null) {
162
193
  result = YEAR.exec(token);
163
194
  if (result) {
164
- var year = result[0];
195
+ year = parseInt(result[0], 10);
165
196
  /* From S5.1.1:
166
197
  * 3. If the year-value is greater than or equal to 70 and less
167
198
  * than or equal to 99, increment the year-value by 1900.
168
199
  * 4. If the year-value is greater than or equal to 0 and less
169
200
  * than or equal to 69, increment the year-value by 2000.
170
201
  */
171
- if (70 <= year && year <= 99)
202
+ if (70 <= year && year <= 99) {
172
203
  year += 1900;
173
- else if (0 <= year && year <= 69)
204
+ } else if (0 <= year && year <= 69) {
174
205
  year += 2000;
206
+ }
175
207
 
176
- if (year < 1601)
208
+ if (year < 1601) {
177
209
  return; // 5. ... the year-value is less than 1601
178
-
179
- found_year = true;
180
- date.setUTCFullYear(year);
210
+ }
181
211
  continue;
182
212
  }
183
213
  }
184
214
  }
185
215
 
186
- if (!(found_time && found_dom && found_month && found_year)) {
216
+ if (seconds === null || day === null || month === null || year === null) {
187
217
  return; // 5. ... at least one of the found-day-of-month, found-month, found-
188
218
  // year, or found-time flags is not set,
189
219
  }
190
220
 
191
- return date;
221
+ return new Date(Date.UTC(year, month, day, hour, minutes, seconds));
192
222
  }
193
223
 
194
224
  function formatDate(date) {
@@ -203,19 +233,24 @@ function formatDate(date) {
203
233
 
204
234
  // S5.1.2 Canonicalized Host Names
205
235
  function canonicalDomain(str) {
206
- if (str == null) return null;
236
+ if (str == null) {
237
+ return null;
238
+ }
207
239
  str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading .
208
240
 
209
241
  // convert to IDN if any non-ASCII characters
210
- if (punycode && /[^\u0001-\u007f]/.test(str))
242
+ if (punycode && /[^\u0001-\u007f]/.test(str)) {
211
243
  str = punycode.toASCII(str);
244
+ }
212
245
 
213
246
  return str.toLowerCase();
214
247
  }
215
248
 
216
249
  // S5.1.3 Domain Matching
217
250
  function domainMatch(str, domStr, canonicalize) {
218
- if (str == null || domStr == null) return null;
251
+ if (str == null || domStr == null) {
252
+ return null;
253
+ }
219
254
  if (canonicalize !== false) {
220
255
  str = canonicalDomain(str);
221
256
  domStr = canonicalDomain(domStr);
@@ -226,25 +261,35 @@ function domainMatch(str, domStr, canonicalize) {
226
261
  * domain string and the string will have been canonicalized to lower case at
227
262
  * this point)"
228
263
  */
229
- if (str == domStr) return true;
264
+ if (str == domStr) {
265
+ return true;
266
+ }
230
267
 
231
268
  /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */
232
269
 
233
270
  /* "* The string is a host name (i.e., not an IP address)." */
234
- if (net.isIP(str)) return false;
271
+ if (net.isIP(str)) {
272
+ return false;
273
+ }
235
274
 
236
275
  /* "* The domain string is a suffix of the string" */
237
276
  var idx = str.indexOf(domStr);
238
- if (idx <= 0) return false; // it's a non-match (-1) or prefix (0)
277
+ if (idx <= 0) {
278
+ return false; // it's a non-match (-1) or prefix (0)
279
+ }
239
280
 
240
281
  // e.g "a.b.c".indexOf("b.c") === 2
241
282
  // 5 === 3+2
242
- if (str.length !== domStr.length + idx) // it's not a suffix
283
+ if (str.length !== domStr.length + idx) { // it's not a suffix
243
284
  return false;
285
+ }
244
286
 
245
287
  /* "* The last character of the string that is not included in the domain
246
288
  * string is a %x2E (".") character." */
247
- if (str.substr(idx-1,1) !== '.') return false;
289
+ if (str.substr(idx-1,1) !== '.') {
290
+ return false;
291
+ }
292
+
248
293
  return true;
249
294
  }
250
295
 
@@ -260,14 +305,20 @@ function domainMatch(str, domStr, canonicalize) {
260
305
  function defaultPath(path) {
261
306
  // "2. If the uri-path is empty or if the first character of the uri-path is not
262
307
  // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps.
263
- if (!path || path.substr(0,1) !== "/") return "/";
308
+ if (!path || path.substr(0,1) !== "/") {
309
+ return "/";
310
+ }
264
311
 
265
312
  // "3. If the uri-path contains no more than one %x2F ("/") character, output
266
313
  // %x2F ("/") and skip the remaining step."
267
- if (path === "/") return path;
314
+ if (path === "/") {
315
+ return path;
316
+ }
268
317
 
269
318
  var rightSlash = path.lastIndexOf("/");
270
- if (rightSlash === 0) return "/";
319
+ if (rightSlash === 0) {
320
+ return "/";
321
+ }
271
322
 
272
323
  // "4. Output the characters of the uri-path from the first character up to,
273
324
  // but not including, the right-most %x2F ("/")."
@@ -280,21 +331,24 @@ function defaultPath(path) {
280
331
  */
281
332
  function pathMatch(reqPath,cookiePath) {
282
333
  // "o The cookie-path and the request-path are identical."
283
- if (cookiePath === reqPath)
334
+ if (cookiePath === reqPath) {
284
335
  return true;
336
+ }
285
337
 
286
338
  var idx = reqPath.indexOf(cookiePath);
287
339
  if (idx === 0) {
288
340
  // "o The cookie-path is a prefix of the request-path, and the last
289
341
  // character of the cookie-path is %x2F ("/")."
290
- if (cookiePath.substr(-1) === "/")
342
+ if (cookiePath.substr(-1) === "/") {
291
343
  return true;
344
+ }
292
345
 
293
346
  // " o The cookie-path is a prefix of the request-path, and the first
294
347
  // character of the request-path that is not included in the cookie- path
295
348
  // is a %x2F ("/") character."
296
- if (reqPath.substr(cookiePath.length,1) === "/")
349
+ if (reqPath.substr(cookiePath.length,1) === "/") {
297
350
  return true;
351
+ }
298
352
  }
299
353
 
300
354
  return false;
@@ -307,7 +361,9 @@ function parse(str, strict) {
307
361
  // If we are not in strict mode we remove the trailing semi-colons.
308
362
  var semiColonCheck = TRAILING_SEMICOLON.exec(str);
309
363
  if (semiColonCheck) {
310
- if (strict) return;
364
+ if (strict) {
365
+ return;
366
+ }
311
367
  str = str.slice(0, semiColonCheck.index);
312
368
  }
313
369
 
@@ -318,13 +374,19 @@ function parse(str, strict) {
318
374
 
319
375
  // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
320
376
  // constraints as well as trimming any whitespace.
321
- if (!result) return;
377
+ if (!result) {
378
+ return;
379
+ }
322
380
 
323
381
  var c = new Cookie();
324
382
  c.key = result[1]; // the regexp should trim() already
325
- c.value = result[3]; // [2] is quotes or empty-string
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();
326
386
 
327
- if (firstSemi === -1) return c;
387
+ if (firstSemi === -1) {
388
+ return c;
389
+ }
328
390
 
329
391
  // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string
330
392
  // (including the %x3B (";") in question)." plus later on in the same section
@@ -333,7 +395,9 @@ function parse(str, strict) {
333
395
 
334
396
  // "If the unparsed-attributes string is empty, skip the rest of these
335
397
  // steps."
336
- if (unparsed.length === 0) return c;
398
+ if (unparsed.length === 0) {
399
+ return c;
400
+ }
337
401
 
338
402
  /*
339
403
  * S5.2 says that when looping over the items "[p]rocess the attribute-name
@@ -347,7 +411,9 @@ function parse(str, strict) {
347
411
  while (cookie_avs.length) {
348
412
  var av = cookie_avs.shift();
349
413
 
350
- if (strict && !EXTENSION_AV.test(av)) return;
414
+ if (strict && !EXTENSION_AV.test(av)) {
415
+ return;
416
+ }
351
417
 
352
418
  var av_sep = av.indexOf('=');
353
419
  var av_key, av_value;
@@ -360,28 +426,32 @@ function parse(str, strict) {
360
426
  }
361
427
 
362
428
  av_key = av_key.trim().toLowerCase();
363
- if (av_value) av_value = av_value.trim();
429
+ if (av_value) {
430
+ av_value = av_value.trim();
431
+ }
364
432
 
365
433
  switch(av_key) {
366
434
  case 'expires': // S5.2.1
367
- if (!av_value) { if(strict){return}else{break;} }
435
+ if (!av_value) {if(strict){return;}else{break;} }
368
436
  var exp = parseDate(av_value,strict);
369
437
  // "If the attribute-value failed to parse as a cookie date, ignore the
370
438
  // cookie-av."
371
- if (exp == null) { if(strict){return}else{break;} }
439
+ if (exp == null) { if(strict){return;}else{break;} }
372
440
  c.expires = exp;
373
441
  // over and underflow not realistically a concern: V8's getTime() seems to
374
442
  // store something larger than a 32-bit time_t (even with 32-bit node)
375
443
  break;
376
444
 
377
445
  case 'max-age': // S5.2.2
378
- if (!av_value) { if(strict){return}else{break;} }
446
+ if (!av_value) { if(strict){return;}else{break;} }
379
447
  // "If the first character of the attribute-value is not a DIGIT or a "-"
380
448
  // character ...[or]... If the remainder of attribute-value contains a
381
449
  // non-DIGIT character, ignore the cookie-av."
382
- if (!/^-?[0-9]+$/.test(av_value)) { if(strict){return}else{break;} }
450
+ if (!/^-?[0-9]+$/.test(av_value)) { if(strict){return;}else{break;} }
383
451
  var delta = parseInt(av_value,10);
384
- if (strict && delta <= 0) return; // S4.1.1
452
+ if (strict && delta <= 0) {
453
+ return; // S4.1.1
454
+ }
385
455
  // "If delta-seconds is less than or equal to zero (0), let expiry-time
386
456
  // be the earliest representable date and time."
387
457
  c.setMaxAge(delta);
@@ -390,11 +460,11 @@ function parse(str, strict) {
390
460
  case 'domain': // S5.2.3
391
461
  // "If the attribute-value is empty, the behavior is undefined. However,
392
462
  // the user agent SHOULD ignore the cookie-av entirely."
393
- if (!av_value) { if(strict){return}else{break;} }
463
+ if (!av_value) { if(strict){return;}else{break;} }
394
464
  // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
395
465
  // (".") character."
396
466
  var domain = av_value.trim().replace(/^\./,'');
397
- if (!domain) { if(strict){return}else{break;} } // see "is empty" above
467
+ if (!domain) { if(strict){return;}else{break;} } // see "is empty" above
398
468
  // "Convert the cookie-domain to lower case."
399
469
  c.domain = domain.toLowerCase();
400
470
  break;
@@ -410,7 +480,10 @@ function parse(str, strict) {
410
480
  * We'll represent the default-path as null since it depends on the
411
481
  * context of the parsing.
412
482
  */
413
- if (!av_value || av_value.substr(0,1) != "/") { if(strict){return}else{break;} }
483
+ if (!av_value || av_value.substr(0,1) != "/") {
484
+ c.path = null;
485
+ if(strict){return;}else{break;}
486
+ }
414
487
  c.path = av_value;
415
488
  break;
416
489
 
@@ -420,12 +493,12 @@ function parse(str, strict) {
420
493
  * the user agent MUST append an attribute to the cookie-attribute-list
421
494
  * with an attribute-name of Secure and an empty attribute-value."
422
495
  */
423
- if (av_value != null) { if(strict){return} }
496
+ if (av_value != null) { if(strict){return;} }
424
497
  c.secure = true;
425
498
  break;
426
499
 
427
500
  case 'httponly': // S5.2.6 -- effectively the same as 'secure'
428
- if (av_value != null) { if(strict){return} }
501
+ if (av_value != null) { if(strict){return;} }
429
502
  c.httpOnly = true;
430
503
  break;
431
504
 
@@ -438,11 +511,17 @@ function parse(str, strict) {
438
511
 
439
512
  // ensure a default date for sorting:
440
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();
441
518
  return c;
442
519
  }
443
520
 
444
521
  function fromJSON(str) {
445
- if (!str) return null;
522
+ if (!str) {
523
+ return null;
524
+ }
446
525
 
447
526
  var obj;
448
527
  try {
@@ -454,7 +533,9 @@ function fromJSON(str) {
454
533
  var c = new Cookie();
455
534
  for (var i=0; i<numCookieProperties; i++) {
456
535
  var prop = cookieProperties[i];
457
- if (obj[prop] == null) continue;
536
+ if (obj[prop] == null) {
537
+ continue;
538
+ }
458
539
  if (prop === 'expires' ||
459
540
  prop === 'creation' ||
460
541
  prop === 'lastAccessed')
@@ -484,18 +565,33 @@ function fromJSON(str) {
484
565
  function cookieCompare(a,b) {
485
566
  // descending for length: b CMP a
486
567
  var deltaLen = (b.path ? b.path.length : 0) - (a.path ? a.path.length : 0);
487
- if (deltaLen !== 0) return deltaLen;
568
+ if (deltaLen !== 0) {
569
+ return deltaLen;
570
+ }
571
+
572
+ var aTime = a.creation ? a.creation.getTime() : MAX_TIME;
573
+ 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;
579
+ }
580
+
488
581
  // ascending for time: a CMP b
489
- return (a.creation ? a.creation.getTime() : MAX_TIME) -
490
- (b.creation ? b.creation.getTime() : MAX_TIME);
582
+ return aTime - bTime;
491
583
  }
492
584
 
493
585
  // Gives the permutation of all possible domainMatch()es of a given domain. The
494
586
  // array is in shortest-to-longest order. Handy for indexing.
495
587
  function permuteDomain(domain) {
496
588
  var pubSuf = pubsuffix.getPublicSuffix(domain);
497
- if (!pubSuf) return null;
498
- if (pubSuf == domain) return [domain];
589
+ if (!pubSuf) {
590
+ return null;
591
+ }
592
+ if (pubSuf == domain) {
593
+ return [domain];
594
+ }
499
595
 
500
596
  var prefix = domain.slice(0,-(pubSuf.length+1)); // ".example.com"
501
597
  var parts = prefix.split('.').reverse();
@@ -511,14 +607,18 @@ function permuteDomain(domain) {
511
607
  // Gives the permutation of all possible pathMatch()es of a given path. The
512
608
  // array is in longest-to-shortest order. Handy for indexing.
513
609
  function permutePath(path) {
514
- var origPath = path;
515
- if (path === '/') return ['/'];
516
- if (path.lastIndexOf('/') === path.length-1)
610
+ if (path === '/') {
611
+ return ['/'];
612
+ }
613
+ if (path.lastIndexOf('/') === path.length-1) {
517
614
  path = path.substr(0,path.length-1);
615
+ }
518
616
  var permutations = [path];
519
617
  while (path.length > 1) {
520
618
  var lindex = path.lastIndexOf('/');
521
- if (lindex === 0) break;
619
+ if (lindex === 0) {
620
+ break;
621
+ }
522
622
  path = path.substr(0,lindex);
523
623
  permutations.push(path);
524
624
  }
@@ -526,9 +626,14 @@ function permutePath(path) {
526
626
  return permutations;
527
627
  }
528
628
 
629
+ function getCookieContext(url) {
630
+ return (url instanceof Object) ? url : urlParse(decodeURI(url));
631
+ }
529
632
 
530
633
  function Cookie (opts) {
531
- if (typeof opts !== "object") return;
634
+ if (typeof opts !== "object") {
635
+ return;
636
+ }
532
637
  Object.keys(opts).forEach(function (key) {
533
638
  if (Cookie.prototype.hasOwnProperty(key)) {
534
639
  this[key] = opts[key] || Cookie.prototype[key];
@@ -555,10 +660,14 @@ Cookie.prototype.extensions = null;
555
660
  Cookie.prototype.hostOnly = null; // boolean when set
556
661
  Cookie.prototype.pathIsDefault = null; // boolean when set
557
662
  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()
558
665
  Cookie.prototype.lastAccessed = null; // Date when set
559
666
 
560
667
  var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function(p) {
561
- if (p instanceof Function) return;
668
+ if (p instanceof Function) {
669
+ return;
670
+ }
562
671
  return p;
563
672
  }));
564
673
  var numCookieProperties = cookieProperties.length;
@@ -573,42 +682,54 @@ Cookie.prototype.inspect = function inspect() {
573
682
  };
574
683
 
575
684
  Cookie.prototype.validate = function validate() {
576
- if (!COOKIE_OCTETS.test(this.value))
685
+ if (!COOKIE_OCTETS.test(this.value)) {
577
686
  return false;
578
- if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires,true))
687
+ }
688
+ if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires,true)) {
579
689
  return false;
580
- if (this.maxAge != null && this.maxAge <= 0)
690
+ }
691
+ if (this.maxAge != null && this.maxAge <= 0) {
581
692
  return false; // "Max-Age=" non-zero-digit *DIGIT
582
- if (this.path != null && !PATH_VALUE.test(this.path))
693
+ }
694
+ if (this.path != null && !PATH_VALUE.test(this.path)) {
583
695
  return false;
696
+ }
584
697
 
585
698
  var cdomain = this.cdomain();
586
699
  if (cdomain) {
587
- if (cdomain.match(/\.$/))
700
+ if (cdomain.match(/\.$/)) {
588
701
  return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this
702
+ }
589
703
  var suffix = pubsuffix.getPublicSuffix(cdomain);
590
- if (suffix == null) // it's a public suffix
704
+ if (suffix == null) { // it's a public suffix
591
705
  return false;
706
+ }
592
707
  }
593
708
  return true;
594
709
  };
595
710
 
596
711
  Cookie.prototype.setExpires = function setExpires(exp) {
597
- if (exp instanceof Date) this.expires = exp;
598
- else this.expires = parseDate(exp) || "Infinity";
712
+ if (exp instanceof Date) {
713
+ this.expires = exp;
714
+ } else {
715
+ this.expires = parseDate(exp) || "Infinity";
716
+ }
599
717
  };
600
718
 
601
719
  Cookie.prototype.setMaxAge = function setMaxAge(age) {
602
- if (age === Infinity || age === -Infinity)
720
+ if (age === Infinity || age === -Infinity) {
603
721
  this.maxAge = age.toString(); // so JSON.stringify() works
604
- else
722
+ } else {
605
723
  this.maxAge = age;
724
+ }
606
725
  };
607
726
 
608
727
  // gives Cookie header format
609
728
  Cookie.prototype.cookieString = function cookieString() {
610
729
  var val = this.value;
611
- if (val == null) val = '';
730
+ if (val == null) {
731
+ val = '';
732
+ }
612
733
  return this.key+'='+val;
613
734
  };
614
735
 
@@ -617,22 +738,30 @@ Cookie.prototype.toString = function toString() {
617
738
  var str = this.cookieString();
618
739
 
619
740
  if (this.expires != Infinity) {
620
- if (this.expires instanceof Date)
741
+ if (this.expires instanceof Date) {
621
742
  str += '; Expires='+formatDate(this.expires);
622
- else
743
+ } else {
623
744
  str += '; Expires='+this.expires;
745
+ }
624
746
  }
625
747
 
626
- if (this.maxAge != null && this.maxAge != Infinity)
748
+ if (this.maxAge != null && this.maxAge != Infinity) {
627
749
  str += '; Max-Age='+this.maxAge;
750
+ }
628
751
 
629
- if (this.domain && !this.hostOnly)
752
+ if (this.domain && !this.hostOnly) {
630
753
  str += '; Domain='+this.domain;
631
- if (this.path)
754
+ }
755
+ if (this.path) {
632
756
  str += '; Path='+this.path;
757
+ }
633
758
 
634
- if (this.secure) str += '; Secure';
635
- if (this.httpOnly) str += '; HttpOnly';
759
+ if (this.secure) {
760
+ str += '; Secure';
761
+ }
762
+ if (this.httpOnly) {
763
+ str += '; HttpOnly';
764
+ }
636
765
  if (this.extensions) {
637
766
  this.extensions.forEach(function(ext) {
638
767
  str += '; '+ext;
@@ -662,8 +791,9 @@ Cookie.prototype.TTL = function TTL(now) {
662
791
  expires = parseDate(expires) || Infinity;
663
792
  }
664
793
 
665
- if (expires == Infinity)
794
+ if (expires == Infinity) {
666
795
  return Infinity;
796
+ }
667
797
 
668
798
  return expires.getTime() - (now || Date.now());
669
799
  }
@@ -680,7 +810,9 @@ Cookie.prototype.expiryTime = function expiryTime(now) {
680
810
  return relativeTo.getTime() + age;
681
811
  }
682
812
 
683
- if (this.expires == Infinity) return Infinity;
813
+ if (this.expires == Infinity) {
814
+ return Infinity;
815
+ }
684
816
  return this.expires.getTime();
685
817
  };
686
818
 
@@ -688,9 +820,13 @@ Cookie.prototype.expiryTime = function expiryTime(now) {
688
820
  // elsewhere), except it returns a Date
689
821
  Cookie.prototype.expiryDate = function expiryDate(now) {
690
822
  var millisec = this.expiryTime(now);
691
- if (millisec == Infinity) return new Date(MAX_TIME);
692
- else if (millisec == -Infinity) return new Date(MIN_TIME);
693
- else return new Date(millisec);
823
+ if (millisec == Infinity) {
824
+ return new Date(MAX_TIME);
825
+ } else if (millisec == -Infinity) {
826
+ return new Date(MIN_TIME);
827
+ } else {
828
+ return new Date(millisec);
829
+ }
694
830
  };
695
831
 
696
832
  // This replaces the "persistent-flag" parts of S5.3 step 3
@@ -701,14 +837,19 @@ Cookie.prototype.isPersistent = function isPersistent() {
701
837
  // Mostly S5.1.2 and S5.2.3:
702
838
  Cookie.prototype.cdomain =
703
839
  Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() {
704
- if (this.domain == null) return null;
840
+ if (this.domain == null) {
841
+ return null;
842
+ }
705
843
  return canonicalDomain(this.domain);
706
844
  };
707
845
 
708
846
 
709
847
  var memstore;
710
848
  function CookieJar(store, rejectPublicSuffixes) {
711
- if (rejectPublicSuffixes != null) this.rejectPublicSuffixes = rejectPublicSuffixes;
849
+ if (rejectPublicSuffixes != null) {
850
+ this.rejectPublicSuffixes = rejectPublicSuffixes;
851
+ }
852
+
712
853
  if (!store) {
713
854
  memstore = memstore || require('./memstore');
714
855
  store = new memstore.MemoryCookieStore();
@@ -717,10 +858,12 @@ function CookieJar(store, rejectPublicSuffixes) {
717
858
  }
718
859
  CookieJar.prototype.store = null;
719
860
  CookieJar.prototype.rejectPublicSuffixes = true;
861
+ var CAN_BE_SYNC = [];
720
862
 
721
- CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
863
+ CAN_BE_SYNC.push('setCookie');
864
+ CookieJar.prototype.setCookie = function(cookie, url, options, cb) {
722
865
  var err;
723
- var context = (url instanceof Object) ? url : urlParse(url);
866
+ var context = getCookieContext(url);
724
867
  if (options instanceof Function) {
725
868
  cb = options;
726
869
  options = {};
@@ -729,8 +872,9 @@ CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
729
872
  var host = canonicalDomain(context.hostname);
730
873
 
731
874
  // S5.3 step 1
732
- if (!(cookie instanceof Cookie))
875
+ if (!(cookie instanceof Cookie)) {
733
876
  cookie = Cookie.parse(cookie, options.strict === true);
877
+ }
734
878
  if (!cookie) {
735
879
  err = new Error("Cookie failed to parse");
736
880
  return cb(options.ignoreError ? null : err);
@@ -759,22 +903,21 @@ CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
759
903
  return cb(options.ignoreError ? null : err);
760
904
  }
761
905
 
762
- if (cookie.hostOnly == null) // don't reset if already set
906
+ if (cookie.hostOnly == null) { // don't reset if already set
763
907
  cookie.hostOnly = false;
908
+ }
764
909
 
765
910
  } else {
766
911
  cookie.hostOnly = true;
767
912
  cookie.domain = host;
768
913
  }
769
914
 
770
- // S5.3 step 7: "Otherwise, set the cookie's path to the default-path of the
771
- // request-uri"
772
- if (!cookie.path) {
915
+ //S5.2.4 If the attribute-value is empty or if the first character of the
916
+ //attribute-value is not %x2F ("/"):
917
+ //Let cookie-path be the default-path.
918
+ if (!cookie.path || cookie.path[0] !== '/') {
773
919
  cookie.path = defaultPath(context.pathname);
774
920
  cookie.pathIsDefault = true;
775
- } else {
776
- if (cookie.path.length > 1 && cookie.path.substr(-1) == '/')
777
- cookie.path = cookie.path.slice(0,-1);
778
921
  }
779
922
 
780
923
  // S5.3 step 8: NOOP; secure attribute
@@ -789,17 +932,22 @@ CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
789
932
  var store = this.store;
790
933
 
791
934
  if (!store.updateCookie) {
792
- store.updateCookie = function stubUpdateCookie(oldCookie, newCookie, cb) {
935
+ store.updateCookie = function(oldCookie, newCookie, cb) {
793
936
  this.putCookie(newCookie, cb);
794
937
  };
795
938
  }
796
939
 
797
- store.findCookie(cookie.domain, cookie.path, cookie.key, function(err,oldCookie) {
798
- if (err) return cb(err);
940
+ function withCookie(err, oldCookie) {
941
+ if (err) {
942
+ return cb(err);
943
+ }
799
944
 
800
945
  var next = function(err) {
801
- if (err) return cb(err);
802
- else cb(null, cookie);
946
+ if (err) {
947
+ return cb(err);
948
+ } else {
949
+ cb(null, cookie);
950
+ }
803
951
  };
804
952
 
805
953
  if (oldCookie) {
@@ -818,12 +966,15 @@ CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
818
966
  cookie.creation = cookie.lastAccessed = now;
819
967
  store.putCookie(cookie, next); // step 12
820
968
  }
821
- });
969
+ }
970
+
971
+ store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie);
822
972
  };
823
973
 
824
974
  // RFC6365 S5.4
825
- CookieJar.prototype.getCookies = function getCookies(url, options, cb) {
826
- var context = (url instanceof Object) ? url : urlParse(url);
975
+ CAN_BE_SYNC.push('getCookies');
976
+ CookieJar.prototype.getCookies = function(url, options, cb) {
977
+ var context = getCookieContext(url);
827
978
  if (options instanceof Function) {
828
979
  cb = options;
829
980
  options = {};
@@ -840,7 +991,9 @@ CookieJar.prototype.getCookies = function getCookies(url, options, cb) {
840
991
  }
841
992
 
842
993
  var http = options.http;
843
- if (http == null) http = true;
994
+ if (http == null) {
995
+ http = true;
996
+ }
844
997
 
845
998
  var now = options.now || Date.now();
846
999
  var expireCheck = options.expire !== false;
@@ -855,24 +1008,31 @@ CookieJar.prototype.getCookies = function getCookies(url, options, cb) {
855
1008
  // The cookie's host-only-flag is false and the canonicalized
856
1009
  // request-host domain-matches the cookie's domain."
857
1010
  if (c.hostOnly) {
858
- if (c.domain != host) return false;
1011
+ if (c.domain != host) {
1012
+ return false;
1013
+ }
859
1014
  } else {
860
- if (!domainMatch(host, c.domain, false)) return false;
1015
+ if (!domainMatch(host, c.domain, false)) {
1016
+ return false;
1017
+ }
861
1018
  }
862
1019
 
863
1020
  // "The request-uri's path path-matches the cookie's path."
864
- if (!allPaths && !pathMatch(path, c.path))
1021
+ if (!allPaths && !pathMatch(path, c.path)) {
865
1022
  return false;
1023
+ }
866
1024
 
867
1025
  // "If the cookie's secure-only-flag is true, then the request-uri's
868
1026
  // scheme must denote a "secure" protocol"
869
- if (c.secure && !secure)
1027
+ if (c.secure && !secure) {
870
1028
  return false;
1029
+ }
871
1030
 
872
1031
  // "If the cookie's http-only-flag is true, then exclude the cookie if the
873
1032
  // cookie-string is being generated for a "non-HTTP" API"
874
- if (c.httpOnly && !http)
1033
+ if (c.httpOnly && !http) {
875
1034
  return false;
1035
+ }
876
1036
 
877
1037
  // deferred from S5.3
878
1038
  // non-RFC: allow retention of expired cookies by choice
@@ -885,50 +1045,96 @@ CookieJar.prototype.getCookies = function getCookies(url, options, cb) {
885
1045
  }
886
1046
 
887
1047
  store.findCookies(host, allPaths ? null : path, function(err,cookies) {
888
- if (err) return cb(err);
1048
+ if (err) {
1049
+ return cb(err);
1050
+ }
889
1051
 
890
1052
  cookies = cookies.filter(matchingCookie);
891
1053
 
892
1054
  // sorting of S5.4 part 2
893
- if (options.sort !== false)
1055
+ if (options.sort !== false) {
894
1056
  cookies = cookies.sort(cookieCompare);
1057
+ }
895
1058
 
896
1059
  // S5.4 part 3
897
1060
  var now = new Date();
898
- cookies.forEach(function(c) { c.lastAccessed = now });
1061
+ cookies.forEach(function(c) {
1062
+ c.lastAccessed = now;
1063
+ });
899
1064
  // TODO persist lastAccessed
900
1065
 
901
1066
  cb(null,cookies);
902
1067
  });
903
1068
  };
904
1069
 
1070
+ CAN_BE_SYNC.push('getCookieString');
905
1071
  CookieJar.prototype.getCookieString = function(/*..., cb*/) {
906
1072
  var args = Array.prototype.slice.call(arguments,0);
907
1073
  var cb = args.pop();
908
1074
  var next = function(err,cookies) {
909
- if (err) cb(err);
910
- else cb(null, cookies.map(function(c){return c.cookieString()}).join('; '));
1075
+ if (err) {
1076
+ cb(err);
1077
+ } else {
1078
+ cb(null, cookies
1079
+ .sort(cookieCompare)
1080
+ .map(function(c){
1081
+ return c.cookieString();
1082
+ })
1083
+ .join('; '));
1084
+ }
911
1085
  };
912
1086
  args.push(next);
913
1087
  this.getCookies.apply(this,args);
914
1088
  };
915
1089
 
1090
+ CAN_BE_SYNC.push('getSetCookieStrings');
916
1091
  CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) {
917
1092
  var args = Array.prototype.slice.call(arguments,0);
918
1093
  var cb = args.pop();
919
1094
  var next = function(err,cookies) {
920
- if (err) cb(err);
921
- else cb(null, cookies.map(function(c){return c.toString()}));
1095
+ if (err) {
1096
+ cb(err);
1097
+ } else {
1098
+ cb(null, cookies.map(function(c){
1099
+ return c.toString();
1100
+ }));
1101
+ }
922
1102
  };
923
1103
  args.push(next);
924
1104
  this.getCookies.apply(this,args);
925
1105
  };
926
1106
 
1107
+ // Use a closure to provide a true imperative API for synchronous stores.
1108
+ function syncWrap(method) {
1109
+ return function() {
1110
+ if (!this.store.synchronous) {
1111
+ throw new Error('CookieJar store is not synchronous; use async API instead.');
1112
+ }
1113
+
1114
+ var args = Array.prototype.slice.call(arguments);
1115
+ var syncErr, syncResult;
1116
+ args.push(function syncCb(err, result) {
1117
+ syncErr = err;
1118
+ syncResult = result;
1119
+ });
1120
+ this[method].apply(this, args);
1121
+
1122
+ if (syncErr) {
1123
+ throw syncErr;
1124
+ }
1125
+ return syncResult;
1126
+ };
1127
+ }
927
1128
 
1129
+ // wrap all declared CAN_BE_SYNC methods in the sync wrapper
1130
+ CAN_BE_SYNC.forEach(function(method) {
1131
+ CookieJar.prototype[method+'Sync'] = syncWrap(method);
1132
+ });
928
1133
 
929
1134
  module.exports = {
930
1135
  CookieJar: CookieJar,
931
1136
  Cookie: Cookie,
1137
+ Store: Store,
932
1138
  parseDate: parseDate,
933
1139
  formatDate: formatDate,
934
1140
  parse: parse,