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:
10 * The above copyright notice and this permission notice shall be included in
11 * all copies or substantial portions of the Software.
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
22 /*jshint regexp:false */
23 var net = require('net');
24 var urlParse = require('url').parse;
25 var pubsuffix = require('./pubsuffix');
29 punycode = require('punycode');
31 console.warn("cookie: can't load punycode; won't use punycode for domain normalization");
34 var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
37 var TOKEN = /[\x21\x23-\x26\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/;
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]+$/;
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/;
48 // RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"'
50 var NON_CTL_SEMICOLON = /[\x20-\x3A\x3C-\x7E]+/;
51 var EXTENSION_AV = NON_CTL_SEMICOLON;
52 var PATH_VALUE = NON_CTL_SEMICOLON;
54 // Used for checking whether or not there is a trailing semi-colon
55 var TRAILING_SEMICOLON = /;+$/;
58 * [fail if] the day-of-month-value is less than 1 or greater than 31
60 var DAY_OF_MONTH = /^(0?[1-9]|[12][0-9]|3[01])$/;
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.
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])$/;
71 var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/i;
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
76 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'
79 'Sun','Mon','Tue','Wed','Thu','Fri','Sat'
82 var YEAR = /^([1-9][0-9]{1,3})$/; // 2 to 4 digits (will check range when parsing)
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
90 // RFC6265 S5.1.1 date parser:
91 function parseDate(str,strict) {
93 var found_time, found_dom, found_month, found_year;
96 * 2. Process each date-token sequentially in the order the date-tokens
97 * appear in the cookie-date
99 var tokens = str.split(DATE_DELIM);
102 var date = new Date();
103 date.setMilliseconds(0);
105 for (var i=0; i<tokens.length; i++) {
106 var token = tokens[i].trim();
107 if (!token.length) continue;
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.
118 result = (strict ? STRICT_TIME : TIME).exec(token);
121 date.setUTCHours(result[1]);
122 date.setUTCMinutes(result[2]);
123 date.setUTCSeconds(result[3]);
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.
134 result = DAY_OF_MONTH.exec(token);
137 date.setUTCDate(result[1]);
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.
148 result = MONTH.exec(token);
151 date.setUTCMonth(MONTH_TO_NUM[result[1].toLowerCase()]);
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.
162 result = YEAR.exec(token);
164 var year = result[0];
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.
171 if (70 <= year && year <= 99)
173 else if (0 <= year && year <= 69)
177 return; // 5. ... the year-value is less than 1601
180 date.setUTCFullYear(year);
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,
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';
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 .
209 // convert to IDN if any non-ASCII characters
210 if (punycode && /[^\u0001-\u007f]/.test(str))
211 str = punycode.toASCII(str);
213 return str.toLowerCase();
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);
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
229 if (str == domStr) return true;
231 /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */
233 /* "* The string is a host name (i.e., not an IP address)." */
234 if (net.isIP(str)) return false;
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)
240 // e.g "a.b.c".indexOf("b.c") === 2
242 if (str.length !== domStr.length + idx) // it's not a suffix
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;
252 // RFC6265 S5.1.4 Paths and Path-Match
255 * "The user agent MUST use an algorithm equivalent to the following algorithm
256 * to compute the default-path of a cookie:"
258 * Assumption: the path (and not query part or absolute uri) is passed in.
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 "/";
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;
269 var rightSlash = path.lastIndexOf("/");
270 if (rightSlash === 0) return "/";
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);
278 * "A request-path path-matches a given cookie-path if at least one of the
279 * following conditions holds:"
281 function pathMatch(reqPath,cookiePath) {
282 // "o The cookie-path and the request-path are identical."
283 if (cookiePath === reqPath)
286 var idx = reqPath.indexOf(cookiePath);
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) === "/")
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) === "/")
303 function parse(str, strict) {
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) {
311 str = str.slice(0, semiColonCheck.index);
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));
319 // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
320 // constraints as well as trimming any whitespace.
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
327 if (firstSemi === -1) return c;
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();
334 // "If the unparsed-attributes string is empty, skip the rest of these
336 if (unparsed.length === 0) return c;
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.
346 var cookie_avs = unparsed.split(/\s*;\s*/);
347 while (cookie_avs.length) {
348 var av = cookie_avs.shift();
350 if (strict && !EXTENSION_AV.test(av)) return;
352 var av_sep = av.indexOf('=');
353 var av_key, av_value;
358 av_key = av.substr(0,av_sep);
359 av_value = av.substr(av_sep+1);
362 av_key = av_key.trim().toLowerCase();
363 if (av_value) av_value = av_value.trim();
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
371 if (exp == null) { if(strict){return}else{break;} }
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)
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."
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
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();
402 case 'path': // S5.2.4
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.
408 * Let cookie-path be the attribute-value."
410 * We'll represent the default-path as null since it depends on the
411 * context of the parsing.
413 if (!av_value || av_value.substr(0,1) != "/") { if(strict){return}else{break;} }
417 case 'secure': // S5.2.5
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."
423 if (av_value != null) { if(strict){return} }
427 case 'httponly': // S5.2.6 -- effectively the same as 'secure'
428 if (av_value != null) { if(strict){return} }
433 c.extensions = c.extensions || [];
434 c.extensions.push(av);
439 // ensure a default date for sorting:
440 c.creation = new Date();
444 function fromJSON(str) {
445 if (!str) return null;
449 obj = JSON.parse(str);
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')
462 c[prop] = obj[prop] == "Infinity" ? "Infinity" : new Date(obj[prop]);
469 // ensure a default date for sorting:
470 c.creation = c.creation || new Date();
475 /* Section 5.4 part 2:
476 * "* Cookies with longer paths are listed before cookies with
479 * * Among cookies that have equal-length path fields, cookies with
480 * earlier creation-times are listed before cookies with later
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);
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];
500 var prefix = domain.slice(0,-(pubSuf.length+1)); // ".example.com"
501 var parts = prefix.split('.').reverse();
503 var permutations = [cur];
504 while (parts.length) {
505 cur = parts.shift()+'.'+cur;
506 permutations.push(cur);
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) {
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);
525 permutations.push('/');
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];
539 Cookie.parse = parse;
540 Cookie.fromJSON = fromJSON;
542 Cookie.prototype.key = "";
543 Cookie.prototype.value = "";
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;
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
560 var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function(p) {
561 if (p instanceof Function) return;
564 var numCookieProperties = cookieProperties.length;
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' : '?') +
575 Cookie.prototype.validate = function validate() {
576 if (!COOKIE_OCTETS.test(this.value))
578 if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires,true))
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))
585 var cdomain = this.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
596 Cookie.prototype.setExpires = function setExpires(exp) {
597 if (exp instanceof Date) this.expires = exp;
598 else this.expires = parseDate(exp) || "Infinity";
601 Cookie.prototype.setMaxAge = function setMaxAge(age) {
602 if (age === Infinity || age === -Infinity)
603 this.maxAge = age.toString(); // so JSON.stringify() works
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;
615 return this.key+'="'+val+'"';
618 // gives Set-Cookie header format
619 Cookie.prototype.toString = function toString() {
620 var str = this.cookieString();
622 if (this.expires != Infinity) {
623 if (this.expires instanceof Date)
624 str += '; Expires='+formatDate(this.expires);
626 str += '; Expires='+this.expires;
629 if (this.maxAge != null && this.maxAge != Infinity)
630 str += '; Max-Age='+this.maxAge;
632 if (this.domain && !this.hostOnly)
633 str += '; Domain='+this.domain;
635 str += '; Path='+this.path;
637 if (this.secure) str += '; Secure';
638 if (this.httpOnly) str += '; HttpOnly';
639 if (this.extensions) {
640 this.extensions.forEach(function(ext) {
648 // TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
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)
658 if (this.maxAge != null) {
659 return this.maxAge<=0 ? 0 : this.maxAge*1000;
662 var expires = this.expires;
663 if (expires != Infinity) {
664 if (!(expires instanceof Date)) {
665 expires = parseDate(expires) || Infinity;
668 if (expires == Infinity)
671 return expires.getTime() - (now || Date.now());
677 // expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
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;
686 if (this.expires == Infinity) return Infinity;
687 return this.expires.getTime();
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);
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);
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);
713 function CookieJar(store, rejectPublicSuffixes) {
714 if (rejectPublicSuffixes != null) this.rejectPublicSuffixes = rejectPublicSuffixes;
716 memstore = memstore || require('./memstore');
717 store = new memstore.MemoryCookieStore();
721 CookieJar.prototype.store = null;
722 CookieJar.prototype.rejectPublicSuffixes = true;
724 CookieJar.prototype.setCookie = function setCookie(cookie, url, options, cb) {
726 var context = (url instanceof Object) ? url : urlParse(url);
727 if (options instanceof Function) {
732 var host = canonicalDomain(context.hostname);
735 if (!(cookie instanceof Cookie))
736 cookie = Cookie.parse(cookie, options.strict === true);
738 err = new Error("Cookie failed to parse");
739 return cb(options.ignoreError ? null : err);
743 var now = options.now || new Date(); // will assign later to save effort in the face of errors
745 // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie()
747 // S5.3 step 4: NOOP; domain is null by default
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);
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);
765 if (cookie.hostOnly == null) // don't reset if already set
766 cookie.hostOnly = false;
769 cookie.hostOnly = true;
770 cookie.domain = host;
773 // S5.3 step 7: "Otherwise, set the cookie's path to the default-path of the
776 cookie.path = defaultPath(context.pathname);
777 cookie.pathIsDefault = true;
779 if (cookie.path.length > 1 && cookie.path.substr(-1) == '/')
780 cookie.path = cookie.path.slice(0,-1);
783 // S5.3 step 8: NOOP; secure attribute
784 // S5.3 step 9: NOOP; httpOnly attribute
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);
792 var store = this.store;
794 if (!store.updateCookie) {
795 store.updateCookie = function stubUpdateCookie(oldCookie, newCookie, cb) {
796 this.putCookie(newCookie, cb);
800 store.findCookie(cookie.domain, cookie.path, cookie.key, function(err,oldCookie) {
801 if (err) return cb(err);
803 var next = function(err) {
804 if (err) return cb(err);
805 else cb(null, cookie);
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);
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
821 cookie.creation = cookie.lastAccessed = now;
822 store.putCookie(cookie, next); // step 12
828 CookieJar.prototype.getCookies = function getCookies(url, options, cb) {
829 var context = (url instanceof Object) ? url : urlParse(url);
830 if (options instanceof Function) {
835 var host = canonicalDomain(context.hostname);
836 var path = context.pathname || '/';
838 var secure = options.secure;
839 if (secure == null && context.protocol &&
840 (context.protocol == 'https:' || context.protocol == 'wss:'))
845 var http = options.http;
846 if (http == null) http = true;
848 var now = options.now || Date.now();
849 var expireCheck = options.expire !== false;
850 var allPaths = !!options.allPaths;
851 var store = this.store;
853 function matchingCookie(c) {
855 // The cookie's host-only-flag is true and the canonicalized
856 // request-host is identical to the cookie's domain.
858 // The cookie's host-only-flag is false and the canonicalized
859 // request-host domain-matches the cookie's domain."
861 if (c.domain != host) return false;
863 if (!domainMatch(host, c.domain, false)) return false;
866 // "The request-uri's path path-matches the cookie's path."
867 if (!allPaths && !pathMatch(path, c.path))
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)
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)
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
890 store.findCookies(host, allPaths ? null : path, function(err,cookies) {
891 if (err) return cb(err);
893 cookies = cookies.filter(matchingCookie);
895 // sorting of S5.4 part 2
896 if (options.sort !== false)
897 cookies = cookies.sort(cookieCompare);
900 var now = new Date();
901 cookies.forEach(function(c) { c.lastAccessed = now });
902 // TODO persist lastAccessed
908 CookieJar.prototype.getCookieString = function(/*..., cb*/) {
909 var args = Array.prototype.slice.call(arguments,0);
911 var next = function(err,cookies) {
913 else cb(null, cookies.map(function(c){return c.cookieString()}).join('; '));
916 this.getCookies.apply(this,args);
919 CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) {
920 var args = Array.prototype.slice.call(arguments,0);
922 var next = function(err,cookies) {
924 else cb(null, cookies.map(function(c){return c.toString()}));
927 this.getCookies.apply(this,args);
933 CookieJar: CookieJar,
935 parseDate: parseDate,
936 formatDate: formatDate,
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,