npm: Upgrade to 1.3.17
[platform/upstream/nodejs.git] / deps / npm / node_modules / request / node_modules / tough-cookie / lib / cookie.js
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:
9  *
10  * The above copyright notice and this permission notice shall be included in
11  * all copies or substantial portions of the Software.
12  *
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.
20  */
21
22 /*jshint regexp:false */
23 var net = require('net');
24 var urlParse = require('url').parse;
25 var pubsuffix = require('./pubsuffix');
26
27 var punycode;
28 try {
29   punycode = require('punycode');
30 } catch(e) {
31   console.warn("cookie: can't load punycode; won't use punycode for domain normalization");
32 }
33
34 var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
35
36 // From RFC2616 S2.2:
37 var TOKEN = /[\x21\x23-\x26\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/;
38
39 // From RFC6265 S4.1.1
40 // note that it excludes \x3B ";"
41 var COOKIE_OCTET  = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/;
42 var COOKIE_OCTETS = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/;
43
44 // The name/key cannot be empty but the value can (S5.2):
45 var COOKIE_PAIR_STRICT = new RegExp('^('+TOKEN.source+'+)=("?)('+COOKIE_OCTET.source+'*)\\2');
46 var COOKIE_PAIR = /^([^=\s]+)\s*=\s*("?)\s*(.*)\s*\2/;
47
48 // RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"'
49 // Note ';' is \x3B
50 var NON_CTL_SEMICOLON = /[\x20-\x3A\x3C-\x7E]+/;
51 var EXTENSION_AV = NON_CTL_SEMICOLON;
52 var PATH_VALUE = NON_CTL_SEMICOLON;
53
54 // Used for checking whether or not there is a trailing semi-colon
55 var TRAILING_SEMICOLON = /;+$/;
56
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])$/;
61
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 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
74 };
75 var NUM_TO_MONTH = [
76   'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'
77 ];
78 var NUM_TO_DAY = [
79   'Sun','Mon','Tue','Wed','Thu','Fri','Sat'
80 ];
81
82 var YEAR = /^([1-9][0-9]{1,3})$/; // 2 to 4 digits (will check range when parsing)
83
84 var MAX_TIME = 2147483647000; // 31-bit max
85 var MAX_DATE = new Date(CookieJar.MAX_TIME); // 31-bit max
86 var MIN_TIME = 0; // 31-bit min
87 var MIN_DATE = new Date(CookieJar.MIN_TIME); // 31-bit min
88
89
90 // RFC6265 S5.1.1 date parser:
91 function parseDate(str,strict) {
92   if (!str) return;
93   var found_time, found_dom, found_month, found_year;
94
95   /* RFC6265 S5.1.1:
96    * 2. Process each date-token sequentially in the order the date-tokens
97    * appear in the cookie-date
98    */
99   var tokens = str.split(DATE_DELIM);
100   if (!tokens) return;
101
102   var date = new Date();
103   date.setMilliseconds(0);
104
105   for (var i=0; i<tokens.length; i++) {
106     var token = tokens[i].trim();
107     if (!token.length) continue;
108
109     var result;
110
111     /* 2.1. If the found-time flag is not set and the token matches the time
112      * production, set the found-time flag and set the hour- value,
113      * minute-value, and second-value to the numbers denoted by the digits in
114      * the date-token, respectively.  Skip the remaining sub-steps and continue
115      * to the next date-token.
116      */
117     if (!found_time) {
118       result = (strict ? STRICT_TIME : TIME).exec(token);
119       if (result) {
120         found_time = true;
121         date.setUTCHours(result[1]);
122         date.setUTCMinutes(result[2]);
123         date.setUTCSeconds(result[3]);
124         continue;
125       }
126     }
127
128     /* 2.2. If the found-day-of-month flag is not set and the date-token matches
129      * the day-of-month production, set the found-day-of- month flag and set
130      * the day-of-month-value to the number denoted by the date-token.  Skip
131      * the remaining sub-steps and continue to the next date-token.
132      */
133     if (!found_dom) {
134       result = DAY_OF_MONTH.exec(token);
135       if (result) {
136         found_dom = true;
137         date.setUTCDate(result[1]);
138         continue;
139       }
140     }
141
142     /* 2.3. If the found-month flag is not set and the date-token matches the
143      * month production, set the found-month flag and set the month-value to
144      * the month denoted by the date-token.  Skip the remaining sub-steps and
145      * continue to the next date-token.
146      */
147     if (!found_month) {
148       result = MONTH.exec(token);
149       if (result) {
150         found_month = true;
151         date.setUTCMonth(MONTH_TO_NUM[result[1].toLowerCase()]);
152         continue;
153       }
154     }
155
156     /* 2.4. If the found-year flag is not set and the date-token matches the year
157      * production, set the found-year flag and set the year-value to the number
158      * denoted by the date-token.  Skip the remaining sub-steps and continue to
159      * the next date-token.
160      */
161     if (!found_year) {
162       result = YEAR.exec(token);
163       if (result) {
164         var year = result[0];
165         /* From S5.1.1:
166          * 3.  If the year-value is greater than or equal to 70 and less
167          * than or equal to 99, increment the year-value by 1900.
168          * 4.  If the year-value is greater than or equal to 0 and less
169          * than or equal to 69, increment the year-value by 2000.
170          */
171         if (70 <= year && year <= 99)
172           year += 1900;
173         else if (0 <= year && year <= 69)
174           year += 2000;
175
176         if (year < 1601)
177           return; // 5. ... the year-value is less than 1601
178
179         found_year = true;
180         date.setUTCFullYear(year);
181         continue;
182       }
183     }
184   }
185
186   if (!(found_time && found_dom && found_month && found_year)) {
187     return; // 5. ... at least one of the found-day-of-month, found-month, found-
188             // year, or found-time flags is not set,
189   }
190
191   return date;
192 }
193
194 function formatDate(date) {
195   var d = date.getUTCDate(); d = d >= 10 ? d : '0'+d;
196   var h = date.getUTCHours(); h = h >= 10 ? h : '0'+h;
197   var m = date.getUTCMinutes(); m = m >= 10 ? m : '0'+m;
198   var s = date.getUTCSeconds(); s = s >= 10 ? s : '0'+s;
199   return NUM_TO_DAY[date.getUTCDay()] + ', ' +
200     d+' '+ NUM_TO_MONTH[date.getUTCMonth()] +' '+ date.getUTCFullYear() +' '+
201     h+':'+m+':'+s+' GMT';
202 }
203
204 // S5.1.2 Canonicalized Host Names
205 function canonicalDomain(str) {
206   if (str == null) return null;
207   str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading .
208
209   // convert to IDN if any non-ASCII characters
210   if (punycode && /[^\u0001-\u007f]/.test(str))
211     str = punycode.toASCII(str);
212
213   return str.toLowerCase();
214 }
215
216 // S5.1.3 Domain Matching
217 function domainMatch(str, domStr, canonicalize) {
218   if (str == null || domStr == null) return null;
219   if (canonicalize !== false) {
220     str = canonicalDomain(str);
221     domStr = canonicalDomain(domStr);
222   }
223
224   /*
225    * "The domain string and the string are identical. (Note that both the
226    * domain string and the string will have been canonicalized to lower case at
227    * this point)"
228    */
229   if (str == domStr) return true;
230
231   /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */
232
233   /* "* The string is a host name (i.e., not an IP address)." */
234   if (net.isIP(str)) return false;
235
236   /* "* The domain string is a suffix of the string" */
237   var idx = str.indexOf(domStr);
238   if (idx <= 0) return false; // it's a non-match (-1) or prefix (0)
239
240   // e.g "a.b.c".indexOf("b.c") === 2
241   // 5 === 3+2
242   if (str.length !== domStr.length + idx) // it's not a suffix
243     return false;
244
245   /* "* The last character of the string that is not included in the domain
246   * string is a %x2E (".") character." */
247   if (str.substr(idx-1,1) !== '.') return false;
248   return true;
249 }
250
251
252 // RFC6265 S5.1.4 Paths and Path-Match
253
254 /*
255  * "The user agent MUST use an algorithm equivalent to the following algorithm
256  * to compute the default-path of a cookie:"
257  *
258  * Assumption: the path (and not query part or absolute uri) is passed in.
259  */
260 function defaultPath(path) {
261   // "2. If the uri-path is empty or if the first character of the uri-path is not
262   // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps.
263   if (!path || path.substr(0,1) !== "/") return "/";
264
265   // "3. If the uri-path contains no more than one %x2F ("/") character, output
266   // %x2F ("/") and skip the remaining step."
267   if (path === "/") return path;
268
269   var rightSlash = path.lastIndexOf("/");
270   if (rightSlash === 0) return "/";
271
272   // "4. Output the characters of the uri-path from the first character up to,
273   // but not including, the right-most %x2F ("/")."
274   return path.slice(0, rightSlash);
275 }
276
277 /*
278  * "A request-path path-matches a given cookie-path if at least one of the
279  * following conditions holds:"
280  */
281 function pathMatch(reqPath,cookiePath) {
282   // "o  The cookie-path and the request-path are identical."
283   if (cookiePath === reqPath)
284     return true;
285
286   var idx = reqPath.indexOf(cookiePath);
287   if (idx === 0) {
288     // "o  The cookie-path is a prefix of the request-path, and the last
289     // character of the cookie-path is %x2F ("/")."
290     if (cookiePath.substr(-1) === "/")
291       return true;
292
293     // " o  The cookie-path is a prefix of the request-path, and the first
294     // character of the request-path that is not included in the cookie- path
295     // is a %x2F ("/") character."
296     if (reqPath.substr(cookiePath.length,1) === "/")
297       return true;
298   }
299
300   return false;
301 }
302
303 function parse(str, strict) {
304   str = str.trim();
305
306   // S4.1.1 Trailing semi-colons are not part of the specification.
307   // If we are not in strict mode we remove the trailing semi-colons.
308   var semiColonCheck = TRAILING_SEMICOLON.exec(str);
309   if (semiColonCheck) {
310     if (strict) return;
311     str = str.slice(0, semiColonCheck.index);
312   }
313
314   // We use a regex to parse the "name-value-pair" part of S5.2
315   var firstSemi = str.indexOf(';'); // S5.2 step 1
316   var pairRx = strict ? COOKIE_PAIR_STRICT : COOKIE_PAIR;
317   var result = pairRx.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
318
319   // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
320   // constraints as well as trimming any whitespace.
321   if (!result) return;
322
323   var c = new Cookie();
324   c.key = result[1]; // the regexp should trim() already
325   c.value = result[3]; // [2] is quotes or empty-string
326
327   if (firstSemi === -1) return c;
328
329   // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string
330   // (including the %x3B (";") in question)." plus later on in the same section
331   // "discard the first ";" and trim".
332   var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/,'').trim();
333
334   // "If the unparsed-attributes string is empty, skip the rest of these
335   // steps."
336   if (unparsed.length === 0) return c;
337
338   /*
339    * S5.2 says that when looping over the items "[p]rocess the attribute-name
340    * and attribute-value according to the requirements in the following
341    * subsections" for every item.  Plus, for many of the individual attributes
342    * in S5.3 it says to use the "attribute-value of the last attribute in the
343    * cookie-attribute-list".  Therefore, in this implementation, we overwrite
344    * the previous value.
345    */
346   var cookie_avs = unparsed.split(/\s*;\s*/);
347   while (cookie_avs.length) {
348     var av = cookie_avs.shift();
349
350     if (strict && !EXTENSION_AV.test(av)) return;
351
352     var av_sep = av.indexOf('=');
353     var av_key, av_value;
354     if (av_sep === -1) {
355       av_key = av;
356       av_value = null;
357     } else {
358       av_key = av.substr(0,av_sep);
359       av_value = av.substr(av_sep+1);
360     }
361
362     av_key = av_key.trim().toLowerCase();
363     if (av_value) av_value = av_value.trim();
364
365     switch(av_key) {
366     case 'expires': // S5.2.1
367       if (!av_value) { if(strict){return}else{break;} }
368       var exp = parseDate(av_value,strict);
369       // "If the attribute-value failed to parse as a cookie date, ignore the
370       // cookie-av."
371       if (exp == null) { if(strict){return}else{break;} }
372       c.expires = exp;
373       // over and underflow not realistically a concern: V8's getTime() seems to
374       // store something larger than a 32-bit time_t (even with 32-bit node)
375       break;
376
377     case 'max-age': // S5.2.2
378       if (!av_value) { if(strict){return}else{break;} }
379       // "If the first character of the attribute-value is not a DIGIT or a "-"
380       // character ...[or]... If the remainder of attribute-value contains a
381       // non-DIGIT character, ignore the cookie-av."
382       if (!/^-?[0-9]+$/.test(av_value)) { if(strict){return}else{break;} }
383       var delta = parseInt(av_value,10);
384       if (strict && delta <= 0) return; // S4.1.1
385       // "If delta-seconds is less than or equal to zero (0), let expiry-time
386       // be the earliest representable date and time."
387       c.setMaxAge(delta);
388       break;
389
390     case 'domain': // S5.2.3
391       // "If the attribute-value is empty, the behavior is undefined.  However,
392       // the user agent SHOULD ignore the cookie-av entirely."
393       if (!av_value) { if(strict){return}else{break;} }
394       // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
395       // (".") character."
396       var domain = av_value.trim().replace(/^\./,'');
397       if (!domain) { if(strict){return}else{break;} } // see "is empty" above
398       // "Convert the cookie-domain to lower case."
399       c.domain = domain.toLowerCase();
400       break;
401
402     case 'path': // S5.2.4
403       /*
404        * "If the attribute-value is empty or if the first character of the
405        * attribute-value is not %x2F ("/"):
406        *   Let cookie-path be the default-path.
407        * Otherwise:
408        *   Let cookie-path be the attribute-value."
409        *
410        * We'll represent the default-path as null since it depends on the
411        * context of the parsing.
412        */
413       if (!av_value || av_value.substr(0,1) != "/") { if(strict){return}else{break;} }
414       c.path = av_value;
415       break;
416
417     case 'secure': // S5.2.5
418       /*
419        * "If the attribute-name case-insensitively matches the string "Secure",
420        * the user agent MUST append an attribute to the cookie-attribute-list
421        * with an attribute-name of Secure and an empty attribute-value."
422        */
423       if (av_value != null) { if(strict){return} }
424       c.secure = true;
425       break;
426
427     case 'httponly': // S5.2.6 -- effectively the same as 'secure'
428       if (av_value != null) { if(strict){return} }
429       c.httpOnly = true;
430       break;
431
432     default:
433       c.extensions = c.extensions || [];
434       c.extensions.push(av);
435       break;
436     }
437   }
438
439   // ensure a default date for sorting:
440   c.creation = new Date();
441   return c;
442 }
443
444 function fromJSON(str) {
445   if (!str) return null;
446
447   var obj;
448   try {
449     obj = JSON.parse(str);
450   } catch (e) {
451     return null;
452   }
453
454   var c = new Cookie();
455   for (var i=0; i<numCookieProperties; i++) {
456     var prop = cookieProperties[i];
457     if (obj[prop] == null) continue;
458     if (prop === 'expires' ||
459         prop === 'creation' ||
460         prop === 'lastAccessed')
461     {
462       c[prop] = obj[prop] == "Infinity" ? "Infinity" : new Date(obj[prop]);
463     } else {
464       c[prop] = obj[prop];
465     }
466   }
467
468
469   // ensure a default date for sorting:
470   c.creation = c.creation || new Date();
471
472   return c;
473 }
474
475 /* Section 5.4 part 2:
476  * "*  Cookies with longer paths are listed before cookies with
477  *     shorter paths.
478  *
479  *  *  Among cookies that have equal-length path fields, cookies with
480  *     earlier creation-times are listed before cookies with later
481  *     creation-times."
482  */
483
484 function cookieCompare(a,b) {
485   // descending for length: b CMP a
486   var deltaLen = (b.path ? b.path.length : 0) - (a.path ? a.path.length : 0);
487   if (deltaLen !== 0) return deltaLen;
488   // ascending for time: a CMP b
489   return (a.creation ? a.creation.getTime() : MAX_TIME) -
490          (b.creation ? b.creation.getTime() : MAX_TIME);
491 }
492
493 // Gives the permutation of all possible domainMatch()es of a given domain. The
494 // array is in shortest-to-longest order.  Handy for indexing.
495 function permuteDomain(domain) {
496   var pubSuf = pubsuffix.getPublicSuffix(domain);
497   if (!pubSuf) return null;
498   if (pubSuf == domain) return [domain];
499
500   var prefix = domain.slice(0,-(pubSuf.length+1)); // ".example.com"
501   var parts = prefix.split('.').reverse();
502   var cur = pubSuf;
503   var permutations = [cur];
504   while (parts.length) {
505     cur = parts.shift()+'.'+cur;
506     permutations.push(cur);
507   }
508   return permutations;
509 }
510
511 // Gives the permutation of all possible pathMatch()es of a given path. The
512 // array is in longest-to-shortest order.  Handy for indexing.
513 function permutePath(path) {
514   var origPath = path;
515   if (path === '/') return ['/'];
516   if (path.lastIndexOf('/') === path.length-1)
517     path = path.substr(0,path.length-1);
518   var permutations = [path];
519   while (path.length > 1) {
520     var lindex = path.lastIndexOf('/');
521     if (lindex === 0) break;
522     path = path.substr(0,lindex);
523     permutations.push(path);
524   }
525   permutations.push('/');
526   return permutations;
527 }
528
529
530 function Cookie (opts) {
531   if (typeof opts !== "object") return;
532   Object.keys(opts).forEach(function (key) {
533     if (Cookie.prototype.hasOwnProperty(key)) {
534       this[key] = opts[key] || Cookie.prototype[key];
535     }
536   }.bind(this));
537 }
538
539 Cookie.parse = parse;
540 Cookie.fromJSON = fromJSON;
541
542 Cookie.prototype.key = "";
543 Cookie.prototype.value = "";
544
545 // the order in which the RFC has them:
546 Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity
547 Cookie.prototype.maxAge = null; // takes precedence over expires for TTL
548 Cookie.prototype.domain = null;
549 Cookie.prototype.path = null;
550 Cookie.prototype.secure = false;
551 Cookie.prototype.httpOnly = false;
552 Cookie.prototype.extensions = null;
553
554 // set by the CookieJar:
555 Cookie.prototype.hostOnly = null; // boolean when set
556 Cookie.prototype.pathIsDefault = null; // boolean when set
557 Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse
558 Cookie.prototype.lastAccessed = null; // Date when set
559
560 var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function(p) {
561   if (p instanceof Function) return;
562   return p;
563 }));
564 var numCookieProperties = cookieProperties.length;
565
566 Cookie.prototype.inspect = function inspect() {
567   var now = Date.now();
568   return 'Cookie="'+this.toString() +
569     '; hostOnly='+(this.hostOnly != null ? this.hostOnly : '?') +
570     '; aAge='+(this.lastAccessed ? (now-this.lastAccessed.getTime())+'ms' : '?') +
571     '; cAge='+(this.creation ? (now-this.creation.getTime())+'ms' : '?') +
572     '"';
573 };
574
575 Cookie.prototype.validate = function validate() {
576   if (!COOKIE_OCTETS.test(this.value))
577     return false;
578   if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires,true))
579     return false;
580   if (this.maxAge != null && this.maxAge <= 0)
581     return false; // "Max-Age=" non-zero-digit *DIGIT
582   if (this.path != null && !PATH_VALUE.test(this.path))
583     return false;
584
585   var cdomain = this.cdomain();
586   if (cdomain) {
587     if (cdomain.match(/\.$/))
588       return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this
589     var suffix = pubsuffix.getPublicSuffix(cdomain);
590     if (suffix == null) // it's a public suffix
591       return false;
592   }
593   return true;
594 };
595
596 Cookie.prototype.setExpires = function setExpires(exp) {
597   if (exp instanceof Date) this.expires = exp;
598   else this.expires = parseDate(exp) || "Infinity";
599 };
600
601 Cookie.prototype.setMaxAge = function setMaxAge(age) {
602   if (age === Infinity || age === -Infinity)
603     this.maxAge = age.toString(); // so JSON.stringify() works
604   else
605     this.maxAge = age;
606 };
607
608 // gives Cookie header format
609 Cookie.prototype.cookieString = function cookieString() {
610   var val = this.value;
611   if (val == null) val = '';
612   if (!val.length || COOKIE_OCTETS.test(val))
613     return this.key+'='+val;
614   else
615     return this.key+'="'+val+'"';
616 };
617
618 // gives Set-Cookie header format
619 Cookie.prototype.toString = function toString() {
620   var str = this.cookieString();
621
622   if (this.expires != Infinity) {
623     if (this.expires instanceof Date)
624       str += '; Expires='+formatDate(this.expires);
625     else
626       str += '; Expires='+this.expires;
627   }
628
629   if (this.maxAge != null && this.maxAge != Infinity)
630     str += '; Max-Age='+this.maxAge;
631
632   if (this.domain && !this.hostOnly)
633     str += '; Domain='+this.domain;
634   if (this.path)
635     str += '; Path='+this.path;
636
637   if (this.secure) str += '; Secure';
638   if (this.httpOnly) str += '; HttpOnly';
639   if (this.extensions) {
640     this.extensions.forEach(function(ext) {
641       str += '; '+ext;
642     });
643   }
644
645   return str;
646 };
647
648 // TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
649 // elsewhere)
650 // S5.3 says to give the "latest representable date" for which we use Infinity
651 // For "expired" we use 0
652 Cookie.prototype.TTL = function TTL(now) {
653   /* RFC6265 S4.1.2.2 If a cookie has both the Max-Age and the Expires
654    * attribute, the Max-Age attribute has precedence and controls the
655    * expiration date of the cookie.
656    * (Concurs with S5.3 step 3)
657    */
658   if (this.maxAge != null) {
659     return this.maxAge<=0 ? 0 : this.maxAge*1000;
660   }
661
662   var expires = this.expires;
663   if (expires != Infinity) {
664     if (!(expires instanceof Date)) {
665       expires = parseDate(expires) || Infinity;
666     }
667
668     if (expires == Infinity)
669       return Infinity;
670
671     return expires.getTime() - (now || Date.now());
672   }
673
674   return Infinity;
675 };
676
677 // expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
678 // elsewhere)
679 Cookie.prototype.expiryTime = function expiryTime(now) {
680   if (this.maxAge != null) {
681     var relativeTo = this.creation || now || new Date();
682     var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000;
683     return relativeTo.getTime() + age;
684   }
685
686   if (this.expires == Infinity) return Infinity;
687   return this.expires.getTime();
688 };
689
690 // expiryDate() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
691 // elsewhere), except it returns a Date
692 Cookie.prototype.expiryDate = function expiryDate(now) {
693   var millisec = this.expiryTime(now);
694   if (millisec == Infinity) return new Date(MAX_TIME);
695   else if (millisec == -Infinity) return new Date(MIN_TIME);
696   else return new Date(millisec);
697 };
698
699 // This replaces the "persistent-flag" parts of S5.3 step 3
700 Cookie.prototype.isPersistent = function isPersistent() {
701   return (this.maxAge != null || this.expires != Infinity);
702 };
703
704 // Mostly S5.1.2 and S5.2.3:
705 Cookie.prototype.cdomain =
706 Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() {
707   if (this.domain == null) return null;
708   return canonicalDomain(this.domain);
709 };
710
711
712 var memstore;
713 function CookieJar(store, rejectPublicSuffixes) {
714   if (rejectPublicSuffixes != null) this.rejectPublicSuffixes = rejectPublicSuffixes;
715   if (!store) {
716     memstore = memstore || require('./memstore');
717     store = new memstore.MemoryCookieStore();
718   }
719   this.store = store;
720 }
721 CookieJar.prototype.store = null;
722 CookieJar.prototype.rejectPublicSuffixes = true;
723
724 CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
725   var err;
726   var context = (url instanceof Object) ? url : urlParse(url);
727   if (options instanceof Function) {
728     cb = options;
729     options = {};
730   }
731
732   var host = canonicalDomain(context.hostname);
733
734   // S5.3 step 1
735   if (!(cookie instanceof Cookie))
736     cookie = Cookie.parse(cookie, options.strict === true);
737   if (!cookie) {
738     err = new Error("Cookie failed to parse");
739     return cb(options.ignoreError ? null : err);
740   }
741
742   // S5.3 step 2
743   var now = options.now || new Date(); // will assign later to save effort in the face of errors
744
745   // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie()
746
747   // S5.3 step 4: NOOP; domain is null by default
748
749   // S5.3 step 5: public suffixes
750   if (this.rejectPublicSuffixes && cookie.domain) {
751     var suffix = pubsuffix.getPublicSuffix(cookie.cdomain());
752     if (suffix == null) { // e.g. "com"
753       err = new Error("Cookie has domain set to a public suffix");
754       return cb(options.ignoreError ? null : err);
755     }
756   }
757
758   // S5.3 step 6:
759   if (cookie.domain) {
760     if (!domainMatch(host, cookie.cdomain(), false)) {
761       err = new Error("Cookie not in this host's domain. Cookie:"+cookie.cdomain()+" Request:"+host);
762       return cb(options.ignoreError ? null : err);
763     }
764
765     if (cookie.hostOnly == null) // don't reset if already set
766       cookie.hostOnly = false;
767
768   } else {
769     cookie.hostOnly = true;
770     cookie.domain = host;
771   }
772
773   // S5.3 step 7: "Otherwise, set the cookie's path to the default-path of the
774   // request-uri"
775   if (!cookie.path) {
776     cookie.path = defaultPath(context.pathname);
777     cookie.pathIsDefault = true;
778   } else {
779     if (cookie.path.length > 1 && cookie.path.substr(-1) == '/')
780       cookie.path = cookie.path.slice(0,-1);
781   }
782
783   // S5.3 step 8: NOOP; secure attribute
784   // S5.3 step 9: NOOP; httpOnly attribute
785
786   // S5.3 step 10
787   if (options.http === false && cookie.httpOnly) {
788     err = new Error("Cookie is HttpOnly and this isn't an HTTP API");
789     return cb(options.ignoreError ? null : err);
790   }
791
792   var store = this.store;
793
794   if (!store.updateCookie) {
795     store.updateCookie = function stubUpdateCookie(oldCookie, newCookie, cb) {
796       this.putCookie(newCookie, cb);
797     };
798   }
799
800   store.findCookie(cookie.domain, cookie.path, cookie.key, function(err,oldCookie) {
801     if (err) return cb(err);
802
803     var next = function(err) {
804       if (err) return cb(err);
805       else cb(null, cookie);
806     };
807
808     if (oldCookie) {
809       // S5.3 step 11 - "If the cookie store contains a cookie with the same name,
810       // domain, and path as the newly created cookie:"
811       if (options.http === false && oldCookie.httpOnly) { // step 11.2
812         err = new Error("old Cookie is HttpOnly and this isn't an HTTP API");
813         return cb(options.ignoreError ? null : err);
814       }
815       cookie.creation = oldCookie.creation; // step 11.3
816       cookie.lastAccessed = now;
817       // Step 11.4 (delete cookie) is implied by just setting the new one:
818       store.updateCookie(oldCookie, cookie, next); // step 12
819
820     } else {
821       cookie.creation = cookie.lastAccessed = now;
822       store.putCookie(cookie, next); // step 12
823     }
824   });
825 };
826
827 // RFC6365 S5.4
828 CookieJar.prototype.getCookies = function getCookies(url, options, cb) {
829   var context = (url instanceof Object) ? url : urlParse(url);
830   if (options instanceof Function) {
831     cb = options;
832     options = {};
833   }
834
835   var host = canonicalDomain(context.hostname);
836   var path = context.pathname || '/';
837
838   var secure = options.secure;
839   if (secure == null && context.protocol &&
840       (context.protocol == 'https:' || context.protocol == 'wss:'))
841   {
842     secure = true;
843   }
844
845   var http = options.http;
846   if (http == null) http = true;
847
848   var now = options.now || Date.now();
849   var expireCheck = options.expire !== false;
850   var allPaths = !!options.allPaths;
851   var store = this.store;
852
853   function matchingCookie(c) {
854     // "Either:
855     //   The cookie's host-only-flag is true and the canonicalized
856     //   request-host is identical to the cookie's domain.
857     // Or:
858     //   The cookie's host-only-flag is false and the canonicalized
859     //   request-host domain-matches the cookie's domain."
860     if (c.hostOnly) {
861       if (c.domain != host) return false;
862     } else {
863       if (!domainMatch(host, c.domain, false)) return false;
864     }
865
866     // "The request-uri's path path-matches the cookie's path."
867     if (!allPaths && !pathMatch(path, c.path))
868       return false;
869
870     // "If the cookie's secure-only-flag is true, then the request-uri's
871     // scheme must denote a "secure" protocol"
872     if (c.secure && !secure)
873       return false;
874
875     // "If the cookie's http-only-flag is true, then exclude the cookie if the
876     // cookie-string is being generated for a "non-HTTP" API"
877     if (c.httpOnly && !http)
878       return false;
879
880     // deferred from S5.3
881     // non-RFC: allow retention of expired cookies by choice
882     if (expireCheck && c.expiryTime() <= now) {
883       store.removeCookie(c.domain, c.path, c.key, function(){}); // result ignored
884       return false;
885     }
886
887     return true;
888   }
889
890   store.findCookies(host, allPaths ? null : path, function(err,cookies) {
891     if (err) return cb(err);
892
893     cookies = cookies.filter(matchingCookie);
894
895     // sorting of S5.4 part 2
896     if (options.sort !== false)
897       cookies = cookies.sort(cookieCompare);
898
899     // S5.4 part 3
900     var now = new Date();
901     cookies.forEach(function(c) { c.lastAccessed = now });
902     // TODO persist lastAccessed
903
904     cb(null,cookies);
905   });
906 };
907
908 CookieJar.prototype.getCookieString = function(/*..., cb*/) {
909   var args = Array.prototype.slice.call(arguments,0);
910   var cb = args.pop();
911   var next = function(err,cookies) {
912     if (err) cb(err);
913     else cb(null, cookies.map(function(c){return c.cookieString()}).join('; '));
914   };
915   args.push(next);
916   this.getCookies.apply(this,args);
917 };
918
919 CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) {
920   var args = Array.prototype.slice.call(arguments,0);
921   var cb = args.pop();
922   var next = function(err,cookies) {
923     if (err) cb(err);
924     else cb(null, cookies.map(function(c){return c.toString()}));
925   };
926   args.push(next);
927   this.getCookies.apply(this,args);
928 };
929
930
931
932 module.exports = {
933   CookieJar: CookieJar,
934   Cookie: Cookie,
935   parseDate: parseDate,
936   formatDate: formatDate,
937   parse: parse,
938   fromJSON: fromJSON,
939   domainMatch: domainMatch,
940   defaultPath: defaultPath,
941   pathMatch: pathMatch,
942   getPublicSuffix: pubsuffix.getPublicSuffix,
943   cookieCompare: cookieCompare,
944   permuteDomain: permuteDomain,
945   permutePath: permutePath,
946   canonicalDomain: canonicalDomain,
947 };