Spaces:
Paused
Paused
| /*! | |
| * Copyright (c) 2015-2020, Salesforce.com, Inc. | |
| * All rights reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions are met: | |
| * | |
| * 1. Redistributions of source code must retain the above copyright notice, | |
| * this list of conditions and the following disclaimer. | |
| * | |
| * 2. Redistributions in binary form must reproduce the above copyright notice, | |
| * this list of conditions and the following disclaimer in the documentation | |
| * and/or other materials provided with the distribution. | |
| * | |
| * 3. Neither the name of Salesforce.com nor the names of its contributors may | |
| * be used to endorse or promote products derived from this software without | |
| * specific prior written permission. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
| * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| * POSSIBILITY OF SUCH DAMAGE. | |
| */ | |
| ; | |
| const punycode = require("punycode/"); | |
| const urlParse = require("url-parse"); | |
| const pubsuffix = require("./pubsuffix-psl"); | |
| const Store = require("./store").Store; | |
| const MemoryCookieStore = require("./memstore").MemoryCookieStore; | |
| const pathMatch = require("./pathMatch").pathMatch; | |
| const validators = require("./validators.js"); | |
| const VERSION = require("./version"); | |
| const { fromCallback } = require("universalify"); | |
| const { getCustomInspectSymbol } = require("./utilHelper"); | |
| // From RFC6265 S4.1.1 | |
| // note that it excludes \x3B ";" | |
| const COOKIE_OCTETS = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/; | |
| const CONTROL_CHARS = /[\x00-\x1F]/; | |
| // From Chromium // '\r', '\n' and '\0' should be treated as a terminator in | |
| // the "relaxed" mode, see: | |
| // https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60 | |
| const TERMINATORS = ["\n", "\r", "\0"]; | |
| // RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"' | |
| // Note ';' is \x3B | |
| const PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/; | |
| // date-time parsing constants (RFC6265 S5.1.1) | |
| const DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/; | |
| const MONTH_TO_NUM = { | |
| jan: 0, | |
| feb: 1, | |
| mar: 2, | |
| apr: 3, | |
| may: 4, | |
| jun: 5, | |
| jul: 6, | |
| aug: 7, | |
| sep: 8, | |
| oct: 9, | |
| nov: 10, | |
| dec: 11 | |
| }; | |
| const MAX_TIME = 2147483647000; // 31-bit max | |
| const MIN_TIME = 0; // 31-bit min | |
| const SAME_SITE_CONTEXT_VAL_ERR = | |
| 'Invalid sameSiteContext option for getCookies(); expected one of "strict", "lax", or "none"'; | |
| function checkSameSiteContext(value) { | |
| validators.validate(validators.isNonEmptyString(value), value); | |
| const context = String(value).toLowerCase(); | |
| if (context === "none" || context === "lax" || context === "strict") { | |
| return context; | |
| } else { | |
| return null; | |
| } | |
| } | |
| const PrefixSecurityEnum = Object.freeze({ | |
| SILENT: "silent", | |
| STRICT: "strict", | |
| DISABLED: "unsafe-disabled" | |
| }); | |
| // Dumped from ip-regex@4.0.0, with the following changes: | |
| // * all capturing groups converted to non-capturing -- "(?:)" | |
| // * support for IPv6 Scoped Literal ("%eth1") removed | |
| // * lowercase hexadecimal only | |
| const IP_REGEX_LOWERCASE = /(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-f\d]{1,4}:){7}(?:[a-f\d]{1,4}|:)|(?:[a-f\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-f\d]{1,4}|:)|(?:[a-f\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,2}|:)|(?:[a-f\d]{1,4}:){4}(?:(?::[a-f\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,3}|:)|(?:[a-f\d]{1,4}:){3}(?:(?::[a-f\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,4}|:)|(?:[a-f\d]{1,4}:){2}(?:(?::[a-f\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,5}|:)|(?:[a-f\d]{1,4}:){1}(?:(?::[a-f\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,6}|:)|(?::(?:(?::[a-f\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-f\d]{1,4}){1,7}|:)))$)/; | |
| const IP_V6_REGEX = ` | |
| \\[?(?: | |
| (?:[a-fA-F\\d]{1,4}:){7}(?:[a-fA-F\\d]{1,4}|:)| | |
| (?:[a-fA-F\\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|:[a-fA-F\\d]{1,4}|:)| | |
| (?:[a-fA-F\\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,2}|:)| | |
| (?:[a-fA-F\\d]{1,4}:){4}(?:(?::[a-fA-F\\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,3}|:)| | |
| (?:[a-fA-F\\d]{1,4}:){3}(?:(?::[a-fA-F\\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,4}|:)| | |
| (?:[a-fA-F\\d]{1,4}:){2}(?:(?::[a-fA-F\\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,5}|:)| | |
| (?:[a-fA-F\\d]{1,4}:){1}(?:(?::[a-fA-F\\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,6}|:)| | |
| (?::(?:(?::[a-fA-F\\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}|(?::[a-fA-F\\d]{1,4}){1,7}|:)) | |
| )(?:%[0-9a-zA-Z]{1,})?\\]? | |
| ` | |
| .replace(/\s*\/\/.*$/gm, "") | |
| .replace(/\n/g, "") | |
| .trim(); | |
| const IP_V6_REGEX_OBJECT = new RegExp(`^${IP_V6_REGEX}$`); | |
| /* | |
| * Parses a Natural number (i.e., non-negative integer) with either the | |
| * <min>*<max>DIGIT ( non-digit *OCTET ) | |
| * or | |
| * <min>*<max>DIGIT | |
| * grammar (RFC6265 S5.1.1). | |
| * | |
| * The "trailingOK" boolean controls if the grammar accepts a | |
| * "( non-digit *OCTET )" trailer. | |
| */ | |
| function parseDigits(token, minDigits, maxDigits, trailingOK) { | |
| let count = 0; | |
| while (count < token.length) { | |
| const c = token.charCodeAt(count); | |
| // "non-digit = %x00-2F / %x3A-FF" | |
| if (c <= 0x2f || c >= 0x3a) { | |
| break; | |
| } | |
| count++; | |
| } | |
| // constrain to a minimum and maximum number of digits. | |
| if (count < minDigits || count > maxDigits) { | |
| return null; | |
| } | |
| if (!trailingOK && count != token.length) { | |
| return null; | |
| } | |
| return parseInt(token.substr(0, count), 10); | |
| } | |
| function parseTime(token) { | |
| const parts = token.split(":"); | |
| const result = [0, 0, 0]; | |
| /* RF6256 S5.1.1: | |
| * time = hms-time ( non-digit *OCTET ) | |
| * hms-time = time-field ":" time-field ":" time-field | |
| * time-field = 1*2DIGIT | |
| */ | |
| if (parts.length !== 3) { | |
| return null; | |
| } | |
| for (let i = 0; i < 3; i++) { | |
| // "time-field" must be strictly "1*2DIGIT", HOWEVER, "hms-time" can be | |
| // followed by "( non-digit *OCTET )" so therefore the last time-field can | |
| // have a trailer | |
| const trailingOK = i == 2; | |
| const num = parseDigits(parts[i], 1, 2, trailingOK); | |
| if (num === null) { | |
| return null; | |
| } | |
| result[i] = num; | |
| } | |
| return result; | |
| } | |
| function parseMonth(token) { | |
| token = String(token) | |
| .substr(0, 3) | |
| .toLowerCase(); | |
| const num = MONTH_TO_NUM[token]; | |
| return num >= 0 ? num : null; | |
| } | |
| /* | |
| * RFC6265 S5.1.1 date parser (see RFC for full grammar) | |
| */ | |
| function parseDate(str) { | |
| if (!str) { | |
| return; | |
| } | |
| /* RFC6265 S5.1.1: | |
| * 2. Process each date-token sequentially in the order the date-tokens | |
| * appear in the cookie-date | |
| */ | |
| const tokens = str.split(DATE_DELIM); | |
| if (!tokens) { | |
| return; | |
| } | |
| let hour = null; | |
| let minute = null; | |
| let second = null; | |
| let dayOfMonth = null; | |
| let month = null; | |
| let year = null; | |
| for (let i = 0; i < tokens.length; i++) { | |
| const token = tokens[i].trim(); | |
| if (!token.length) { | |
| continue; | |
| } | |
| let result; | |
| /* 2.1. If the found-time flag is not set and the token matches the time | |
| * production, set the found-time flag and set the hour- value, | |
| * minute-value, and second-value to the numbers denoted by the digits in | |
| * the date-token, respectively. Skip the remaining sub-steps and continue | |
| * to the next date-token. | |
| */ | |
| if (second === null) { | |
| result = parseTime(token); | |
| if (result) { | |
| hour = result[0]; | |
| minute = result[1]; | |
| second = result[2]; | |
| continue; | |
| } | |
| } | |
| /* 2.2. If the found-day-of-month flag is not set and the date-token matches | |
| * the day-of-month production, set the found-day-of- month flag and set | |
| * the day-of-month-value to the number denoted by the date-token. Skip | |
| * the remaining sub-steps and continue to the next date-token. | |
| */ | |
| if (dayOfMonth === null) { | |
| // "day-of-month = 1*2DIGIT ( non-digit *OCTET )" | |
| result = parseDigits(token, 1, 2, true); | |
| if (result !== null) { | |
| dayOfMonth = result; | |
| continue; | |
| } | |
| } | |
| /* 2.3. If the found-month flag is not set and the date-token matches the | |
| * month production, set the found-month flag and set the month-value to | |
| * the month denoted by the date-token. Skip the remaining sub-steps and | |
| * continue to the next date-token. | |
| */ | |
| if (month === null) { | |
| result = parseMonth(token); | |
| if (result !== null) { | |
| month = result; | |
| continue; | |
| } | |
| } | |
| /* 2.4. If the found-year flag is not set and the date-token matches the | |
| * year production, set the found-year flag and set the year-value to the | |
| * number denoted by the date-token. Skip the remaining sub-steps and | |
| * continue to the next date-token. | |
| */ | |
| if (year === null) { | |
| // "year = 2*4DIGIT ( non-digit *OCTET )" | |
| result = parseDigits(token, 2, 4, true); | |
| if (result !== null) { | |
| year = result; | |
| /* From S5.1.1: | |
| * 3. If the year-value is greater than or equal to 70 and less | |
| * than or equal to 99, increment the year-value by 1900. | |
| * 4. If the year-value is greater than or equal to 0 and less | |
| * than or equal to 69, increment the year-value by 2000. | |
| */ | |
| if (year >= 70 && year <= 99) { | |
| year += 1900; | |
| } else if (year >= 0 && year <= 69) { | |
| year += 2000; | |
| } | |
| } | |
| } | |
| } | |
| /* RFC 6265 S5.1.1 | |
| * "5. Abort these steps and fail to parse the cookie-date if: | |
| * * at least one of the found-day-of-month, found-month, found- | |
| * year, or found-time flags is not set, | |
| * * the day-of-month-value is less than 1 or greater than 31, | |
| * * the year-value is less than 1601, | |
| * * the hour-value is greater than 23, | |
| * * the minute-value is greater than 59, or | |
| * * the second-value is greater than 59. | |
| * (Note that leap seconds cannot be represented in this syntax.)" | |
| * | |
| * So, in order as above: | |
| */ | |
| if ( | |
| dayOfMonth === null || | |
| month === null || | |
| year === null || | |
| second === null || | |
| dayOfMonth < 1 || | |
| dayOfMonth > 31 || | |
| year < 1601 || | |
| hour > 23 || | |
| minute > 59 || | |
| second > 59 | |
| ) { | |
| return; | |
| } | |
| return new Date(Date.UTC(year, month, dayOfMonth, hour, minute, second)); | |
| } | |
| function formatDate(date) { | |
| validators.validate(validators.isDate(date), date); | |
| return date.toUTCString(); | |
| } | |
| // S5.1.2 Canonicalized Host Names | |
| function canonicalDomain(str) { | |
| if (str == null) { | |
| return null; | |
| } | |
| str = str.trim().replace(/^\./, ""); // S4.1.2.3 & S5.2.3: ignore leading . | |
| if (IP_V6_REGEX_OBJECT.test(str)) { | |
| str = str.replace("[", "").replace("]", ""); | |
| } | |
| // convert to IDN if any non-ASCII characters | |
| if (punycode && /[^\u0001-\u007f]/.test(str)) { | |
| str = punycode.toASCII(str); | |
| } | |
| return str.toLowerCase(); | |
| } | |
| // S5.1.3 Domain Matching | |
| function domainMatch(str, domStr, canonicalize) { | |
| if (str == null || domStr == null) { | |
| return null; | |
| } | |
| if (canonicalize !== false) { | |
| str = canonicalDomain(str); | |
| domStr = canonicalDomain(domStr); | |
| } | |
| /* | |
| * S5.1.3: | |
| * "A string domain-matches a given domain string if at least one of the | |
| * following conditions hold:" | |
| * | |
| * " o The domain string and the string are identical. (Note that both the | |
| * domain string and the string will have been canonicalized to lower case at | |
| * this point)" | |
| */ | |
| if (str == domStr) { | |
| return true; | |
| } | |
| /* " o All of the following [three] conditions hold:" */ | |
| /* "* The domain string is a suffix of the string" */ | |
| const idx = str.lastIndexOf(domStr); | |
| if (idx <= 0) { | |
| return false; // it's a non-match (-1) or prefix (0) | |
| } | |
| // next, check it's a proper suffix | |
| // e.g., "a.b.c".indexOf("b.c") === 2 | |
| // 5 === 3+2 | |
| if (str.length !== domStr.length + idx) { | |
| return false; // it's not a suffix | |
| } | |
| /* " * The last character of the string that is not included in the | |
| * domain string is a %x2E (".") character." */ | |
| if (str.substr(idx - 1, 1) !== ".") { | |
| return false; // doesn't align on "." | |
| } | |
| /* " * The string is a host name (i.e., not an IP address)." */ | |
| if (IP_REGEX_LOWERCASE.test(str)) { | |
| return false; // it's an IP address | |
| } | |
| return true; | |
| } | |
| // RFC6265 S5.1.4 Paths and Path-Match | |
| /* | |
| * "The user agent MUST use an algorithm equivalent to the following algorithm | |
| * to compute the default-path of a cookie:" | |
| * | |
| * Assumption: the path (and not query part or absolute uri) is passed in. | |
| */ | |
| function defaultPath(path) { | |
| // "2. If the uri-path is empty or if the first character of the uri-path is not | |
| // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. | |
| if (!path || path.substr(0, 1) !== "/") { | |
| return "/"; | |
| } | |
| // "3. If the uri-path contains no more than one %x2F ("/") character, output | |
| // %x2F ("/") and skip the remaining step." | |
| if (path === "/") { | |
| return path; | |
| } | |
| const rightSlash = path.lastIndexOf("/"); | |
| if (rightSlash === 0) { | |
| return "/"; | |
| } | |
| // "4. Output the characters of the uri-path from the first character up to, | |
| // but not including, the right-most %x2F ("/")." | |
| return path.slice(0, rightSlash); | |
| } | |
| function trimTerminator(str) { | |
| if (validators.isEmptyString(str)) return str; | |
| for (let t = 0; t < TERMINATORS.length; t++) { | |
| const terminatorIdx = str.indexOf(TERMINATORS[t]); | |
| if (terminatorIdx !== -1) { | |
| str = str.substr(0, terminatorIdx); | |
| } | |
| } | |
| return str; | |
| } | |
| function parseCookiePair(cookiePair, looseMode) { | |
| cookiePair = trimTerminator(cookiePair); | |
| validators.validate(validators.isString(cookiePair), cookiePair); | |
| let firstEq = cookiePair.indexOf("="); | |
| if (looseMode) { | |
| if (firstEq === 0) { | |
| // '=' is immediately at start | |
| cookiePair = cookiePair.substr(1); | |
| firstEq = cookiePair.indexOf("="); // might still need to split on '=' | |
| } | |
| } else { | |
| // non-loose mode | |
| if (firstEq <= 0) { | |
| // no '=' or is at start | |
| return; // needs to have non-empty "cookie-name" | |
| } | |
| } | |
| let cookieName, cookieValue; | |
| if (firstEq <= 0) { | |
| cookieName = ""; | |
| cookieValue = cookiePair.trim(); | |
| } else { | |
| cookieName = cookiePair.substr(0, firstEq).trim(); | |
| cookieValue = cookiePair.substr(firstEq + 1).trim(); | |
| } | |
| if (CONTROL_CHARS.test(cookieName) || CONTROL_CHARS.test(cookieValue)) { | |
| return; | |
| } | |
| const c = new Cookie(); | |
| c.key = cookieName; | |
| c.value = cookieValue; | |
| return c; | |
| } | |
| function parse(str, options) { | |
| if (!options || typeof options !== "object") { | |
| options = {}; | |
| } | |
| if (validators.isEmptyString(str) || !validators.isString(str)) { | |
| return null; | |
| } | |
| str = str.trim(); | |
| // We use a regex to parse the "name-value-pair" part of S5.2 | |
| const firstSemi = str.indexOf(";"); // S5.2 step 1 | |
| const cookiePair = firstSemi === -1 ? str : str.substr(0, firstSemi); | |
| const c = parseCookiePair(cookiePair, !!options.loose); | |
| if (!c) { | |
| return; | |
| } | |
| if (firstSemi === -1) { | |
| return c; | |
| } | |
| // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string | |
| // (including the %x3B (";") in question)." plus later on in the same section | |
| // "discard the first ";" and trim". | |
| const unparsed = str.slice(firstSemi + 1).trim(); | |
| // "If the unparsed-attributes string is empty, skip the rest of these | |
| // steps." | |
| if (unparsed.length === 0) { | |
| return c; | |
| } | |
| /* | |
| * S5.2 says that when looping over the items "[p]rocess the attribute-name | |
| * and attribute-value according to the requirements in the following | |
| * subsections" for every item. Plus, for many of the individual attributes | |
| * in S5.3 it says to use the "attribute-value of the last attribute in the | |
| * cookie-attribute-list". Therefore, in this implementation, we overwrite | |
| * the previous value. | |
| */ | |
| const cookie_avs = unparsed.split(";"); | |
| while (cookie_avs.length) { | |
| const av = cookie_avs.shift().trim(); | |
| if (av.length === 0) { | |
| // happens if ";;" appears | |
| continue; | |
| } | |
| const av_sep = av.indexOf("="); | |
| let av_key, av_value; | |
| if (av_sep === -1) { | |
| av_key = av; | |
| av_value = null; | |
| } else { | |
| av_key = av.substr(0, av_sep); | |
| av_value = av.substr(av_sep + 1); | |
| } | |
| av_key = av_key.trim().toLowerCase(); | |
| if (av_value) { | |
| av_value = av_value.trim(); | |
| } | |
| switch (av_key) { | |
| case "expires": // S5.2.1 | |
| if (av_value) { | |
| const exp = parseDate(av_value); | |
| // "If the attribute-value failed to parse as a cookie date, ignore the | |
| // cookie-av." | |
| if (exp) { | |
| // over and underflow not realistically a concern: V8's getTime() seems to | |
| // store something larger than a 32-bit time_t (even with 32-bit node) | |
| c.expires = exp; | |
| } | |
| } | |
| break; | |
| case "max-age": // S5.2.2 | |
| if (av_value) { | |
| // "If the first character of the attribute-value is not a DIGIT or a "-" | |
| // character ...[or]... If the remainder of attribute-value contains a | |
| // non-DIGIT character, ignore the cookie-av." | |
| if (/^-?[0-9]+$/.test(av_value)) { | |
| const delta = parseInt(av_value, 10); | |
| // "If delta-seconds is less than or equal to zero (0), let expiry-time | |
| // be the earliest representable date and time." | |
| c.setMaxAge(delta); | |
| } | |
| } | |
| break; | |
| case "domain": // S5.2.3 | |
| // "If the attribute-value is empty, the behavior is undefined. However, | |
| // the user agent SHOULD ignore the cookie-av entirely." | |
| if (av_value) { | |
| // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E | |
| // (".") character." | |
| const domain = av_value.trim().replace(/^\./, ""); | |
| if (domain) { | |
| // "Convert the cookie-domain to lower case." | |
| c.domain = domain.toLowerCase(); | |
| } | |
| } | |
| break; | |
| case "path": // S5.2.4 | |
| /* | |
| * "If the attribute-value is empty or if the first character of the | |
| * attribute-value is not %x2F ("/"): | |
| * Let cookie-path be the default-path. | |
| * Otherwise: | |
| * Let cookie-path be the attribute-value." | |
| * | |
| * We'll represent the default-path as null since it depends on the | |
| * context of the parsing. | |
| */ | |
| c.path = av_value && av_value[0] === "/" ? av_value : null; | |
| break; | |
| case "secure": // S5.2.5 | |
| /* | |
| * "If the attribute-name case-insensitively matches the string "Secure", | |
| * the user agent MUST append an attribute to the cookie-attribute-list | |
| * with an attribute-name of Secure and an empty attribute-value." | |
| */ | |
| c.secure = true; | |
| break; | |
| case "httponly": // S5.2.6 -- effectively the same as 'secure' | |
| c.httpOnly = true; | |
| break; | |
| case "samesite": // RFC6265bis-02 S5.3.7 | |
| const enforcement = av_value ? av_value.toLowerCase() : ""; | |
| switch (enforcement) { | |
| case "strict": | |
| c.sameSite = "strict"; | |
| break; | |
| case "lax": | |
| c.sameSite = "lax"; | |
| break; | |
| case "none": | |
| c.sameSite = "none"; | |
| break; | |
| default: | |
| c.sameSite = undefined; | |
| break; | |
| } | |
| break; | |
| default: | |
| c.extensions = c.extensions || []; | |
| c.extensions.push(av); | |
| break; | |
| } | |
| } | |
| return c; | |
| } | |
| /** | |
| * If the cookie-name begins with a case-sensitive match for the | |
| * string "__Secure-", abort these steps and ignore the cookie | |
| * entirely unless the cookie's secure-only-flag is true. | |
| * @param cookie | |
| * @returns boolean | |
| */ | |
| function isSecurePrefixConditionMet(cookie) { | |
| validators.validate(validators.isObject(cookie), cookie); | |
| return !cookie.key.startsWith("__Secure-") || cookie.secure; | |
| } | |
| /** | |
| * If the cookie-name begins with a case-sensitive match for the | |
| * string "__Host-", abort these steps and ignore the cookie | |
| * entirely unless the cookie meets all the following criteria: | |
| * 1. The cookie's secure-only-flag is true. | |
| * 2. The cookie's host-only-flag is true. | |
| * 3. The cookie-attribute-list contains an attribute with an | |
| * attribute-name of "Path", and the cookie's path is "/". | |
| * @param cookie | |
| * @returns boolean | |
| */ | |
| function isHostPrefixConditionMet(cookie) { | |
| validators.validate(validators.isObject(cookie)); | |
| return ( | |
| !cookie.key.startsWith("__Host-") || | |
| (cookie.secure && | |
| cookie.hostOnly && | |
| cookie.path != null && | |
| cookie.path === "/") | |
| ); | |
| } | |
| // avoid the V8 deoptimization monster! | |
| function jsonParse(str) { | |
| let obj; | |
| try { | |
| obj = JSON.parse(str); | |
| } catch (e) { | |
| return e; | |
| } | |
| return obj; | |
| } | |
| function fromJSON(str) { | |
| if (!str || validators.isEmptyString(str)) { | |
| return null; | |
| } | |
| let obj; | |
| if (typeof str === "string") { | |
| obj = jsonParse(str); | |
| if (obj instanceof Error) { | |
| return null; | |
| } | |
| } else { | |
| // assume it's an Object | |
| obj = str; | |
| } | |
| const c = new Cookie(); | |
| for (let i = 0; i < Cookie.serializableProperties.length; i++) { | |
| const prop = Cookie.serializableProperties[i]; | |
| if (obj[prop] === undefined || obj[prop] === cookieDefaults[prop]) { | |
| continue; // leave as prototype default | |
| } | |
| if (prop === "expires" || prop === "creation" || prop === "lastAccessed") { | |
| if (obj[prop] === null) { | |
| c[prop] = null; | |
| } else { | |
| c[prop] = obj[prop] == "Infinity" ? "Infinity" : new Date(obj[prop]); | |
| } | |
| } else { | |
| c[prop] = obj[prop]; | |
| } | |
| } | |
| return c; | |
| } | |
| /* Section 5.4 part 2: | |
| * "* Cookies with longer paths are listed before cookies with | |
| * shorter paths. | |
| * | |
| * * Among cookies that have equal-length path fields, cookies with | |
| * earlier creation-times are listed before cookies with later | |
| * creation-times." | |
| */ | |
| function cookieCompare(a, b) { | |
| validators.validate(validators.isObject(a), a); | |
| validators.validate(validators.isObject(b), b); | |
| let cmp = 0; | |
| // descending for length: b CMP a | |
| const aPathLen = a.path ? a.path.length : 0; | |
| const bPathLen = b.path ? b.path.length : 0; | |
| cmp = bPathLen - aPathLen; | |
| if (cmp !== 0) { | |
| return cmp; | |
| } | |
| // ascending for time: a CMP b | |
| const aTime = a.creation ? a.creation.getTime() : MAX_TIME; | |
| const bTime = b.creation ? b.creation.getTime() : MAX_TIME; | |
| cmp = aTime - bTime; | |
| if (cmp !== 0) { | |
| return cmp; | |
| } | |
| // break ties for the same millisecond (precision of JavaScript's clock) | |
| cmp = a.creationIndex - b.creationIndex; | |
| return cmp; | |
| } | |
| // Gives the permutation of all possible pathMatch()es of a given path. The | |
| // array is in longest-to-shortest order. Handy for indexing. | |
| function permutePath(path) { | |
| validators.validate(validators.isString(path)); | |
| if (path === "/") { | |
| return ["/"]; | |
| } | |
| const permutations = [path]; | |
| while (path.length > 1) { | |
| const lindex = path.lastIndexOf("/"); | |
| if (lindex === 0) { | |
| break; | |
| } | |
| path = path.substr(0, lindex); | |
| permutations.push(path); | |
| } | |
| permutations.push("/"); | |
| return permutations; | |
| } | |
| function getCookieContext(url) { | |
| if (url instanceof Object) { | |
| return url; | |
| } | |
| // NOTE: decodeURI will throw on malformed URIs (see GH-32). | |
| // Therefore, we will just skip decoding for such URIs. | |
| try { | |
| url = decodeURI(url); | |
| } catch (err) { | |
| // Silently swallow error | |
| } | |
| return urlParse(url); | |
| } | |
| const cookieDefaults = { | |
| // the order in which the RFC has them: | |
| key: "", | |
| value: "", | |
| expires: "Infinity", | |
| maxAge: null, | |
| domain: null, | |
| path: null, | |
| secure: false, | |
| httpOnly: false, | |
| extensions: null, | |
| // set by the CookieJar: | |
| hostOnly: null, | |
| pathIsDefault: null, | |
| creation: null, | |
| lastAccessed: null, | |
| sameSite: undefined | |
| }; | |
| class Cookie { | |
| constructor(options = {}) { | |
| const customInspectSymbol = getCustomInspectSymbol(); | |
| if (customInspectSymbol) { | |
| this[customInspectSymbol] = this.inspect; | |
| } | |
| Object.assign(this, cookieDefaults, options); | |
| this.creation = this.creation || new Date(); | |
| // used to break creation ties in cookieCompare(): | |
| Object.defineProperty(this, "creationIndex", { | |
| configurable: false, | |
| enumerable: false, // important for assert.deepEqual checks | |
| writable: true, | |
| value: ++Cookie.cookiesCreated | |
| }); | |
| } | |
| inspect() { | |
| const now = Date.now(); | |
| const hostOnly = this.hostOnly != null ? this.hostOnly : "?"; | |
| const createAge = this.creation | |
| ? `${now - this.creation.getTime()}ms` | |
| : "?"; | |
| const accessAge = this.lastAccessed | |
| ? `${now - this.lastAccessed.getTime()}ms` | |
| : "?"; | |
| return `Cookie="${this.toString()}; hostOnly=${hostOnly}; aAge=${accessAge}; cAge=${createAge}"`; | |
| } | |
| toJSON() { | |
| const obj = {}; | |
| for (const prop of Cookie.serializableProperties) { | |
| if (this[prop] === cookieDefaults[prop]) { | |
| continue; // leave as prototype default | |
| } | |
| if ( | |
| prop === "expires" || | |
| prop === "creation" || | |
| prop === "lastAccessed" | |
| ) { | |
| if (this[prop] === null) { | |
| obj[prop] = null; | |
| } else { | |
| obj[prop] = | |
| this[prop] == "Infinity" // intentionally not === | |
| ? "Infinity" | |
| : this[prop].toISOString(); | |
| } | |
| } else if (prop === "maxAge") { | |
| if (this[prop] !== null) { | |
| // again, intentionally not === | |
| obj[prop] = | |
| this[prop] == Infinity || this[prop] == -Infinity | |
| ? this[prop].toString() | |
| : this[prop]; | |
| } | |
| } else { | |
| if (this[prop] !== cookieDefaults[prop]) { | |
| obj[prop] = this[prop]; | |
| } | |
| } | |
| } | |
| return obj; | |
| } | |
| clone() { | |
| return fromJSON(this.toJSON()); | |
| } | |
| validate() { | |
| if (!COOKIE_OCTETS.test(this.value)) { | |
| return false; | |
| } | |
| if ( | |
| this.expires != Infinity && | |
| !(this.expires instanceof Date) && | |
| !parseDate(this.expires) | |
| ) { | |
| return false; | |
| } | |
| if (this.maxAge != null && this.maxAge <= 0) { | |
| return false; // "Max-Age=" non-zero-digit *DIGIT | |
| } | |
| if (this.path != null && !PATH_VALUE.test(this.path)) { | |
| return false; | |
| } | |
| const cdomain = this.cdomain(); | |
| if (cdomain) { | |
| if (cdomain.match(/\.$/)) { | |
| return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this | |
| } | |
| const suffix = pubsuffix.getPublicSuffix(cdomain); | |
| if (suffix == null) { | |
| // it's a public suffix | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| setExpires(exp) { | |
| if (exp instanceof Date) { | |
| this.expires = exp; | |
| } else { | |
| this.expires = parseDate(exp) || "Infinity"; | |
| } | |
| } | |
| setMaxAge(age) { | |
| if (age === Infinity || age === -Infinity) { | |
| this.maxAge = age.toString(); // so JSON.stringify() works | |
| } else { | |
| this.maxAge = age; | |
| } | |
| } | |
| cookieString() { | |
| let val = this.value; | |
| if (val == null) { | |
| val = ""; | |
| } | |
| if (this.key === "") { | |
| return val; | |
| } | |
| return `${this.key}=${val}`; | |
| } | |
| // gives Set-Cookie header format | |
| toString() { | |
| let str = this.cookieString(); | |
| if (this.expires != Infinity) { | |
| if (this.expires instanceof Date) { | |
| str += `; Expires=${formatDate(this.expires)}`; | |
| } else { | |
| str += `; Expires=${this.expires}`; | |
| } | |
| } | |
| if (this.maxAge != null && this.maxAge != Infinity) { | |
| str += `; Max-Age=${this.maxAge}`; | |
| } | |
| if (this.domain && !this.hostOnly) { | |
| str += `; Domain=${this.domain}`; | |
| } | |
| if (this.path) { | |
| str += `; Path=${this.path}`; | |
| } | |
| if (this.secure) { | |
| str += "; Secure"; | |
| } | |
| if (this.httpOnly) { | |
| str += "; HttpOnly"; | |
| } | |
| if (this.sameSite && this.sameSite !== "none") { | |
| const ssCanon = Cookie.sameSiteCanonical[this.sameSite.toLowerCase()]; | |
| str += `; SameSite=${ssCanon ? ssCanon : this.sameSite}`; | |
| } | |
| if (this.extensions) { | |
| this.extensions.forEach(ext => { | |
| str += `; ${ext}`; | |
| }); | |
| } | |
| return str; | |
| } | |
| // TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie() | |
| // elsewhere) | |
| // S5.3 says to give the "latest representable date" for which we use Infinity | |
| // For "expired" we use 0 | |
| TTL(now) { | |
| /* RFC6265 S4.1.2.2 If a cookie has both the Max-Age and the Expires | |
| * attribute, the Max-Age attribute has precedence and controls the | |
| * expiration date of the cookie. | |
| * (Concurs with S5.3 step 3) | |
| */ | |
| if (this.maxAge != null) { | |
| return this.maxAge <= 0 ? 0 : this.maxAge * 1000; | |
| } | |
| let expires = this.expires; | |
| if (expires != Infinity) { | |
| if (!(expires instanceof Date)) { | |
| expires = parseDate(expires) || Infinity; | |
| } | |
| if (expires == Infinity) { | |
| return Infinity; | |
| } | |
| return expires.getTime() - (now || Date.now()); | |
| } | |
| return Infinity; | |
| } | |
| // expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie() | |
| // elsewhere) | |
| expiryTime(now) { | |
| if (this.maxAge != null) { | |
| const relativeTo = now || this.creation || new Date(); | |
| const age = this.maxAge <= 0 ? -Infinity : this.maxAge * 1000; | |
| return relativeTo.getTime() + age; | |
| } | |
| if (this.expires == Infinity) { | |
| return Infinity; | |
| } | |
| return this.expires.getTime(); | |
| } | |
| // expiryDate() replaces the "expiry-time" parts of S5.3 step 3 (setCookie() | |
| // elsewhere), except it returns a Date | |
| expiryDate(now) { | |
| const millisec = this.expiryTime(now); | |
| if (millisec == Infinity) { | |
| return new Date(MAX_TIME); | |
| } else if (millisec == -Infinity) { | |
| return new Date(MIN_TIME); | |
| } else { | |
| return new Date(millisec); | |
| } | |
| } | |
| // This replaces the "persistent-flag" parts of S5.3 step 3 | |
| isPersistent() { | |
| return this.maxAge != null || this.expires != Infinity; | |
| } | |
| // Mostly S5.1.2 and S5.2.3: | |
| canonicalizedDomain() { | |
| if (this.domain == null) { | |
| return null; | |
| } | |
| return canonicalDomain(this.domain); | |
| } | |
| cdomain() { | |
| return this.canonicalizedDomain(); | |
| } | |
| } | |
| Cookie.cookiesCreated = 0; | |
| Cookie.parse = parse; | |
| Cookie.fromJSON = fromJSON; | |
| Cookie.serializableProperties = Object.keys(cookieDefaults); | |
| Cookie.sameSiteLevel = { | |
| strict: 3, | |
| lax: 2, | |
| none: 1 | |
| }; | |
| Cookie.sameSiteCanonical = { | |
| strict: "Strict", | |
| lax: "Lax" | |
| }; | |
| function getNormalizedPrefixSecurity(prefixSecurity) { | |
| if (prefixSecurity != null) { | |
| const normalizedPrefixSecurity = prefixSecurity.toLowerCase(); | |
| /* The three supported options */ | |
| switch (normalizedPrefixSecurity) { | |
| case PrefixSecurityEnum.STRICT: | |
| case PrefixSecurityEnum.SILENT: | |
| case PrefixSecurityEnum.DISABLED: | |
| return normalizedPrefixSecurity; | |
| } | |
| } | |
| /* Default is SILENT */ | |
| return PrefixSecurityEnum.SILENT; | |
| } | |
| class CookieJar { | |
| constructor(store, options = { rejectPublicSuffixes: true }) { | |
| if (typeof options === "boolean") { | |
| options = { rejectPublicSuffixes: options }; | |
| } | |
| validators.validate(validators.isObject(options), options); | |
| this.rejectPublicSuffixes = options.rejectPublicSuffixes; | |
| this.enableLooseMode = !!options.looseMode; | |
| this.allowSpecialUseDomain = | |
| typeof options.allowSpecialUseDomain === "boolean" | |
| ? options.allowSpecialUseDomain | |
| : true; | |
| this.store = store || new MemoryCookieStore(); | |
| this.prefixSecurity = getNormalizedPrefixSecurity(options.prefixSecurity); | |
| this._cloneSync = syncWrap("clone"); | |
| this._importCookiesSync = syncWrap("_importCookies"); | |
| this.getCookiesSync = syncWrap("getCookies"); | |
| this.getCookieStringSync = syncWrap("getCookieString"); | |
| this.getSetCookieStringsSync = syncWrap("getSetCookieStrings"); | |
| this.removeAllCookiesSync = syncWrap("removeAllCookies"); | |
| this.setCookieSync = syncWrap("setCookie"); | |
| this.serializeSync = syncWrap("serialize"); | |
| } | |
| setCookie(cookie, url, options, cb) { | |
| validators.validate(validators.isUrlStringOrObject(url), cb, options); | |
| let err; | |
| if (validators.isFunction(url)) { | |
| cb = url; | |
| return cb(new Error("No URL was specified")); | |
| } | |
| const context = getCookieContext(url); | |
| if (validators.isFunction(options)) { | |
| cb = options; | |
| options = {}; | |
| } | |
| validators.validate(validators.isFunction(cb), cb); | |
| if ( | |
| !validators.isNonEmptyString(cookie) && | |
| !validators.isObject(cookie) && | |
| cookie instanceof String && | |
| cookie.length == 0 | |
| ) { | |
| return cb(null); | |
| } | |
| const host = canonicalDomain(context.hostname); | |
| const loose = options.loose || this.enableLooseMode; | |
| let sameSiteContext = null; | |
| if (options.sameSiteContext) { | |
| sameSiteContext = checkSameSiteContext(options.sameSiteContext); | |
| if (!sameSiteContext) { | |
| return cb(new Error(SAME_SITE_CONTEXT_VAL_ERR)); | |
| } | |
| } | |
| // S5.3 step 1 | |
| if (typeof cookie === "string" || cookie instanceof String) { | |
| cookie = Cookie.parse(cookie, { loose: loose }); | |
| if (!cookie) { | |
| err = new Error("Cookie failed to parse"); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| } else if (!(cookie instanceof Cookie)) { | |
| // If you're seeing this error, and are passing in a Cookie object, | |
| // it *might* be a Cookie object from another loaded version of tough-cookie. | |
| err = new Error( | |
| "First argument to setCookie must be a Cookie object or string" | |
| ); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| // S5.3 step 2 | |
| const now = options.now || new Date(); // will assign later to save effort in the face of errors | |
| // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie() | |
| // S5.3 step 4: NOOP; domain is null by default | |
| // S5.3 step 5: public suffixes | |
| if (this.rejectPublicSuffixes && cookie.domain) { | |
| const suffix = pubsuffix.getPublicSuffix(cookie.cdomain(), { | |
| allowSpecialUseDomain: this.allowSpecialUseDomain, | |
| ignoreError: options.ignoreError | |
| }); | |
| if (suffix == null && !IP_V6_REGEX_OBJECT.test(cookie.domain)) { | |
| // e.g. "com" | |
| err = new Error("Cookie has domain set to a public suffix"); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| } | |
| // S5.3 step 6: | |
| if (cookie.domain) { | |
| if (!domainMatch(host, cookie.cdomain(), false)) { | |
| err = new Error( | |
| `Cookie not in this host's domain. Cookie:${cookie.cdomain()} Request:${host}` | |
| ); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| if (cookie.hostOnly == null) { | |
| // don't reset if already set | |
| cookie.hostOnly = false; | |
| } | |
| } else { | |
| cookie.hostOnly = true; | |
| cookie.domain = host; | |
| } | |
| //S5.2.4 If the attribute-value is empty or if the first character of the | |
| //attribute-value is not %x2F ("/"): | |
| //Let cookie-path be the default-path. | |
| if (!cookie.path || cookie.path[0] !== "/") { | |
| cookie.path = defaultPath(context.pathname); | |
| cookie.pathIsDefault = true; | |
| } | |
| // S5.3 step 8: NOOP; secure attribute | |
| // S5.3 step 9: NOOP; httpOnly attribute | |
| // S5.3 step 10 | |
| if (options.http === false && cookie.httpOnly) { | |
| err = new Error("Cookie is HttpOnly and this isn't an HTTP API"); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| // 6252bis-02 S5.4 Step 13 & 14: | |
| if ( | |
| cookie.sameSite !== "none" && | |
| cookie.sameSite !== undefined && | |
| sameSiteContext | |
| ) { | |
| // "If the cookie's "same-site-flag" is not "None", and the cookie | |
| // is being set from a context whose "site for cookies" is not an | |
| // exact match for request-uri's host's registered domain, then | |
| // abort these steps and ignore the newly created cookie entirely." | |
| if (sameSiteContext === "none") { | |
| err = new Error( | |
| "Cookie is SameSite but this is a cross-origin request" | |
| ); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| } | |
| /* 6265bis-02 S5.4 Steps 15 & 16 */ | |
| const ignoreErrorForPrefixSecurity = | |
| this.prefixSecurity === PrefixSecurityEnum.SILENT; | |
| const prefixSecurityDisabled = | |
| this.prefixSecurity === PrefixSecurityEnum.DISABLED; | |
| /* If prefix checking is not disabled ...*/ | |
| if (!prefixSecurityDisabled) { | |
| let errorFound = false; | |
| let errorMsg; | |
| /* Check secure prefix condition */ | |
| if (!isSecurePrefixConditionMet(cookie)) { | |
| errorFound = true; | |
| errorMsg = "Cookie has __Secure prefix but Secure attribute is not set"; | |
| } else if (!isHostPrefixConditionMet(cookie)) { | |
| /* Check host prefix condition */ | |
| errorFound = true; | |
| errorMsg = | |
| "Cookie has __Host prefix but either Secure or HostOnly attribute is not set or Path is not '/'"; | |
| } | |
| if (errorFound) { | |
| return cb( | |
| options.ignoreError || ignoreErrorForPrefixSecurity | |
| ? null | |
| : new Error(errorMsg) | |
| ); | |
| } | |
| } | |
| const store = this.store; | |
| if (!store.updateCookie) { | |
| store.updateCookie = function(oldCookie, newCookie, cb) { | |
| this.putCookie(newCookie, cb); | |
| }; | |
| } | |
| function withCookie(err, oldCookie) { | |
| if (err) { | |
| return cb(err); | |
| } | |
| const next = function(err) { | |
| if (err) { | |
| return cb(err); | |
| } else { | |
| cb(null, cookie); | |
| } | |
| }; | |
| if (oldCookie) { | |
| // S5.3 step 11 - "If the cookie store contains a cookie with the same name, | |
| // domain, and path as the newly created cookie:" | |
| if (options.http === false && oldCookie.httpOnly) { | |
| // step 11.2 | |
| err = new Error("old Cookie is HttpOnly and this isn't an HTTP API"); | |
| return cb(options.ignoreError ? null : err); | |
| } | |
| cookie.creation = oldCookie.creation; // step 11.3 | |
| cookie.creationIndex = oldCookie.creationIndex; // preserve tie-breaker | |
| cookie.lastAccessed = now; | |
| // Step 11.4 (delete cookie) is implied by just setting the new one: | |
| store.updateCookie(oldCookie, cookie, next); // step 12 | |
| } else { | |
| cookie.creation = cookie.lastAccessed = now; | |
| store.putCookie(cookie, next); // step 12 | |
| } | |
| } | |
| store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie); | |
| } | |
| // RFC6365 S5.4 | |
| getCookies(url, options, cb) { | |
| validators.validate(validators.isUrlStringOrObject(url), cb, url); | |
| const context = getCookieContext(url); | |
| if (validators.isFunction(options)) { | |
| cb = options; | |
| options = {}; | |
| } | |
| validators.validate(validators.isObject(options), cb, options); | |
| validators.validate(validators.isFunction(cb), cb); | |
| const host = canonicalDomain(context.hostname); | |
| const path = context.pathname || "/"; | |
| let secure = options.secure; | |
| if ( | |
| secure == null && | |
| context.protocol && | |
| (context.protocol == "https:" || context.protocol == "wss:") | |
| ) { | |
| secure = true; | |
| } | |
| let sameSiteLevel = 0; | |
| if (options.sameSiteContext) { | |
| const sameSiteContext = checkSameSiteContext(options.sameSiteContext); | |
| sameSiteLevel = Cookie.sameSiteLevel[sameSiteContext]; | |
| if (!sameSiteLevel) { | |
| return cb(new Error(SAME_SITE_CONTEXT_VAL_ERR)); | |
| } | |
| } | |
| let http = options.http; | |
| if (http == null) { | |
| http = true; | |
| } | |
| const now = options.now || Date.now(); | |
| const expireCheck = options.expire !== false; | |
| const allPaths = !!options.allPaths; | |
| const store = this.store; | |
| function matchingCookie(c) { | |
| // "Either: | |
| // The cookie's host-only-flag is true and the canonicalized | |
| // request-host is identical to the cookie's domain. | |
| // Or: | |
| // The cookie's host-only-flag is false and the canonicalized | |
| // request-host domain-matches the cookie's domain." | |
| if (c.hostOnly) { | |
| if (c.domain != host) { | |
| return false; | |
| } | |
| } else { | |
| if (!domainMatch(host, c.domain, false)) { | |
| return false; | |
| } | |
| } | |
| // "The request-uri's path path-matches the cookie's path." | |
| if (!allPaths && !pathMatch(path, c.path)) { | |
| return false; | |
| } | |
| // "If the cookie's secure-only-flag is true, then the request-uri's | |
| // scheme must denote a "secure" protocol" | |
| if (c.secure && !secure) { | |
| return false; | |
| } | |
| // "If the cookie's http-only-flag is true, then exclude the cookie if the | |
| // cookie-string is being generated for a "non-HTTP" API" | |
| if (c.httpOnly && !http) { | |
| return false; | |
| } | |
| // RFC6265bis-02 S5.3.7 | |
| if (sameSiteLevel) { | |
| const cookieLevel = Cookie.sameSiteLevel[c.sameSite || "none"]; | |
| if (cookieLevel > sameSiteLevel) { | |
| // only allow cookies at or below the request level | |
| return false; | |
| } | |
| } | |
| // deferred from S5.3 | |
| // non-RFC: allow retention of expired cookies by choice | |
| if (expireCheck && c.expiryTime() <= now) { | |
| store.removeCookie(c.domain, c.path, c.key, () => {}); // result ignored | |
| return false; | |
| } | |
| return true; | |
| } | |
| store.findCookies( | |
| host, | |
| allPaths ? null : path, | |
| this.allowSpecialUseDomain, | |
| (err, cookies) => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| cookies = cookies.filter(matchingCookie); | |
| // sorting of S5.4 part 2 | |
| if (options.sort !== false) { | |
| cookies = cookies.sort(cookieCompare); | |
| } | |
| // S5.4 part 3 | |
| const now = new Date(); | |
| for (const cookie of cookies) { | |
| cookie.lastAccessed = now; | |
| } | |
| // TODO persist lastAccessed | |
| cb(null, cookies); | |
| } | |
| ); | |
| } | |
| getCookieString(...args) { | |
| const cb = args.pop(); | |
| validators.validate(validators.isFunction(cb), cb); | |
| const next = function(err, cookies) { | |
| if (err) { | |
| cb(err); | |
| } else { | |
| cb( | |
| null, | |
| cookies | |
| .sort(cookieCompare) | |
| .map(c => c.cookieString()) | |
| .join("; ") | |
| ); | |
| } | |
| }; | |
| args.push(next); | |
| this.getCookies.apply(this, args); | |
| } | |
| getSetCookieStrings(...args) { | |
| const cb = args.pop(); | |
| validators.validate(validators.isFunction(cb), cb); | |
| const next = function(err, cookies) { | |
| if (err) { | |
| cb(err); | |
| } else { | |
| cb( | |
| null, | |
| cookies.map(c => { | |
| return c.toString(); | |
| }) | |
| ); | |
| } | |
| }; | |
| args.push(next); | |
| this.getCookies.apply(this, args); | |
| } | |
| serialize(cb) { | |
| validators.validate(validators.isFunction(cb), cb); | |
| let type = this.store.constructor.name; | |
| if (validators.isObject(type)) { | |
| type = null; | |
| } | |
| // update README.md "Serialization Format" if you change this, please! | |
| const serialized = { | |
| // The version of tough-cookie that serialized this jar. Generally a good | |
| // practice since future versions can make data import decisions based on | |
| // known past behavior. When/if this matters, use `semver`. | |
| version: `tough-cookie@${VERSION}`, | |
| // add the store type, to make humans happy: | |
| storeType: type, | |
| // CookieJar configuration: | |
| rejectPublicSuffixes: !!this.rejectPublicSuffixes, | |
| enableLooseMode: !!this.enableLooseMode, | |
| allowSpecialUseDomain: !!this.allowSpecialUseDomain, | |
| prefixSecurity: getNormalizedPrefixSecurity(this.prefixSecurity), | |
| // this gets filled from getAllCookies: | |
| cookies: [] | |
| }; | |
| if ( | |
| !( | |
| this.store.getAllCookies && | |
| typeof this.store.getAllCookies === "function" | |
| ) | |
| ) { | |
| return cb( | |
| new Error( | |
| "store does not support getAllCookies and cannot be serialized" | |
| ) | |
| ); | |
| } | |
| this.store.getAllCookies((err, cookies) => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| serialized.cookies = cookies.map(cookie => { | |
| // convert to serialized 'raw' cookies | |
| cookie = cookie instanceof Cookie ? cookie.toJSON() : cookie; | |
| // Remove the index so new ones get assigned during deserialization | |
| delete cookie.creationIndex; | |
| return cookie; | |
| }); | |
| return cb(null, serialized); | |
| }); | |
| } | |
| toJSON() { | |
| return this.serializeSync(); | |
| } | |
| // use the class method CookieJar.deserialize instead of calling this directly | |
| _importCookies(serialized, cb) { | |
| let cookies = serialized.cookies; | |
| if (!cookies || !Array.isArray(cookies)) { | |
| return cb(new Error("serialized jar has no cookies array")); | |
| } | |
| cookies = cookies.slice(); // do not modify the original | |
| const putNext = err => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| if (!cookies.length) { | |
| return cb(err, this); | |
| } | |
| let cookie; | |
| try { | |
| cookie = fromJSON(cookies.shift()); | |
| } catch (e) { | |
| return cb(e); | |
| } | |
| if (cookie === null) { | |
| return putNext(null); // skip this cookie | |
| } | |
| this.store.putCookie(cookie, putNext); | |
| }; | |
| putNext(); | |
| } | |
| clone(newStore, cb) { | |
| if (arguments.length === 1) { | |
| cb = newStore; | |
| newStore = null; | |
| } | |
| this.serialize((err, serialized) => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| CookieJar.deserialize(serialized, newStore, cb); | |
| }); | |
| } | |
| cloneSync(newStore) { | |
| if (arguments.length === 0) { | |
| return this._cloneSync(); | |
| } | |
| if (!newStore.synchronous) { | |
| throw new Error( | |
| "CookieJar clone destination store is not synchronous; use async API instead." | |
| ); | |
| } | |
| return this._cloneSync(newStore); | |
| } | |
| removeAllCookies(cb) { | |
| validators.validate(validators.isFunction(cb), cb); | |
| const store = this.store; | |
| // Check that the store implements its own removeAllCookies(). The default | |
| // implementation in Store will immediately call the callback with a "not | |
| // implemented" Error. | |
| if ( | |
| typeof store.removeAllCookies === "function" && | |
| store.removeAllCookies !== Store.prototype.removeAllCookies | |
| ) { | |
| return store.removeAllCookies(cb); | |
| } | |
| store.getAllCookies((err, cookies) => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| if (cookies.length === 0) { | |
| return cb(null); | |
| } | |
| let completedCount = 0; | |
| const removeErrors = []; | |
| function removeCookieCb(removeErr) { | |
| if (removeErr) { | |
| removeErrors.push(removeErr); | |
| } | |
| completedCount++; | |
| if (completedCount === cookies.length) { | |
| return cb(removeErrors.length ? removeErrors[0] : null); | |
| } | |
| } | |
| cookies.forEach(cookie => { | |
| store.removeCookie( | |
| cookie.domain, | |
| cookie.path, | |
| cookie.key, | |
| removeCookieCb | |
| ); | |
| }); | |
| }); | |
| } | |
| static deserialize(strOrObj, store, cb) { | |
| if (arguments.length !== 3) { | |
| // store is optional | |
| cb = store; | |
| store = null; | |
| } | |
| validators.validate(validators.isFunction(cb), cb); | |
| let serialized; | |
| if (typeof strOrObj === "string") { | |
| serialized = jsonParse(strOrObj); | |
| if (serialized instanceof Error) { | |
| return cb(serialized); | |
| } | |
| } else { | |
| serialized = strOrObj; | |
| } | |
| const jar = new CookieJar(store, { | |
| rejectPublicSuffixes: serialized.rejectPublicSuffixes, | |
| looseMode: serialized.enableLooseMode, | |
| allowSpecialUseDomain: serialized.allowSpecialUseDomain, | |
| prefixSecurity: serialized.prefixSecurity | |
| }); | |
| jar._importCookies(serialized, err => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| cb(null, jar); | |
| }); | |
| } | |
| static deserializeSync(strOrObj, store) { | |
| const serialized = | |
| typeof strOrObj === "string" ? JSON.parse(strOrObj) : strOrObj; | |
| const jar = new CookieJar(store, { | |
| rejectPublicSuffixes: serialized.rejectPublicSuffixes, | |
| looseMode: serialized.enableLooseMode | |
| }); | |
| // catch this mistake early: | |
| if (!jar.store.synchronous) { | |
| throw new Error( | |
| "CookieJar store is not synchronous; use async API instead." | |
| ); | |
| } | |
| jar._importCookiesSync(serialized); | |
| return jar; | |
| } | |
| } | |
| CookieJar.fromJSON = CookieJar.deserializeSync; | |
| [ | |
| "_importCookies", | |
| "clone", | |
| "getCookies", | |
| "getCookieString", | |
| "getSetCookieStrings", | |
| "removeAllCookies", | |
| "serialize", | |
| "setCookie" | |
| ].forEach(name => { | |
| CookieJar.prototype[name] = fromCallback(CookieJar.prototype[name]); | |
| }); | |
| CookieJar.deserialize = fromCallback(CookieJar.deserialize); | |
| // Use a closure to provide a true imperative API for synchronous stores. | |
| function syncWrap(method) { | |
| return function(...args) { | |
| if (!this.store.synchronous) { | |
| throw new Error( | |
| "CookieJar store is not synchronous; use async API instead." | |
| ); | |
| } | |
| let syncErr, syncResult; | |
| this[method](...args, (err, result) => { | |
| syncErr = err; | |
| syncResult = result; | |
| }); | |
| if (syncErr) { | |
| throw syncErr; | |
| } | |
| return syncResult; | |
| }; | |
| } | |
| exports.version = VERSION; | |
| exports.CookieJar = CookieJar; | |
| exports.Cookie = Cookie; | |
| exports.Store = Store; | |
| exports.MemoryCookieStore = MemoryCookieStore; | |
| exports.parseDate = parseDate; | |
| exports.formatDate = formatDate; | |
| exports.parse = parse; | |
| exports.fromJSON = fromJSON; | |
| exports.domainMatch = domainMatch; | |
| exports.defaultPath = defaultPath; | |
| exports.pathMatch = pathMatch; | |
| exports.getPublicSuffix = pubsuffix.getPublicSuffix; | |
| exports.cookieCompare = cookieCompare; | |
| exports.permuteDomain = require("./permuteDomain").permuteDomain; | |
| exports.permutePath = permutePath; | |
| exports.canonicalDomain = canonicalDomain; | |
| exports.PrefixSecurityEnum = PrefixSecurityEnum; | |
| exports.ParameterError = validators.ParameterError; | |