cookie.js 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309
  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 pubsuffix = require('./pubsuffix');
  35. var Store = require('./store').Store;
  36. var MemoryCookieStore = require('./memstore').MemoryCookieStore;
  37. var pathMatch = require('./pathMatch').pathMatch;
  38. var VERSION = require('../package.json').version;
  39. var punycode;
  40. try {
  41. punycode = require('punycode');
  42. } catch(e) {
  43. console.warn("cookie: can't load punycode; won't use punycode for domain normalization");
  44. }
  45. var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
  46. // From RFC6265 S4.1.1
  47. // note that it excludes \x3B ";"
  48. var COOKIE_OCTET = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/;
  49. var COOKIE_OCTETS = new RegExp('^'+COOKIE_OCTET.source+'$');
  50. // Double quotes are part of the value (see: S4.1.1).
  51. // '\r', '\n' and '\0' should be treated as a terminator in the "relaxed" mode
  52. // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60)
  53. // '=' and ';' are attribute/values separators
  54. // (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L64)
  55. var COOKIE_PAIR = /^([^=;]+)\s*=\s*(("?)[^\n\r\0]*\3)/;
  56. // RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"'
  57. // Note ';' is \x3B
  58. var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/;
  59. // Used for checking whether or not there is a trailing semi-colon
  60. var TRAILING_SEMICOLON = /;+$/;
  61. var DAY_OF_MONTH = /^(\d{1,2})[^\d]*$/;
  62. var TIME = /^(\d{1,2})[^\d]*:(\d{1,2})[^\d]*:(\d{1,2})[^\d]*$/;
  63. var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i;
  64. var MONTH_TO_NUM = {
  65. jan:0, feb:1, mar:2, apr:3, may:4, jun:5,
  66. jul:6, aug:7, sep:8, oct:9, nov:10, dec:11
  67. };
  68. var NUM_TO_MONTH = [
  69. 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'
  70. ];
  71. var NUM_TO_DAY = [
  72. 'Sun','Mon','Tue','Wed','Thu','Fri','Sat'
  73. ];
  74. var YEAR = /^(\d{2}|\d{4})$/; // 2 to 4 digits
  75. var MAX_TIME = 2147483647000; // 31-bit max
  76. var MIN_TIME = 0; // 31-bit min
  77. // RFC6265 S5.1.1 date parser:
  78. function parseDate(str) {
  79. if (!str) {
  80. return;
  81. }
  82. /* RFC6265 S5.1.1:
  83. * 2. Process each date-token sequentially in the order the date-tokens
  84. * appear in the cookie-date
  85. */
  86. var tokens = str.split(DATE_DELIM);
  87. if (!tokens) {
  88. return;
  89. }
  90. var hour = null;
  91. var minutes = null;
  92. var seconds = null;
  93. var day = null;
  94. var month = null;
  95. var year = null;
  96. for (var i=0; i<tokens.length; i++) {
  97. var token = tokens[i].trim();
  98. if (!token.length) {
  99. continue;
  100. }
  101. var result;
  102. /* 2.1. If the found-time flag is not set and the token matches the time
  103. * production, set the found-time flag and set the hour- value,
  104. * minute-value, and second-value to the numbers denoted by the digits in
  105. * the date-token, respectively. Skip the remaining sub-steps and continue
  106. * to the next date-token.
  107. */
  108. if (seconds === null) {
  109. result = TIME.exec(token);
  110. if (result) {
  111. hour = parseInt(result[1], 10);
  112. minutes = parseInt(result[2], 10);
  113. seconds = parseInt(result[3], 10);
  114. /* RFC6265 S5.1.1.5:
  115. * [fail if]
  116. * * the hour-value is greater than 23,
  117. * * the minute-value is greater than 59, or
  118. * * the second-value is greater than 59.
  119. */
  120. if(hour > 23 || minutes > 59 || seconds > 59) {
  121. return;
  122. }
  123. continue;
  124. }
  125. }
  126. /* 2.2. If the found-day-of-month flag is not set and the date-token matches
  127. * the day-of-month production, set the found-day-of- month flag and set
  128. * the day-of-month-value to the number denoted by the date-token. Skip
  129. * the remaining sub-steps and continue to the next date-token.
  130. */
  131. if (day === null) {
  132. result = DAY_OF_MONTH.exec(token);
  133. if (result) {
  134. day = parseInt(result, 10);
  135. /* RFC6265 S5.1.1.5:
  136. * [fail if] the day-of-month-value is less than 1 or greater than 31
  137. */
  138. if(day < 1 || day > 31) {
  139. return;
  140. }
  141. continue;
  142. }
  143. }
  144. /* 2.3. If the found-month flag is not set and the date-token matches the
  145. * month production, set the found-month flag and set the month-value to
  146. * the month denoted by the date-token. Skip the remaining sub-steps and
  147. * continue to the next date-token.
  148. */
  149. if (month === null) {
  150. result = MONTH.exec(token);
  151. if (result) {
  152. month = MONTH_TO_NUM[result[1].toLowerCase()];
  153. continue;
  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 (year === null) {
  162. result = YEAR.exec(token);
  163. if (result) {
  164. year = parseInt(result[0], 10);
  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. }
  180. }
  181. }
  182. if (seconds === null || day === null || month === null || year === null) {
  183. return; // 5. ... at least one of the found-day-of-month, found-month, found-
  184. // year, or found-time flags is not set,
  185. }
  186. return new Date(Date.UTC(year, month, day, hour, minutes, seconds));
  187. }
  188. function formatDate(date) {
  189. var d = date.getUTCDate(); d = d >= 10 ? d : '0'+d;
  190. var h = date.getUTCHours(); h = h >= 10 ? h : '0'+h;
  191. var m = date.getUTCMinutes(); m = m >= 10 ? m : '0'+m;
  192. var s = date.getUTCSeconds(); s = s >= 10 ? s : '0'+s;
  193. return NUM_TO_DAY[date.getUTCDay()] + ', ' +
  194. d+' '+ NUM_TO_MONTH[date.getUTCMonth()] +' '+ date.getUTCFullYear() +' '+
  195. h+':'+m+':'+s+' GMT';
  196. }
  197. // S5.1.2 Canonicalized Host Names
  198. function canonicalDomain(str) {
  199. if (str == null) {
  200. return null;
  201. }
  202. str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading .
  203. // convert to IDN if any non-ASCII characters
  204. if (punycode && /[^\u0001-\u007f]/.test(str)) {
  205. str = punycode.toASCII(str);
  206. }
  207. return str.toLowerCase();
  208. }
  209. // S5.1.3 Domain Matching
  210. function domainMatch(str, domStr, canonicalize) {
  211. if (str == null || domStr == null) {
  212. return null;
  213. }
  214. if (canonicalize !== false) {
  215. str = canonicalDomain(str);
  216. domStr = canonicalDomain(domStr);
  217. }
  218. /*
  219. * "The domain string and the string are identical. (Note that both the
  220. * domain string and the string will have been canonicalized to lower case at
  221. * this point)"
  222. */
  223. if (str == domStr) {
  224. return true;
  225. }
  226. /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */
  227. /* "* The string is a host name (i.e., not an IP address)." */
  228. if (net.isIP(str)) {
  229. return false;
  230. }
  231. /* "* The domain string is a suffix of the string" */
  232. var idx = str.indexOf(domStr);
  233. if (idx <= 0) {
  234. return false; // it's a non-match (-1) or prefix (0)
  235. }
  236. // e.g "a.b.c".indexOf("b.c") === 2
  237. // 5 === 3+2
  238. if (str.length !== domStr.length + idx) { // it's not a suffix
  239. return false;
  240. }
  241. /* "* The last character of the string that is not included in the domain
  242. * string is a %x2E (".") character." */
  243. if (str.substr(idx-1,1) !== '.') {
  244. return false;
  245. }
  246. return true;
  247. }
  248. // RFC6265 S5.1.4 Paths and Path-Match
  249. /*
  250. * "The user agent MUST use an algorithm equivalent to the following algorithm
  251. * to compute the default-path of a cookie:"
  252. *
  253. * Assumption: the path (and not query part or absolute uri) is passed in.
  254. */
  255. function defaultPath(path) {
  256. // "2. If the uri-path is empty or if the first character of the uri-path is not
  257. // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps.
  258. if (!path || path.substr(0,1) !== "/") {
  259. return "/";
  260. }
  261. // "3. If the uri-path contains no more than one %x2F ("/") character, output
  262. // %x2F ("/") and skip the remaining step."
  263. if (path === "/") {
  264. return path;
  265. }
  266. var rightSlash = path.lastIndexOf("/");
  267. if (rightSlash === 0) {
  268. return "/";
  269. }
  270. // "4. Output the characters of the uri-path from the first character up to,
  271. // but not including, the right-most %x2F ("/")."
  272. return path.slice(0, rightSlash);
  273. }
  274. function parse(str) {
  275. str = str.trim();
  276. // S4.1.1 Trailing semi-colons are not part of the specification.
  277. var semiColonCheck = TRAILING_SEMICOLON.exec(str);
  278. if (semiColonCheck) {
  279. str = str.slice(0, semiColonCheck.index);
  280. }
  281. // We use a regex to parse the "name-value-pair" part of S5.2
  282. var firstSemi = str.indexOf(';'); // S5.2 step 1
  283. var result = COOKIE_PAIR.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
  284. // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
  285. // constraints as well as trimming any whitespace.
  286. if (!result) {
  287. return;
  288. }
  289. var c = new Cookie();
  290. c.key = result[1].trim();
  291. c.value = result[2].trim();
  292. if (firstSemi === -1) {
  293. return c;
  294. }
  295. // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string
  296. // (including the %x3B (";") in question)." plus later on in the same section
  297. // "discard the first ";" and trim".
  298. var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/,'').trim();
  299. // "If the unparsed-attributes string is empty, skip the rest of these
  300. // steps."
  301. if (unparsed.length === 0) {
  302. return c;
  303. }
  304. /*
  305. * S5.2 says that when looping over the items "[p]rocess the attribute-name
  306. * and attribute-value according to the requirements in the following
  307. * subsections" for every item. Plus, for many of the individual attributes
  308. * in S5.3 it says to use the "attribute-value of the last attribute in the
  309. * cookie-attribute-list". Therefore, in this implementation, we overwrite
  310. * the previous value.
  311. */
  312. var cookie_avs = unparsed.split(/\s*;\s*/);
  313. while (cookie_avs.length) {
  314. var av = cookie_avs.shift();
  315. var av_sep = av.indexOf('=');
  316. var av_key, av_value;
  317. if (av_sep === -1) {
  318. av_key = av;
  319. av_value = null;
  320. } else {
  321. av_key = av.substr(0,av_sep);
  322. av_value = av.substr(av_sep+1);
  323. }
  324. av_key = av_key.trim().toLowerCase();
  325. if (av_value) {
  326. av_value = av_value.trim();
  327. }
  328. switch(av_key) {
  329. case 'expires': // S5.2.1
  330. if (av_value) {
  331. var exp = parseDate(av_value);
  332. // "If the attribute-value failed to parse as a cookie date, ignore the
  333. // cookie-av."
  334. if (exp) {
  335. // over and underflow not realistically a concern: V8's getTime() seems to
  336. // store something larger than a 32-bit time_t (even with 32-bit node)
  337. c.expires = exp;
  338. }
  339. }
  340. break;
  341. case 'max-age': // S5.2.2
  342. if (av_value) {
  343. // "If the first character of the attribute-value is not a DIGIT or a "-"
  344. // character ...[or]... If the remainder of attribute-value contains a
  345. // non-DIGIT character, ignore the cookie-av."
  346. if (/^-?[0-9]+$/.test(av_value)) {
  347. var delta = parseInt(av_value, 10);
  348. // "If delta-seconds is less than or equal to zero (0), let expiry-time
  349. // be the earliest representable date and time."
  350. c.setMaxAge(delta);
  351. }
  352. }
  353. break;
  354. case 'domain': // S5.2.3
  355. // "If the attribute-value is empty, the behavior is undefined. However,
  356. // the user agent SHOULD ignore the cookie-av entirely."
  357. if (av_value) {
  358. // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
  359. // (".") character."
  360. var domain = av_value.trim().replace(/^\./, '');
  361. if (domain) {
  362. // "Convert the cookie-domain to lower case."
  363. c.domain = domain.toLowerCase();
  364. }
  365. }
  366. break;
  367. case 'path': // S5.2.4
  368. /*
  369. * "If the attribute-value is empty or if the first character of the
  370. * attribute-value is not %x2F ("/"):
  371. * Let cookie-path be the default-path.
  372. * Otherwise:
  373. * Let cookie-path be the attribute-value."
  374. *
  375. * We'll represent the default-path as null since it depends on the
  376. * context of the parsing.
  377. */
  378. c.path = av_value && av_value[0] === "/" ? av_value : null;
  379. break;
  380. case 'secure': // S5.2.5
  381. /*
  382. * "If the attribute-name case-insensitively matches the string "Secure",
  383. * the user agent MUST append an attribute to the cookie-attribute-list
  384. * with an attribute-name of Secure and an empty attribute-value."
  385. */
  386. c.secure = true;
  387. break;
  388. case 'httponly': // S5.2.6 -- effectively the same as 'secure'
  389. c.httpOnly = true;
  390. break;
  391. default:
  392. c.extensions = c.extensions || [];
  393. c.extensions.push(av);
  394. break;
  395. }
  396. }
  397. return c;
  398. }
  399. // avoid the V8 deoptimization monster!
  400. function jsonParse(str) {
  401. var obj;
  402. try {
  403. obj = JSON.parse(str);
  404. } catch (e) {
  405. return e;
  406. }
  407. return obj;
  408. }
  409. function fromJSON(str) {
  410. if (!str) {
  411. return null;
  412. }
  413. var obj;
  414. if (typeof str === 'string') {
  415. obj = jsonParse(str);
  416. if (obj instanceof Error) {
  417. return null;
  418. }
  419. } else {
  420. // assume it's an Object
  421. obj = str;
  422. }
  423. var c = new Cookie();
  424. for (var i=0; i<Cookie.serializableProperties.length; i++) {
  425. var prop = Cookie.serializableProperties[i];
  426. if (obj[prop] === undefined ||
  427. obj[prop] === Cookie.prototype[prop])
  428. {
  429. continue; // leave as prototype default
  430. }
  431. if (prop === 'expires' ||
  432. prop === 'creation' ||
  433. prop === 'lastAccessed')
  434. {
  435. if (obj[prop] === null) {
  436. c[prop] = null;
  437. } else {
  438. c[prop] = obj[prop] == "Infinity" ?
  439. "Infinity" : new Date(obj[prop]);
  440. }
  441. } else {
  442. c[prop] = obj[prop];
  443. }
  444. }
  445. return c;
  446. }
  447. /* Section 5.4 part 2:
  448. * "* Cookies with longer paths are listed before cookies with
  449. * shorter paths.
  450. *
  451. * * Among cookies that have equal-length path fields, cookies with
  452. * earlier creation-times are listed before cookies with later
  453. * creation-times."
  454. */
  455. function cookieCompare(a,b) {
  456. var cmp = 0;
  457. // descending for length: b CMP a
  458. var aPathLen = a.path ? a.path.length : 0;
  459. var bPathLen = b.path ? b.path.length : 0;
  460. cmp = bPathLen - aPathLen;
  461. if (cmp !== 0) {
  462. return cmp;
  463. }
  464. // ascending for time: a CMP b
  465. var aTime = a.creation ? a.creation.getTime() : MAX_TIME;
  466. var bTime = b.creation ? b.creation.getTime() : MAX_TIME;
  467. cmp = aTime - bTime;
  468. if (cmp !== 0) {
  469. return cmp;
  470. }
  471. // break ties for the same millisecond (precision of JavaScript's clock)
  472. cmp = a.creationIndex - b.creationIndex;
  473. return cmp;
  474. }
  475. // Gives the permutation of all possible pathMatch()es of a given path. The
  476. // array is in longest-to-shortest order. Handy for indexing.
  477. function permutePath(path) {
  478. if (path === '/') {
  479. return ['/'];
  480. }
  481. if (path.lastIndexOf('/') === path.length-1) {
  482. path = path.substr(0,path.length-1);
  483. }
  484. var permutations = [path];
  485. while (path.length > 1) {
  486. var lindex = path.lastIndexOf('/');
  487. if (lindex === 0) {
  488. break;
  489. }
  490. path = path.substr(0,lindex);
  491. permutations.push(path);
  492. }
  493. permutations.push('/');
  494. return permutations;
  495. }
  496. function getCookieContext(url) {
  497. if (url instanceof Object) {
  498. return url;
  499. }
  500. // NOTE: decodeURI will throw on malformed URIs (see GH-32).
  501. // Therefore, we will just skip decoding for such URIs.
  502. try {
  503. url = decodeURI(url);
  504. }
  505. catch(err) {
  506. // Silently swallow error
  507. }
  508. return urlParse(url);
  509. }
  510. function Cookie(opts) {
  511. opts = opts || {};
  512. Object.keys(opts).forEach(function(prop) {
  513. if (Cookie.prototype.hasOwnProperty(prop) &&
  514. Cookie.prototype[prop] !== opts[prop] &&
  515. prop.substr(0,1) !== '_')
  516. {
  517. this[prop] = opts[prop];
  518. }
  519. }, this);
  520. this.creation = this.creation || new Date();
  521. // used to break creation ties in cookieCompare():
  522. Object.defineProperty(this, 'creationIndex', {
  523. configurable: false,
  524. enumerable: false, // important for assert.deepEqual checks
  525. writable: true,
  526. value: ++Cookie.cookiesCreated
  527. });
  528. }
  529. Cookie.cookiesCreated = 0; // incremented each time a cookie is created
  530. Cookie.parse = parse;
  531. Cookie.fromJSON = fromJSON;
  532. Cookie.prototype.key = "";
  533. Cookie.prototype.value = "";
  534. // the order in which the RFC has them:
  535. Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity
  536. Cookie.prototype.maxAge = null; // takes precedence over expires for TTL
  537. Cookie.prototype.domain = null;
  538. Cookie.prototype.path = null;
  539. Cookie.prototype.secure = false;
  540. Cookie.prototype.httpOnly = false;
  541. Cookie.prototype.extensions = null;
  542. // set by the CookieJar:
  543. Cookie.prototype.hostOnly = null; // boolean when set
  544. Cookie.prototype.pathIsDefault = null; // boolean when set
  545. Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse
  546. Cookie.prototype.lastAccessed = null; // Date when set
  547. Object.defineProperty(Cookie.prototype, 'creationIndex', {
  548. configurable: true,
  549. enumerable: false,
  550. writable: true,
  551. value: 0
  552. });
  553. Cookie.serializableProperties = Object.keys(Cookie.prototype)
  554. .filter(function(prop) {
  555. return !(
  556. Cookie.prototype[prop] instanceof Function ||
  557. prop === 'creationIndex' ||
  558. prop.substr(0,1) === '_'
  559. );
  560. });
  561. Cookie.prototype.inspect = function inspect() {
  562. var now = Date.now();
  563. return 'Cookie="'+this.toString() +
  564. '; hostOnly='+(this.hostOnly != null ? this.hostOnly : '?') +
  565. '; aAge='+(this.lastAccessed ? (now-this.lastAccessed.getTime())+'ms' : '?') +
  566. '; cAge='+(this.creation ? (now-this.creation.getTime())+'ms' : '?') +
  567. '"';
  568. };
  569. Cookie.prototype.toJSON = function() {
  570. var obj = {};
  571. var props = Cookie.serializableProperties;
  572. for (var i=0; i<props.length; i++) {
  573. var prop = props[i];
  574. if (this[prop] === Cookie.prototype[prop]) {
  575. continue; // leave as prototype default
  576. }
  577. if (prop === 'expires' ||
  578. prop === 'creation' ||
  579. prop === 'lastAccessed')
  580. {
  581. if (this[prop] === null) {
  582. obj[prop] = null;
  583. } else {
  584. obj[prop] = this[prop] == "Infinity" ? // intentionally not ===
  585. "Infinity" : this[prop].toISOString();
  586. }
  587. } else if (prop === 'maxAge') {
  588. if (this[prop] !== null) {
  589. // again, intentionally not ===
  590. obj[prop] = (this[prop] == Infinity || this[prop] == -Infinity) ?
  591. this[prop].toString() : this[prop];
  592. }
  593. } else {
  594. if (this[prop] !== Cookie.prototype[prop]) {
  595. obj[prop] = this[prop];
  596. }
  597. }
  598. }
  599. return obj;
  600. };
  601. Cookie.prototype.clone = function() {
  602. return fromJSON(this.toJSON());
  603. };
  604. Cookie.prototype.validate = function validate() {
  605. if (!COOKIE_OCTETS.test(this.value)) {
  606. return false;
  607. }
  608. if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires)) {
  609. return false;
  610. }
  611. if (this.maxAge != null && this.maxAge <= 0) {
  612. return false; // "Max-Age=" non-zero-digit *DIGIT
  613. }
  614. if (this.path != null && !PATH_VALUE.test(this.path)) {
  615. return false;
  616. }
  617. var cdomain = this.cdomain();
  618. if (cdomain) {
  619. if (cdomain.match(/\.$/)) {
  620. return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this
  621. }
  622. var suffix = pubsuffix.getPublicSuffix(cdomain);
  623. if (suffix == null) { // it's a public suffix
  624. return false;
  625. }
  626. }
  627. return true;
  628. };
  629. Cookie.prototype.setExpires = function setExpires(exp) {
  630. if (exp instanceof Date) {
  631. this.expires = exp;
  632. } else {
  633. this.expires = parseDate(exp) || "Infinity";
  634. }
  635. };
  636. Cookie.prototype.setMaxAge = function setMaxAge(age) {
  637. if (age === Infinity || age === -Infinity) {
  638. this.maxAge = age.toString(); // so JSON.stringify() works
  639. } else {
  640. this.maxAge = age;
  641. }
  642. };
  643. // gives Cookie header format
  644. Cookie.prototype.cookieString = function cookieString() {
  645. var val = this.value;
  646. if (val == null) {
  647. val = '';
  648. }
  649. return this.key+'='+val;
  650. };
  651. // gives Set-Cookie header format
  652. Cookie.prototype.toString = function toString() {
  653. var str = this.cookieString();
  654. if (this.expires != Infinity) {
  655. if (this.expires instanceof Date) {
  656. str += '; Expires='+formatDate(this.expires);
  657. } else {
  658. str += '; Expires='+this.expires;
  659. }
  660. }
  661. if (this.maxAge != null && this.maxAge != Infinity) {
  662. str += '; Max-Age='+this.maxAge;
  663. }
  664. if (this.domain && !this.hostOnly) {
  665. str += '; Domain='+this.domain;
  666. }
  667. if (this.path) {
  668. str += '; Path='+this.path;
  669. }
  670. if (this.secure) {
  671. str += '; Secure';
  672. }
  673. if (this.httpOnly) {
  674. str += '; HttpOnly';
  675. }
  676. if (this.extensions) {
  677. this.extensions.forEach(function(ext) {
  678. str += '; '+ext;
  679. });
  680. }
  681. return str;
  682. };
  683. // TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
  684. // elsewhere)
  685. // S5.3 says to give the "latest representable date" for which we use Infinity
  686. // For "expired" we use 0
  687. Cookie.prototype.TTL = function TTL(now) {
  688. /* RFC6265 S4.1.2.2 If a cookie has both the Max-Age and the Expires
  689. * attribute, the Max-Age attribute has precedence and controls the
  690. * expiration date of the cookie.
  691. * (Concurs with S5.3 step 3)
  692. */
  693. if (this.maxAge != null) {
  694. return this.maxAge<=0 ? 0 : this.maxAge*1000;
  695. }
  696. var expires = this.expires;
  697. if (expires != Infinity) {
  698. if (!(expires instanceof Date)) {
  699. expires = parseDate(expires) || Infinity;
  700. }
  701. if (expires == Infinity) {
  702. return Infinity;
  703. }
  704. return expires.getTime() - (now || Date.now());
  705. }
  706. return Infinity;
  707. };
  708. // expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
  709. // elsewhere)
  710. Cookie.prototype.expiryTime = function expiryTime(now) {
  711. if (this.maxAge != null) {
  712. var relativeTo = now || this.creation || new Date();
  713. var age = (this.maxAge <= 0) ? -Infinity : this.maxAge*1000;
  714. return relativeTo.getTime() + age;
  715. }
  716. if (this.expires == Infinity) {
  717. return Infinity;
  718. }
  719. return this.expires.getTime();
  720. };
  721. // expiryDate() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
  722. // elsewhere), except it returns a Date
  723. Cookie.prototype.expiryDate = function expiryDate(now) {
  724. var millisec = this.expiryTime(now);
  725. if (millisec == Infinity) {
  726. return new Date(MAX_TIME);
  727. } else if (millisec == -Infinity) {
  728. return new Date(MIN_TIME);
  729. } else {
  730. return new Date(millisec);
  731. }
  732. };
  733. // This replaces the "persistent-flag" parts of S5.3 step 3
  734. Cookie.prototype.isPersistent = function isPersistent() {
  735. return (this.maxAge != null || this.expires != Infinity);
  736. };
  737. // Mostly S5.1.2 and S5.2.3:
  738. Cookie.prototype.cdomain =
  739. Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() {
  740. if (this.domain == null) {
  741. return null;
  742. }
  743. return canonicalDomain(this.domain);
  744. };
  745. function CookieJar(store, rejectPublicSuffixes) {
  746. if (rejectPublicSuffixes != null) {
  747. this.rejectPublicSuffixes = rejectPublicSuffixes;
  748. }
  749. if (!store) {
  750. store = new MemoryCookieStore();
  751. }
  752. this.store = store;
  753. }
  754. CookieJar.prototype.store = null;
  755. CookieJar.prototype.rejectPublicSuffixes = true;
  756. var CAN_BE_SYNC = [];
  757. CAN_BE_SYNC.push('setCookie');
  758. CookieJar.prototype.setCookie = function(cookie, url, options, cb) {
  759. var err;
  760. var context = getCookieContext(url);
  761. if (options instanceof Function) {
  762. cb = options;
  763. options = {};
  764. }
  765. var host = canonicalDomain(context.hostname);
  766. // S5.3 step 1
  767. if (!(cookie instanceof Cookie)) {
  768. cookie = Cookie.parse(cookie);
  769. }
  770. if (!cookie) {
  771. err = new Error("Cookie failed to parse");
  772. return cb(options.ignoreError ? null : err);
  773. }
  774. // S5.3 step 2
  775. var now = options.now || new Date(); // will assign later to save effort in the face of errors
  776. // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie()
  777. // S5.3 step 4: NOOP; domain is null by default
  778. // S5.3 step 5: public suffixes
  779. if (this.rejectPublicSuffixes && cookie.domain) {
  780. var suffix = pubsuffix.getPublicSuffix(cookie.cdomain());
  781. if (suffix == null) { // e.g. "com"
  782. err = new Error("Cookie has domain set to a public suffix");
  783. return cb(options.ignoreError ? null : err);
  784. }
  785. }
  786. // S5.3 step 6:
  787. if (cookie.domain) {
  788. if (!domainMatch(host, cookie.cdomain(), false)) {
  789. err = new Error("Cookie not in this host's domain. Cookie:"+cookie.cdomain()+" Request:"+host);
  790. return cb(options.ignoreError ? null : err);
  791. }
  792. if (cookie.hostOnly == null) { // don't reset if already set
  793. cookie.hostOnly = false;
  794. }
  795. } else {
  796. cookie.hostOnly = true;
  797. cookie.domain = host;
  798. }
  799. //S5.2.4 If the attribute-value is empty or if the first character of the
  800. //attribute-value is not %x2F ("/"):
  801. //Let cookie-path be the default-path.
  802. if (!cookie.path || cookie.path[0] !== '/') {
  803. cookie.path = defaultPath(context.pathname);
  804. cookie.pathIsDefault = true;
  805. }
  806. // S5.3 step 8: NOOP; secure attribute
  807. // S5.3 step 9: NOOP; httpOnly attribute
  808. // S5.3 step 10
  809. if (options.http === false && cookie.httpOnly) {
  810. err = new Error("Cookie is HttpOnly and this isn't an HTTP API");
  811. return cb(options.ignoreError ? null : err);
  812. }
  813. var store = this.store;
  814. if (!store.updateCookie) {
  815. store.updateCookie = function(oldCookie, newCookie, cb) {
  816. this.putCookie(newCookie, cb);
  817. };
  818. }
  819. function withCookie(err, oldCookie) {
  820. if (err) {
  821. return cb(err);
  822. }
  823. var next = function(err) {
  824. if (err) {
  825. return cb(err);
  826. } else {
  827. cb(null, cookie);
  828. }
  829. };
  830. if (oldCookie) {
  831. // S5.3 step 11 - "If the cookie store contains a cookie with the same name,
  832. // domain, and path as the newly created cookie:"
  833. if (options.http === false && oldCookie.httpOnly) { // step 11.2
  834. err = new Error("old Cookie is HttpOnly and this isn't an HTTP API");
  835. return cb(options.ignoreError ? null : err);
  836. }
  837. cookie.creation = oldCookie.creation; // step 11.3
  838. cookie.creationIndex = oldCookie.creationIndex; // preserve tie-breaker
  839. cookie.lastAccessed = now;
  840. // Step 11.4 (delete cookie) is implied by just setting the new one:
  841. store.updateCookie(oldCookie, cookie, next); // step 12
  842. } else {
  843. cookie.creation = cookie.lastAccessed = now;
  844. store.putCookie(cookie, next); // step 12
  845. }
  846. }
  847. store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie);
  848. };
  849. // RFC6365 S5.4
  850. CAN_BE_SYNC.push('getCookies');
  851. CookieJar.prototype.getCookies = function(url, options, cb) {
  852. var context = getCookieContext(url);
  853. if (options instanceof Function) {
  854. cb = options;
  855. options = {};
  856. }
  857. var host = canonicalDomain(context.hostname);
  858. var path = context.pathname || '/';
  859. var secure = options.secure;
  860. if (secure == null && context.protocol &&
  861. (context.protocol == 'https:' || context.protocol == 'wss:'))
  862. {
  863. secure = true;
  864. }
  865. var http = options.http;
  866. if (http == null) {
  867. http = true;
  868. }
  869. var now = options.now || Date.now();
  870. var expireCheck = options.expire !== false;
  871. var allPaths = !!options.allPaths;
  872. var store = this.store;
  873. function matchingCookie(c) {
  874. // "Either:
  875. // The cookie's host-only-flag is true and the canonicalized
  876. // request-host is identical to the cookie's domain.
  877. // Or:
  878. // The cookie's host-only-flag is false and the canonicalized
  879. // request-host domain-matches the cookie's domain."
  880. if (c.hostOnly) {
  881. if (c.domain != host) {
  882. return false;
  883. }
  884. } else {
  885. if (!domainMatch(host, c.domain, false)) {
  886. return false;
  887. }
  888. }
  889. // "The request-uri's path path-matches the cookie's path."
  890. if (!allPaths && !pathMatch(path, c.path)) {
  891. return false;
  892. }
  893. // "If the cookie's secure-only-flag is true, then the request-uri's
  894. // scheme must denote a "secure" protocol"
  895. if (c.secure && !secure) {
  896. return false;
  897. }
  898. // "If the cookie's http-only-flag is true, then exclude the cookie if the
  899. // cookie-string is being generated for a "non-HTTP" API"
  900. if (c.httpOnly && !http) {
  901. return false;
  902. }
  903. // deferred from S5.3
  904. // non-RFC: allow retention of expired cookies by choice
  905. if (expireCheck && c.expiryTime() <= now) {
  906. store.removeCookie(c.domain, c.path, c.key, function(){}); // result ignored
  907. return false;
  908. }
  909. return true;
  910. }
  911. store.findCookies(host, allPaths ? null : path, function(err,cookies) {
  912. if (err) {
  913. return cb(err);
  914. }
  915. cookies = cookies.filter(matchingCookie);
  916. // sorting of S5.4 part 2
  917. if (options.sort !== false) {
  918. cookies = cookies.sort(cookieCompare);
  919. }
  920. // S5.4 part 3
  921. var now = new Date();
  922. cookies.forEach(function(c) {
  923. c.lastAccessed = now;
  924. });
  925. // TODO persist lastAccessed
  926. cb(null,cookies);
  927. });
  928. };
  929. CAN_BE_SYNC.push('getCookieString');
  930. CookieJar.prototype.getCookieString = function(/*..., cb*/) {
  931. var args = Array.prototype.slice.call(arguments,0);
  932. var cb = args.pop();
  933. var next = function(err,cookies) {
  934. if (err) {
  935. cb(err);
  936. } else {
  937. cb(null, cookies
  938. .sort(cookieCompare)
  939. .map(function(c){
  940. return c.cookieString();
  941. })
  942. .join('; '));
  943. }
  944. };
  945. args.push(next);
  946. this.getCookies.apply(this,args);
  947. };
  948. CAN_BE_SYNC.push('getSetCookieStrings');
  949. CookieJar.prototype.getSetCookieStrings = function(/*..., cb*/) {
  950. var args = Array.prototype.slice.call(arguments,0);
  951. var cb = args.pop();
  952. var next = function(err,cookies) {
  953. if (err) {
  954. cb(err);
  955. } else {
  956. cb(null, cookies.map(function(c){
  957. return c.toString();
  958. }));
  959. }
  960. };
  961. args.push(next);
  962. this.getCookies.apply(this,args);
  963. };
  964. CAN_BE_SYNC.push('serialize');
  965. CookieJar.prototype.serialize = function(cb) {
  966. var type = this.store.constructor.name;
  967. if (type === 'Object') {
  968. type = null;
  969. }
  970. // update README.md "Serialization Format" if you change this, please!
  971. var serialized = {
  972. // The version of tough-cookie that serialized this jar. Generally a good
  973. // practice since future versions can make data import decisions based on
  974. // known past behavior. When/if this matters, use `semver`.
  975. version: 'tough-cookie@'+VERSION,
  976. // add the store type, to make humans happy:
  977. storeType: type,
  978. // CookieJar configuration:
  979. rejectPublicSuffixes: !!this.rejectPublicSuffixes,
  980. // this gets filled from getAllCookies:
  981. cookies: []
  982. };
  983. if (!(this.store.getAllCookies &&
  984. typeof this.store.getAllCookies === 'function'))
  985. {
  986. return cb(new Error('store does not support getAllCookies and cannot be serialized'));
  987. }
  988. this.store.getAllCookies(function(err,cookies) {
  989. if (err) {
  990. return cb(err);
  991. }
  992. serialized.cookies = cookies.map(function(cookie) {
  993. // convert to serialized 'raw' cookies
  994. cookie = (cookie instanceof Cookie) ? cookie.toJSON() : cookie;
  995. // Remove the index so new ones get assigned during deserialization
  996. delete cookie.creationIndex;
  997. return cookie;
  998. });
  999. return cb(null, serialized);
  1000. });
  1001. };
  1002. // well-known name that JSON.stringify calls
  1003. CookieJar.prototype.toJSON = function() {
  1004. return this.serializeSync();
  1005. };
  1006. // use the class method CookieJar.deserialize instead of calling this directly
  1007. CAN_BE_SYNC.push('_importCookies');
  1008. CookieJar.prototype._importCookies = function(serialized, cb) {
  1009. var jar = this;
  1010. var cookies = serialized.cookies;
  1011. if (!cookies || !Array.isArray(cookies)) {
  1012. return cb(new Error('serialized jar has no cookies array'));
  1013. }
  1014. function putNext(err) {
  1015. if (err) {
  1016. return cb(err);
  1017. }
  1018. if (!cookies.length) {
  1019. return cb(err, jar);
  1020. }
  1021. var cookie;
  1022. try {
  1023. cookie = fromJSON(cookies.shift());
  1024. } catch (e) {
  1025. return cb(e);
  1026. }
  1027. if (cookie === null) {
  1028. return putNext(null); // skip this cookie
  1029. }
  1030. jar.store.putCookie(cookie, putNext);
  1031. }
  1032. putNext();
  1033. };
  1034. CookieJar.deserialize = function(strOrObj, store, cb) {
  1035. if (arguments.length !== 3) {
  1036. // store is optional
  1037. cb = store;
  1038. store = null;
  1039. }
  1040. var serialized;
  1041. if (typeof strOrObj === 'string') {
  1042. serialized = jsonParse(strOrObj);
  1043. if (serialized instanceof Error) {
  1044. return cb(serialized);
  1045. }
  1046. } else {
  1047. serialized = strOrObj;
  1048. }
  1049. var jar = new CookieJar(store, serialized.rejectPublicSuffixes);
  1050. jar._importCookies(serialized, function(err) {
  1051. if (err) {
  1052. return cb(err);
  1053. }
  1054. cb(null, jar);
  1055. });
  1056. };
  1057. CookieJar.fromJSON = CookieJar.deserializeSync;
  1058. CookieJar.deserializeSync = function(strOrObj, store) {
  1059. var serialized = typeof strOrObj === 'string' ?
  1060. JSON.parse(strOrObj) : strOrObj;
  1061. var jar = new CookieJar(store, serialized.rejectPublicSuffixes);
  1062. // catch this mistake early:
  1063. if (!jar.store.synchronous) {
  1064. throw new Error('CookieJar store is not synchronous; use async API instead.');
  1065. }
  1066. jar._importCookiesSync(serialized);
  1067. return jar;
  1068. };
  1069. CAN_BE_SYNC.push('clone');
  1070. CookieJar.prototype.clone = function(newStore, cb) {
  1071. if (arguments.length === 1) {
  1072. cb = newStore;
  1073. newStore = null;
  1074. }
  1075. this.serialize(function(err,serialized) {
  1076. if (err) {
  1077. return cb(err);
  1078. }
  1079. CookieJar.deserialize(newStore, serialized, cb);
  1080. });
  1081. };
  1082. // Use a closure to provide a true imperative API for synchronous stores.
  1083. function syncWrap(method) {
  1084. return function() {
  1085. if (!this.store.synchronous) {
  1086. throw new Error('CookieJar store is not synchronous; use async API instead.');
  1087. }
  1088. var args = Array.prototype.slice.call(arguments);
  1089. var syncErr, syncResult;
  1090. args.push(function syncCb(err, result) {
  1091. syncErr = err;
  1092. syncResult = result;
  1093. });
  1094. this[method].apply(this, args);
  1095. if (syncErr) {
  1096. throw syncErr;
  1097. }
  1098. return syncResult;
  1099. };
  1100. }
  1101. // wrap all declared CAN_BE_SYNC methods in the sync wrapper
  1102. CAN_BE_SYNC.forEach(function(method) {
  1103. CookieJar.prototype[method+'Sync'] = syncWrap(method);
  1104. });
  1105. module.exports = {
  1106. CookieJar: CookieJar,
  1107. Cookie: Cookie,
  1108. Store: Store,
  1109. MemoryCookieStore: MemoryCookieStore,
  1110. parseDate: parseDate,
  1111. formatDate: formatDate,
  1112. parse: parse,
  1113. fromJSON: fromJSON,
  1114. domainMatch: domainMatch,
  1115. defaultPath: defaultPath,
  1116. pathMatch: pathMatch,
  1117. getPublicSuffix: pubsuffix.getPublicSuffix,
  1118. cookieCompare: cookieCompare,
  1119. permuteDomain: require('./permuteDomain').permuteDomain,
  1120. permutePath: permutePath,
  1121. canonicalDomain: canonicalDomain
  1122. };