cookie.js (40917B)
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. 14 * 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. 18 * 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. 30 */ 31 'use strict'; 32 var net = require('net'); 33 var urlParse = require('url').parse; 34 var util = require('util'); 35 var pubsuffix = require('./pubsuffix-psl'); 36 var Store = require('./store').Store; 37 var MemoryCookieStore = require('./memstore').MemoryCookieStore; 38 var pathMatch = require('./pathMatch').pathMatch; 39 var VERSION = require('./version'); 40 41 var punycode; 42 try { 43 punycode = require('punycode'); 44 } catch(e) { 45 console.warn("tough-cookie: can't load punycode; won't use punycode for domain normalization"); 46 } 47 48 // From RFC6265 S4.1.1 49 // note that it excludes \x3B ";" 50 var COOKIE_OCTETS = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/; 51 52 var CONTROL_CHARS = /[\x00-\x1F]/; 53 54 // From Chromium // '\r', '\n' and '\0' should be treated as a terminator in 55 // the "relaxed" mode, see: 56 // https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60 57 var TERMINATORS = ['\n', '\r', '\0']; 58 59 // RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"' 60 // Note ';' is \x3B 61 var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/; 62 63 // date-time parsing constants (RFC6265 S5.1.1) 64 65 var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/; 66 67 var MONTH_TO_NUM = { 68 jan:0, feb:1, mar:2, apr:3, may:4, jun:5, 69 jul:6, aug:7, sep:8, oct:9, nov:10, dec:11 70 }; 71 var NUM_TO_MONTH = [ 72 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec' 73 ]; 74 var NUM_TO_DAY = [ 75 'Sun','Mon','Tue','Wed','Thu','Fri','Sat' 76 ]; 77 78 var MAX_TIME = 2147483647000; // 31-bit max 79 var MIN_TIME = 0; // 31-bit min 80 81 /* 82 * Parses a Natural number (i.e., non-negative integer) with either the 83 * <min>*<max>DIGIT ( non-digit *OCTET ) 84 * or 85 * <min>*<max>DIGIT 86 * grammar (RFC6265 S5.1.1). 87 * 88 * The "trailingOK" boolean controls if the grammar accepts a 89 * "( non-digit *OCTET )" trailer. 90 */ 91 function parseDigits(token, minDigits, maxDigits, trailingOK) { 92 var count = 0; 93 while (count < token.length) { 94 var c = token.charCodeAt(count); 95 // "non-digit = %x00-2F / %x3A-FF" 96 if (c <= 0x2F || c >= 0x3A) { 97 break; 98 } 99 count++; 100 } 101 102 // constrain to a minimum and maximum number of digits. 103 if (count < minDigits || count > maxDigits) { 104 return null; 105 } 106 107 if (!trailingOK && count != token.length) { 108 return null; 109 } 110 111 return parseInt(token.substr(0,count), 10); 112 } 113 114 function parseTime(token) { 115 var parts = token.split(':'); 116 var result = [0,0,0]; 117 118 /* RF6256 S5.1.1: 119 * time = hms-time ( non-digit *OCTET ) 120 * hms-time = time-field ":" time-field ":" time-field 121 * time-field = 1*2DIGIT 122 */ 123 124 if (parts.length !== 3) { 125 return null; 126 } 127 128 for (var i = 0; i < 3; i++) { 129 // "time-field" must be strictly "1*2DIGIT", HOWEVER, "hms-time" can be 130 // followed by "( non-digit *OCTET )" so therefore the last time-field can 131 // have a trailer 132 var trailingOK = (i == 2); 133 var num = parseDigits(parts[i], 1, 2, trailingOK); 134 if (num === null) { 135 return null; 136 } 137 result[i] = num; 138 } 139 140 return result; 141 } 142 143 function parseMonth(token) { 144 token = String(token).substr(0,3).toLowerCase(); 145 var num = MONTH_TO_NUM[token]; 146 return num >= 0 ? num : null; 147 } 148 149 /* 150 * RFC6265 S5.1.1 date parser (see RFC for full grammar) 151 */ 152 function parseDate(str) { 153 if (!str) { 154 return; 155 } 156 157 /* RFC6265 S5.1.1: 158 * 2. Process each date-token sequentially in the order the date-tokens 159 * appear in the cookie-date 160 */ 161 var tokens = str.split(DATE_DELIM); 162 if (!tokens) { 163 return; 164 } 165 166 var hour = null; 167 var minute = null; 168 var second = null; 169 var dayOfMonth = null; 170 var month = null; 171 var year = null; 172 173 for (var i=0; i<tokens.length; i++) { 174 var token = tokens[i].trim(); 175 if (!token.length) { 176 continue; 177 } 178 179 var result; 180 181 /* 2.1. If the found-time flag is not set and the token matches the time 182 * production, set the found-time flag and set the hour- value, 183 * minute-value, and second-value to the numbers denoted by the digits in 184 * the date-token, respectively. Skip the remaining sub-steps and continue 185 * to the next date-token. 186 */ 187 if (second === null) { 188 result = parseTime(token); 189 if (result) { 190 hour = result[0]; 191 minute = result[1]; 192 second = result[2]; 193 continue; 194 } 195 } 196 197 /* 2.2. If the found-day-of-month flag is not set and the date-token matches 198 * the day-of-month production, set the found-day-of- month flag and set 199 * the day-of-month-value to the number denoted by the date-token. Skip 200 * the remaining sub-steps and continue to the next date-token. 201 */ 202 if (dayOfMonth === null) { 203 // "day-of-month = 1*2DIGIT ( non-digit *OCTET )" 204 result = parseDigits(token, 1, 2, true); 205 if (result !== null) { 206 dayOfMonth = result; 207 continue; 208 } 209 } 210 211 /* 2.3. If the found-month flag is not set and the date-token matches the 212 * month production, set the found-month flag and set the month-value to 213 * the month denoted by the date-token. Skip the remaining sub-steps and 214 * continue to the next date-token. 215 */ 216 if (month === null) { 217 result = parseMonth(token); 218 if (result !== null) { 219 month = result; 220 continue; 221 } 222 } 223 224 /* 2.4. If the found-year flag is not set and the date-token matches the 225 * year production, set the found-year flag and set the year-value to the 226 * number denoted by the date-token. Skip the remaining sub-steps and 227 * continue to the next date-token. 228 */ 229 if (year === null) { 230 // "year = 2*4DIGIT ( non-digit *OCTET )" 231 result = parseDigits(token, 2, 4, true); 232 if (result !== null) { 233 year = result; 234 /* From S5.1.1: 235 * 3. If the year-value is greater than or equal to 70 and less 236 * than or equal to 99, increment the year-value by 1900. 237 * 4. If the year-value is greater than or equal to 0 and less 238 * than or equal to 69, increment the year-value by 2000. 239 */ 240 if (year >= 70 && year <= 99) { 241 year += 1900; 242 } else if (year >= 0 && year <= 69) { 243 year += 2000; 244 } 245 } 246 } 247 } 248 249 /* RFC 6265 S5.1.1 250 * "5. Abort these steps and fail to parse the cookie-date if: 251 * * at least one of the found-day-of-month, found-month, found- 252 * year, or found-time flags is not set, 253 * * the day-of-month-value is less than 1 or greater than 31, 254 * * the year-value is less than 1601, 255 * * the hour-value is greater than 23, 256 * * the minute-value is greater than 59, or 257 * * the second-value is greater than 59. 258 * (Note that leap seconds cannot be represented in this syntax.)" 259 * 260 * So, in order as above: 261 */ 262 if ( 263 dayOfMonth === null || month === null || year === null || second === null || 264 dayOfMonth < 1 || dayOfMonth > 31 || 265 year < 1601 || 266 hour > 23 || 267 minute > 59 || 268 second > 59 269 ) { 270 return; 271 } 272 273 return new Date(Date.UTC(year, month, dayOfMonth, hour, minute, second)); 274 } 275 276 function formatDate(date) { 277 var d = date.getUTCDate(); d = d >= 10 ? d : '0'+d; 278 var h = date.getUTCHours(); h = h >= 10 ? h : '0'+h; 279 var m = date.getUTCMinutes(); m = m >= 10 ? m : '0'+m; 280 var s = date.getUTCSeconds(); s = s >= 10 ? s : '0'+s; 281 return NUM_TO_DAY[date.getUTCDay()] + ', ' + 282 d+' '+ NUM_TO_MONTH[date.getUTCMonth()] +' '+ date.getUTCFullYear() +' '+ 283 h+':'+m+':'+s+' GMT'; 284 } 285 286 // S5.1.2 Canonicalized Host Names 287 function canonicalDomain(str) { 288 if (str == null) { 289 return null; 290 } 291 str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading . 292 293 // convert to IDN if any non-ASCII characters 294 if (punycode && /[^\u0001-\u007f]/.test(str)) { 295 str = punycode.toASCII(str); 296 } 297 298 return str.toLowerCase(); 299 } 300 301 // S5.1.3 Domain Matching 302 function domainMatch(str, domStr, canonicalize) { 303 if (str == null || domStr == null) { 304 return null; 305 } 306 if (canonicalize !== false) { 307 str = canonicalDomain(str); 308 domStr = canonicalDomain(domStr); 309 } 310 311 /* 312 * "The domain string and the string are identical. (Note that both the 313 * domain string and the string will have been canonicalized to lower case at 314 * this point)" 315 */ 316 if (str == domStr) { 317 return true; 318 } 319 320 /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */ 321 322 /* "* The string is a host name (i.e., not an IP address)." */ 323 if (net.isIP(str)) { 324 return false; 325 } 326 327 /* "* The domain string is a suffix of the string" */ 328 var idx = str.indexOf(domStr); 329 if (idx <= 0) { 330 return false; // it's a non-match (-1) or prefix (0) 331 } 332 333 // e.g "a.b.c".indexOf("b.c") === 2 334 // 5 === 3+2 335 if (str.length !== domStr.length + idx) { // it's not a suffix 336 return false; 337 } 338 339 /* "* The last character of the string that is not included in the domain 340 * string is a %x2E (".") character." */ 341 if (str.substr(idx-1,1) !== '.') { 342 return false; 343 } 344 345 return true; 346 } 347 348 349 // RFC6265 S5.1.4 Paths and Path-Match 350 351 /* 352 * "The user agent MUST use an algorithm equivalent to the following algorithm 353 * to compute the default-path of a cookie:" 354 * 355 * Assumption: the path (and not query part or absolute uri) is passed in. 356 */ 357 function defaultPath(path) { 358 // "2. If the uri-path is empty or if the first character of the uri-path is not 359 // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. 360 if (!path || path.substr(0,1) !== "/") { 361 return "/"; 362 } 363 364 // "3. If the uri-path contains no more than one %x2F ("/") character, output 365 // %x2F ("/") and skip the remaining step." 366 if (path === "/") { 367 return path; 368 } 369 370 var rightSlash = path.lastIndexOf("/"); 371 if (rightSlash === 0) { 372 return "/"; 373 } 374 375 // "4. Output the characters of the uri-path from the first character up to, 376 // but not including, the right-most %x2F ("/")." 377 return path.slice(0, rightSlash); 378 } 379 380 function trimTerminator(str) { 381 for (var t = 0; t < TERMINATORS.length; t++) { 382 var terminatorIdx = str.indexOf(TERMINATORS[t]); 383 if (terminatorIdx !== -1) { 384 str = str.substr(0,terminatorIdx); 385 } 386 } 387 388 return str; 389 } 390 391 function parseCookiePair(cookiePair, looseMode) { 392 cookiePair = trimTerminator(cookiePair); 393 394 var firstEq = cookiePair.indexOf('='); 395 if (looseMode) { 396 if (firstEq === 0) { // '=' is immediately at start 397 cookiePair = cookiePair.substr(1); 398 firstEq = cookiePair.indexOf('='); // might still need to split on '=' 399 } 400 } else { // non-loose mode 401 if (firstEq <= 0) { // no '=' or is at start 402 return; // needs to have non-empty "cookie-name" 403 } 404 } 405 406 var cookieName, cookieValue; 407 if (firstEq <= 0) { 408 cookieName = ""; 409 cookieValue = cookiePair.trim(); 410 } else { 411 cookieName = cookiePair.substr(0, firstEq).trim(); 412 cookieValue = cookiePair.substr(firstEq+1).trim(); 413 } 414 415 if (CONTROL_CHARS.test(cookieName) || CONTROL_CHARS.test(cookieValue)) { 416 return; 417 } 418 419 var c = new Cookie(); 420 c.key = cookieName; 421 c.value = cookieValue; 422 return c; 423 } 424 425 function parse(str, options) { 426 if (!options || typeof options !== 'object') { 427 options = {}; 428 } 429 str = str.trim(); 430 431 // We use a regex to parse the "name-value-pair" part of S5.2 432 var firstSemi = str.indexOf(';'); // S5.2 step 1 433 var cookiePair = (firstSemi === -1) ? str : str.substr(0, firstSemi); 434 var c = parseCookiePair(cookiePair, !!options.loose); 435 if (!c) { 436 return; 437 } 438 439 if (firstSemi === -1) { 440 return c; 441 } 442 443 // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string 444 // (including the %x3B (";") in question)." plus later on in the same section 445 // "discard the first ";" and trim". 446 var unparsed = str.slice(firstSemi + 1).trim(); 447 448 // "If the unparsed-attributes string is empty, skip the rest of these 449 // steps." 450 if (unparsed.length === 0) { 451 return c; 452 } 453 454 /* 455 * S5.2 says that when looping over the items "[p]rocess the attribute-name 456 * and attribute-value according to the requirements in the following 457 * subsections" for every item. Plus, for many of the individual attributes 458 * in S5.3 it says to use the "attribute-value of the last attribute in the 459 * cookie-attribute-list". Therefore, in this implementation, we overwrite 460 * the previous value. 461 */ 462 var cookie_avs = unparsed.split(';'); 463 while (cookie_avs.length) { 464 var av = cookie_avs.shift().trim(); 465 if (av.length === 0) { // happens if ";;" appears 466 continue; 467 } 468 var av_sep = av.indexOf('='); 469 var av_key, av_value; 470 471 if (av_sep === -1) { 472 av_key = av; 473 av_value = null; 474 } else { 475 av_key = av.substr(0,av_sep); 476 av_value = av.substr(av_sep+1); 477 } 478 479 av_key = av_key.trim().toLowerCase(); 480 481 if (av_value) { 482 av_value = av_value.trim(); 483 } 484 485 switch(av_key) { 486 case 'expires': // S5.2.1 487 if (av_value) { 488 var exp = parseDate(av_value); 489 // "If the attribute-value failed to parse as a cookie date, ignore the 490 // cookie-av." 491 if (exp) { 492 // over and underflow not realistically a concern: V8's getTime() seems to 493 // store something larger than a 32-bit time_t (even with 32-bit node) 494 c.expires = exp; 495 } 496 } 497 break; 498 499 case 'max-age': // S5.2.2 500 if (av_value) { 501 // "If the first character of the attribute-value is not a DIGIT or a "-" 502 // character ...[or]... If the remainder of attribute-value contains a 503 // non-DIGIT character, ignore the cookie-av." 504 if (/^-?[0-9]+$/.test(av_value)) { 505 var delta = parseInt(av_value, 10); 506 // "If delta-seconds is less than or equal to zero (0), let expiry-time 507 // be the earliest representable date and time." 508 c.setMaxAge(delta); 509 } 510 } 511 break; 512 513 case 'domain': // S5.2.3 514 // "If the attribute-value is empty, the behavior is undefined. However, 515 // the user agent SHOULD ignore the cookie-av entirely." 516 if (av_value) { 517 // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E 518 // (".") character." 519 var domain = av_value.trim().replace(/^\./, ''); 520 if (domain) { 521 // "Convert the cookie-domain to lower case." 522 c.domain = domain.toLowerCase(); 523 } 524 } 525 break; 526 527 case 'path': // S5.2.4 528 /* 529 * "If the attribute-value is empty or if the first character of the 530 * attribute-value is not %x2F ("/"): 531 * Let cookie-path be the default-path. 532 * Otherwise: 533 * Let cookie-path be the attribute-value." 534 * 535 * We'll represent the default-path as null since it depends on the 536 * context of the parsing. 537 */ 538 c.path = av_value && av_value[0] === "/" ? av_value : null; 539 break; 540 541 case 'secure': // S5.2.5 542 /* 543 * "If the attribute-name case-insensitively matches the string "Secure", 544 * the user agent MUST append an attribute to the cookie-attribute-list 545 * with an attribute-name of Secure and an empty attribute-value." 546 */ 547 c.secure = true; 548 break; 549 550 case 'httponly': // S5.2.6 -- effectively the same as 'secure' 551 c.httpOnly = true; 552 break; 553 554 default: 555 c.extensions = c.extensions || []; 556 c.extensions.push(av); 557 break; 558 } 559 } 560 561 return c; 562 } 563 564 // avoid the V8 deoptimization monster! 565 function jsonParse(str) { 566 var obj; 567 try { 568 obj = JSON.parse(str); 569 } catch (e) { 570 return e; 571 } 572 return obj; 573 } 574 575 function fromJSON(str) { 576 if (!str) { 577 return null; 578 } 579 580 var obj; 581 if (typeof str === 'string') { 582 obj = jsonParse(str); 583 if (obj instanceof Error) { 584 return null; 585 } 586 } else { 587 // assume it's an Object 588 obj = str; 589 } 590 591 var c = new Cookie(); 592 for (var i=0; i<Cookie.serializableProperties.length; i++) { 593 var prop = Cookie.serializableProperties[i]; 594 if (obj[prop] === undefined || 595 obj[prop] === Cookie.prototype[prop]) 596 { 597 continue; // leave as prototype default 598 } 599 600 if (prop === 'expires' || 601 prop === 'creation' || 602 prop === 'lastAccessed') 603 { 604 if (obj[prop] === null) { 605 c[prop] = null; 606 } else { 607 c[prop] = obj[prop] == "Infinity" ? 608 "Infinity" : new Date(obj[prop]); 609 } 610 } else { 611 c[prop] = obj[prop]; 612 } 613 } 614 615 return c; 616 } 617 618 /* Section 5.4 part 2: 619 * "* Cookies with longer paths are listed before cookies with 620 * shorter paths. 621 * 622 * * Among cookies that have equal-length path fields, cookies with 623 * earlier creation-times are listed before cookies with later 624 * creation-times." 625 */ 626 627 function cookieCompare(a,b) { 628 var cmp = 0; 629 630 // descending for length: b CMP a 631 var aPathLen = a.path ? a.path.length : 0; 632 var bPathLen = b.path ? b.path.length : 0; 633 cmp = bPathLen - aPathLen; 634 if (cmp !== 0) { 635 return cmp; 636 } 637 638 // ascending for time: a CMP b 639 var aTime = a.creation ? a.creation.getTime() : MAX_TIME; 640 var bTime = b.creation ? b.creation.getTime() : MAX_TIME; 641 cmp = aTime - bTime; 642 if (cmp !== 0) { 643 return cmp; 644 } 645 646 // break ties for the same millisecond (precision of JavaScript's clock) 647 cmp = a.creationIndex - b.creationIndex; 648 649 return cmp; 650 } 651 652 // Gives the permutation of all possible pathMatch()es of a given path. The 653 // array is in longest-to-shortest order. Handy for indexing. 654 function permutePath(path) { 655 if (path === '/') { 656 return ['/']; 657 } 658 if (path.lastIndexOf('/') === path.length-1) { 659 path = path.substr(0,path.length-1); 660 } 661 var permutations = [path]; 662 while (path.length > 1) { 663 var lindex = path.lastIndexOf('/'); 664 if (lindex === 0) { 665 break; 666 } 667 path = path.substr(0,lindex); 668 permutations.push(path); 669 } 670 permutations.push('/'); 671 return permutations; 672 } 673 674 function getCookieContext(url) { 675 if (url instanceof Object) { 676 return url; 677 } 678 // NOTE: decodeURI will throw on malformed URIs (see GH-32). 679 // Therefore, we will just skip decoding for such URIs. 680 try { 681 url = decodeURI(url); 682 } 683 catch(err) { 684 // Silently swallow error 685 } 686 687 return urlParse(url); 688 } 689 690 function Cookie(options) { 691 options = options || {}; 692 693 Object.keys(options).forEach(function(prop) { 694 if (Cookie.prototype.hasOwnProperty(prop) && 695 Cookie.prototype[prop] !== options[prop] && 696 prop.substr(0,1) !== '_') 697 { 698 this[prop] = options[prop]; 699 } 700 }, this); 701 702 this.creation = this.creation || new Date(); 703 704 // used to break creation ties in cookieCompare(): 705 Object.defineProperty(this, 'creationIndex', { 706 configurable: false, 707 enumerable: false, // important for assert.deepEqual checks 708 writable: true, 709 value: ++Cookie.cookiesCreated 710 }); 711 } 712 713 Cookie.cookiesCreated = 0; // incremented each time a cookie is created 714 715 Cookie.parse = parse; 716 Cookie.fromJSON = fromJSON; 717 718 Cookie.prototype.key = ""; 719 Cookie.prototype.value = ""; 720 721 // the order in which the RFC has them: 722 Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity 723 Cookie.prototype.maxAge = null; // takes precedence over expires for TTL 724 Cookie.prototype.domain = null; 725 Cookie.prototype.path = null; 726 Cookie.prototype.secure = false; 727 Cookie.prototype.httpOnly = false; 728 Cookie.prototype.extensions = null; 729 730 // set by the CookieJar: 731 Cookie.prototype.hostOnly = null; // boolean when set 732 Cookie.prototype.pathIsDefault = null; // boolean when set 733 Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse 734 Cookie.prototype.lastAccessed = null; // Date when set 735 Object.defineProperty(Cookie.prototype, 'creationIndex', { 736 configurable: true, 737 enumerable: false, 738 writable: true, 739 value: 0 740 }); 741 742 Cookie.serializableProperties = Object.keys(Cookie.prototype) 743 .filter(function(prop) { 744 return !( 745 Cookie.prototype[prop] instanceof Function || 746 prop === 'creationIndex' || 747 prop.substr(0,1) === '_' 748 ); 749 }); 750 751 Cookie.prototype.inspect = function inspect() { 752 var now = Date.now(); 753 return 'Cookie="'+this.toString() + 754 '; hostOnly='+(this.hostOnly != null ? this.hostOnly : '?') + 755 '; aAge='+(this.lastAccessed ? (now-this.lastAccessed.getTime())+'ms' : '?') + 756 '; cAge='+(this.creation ? (now-this.creation.getTime())+'ms' : '?') + 757 '"'; 758 }; 759 760 // Use the new custom inspection symbol to add the custom inspect function if 761 // available. 762 if (util.inspect.custom) { 763 Cookie.prototype[util.inspect.custom] = Cookie.prototype.inspect; 764 } 765 766 Cookie.prototype.toJSON = function() { 767 var obj = {}; 768 769 var props = Cookie.serializableProperties; 770 for (var i=0; i<props.length; i++) { 771 var prop = props[i]; 772 if (this[prop] === Cookie.prototype[prop]) { 773 continue; // leave as prototype default 774 } 775 776 if (prop === 'expires' || 777 prop === 'creation' || 778 prop === 'lastAccessed') 779 { 780 if (this[prop] === null) { 781 obj[prop] = null; 782 } else { 783 obj[prop] = this[prop] == "Infinity" ? // intentionally not === 784 "Infinity" : this[prop].toISOString(); 785 } 786 } else if (prop === 'maxAge') { 787 if (this[prop] !== null) { 788 // again, intentionally not === 789 obj[prop] = (this[prop] == Infinity || this[prop] == -Infinity) ? 790 this[prop].toString() : this[prop]; 791 } 792 } else { 793 if (this[prop] !== Cookie.prototype[prop]) { 794 obj[prop] = this[prop]; 795 } 796 } 797 } 798 799 return obj; 800 }; 801 802 Cookie.prototype.clone = function() { 803 return fromJSON(this.toJSON()); 804 }; 805 806 Cookie.prototype.validate = function validate() { 807 if (!COOKIE_OCTETS.test(this.value)) { 808 return false; 809 } 810 if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires)) { 811 return false; 812 } 813 if (this.maxAge != null && this.maxAge <= 0) { 814 return false; // "Max-Age=" non-zero-digit *DIGIT 815 } 816 if (this.path != null && !PATH_VALUE.test(this.path)) { 817 return false; 818 } 819 820 var cdomain = this.cdomain(); 821 if (cdomain) { 822 if (cdomain.match(/\.$/)) { 823 return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this 824 } 825 var suffix = pubsuffix.getPublicSuffix(cdomain); 826 if (suffix == null) { // it's a public suffix 827 return false; 828 } 829 } 830 return true; 831 }; 832 833 Cookie.prototype.setExpires = function setExpires(exp) { 834 if (exp instanceof Date) { 835 this.expires = exp; 836 } else { 837 this.expires = parseDate(exp) || "Infinity"; 838 } 839 }; 840 841 Cookie.prototype.setMaxAge = function setMaxAge(age) { 842 if (age === Infinity || age === -Infinity) { 843 this.maxAge = age.toString(); // so JSON.stringify() works 844 } else { 845 this.maxAge = age; 846 } 847 }; 848 849 // gives Cookie header format 850 Cookie.prototype.cookieString = function cookieString() { 851 var val = this.value; 852 if (val == null) { 853 val = ''; 854 } 855 if (this.key === '') { 856 return val; 857 } 858 return this.key+'='+val; 859 }; 860 861 // gives Set-Cookie header format 862 Cookie.prototype.toString = function toString() { 863 var str = this.cookieString(); 864 865 if (this.expires != Infinity) { 866 if (this.expires instanceof Date) { 867 str += '; Expires='+formatDate(this.expires); 868 } else { 869 str += '; Expires='+this.expires; 870 } 871 } 872 873 if (this.maxAge != null && this.maxAge != Infinity) { 874 str += '; Max-Age='+this.maxAge; 875 } 876 877 if (this.domain && !this.hostOnly) { 878 str += '; Domain='+this.domain; 879 } 880 if (this.path) { 881 str += '; Path='+this.path; 882 } 883 884 if (this.secure) { 885 str += '; Secure'; 886 } 887 if (this.httpOnly) { 888 str += '; HttpOnly'; 889 } 890 if (this.extensions) { 891 this.extensions.forEach(function(ext) { 892 str += '; '+ext; 893 }); 894 } 895 896 return str; 897 }; 898 899 // TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie() 900 // elsewhere) 901 // S5.3 says to give the "latest representable date" for which we use Infinity 902 // For "expired" we use 0 903 Cookie.prototype.TTL = function TTL(now) { 904 /* RFC6265 S4.1.2.2 If a cookie has both the Max-Age and the Expires 905 * attribute, the Max-Age attribute has precedence and controls the 906 * expiration date of the cookie. 907 * (Concurs with S5.3 step 3) 908 */ 909 if (this.maxAge != null) { 910 return this.maxAge<=0 ? 0 : this.maxAge*1000; 911 } 912 913 var expires = this.expires; 914 if (expires != Infinity) { 915 if (!(expires instanceof Date)) { 916 expires = parseDate(expires) || Infinity; 917 } 918 919 if (expires == Infinity) { 920 return Infinity; 921 } 922 923 return expires.getTime() - (now || Date.now()); 924 } 925 926 return Infinity; 927 }; 928 929 // expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie() 930 // elsewhere) 931 Cookie.prototype.expiryTime = function expiryTime(now) { 932 if (this.maxAge != null) { 933 var relativeTo = now || this.creation || new Date(); 934 var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000; 935 return relativeTo.getTime() + age; 936 } 937 938 if (this.expires == Infinity) { 939 return Infinity; 940 } 941 return this.expires.getTime(); 942 }; 943 944 // expiryDate() replaces the "expiry-time" parts of S5.3 step 3 (setCookie() 945 // elsewhere), except it returns a Date 946 Cookie.prototype.expiryDate = function expiryDate(now) { 947 var millisec = this.expiryTime(now); 948 if (millisec == Infinity) { 949 return new Date(MAX_TIME); 950 } else if (millisec == -Infinity) { 951 return new Date(MIN_TIME); 952 } else { 953 return new Date(millisec); 954 } 955 }; 956 957 // This replaces the "persistent-flag" parts of S5.3 step 3 958 Cookie.prototype.isPersistent = function isPersistent() { 959 return (this.maxAge != null || this.expires != Infinity); 960 }; 961 962 // Mostly S5.1.2 and S5.2.3: 963 Cookie.prototype.cdomain = 964 Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() { 965 if (this.domain == null) { 966 return null; 967 } 968 return canonicalDomain(this.domain); 969 }; 970 971 function CookieJar(store, options) { 972 if (typeof options === "boolean") { 973 options = {rejectPublicSuffixes: options}; 974 } else if (options == null) { 975 options = {}; 976 } 977 if (options.rejectPublicSuffixes != null) { 978 this.rejectPublicSuffixes = options.rejectPublicSuffixes; 979 } 980 if (options.looseMode != null) { 981 this.enableLooseMode = options.looseMode; 982 } 983 984 if (!store) { 985 store = new MemoryCookieStore(); 986 } 987 this.store = store; 988 } 989 CookieJar.prototype.store = null; 990 CookieJar.prototype.rejectPublicSuffixes = true; 991 CookieJar.prototype.enableLooseMode = false; 992 var CAN_BE_SYNC = []; 993 994 CAN_BE_SYNC.push('setCookie'); 995 CookieJar.prototype.setCookie = function(cookie, url, options, cb) { 996 var err; 997 var context = getCookieContext(url); 998 if (options instanceof Function) { 999 cb = options; 1000 options = {}; 1001 } 1002 1003 var host = canonicalDomain(context.hostname); 1004 var loose = this.enableLooseMode; 1005 if (options.loose != null) { 1006 loose = options.loose; 1007 } 1008 1009 // S5.3 step 1 1010 if (!(cookie instanceof Cookie)) { 1011 cookie = Cookie.parse(cookie, { loose: loose }); 1012 } 1013 if (!cookie) { 1014 err = new Error("Cookie failed to parse"); 1015 return cb(options.ignoreError ? null : err); 1016 } 1017 1018 // S5.3 step 2 1019 var now = options.now || new Date(); // will assign later to save effort in the face of errors 1020 1021 // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie() 1022 1023 // S5.3 step 4: NOOP; domain is null by default 1024 1025 // S5.3 step 5: public suffixes 1026 if (this.rejectPublicSuffixes && cookie.domain) { 1027 var suffix = pubsuffix.getPublicSuffix(cookie.cdomain()); 1028 if (suffix == null) { // e.g. "com" 1029 err = new Error("Cookie has domain set to a public suffix"); 1030 return cb(options.ignoreError ? null : err); 1031 } 1032 } 1033 1034 // S5.3 step 6: 1035 if (cookie.domain) { 1036 if (!domainMatch(host, cookie.cdomain(), false)) { 1037 err = new Error("Cookie not in this host's domain. Cookie:"+cookie.cdomain()+" Request:"+host); 1038 return cb(options.ignoreError ? null : err); 1039 } 1040 1041 if (cookie.hostOnly == null) { // don't reset if already set 1042 cookie.hostOnly = false; 1043 } 1044 1045 } else { 1046 cookie.hostOnly = true; 1047 cookie.domain = host; 1048 } 1049 1050 //S5.2.4 If the attribute-value is empty or if the first character of the 1051 //attribute-value is not %x2F ("/"): 1052 //Let cookie-path be the default-path. 1053 if (!cookie.path || cookie.path[0] !== '/') { 1054 cookie.path = defaultPath(context.pathname); 1055 cookie.pathIsDefault = true; 1056 } 1057 1058 // S5.3 step 8: NOOP; secure attribute 1059 // S5.3 step 9: NOOP; httpOnly attribute 1060 1061 // S5.3 step 10 1062 if (options.http === false && cookie.httpOnly) { 1063 err = new Error("Cookie is HttpOnly and this isn't an HTTP API"); 1064 return cb(options.ignoreError ? null : err); 1065 } 1066 1067 var store = this.store; 1068 1069 if (!store.updateCookie) { 1070 store.updateCookie = function(oldCookie, newCookie, cb) { 1071 this.putCookie(newCookie, cb); 1072 }; 1073 } 1074 1075 function withCookie(err, oldCookie) { 1076 if (err) { 1077 return cb(err); 1078 } 1079 1080 var next = function(err) { 1081 if (err) { 1082 return cb(err); 1083 } else { 1084 cb(null, cookie); 1085 } 1086 }; 1087 1088 if (oldCookie) { 1089 // S5.3 step 11 - "If the cookie store contains a cookie with the same name, 1090 // domain, and path as the newly created cookie:" 1091 if (options.http === false && oldCookie.httpOnly) { // step 11.2 1092 err = new Error("old Cookie is HttpOnly and this isn't an HTTP API"); 1093 return cb(options.ignoreError ? null : err); 1094 } 1095 cookie.creation = oldCookie.creation; // step 11.3 1096 cookie.creationIndex = oldCookie.creationIndex; // preserve tie-breaker 1097 cookie.lastAccessed = now; 1098 // Step 11.4 (delete cookie) is implied by just setting the new one: 1099 store.updateCookie(oldCookie, cookie, next); // step 12 1100 1101 } else { 1102 cookie.creation = cookie.lastAccessed = now; 1103 store.putCookie(cookie, next); // step 12 1104 } 1105 } 1106 1107 store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie); 1108 }; 1109 1110 // RFC6365 S5.4 1111 CAN_BE_SYNC.push('getCookies'); 1112 CookieJar.prototype.getCookies = function(url, options, cb) { 1113 var context = getCookieContext(url); 1114 if (options instanceof Function) { 1115 cb = options; 1116 options = {}; 1117 } 1118 1119 var host = canonicalDomain(context.hostname); 1120 var path = context.pathname || '/'; 1121 1122 var secure = options.secure; 1123 if (secure == null && context.protocol && 1124 (context.protocol == 'https:' || context.protocol == 'wss:')) 1125 { 1126 secure = true; 1127 } 1128 1129 var http = options.http; 1130 if (http == null) { 1131 http = true; 1132 } 1133 1134 var now = options.now || Date.now(); 1135 var expireCheck = options.expire !== false; 1136 var allPaths = !!options.allPaths; 1137 var store = this.store; 1138 1139 function matchingCookie(c) { 1140 // "Either: 1141 // The cookie's host-only-flag is true and the canonicalized 1142 // request-host is identical to the cookie's domain. 1143 // Or: 1144 // The cookie's host-only-flag is false and the canonicalized 1145 // request-host domain-matches the cookie's domain." 1146 if (c.hostOnly) { 1147 if (c.domain != host) { 1148 return false; 1149 } 1150 } else { 1151 if (!domainMatch(host, c.domain, false)) { 1152 return false; 1153 } 1154 } 1155 1156 // "The request-uri's path path-matches the cookie's path." 1157 if (!allPaths && !pathMatch(path, c.path)) { 1158 return false; 1159 } 1160 1161 // "If the cookie's secure-only-flag is true, then the request-uri's 1162 // scheme must denote a "secure" protocol" 1163 if (c.secure && !secure) { 1164 return false; 1165 } 1166 1167 // "If the cookie's http-only-flag is true, then exclude the cookie if the 1168 // cookie-string is being generated for a "non-HTTP" API" 1169 if (c.httpOnly && !http) { 1170 return false; 1171 } 1172 1173 // deferred from S5.3 1174 // non-RFC: allow retention of expired cookies by choice 1175 if (expireCheck && c.expiryTime() <= now) { 1176 store.removeCookie(c.domain, c.path, c.key, function(){}); // result ignored 1177 return false; 1178 } 1179 1180 return true; 1181 } 1182 1183 store.findCookies(host, allPaths ? null : path, function(err,cookies) { 1184 if (err) { 1185 return cb(err); 1186 } 1187 1188 cookies = cookies.filter(matchingCookie); 1189 1190 // sorting of S5.4 part 2 1191 if (options.sort !== false) { 1192 cookies = cookies.sort(cookieCompare); 1193 } 1194 1195 // S5.4 part 3 1196 var now = new Date(); 1197 cookies.forEach(function(c) { 1198 c.lastAccessed = now; 1199 }); 1200 // TODO persist lastAccessed 1201 1202 cb(null,cookies); 1203 }); 1204 }; 1205 1206 CAN_BE_SYNC.push('getCookieString'); 1207 CookieJar.prototype.getCookieString = function(/*..., cb*/) { 1208 var args = Array.prototype.slice.call(arguments,0); 1209 var cb = args.pop(); 1210 var next = function(err,cookies) { 1211 if (err) { 1212 cb(err); 1213 } else { 1214 cb(null, cookies 1215 .sort(cookieCompare) 1216 .map(function(c){ 1217 return c.cookieString(); 1218 }) 1219 .join('; ')); 1220 } 1221 }; 1222 args.push(next); 1223 this.getCookies.apply(this,args); 1224 }; 1225 1226 CAN_BE_SYNC.push('getSetCookieStrings'); 1227 CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) { 1228 var args = Array.prototype.slice.call(arguments,0); 1229 var cb = args.pop(); 1230 var next = function(err,cookies) { 1231 if (err) { 1232 cb(err); 1233 } else { 1234 cb(null, cookies.map(function(c){ 1235 return c.toString(); 1236 })); 1237 } 1238 }; 1239 args.push(next); 1240 this.getCookies.apply(this,args); 1241 }; 1242 1243 CAN_BE_SYNC.push('serialize'); 1244 CookieJar.prototype.serialize = function(cb) { 1245 var type = this.store.constructor.name; 1246 if (type === 'Object') { 1247 type = null; 1248 } 1249 1250 // update README.md "Serialization Format" if you change this, please! 1251 var serialized = { 1252 // The version of tough-cookie that serialized this jar. Generally a good 1253 // practice since future versions can make data import decisions based on 1254 // known past behavior. When/if this matters, use `semver`. 1255 version: 'tough-cookie@'+VERSION, 1256 1257 // add the store type, to make humans happy: 1258 storeType: type, 1259 1260 // CookieJar configuration: 1261 rejectPublicSuffixes: !!this.rejectPublicSuffixes, 1262 1263 // this gets filled from getAllCookies: 1264 cookies: [] 1265 }; 1266 1267 if (!(this.store.getAllCookies && 1268 typeof this.store.getAllCookies === 'function')) 1269 { 1270 return cb(new Error('store does not support getAllCookies and cannot be serialized')); 1271 } 1272 1273 this.store.getAllCookies(function(err,cookies) { 1274 if (err) { 1275 return cb(err); 1276 } 1277 1278 serialized.cookies = cookies.map(function(cookie) { 1279 // convert to serialized 'raw' cookies 1280 cookie = (cookie instanceof Cookie) ? cookie.toJSON() : cookie; 1281 1282 // Remove the index so new ones get assigned during deserialization 1283 delete cookie.creationIndex; 1284 1285 return cookie; 1286 }); 1287 1288 return cb(null, serialized); 1289 }); 1290 }; 1291 1292 // well-known name that JSON.stringify calls 1293 CookieJar.prototype.toJSON = function() { 1294 return this.serializeSync(); 1295 }; 1296 1297 // use the class method CookieJar.deserialize instead of calling this directly 1298 CAN_BE_SYNC.push('_importCookies'); 1299 CookieJar.prototype._importCookies = function(serialized, cb) { 1300 var jar = this; 1301 var cookies = serialized.cookies; 1302 if (!cookies || !Array.isArray(cookies)) { 1303 return cb(new Error('serialized jar has no cookies array')); 1304 } 1305 cookies = cookies.slice(); // do not modify the original 1306 1307 function putNext(err) { 1308 if (err) { 1309 return cb(err); 1310 } 1311 1312 if (!cookies.length) { 1313 return cb(err, jar); 1314 } 1315 1316 var cookie; 1317 try { 1318 cookie = fromJSON(cookies.shift()); 1319 } catch (e) { 1320 return cb(e); 1321 } 1322 1323 if (cookie === null) { 1324 return putNext(null); // skip this cookie 1325 } 1326 1327 jar.store.putCookie(cookie, putNext); 1328 } 1329 1330 putNext(); 1331 }; 1332 1333 CookieJar.deserialize = function(strOrObj, store, cb) { 1334 if (arguments.length !== 3) { 1335 // store is optional 1336 cb = store; 1337 store = null; 1338 } 1339 1340 var serialized; 1341 if (typeof strOrObj === 'string') { 1342 serialized = jsonParse(strOrObj); 1343 if (serialized instanceof Error) { 1344 return cb(serialized); 1345 } 1346 } else { 1347 serialized = strOrObj; 1348 } 1349 1350 var jar = new CookieJar(store, serialized.rejectPublicSuffixes); 1351 jar._importCookies(serialized, function(err) { 1352 if (err) { 1353 return cb(err); 1354 } 1355 cb(null, jar); 1356 }); 1357 }; 1358 1359 CookieJar.deserializeSync = function(strOrObj, store) { 1360 var serialized = typeof strOrObj === 'string' ? 1361 JSON.parse(strOrObj) : strOrObj; 1362 var jar = new CookieJar(store, serialized.rejectPublicSuffixes); 1363 1364 // catch this mistake early: 1365 if (!jar.store.synchronous) { 1366 throw new Error('CookieJar store is not synchronous; use async API instead.'); 1367 } 1368 1369 jar._importCookiesSync(serialized); 1370 return jar; 1371 }; 1372 CookieJar.fromJSON = CookieJar.deserializeSync; 1373 1374 CookieJar.prototype.clone = function(newStore, cb) { 1375 if (arguments.length === 1) { 1376 cb = newStore; 1377 newStore = null; 1378 } 1379 1380 this.serialize(function(err,serialized) { 1381 if (err) { 1382 return cb(err); 1383 } 1384 CookieJar.deserialize(serialized, newStore, cb); 1385 }); 1386 }; 1387 1388 CAN_BE_SYNC.push('removeAllCookies'); 1389 CookieJar.prototype.removeAllCookies = function(cb) { 1390 var store = this.store; 1391 1392 // Check that the store implements its own removeAllCookies(). The default 1393 // implementation in Store will immediately call the callback with a "not 1394 // implemented" Error. 1395 if (store.removeAllCookies instanceof Function && 1396 store.removeAllCookies !== Store.prototype.removeAllCookies) 1397 { 1398 return store.removeAllCookies(cb); 1399 } 1400 1401 store.getAllCookies(function(err, cookies) { 1402 if (err) { 1403 return cb(err); 1404 } 1405 1406 if (cookies.length === 0) { 1407 return cb(null); 1408 } 1409 1410 var completedCount = 0; 1411 var removeErrors = []; 1412 1413 function removeCookieCb(removeErr) { 1414 if (removeErr) { 1415 removeErrors.push(removeErr); 1416 } 1417 1418 completedCount++; 1419 1420 if (completedCount === cookies.length) { 1421 return cb(removeErrors.length ? removeErrors[0] : null); 1422 } 1423 } 1424 1425 cookies.forEach(function(cookie) { 1426 store.removeCookie(cookie.domain, cookie.path, cookie.key, removeCookieCb); 1427 }); 1428 }); 1429 }; 1430 1431 CookieJar.prototype._cloneSync = syncWrap('clone'); 1432 CookieJar.prototype.cloneSync = function(newStore) { 1433 if (!newStore.synchronous) { 1434 throw new Error('CookieJar clone destination store is not synchronous; use async API instead.'); 1435 } 1436 return this._cloneSync(newStore); 1437 }; 1438 1439 // Use a closure to provide a true imperative API for synchronous stores. 1440 function syncWrap(method) { 1441 return function() { 1442 if (!this.store.synchronous) { 1443 throw new Error('CookieJar store is not synchronous; use async API instead.'); 1444 } 1445 1446 var args = Array.prototype.slice.call(arguments); 1447 var syncErr, syncResult; 1448 args.push(function syncCb(err, result) { 1449 syncErr = err; 1450 syncResult = result; 1451 }); 1452 this[method].apply(this, args); 1453 1454 if (syncErr) { 1455 throw syncErr; 1456 } 1457 return syncResult; 1458 }; 1459 } 1460 1461 // wrap all declared CAN_BE_SYNC methods in the sync wrapper 1462 CAN_BE_SYNC.forEach(function(method) { 1463 CookieJar.prototype[method+'Sync'] = syncWrap(method); 1464 }); 1465 1466 exports.version = VERSION; 1467 exports.CookieJar = CookieJar; 1468 exports.Cookie = Cookie; 1469 exports.Store = Store; 1470 exports.MemoryCookieStore = MemoryCookieStore; 1471 exports.parseDate = parseDate; 1472 exports.formatDate = formatDate; 1473 exports.parse = parse; 1474 exports.fromJSON = fromJSON; 1475 exports.domainMatch = domainMatch; 1476 exports.defaultPath = defaultPath; 1477 exports.pathMatch = pathMatch; 1478 exports.getPublicSuffix = pubsuffix.getPublicSuffix; 1479 exports.cookieCompare = cookieCompare; 1480 exports.permuteDomain = require('./permuteDomain').permuteDomain; 1481 exports.permutePath = permutePath; 1482 exports.canonicalDomain = canonicalDomain;