1000 lines
40 KiB
JavaScript
1000 lines
40 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.CookieJar = void 0;
|
|
const getPublicSuffix_1 = require("../getPublicSuffix");
|
|
const validators = __importStar(require("../validators"));
|
|
const validators_1 = require("../validators");
|
|
const store_1 = require("../store");
|
|
const memstore_1 = require("../memstore");
|
|
const pathMatch_1 = require("../pathMatch");
|
|
const cookie_1 = require("./cookie");
|
|
const utils_1 = require("../utils");
|
|
const canonicalDomain_1 = require("./canonicalDomain");
|
|
const constants_1 = require("./constants");
|
|
const defaultPath_1 = require("./defaultPath");
|
|
const domainMatch_1 = require("./domainMatch");
|
|
const cookieCompare_1 = require("./cookieCompare");
|
|
const version_1 = require("../version");
|
|
const defaultSetCookieOptions = {
|
|
loose: false,
|
|
sameSiteContext: undefined,
|
|
ignoreError: false,
|
|
http: true,
|
|
};
|
|
const defaultGetCookieOptions = {
|
|
http: true,
|
|
expire: true,
|
|
allPaths: false,
|
|
sameSiteContext: undefined,
|
|
sort: undefined,
|
|
};
|
|
const SAME_SITE_CONTEXT_VAL_ERR = 'Invalid sameSiteContext option for getCookies(); expected one of "strict", "lax", or "none"';
|
|
function getCookieContext(url) {
|
|
if (url &&
|
|
typeof url === 'object' &&
|
|
'hostname' in url &&
|
|
typeof url.hostname === 'string' &&
|
|
'pathname' in url &&
|
|
typeof url.pathname === 'string' &&
|
|
'protocol' in url &&
|
|
typeof url.protocol === 'string') {
|
|
return {
|
|
hostname: url.hostname,
|
|
pathname: url.pathname,
|
|
protocol: url.protocol,
|
|
};
|
|
}
|
|
else if (typeof url === 'string') {
|
|
try {
|
|
return new URL(decodeURI(url));
|
|
}
|
|
catch {
|
|
return new URL(url);
|
|
}
|
|
}
|
|
else {
|
|
throw new validators_1.ParameterError('`url` argument is not a string or URL.');
|
|
}
|
|
}
|
|
function checkSameSiteContext(value) {
|
|
const context = String(value).toLowerCase();
|
|
if (context === 'none' || context === 'lax' || context === 'strict') {
|
|
return context;
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
/**
|
|
* 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) {
|
|
const startsWithSecurePrefix = typeof cookie.key === 'string' && cookie.key.startsWith('__Secure-');
|
|
return !startsWithSecurePrefix || 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) {
|
|
const startsWithHostPrefix = typeof cookie.key === 'string' && cookie.key.startsWith('__Host-');
|
|
return (!startsWithHostPrefix ||
|
|
Boolean(cookie.secure &&
|
|
cookie.hostOnly &&
|
|
cookie.path != null &&
|
|
cookie.path === '/'));
|
|
}
|
|
function getNormalizedPrefixSecurity(prefixSecurity) {
|
|
const normalizedPrefixSecurity = prefixSecurity.toLowerCase();
|
|
/* The three supported options */
|
|
switch (normalizedPrefixSecurity) {
|
|
case constants_1.PrefixSecurityEnum.STRICT:
|
|
case constants_1.PrefixSecurityEnum.SILENT:
|
|
case constants_1.PrefixSecurityEnum.DISABLED:
|
|
return normalizedPrefixSecurity;
|
|
default:
|
|
return constants_1.PrefixSecurityEnum.SILENT;
|
|
}
|
|
}
|
|
/**
|
|
* A CookieJar is for storage and retrieval of {@link Cookie} objects as defined in
|
|
* {@link https://www.rfc-editor.org/rfc/rfc6265.html#section-5.3 | RFC6265 - Section 5.3}.
|
|
*
|
|
* It also supports a pluggable persistence layer via {@link Store}.
|
|
* @public
|
|
*/
|
|
class CookieJar {
|
|
/**
|
|
* Creates a new `CookieJar` instance.
|
|
*
|
|
* @remarks
|
|
* - If a custom store is not passed to the constructor, an in-memory store ({@link MemoryCookieStore} will be created and used.
|
|
* - If a boolean value is passed as the `options` parameter, this is equivalent to passing `{ rejectPublicSuffixes: <value> }`
|
|
*
|
|
* @param store - a custom {@link Store} implementation (defaults to {@link MemoryCookieStore})
|
|
* @param options - configures how cookies are processed by the cookie jar
|
|
*/
|
|
constructor(store, options) {
|
|
if (typeof options === 'boolean') {
|
|
options = { rejectPublicSuffixes: options };
|
|
}
|
|
this.rejectPublicSuffixes = options?.rejectPublicSuffixes ?? true;
|
|
this.enableLooseMode = options?.looseMode ?? false;
|
|
this.allowSpecialUseDomain = options?.allowSpecialUseDomain ?? true;
|
|
this.prefixSecurity = getNormalizedPrefixSecurity(options?.prefixSecurity ?? 'silent');
|
|
this.store = store ?? new memstore_1.MemoryCookieStore();
|
|
}
|
|
callSync(fn) {
|
|
if (!this.store.synchronous) {
|
|
throw new Error('CookieJar store is not synchronous; use async API instead.');
|
|
}
|
|
let syncErr = null;
|
|
let syncResult = undefined;
|
|
try {
|
|
fn.call(this, (error, result) => {
|
|
syncErr = error;
|
|
syncResult = result;
|
|
});
|
|
}
|
|
catch (err) {
|
|
syncErr = err;
|
|
}
|
|
if (syncErr)
|
|
throw syncErr;
|
|
return syncResult;
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
setCookie(cookie, url, options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
const cb = promiseCallback.callback;
|
|
let context;
|
|
try {
|
|
if (typeof url === 'string') {
|
|
validators.validate(validators.isNonEmptyString(url), callback, (0, utils_1.safeToString)(options));
|
|
}
|
|
context = getCookieContext(url);
|
|
if (typeof url === 'function') {
|
|
return promiseCallback.reject(new Error('No URL was specified'));
|
|
}
|
|
if (typeof options === 'function') {
|
|
options = defaultSetCookieOptions;
|
|
}
|
|
validators.validate(typeof cb === 'function', cb);
|
|
if (!validators.isNonEmptyString(cookie) &&
|
|
!validators.isObject(cookie) &&
|
|
cookie instanceof String &&
|
|
cookie.length == 0) {
|
|
return promiseCallback.resolve(undefined);
|
|
}
|
|
}
|
|
catch (err) {
|
|
return promiseCallback.reject(err);
|
|
}
|
|
const host = (0, canonicalDomain_1.canonicalDomain)(context.hostname) ?? null;
|
|
const loose = options?.loose || this.enableLooseMode;
|
|
let sameSiteContext = null;
|
|
if (options?.sameSiteContext) {
|
|
sameSiteContext = checkSameSiteContext(options.sameSiteContext);
|
|
if (!sameSiteContext) {
|
|
return promiseCallback.reject(new Error(SAME_SITE_CONTEXT_VAL_ERR));
|
|
}
|
|
}
|
|
// S5.3 step 1
|
|
if (typeof cookie === 'string' || cookie instanceof String) {
|
|
const parsedCookie = cookie_1.Cookie.parse(cookie.toString(), { loose: loose });
|
|
if (!parsedCookie) {
|
|
const err = new Error('Cookie failed to parse');
|
|
return options?.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(err);
|
|
}
|
|
cookie = parsedCookie;
|
|
}
|
|
else if (!(cookie instanceof cookie_1.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.
|
|
const err = new Error('First argument to setCookie must be a Cookie object or string');
|
|
return options?.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(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) {
|
|
try {
|
|
const cdomain = cookie.cdomain();
|
|
const suffix = typeof cdomain === 'string'
|
|
? (0, getPublicSuffix_1.getPublicSuffix)(cdomain, {
|
|
allowSpecialUseDomain: this.allowSpecialUseDomain,
|
|
ignoreError: options?.ignoreError,
|
|
})
|
|
: null;
|
|
if (suffix == null && !constants_1.IP_V6_REGEX_OBJECT.test(cookie.domain)) {
|
|
// e.g. "com"
|
|
const err = new Error('Cookie has domain set to a public suffix');
|
|
return options?.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(err);
|
|
}
|
|
// Using `any` here rather than `unknown` to avoid a type assertion, at the cost of needing
|
|
// to disable eslint directives. It's easier to have this one spot of technically incorrect
|
|
// types, rather than having to deal with _all_ callback errors being `unknown`.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}
|
|
catch (err) {
|
|
return options?.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
promiseCallback.reject(err);
|
|
}
|
|
}
|
|
// S5.3 step 6:
|
|
if (cookie.domain) {
|
|
if (!(0, domainMatch_1.domainMatch)(host ?? undefined, cookie.cdomain() ?? undefined, false)) {
|
|
const err = new Error(`Cookie not in this host's domain. Cookie:${cookie.cdomain() ?? 'null'} Request:${host ?? 'null'}`);
|
|
return options?.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(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 = (0, defaultPath_1.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) {
|
|
const err = new Error("Cookie is HttpOnly and this isn't an HTTP API");
|
|
return options.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(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') {
|
|
const err = new Error('Cookie is SameSite but this is a cross-origin request');
|
|
return options?.ignoreError
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(err);
|
|
}
|
|
}
|
|
/* 6265bis-02 S5.4 Steps 15 & 16 */
|
|
const ignoreErrorForPrefixSecurity = this.prefixSecurity === constants_1.PrefixSecurityEnum.SILENT;
|
|
const prefixSecurityDisabled = this.prefixSecurity === constants_1.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 options?.ignoreError || ignoreErrorForPrefixSecurity
|
|
? promiseCallback.resolve(undefined)
|
|
: promiseCallback.reject(new Error(errorMsg));
|
|
}
|
|
}
|
|
const store = this.store;
|
|
// TODO: It feels weird to be manipulating the store as a side effect of a method.
|
|
// We should either do it in the constructor or not at all.
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (!store.updateCookie) {
|
|
store.updateCookie = async function (_oldCookie, newCookie, cb) {
|
|
return this.putCookie(newCookie).then(() => cb?.(null), (error) => cb?.(error));
|
|
};
|
|
}
|
|
const withCookie = function withCookie(err, oldCookie) {
|
|
if (err) {
|
|
cb(err);
|
|
return;
|
|
}
|
|
const next = function (err) {
|
|
if (err) {
|
|
cb(err);
|
|
}
|
|
else if (typeof cookie === 'string') {
|
|
cb(null, undefined);
|
|
}
|
|
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' in options &&
|
|
options.http === false &&
|
|
oldCookie.httpOnly) {
|
|
// step 11.2
|
|
err = new Error("old Cookie is HttpOnly and this isn't an HTTP API");
|
|
if (options.ignoreError)
|
|
cb(null, undefined);
|
|
else
|
|
cb(err);
|
|
return;
|
|
}
|
|
if (cookie instanceof cookie_1.Cookie) {
|
|
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 {
|
|
if (cookie instanceof cookie_1.Cookie) {
|
|
cookie.creation = cookie.lastAccessed = now;
|
|
store.putCookie(cookie, next); // step 12
|
|
}
|
|
}
|
|
};
|
|
// TODO: Refactor to avoid using a callback
|
|
store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie);
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* Synchronously attempt to set the {@link Cookie} in the {@link CookieJar}.
|
|
*
|
|
* <strong>Note:</strong> Only works if the configured {@link Store} is also synchronous.
|
|
*
|
|
* @remarks
|
|
* - If successfully persisted, the {@link Cookie} will have updated
|
|
* {@link Cookie.creation}, {@link Cookie.lastAccessed} and {@link Cookie.hostOnly}
|
|
* properties.
|
|
*
|
|
* - As per the RFC, the {@link Cookie.hostOnly} flag is set if there was no `Domain={value}`
|
|
* atttribute on the cookie string. The {@link Cookie.domain} property is set to the
|
|
* fully-qualified hostname of `currentUrl` in this case. Matching this cookie requires an
|
|
* exact hostname match (not a {@link domainMatch} as per usual)
|
|
*
|
|
* @param cookie - The cookie object or cookie string to store. A string value will be parsed into a cookie using {@link Cookie.parse}.
|
|
* @param url - The domain to store the cookie with.
|
|
* @param options - Configuration settings to use when storing the cookie.
|
|
* @public
|
|
*/
|
|
setCookieSync(cookie, url, options) {
|
|
const setCookieFn = options
|
|
? this.setCookie.bind(this, cookie, url, options)
|
|
: this.setCookie.bind(this, cookie, url);
|
|
return this.callSync(setCookieFn);
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
getCookies(url, options, callback) {
|
|
// RFC6365 S5.4
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = defaultGetCookieOptions;
|
|
}
|
|
else if (options === undefined) {
|
|
options = defaultGetCookieOptions;
|
|
}
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
const cb = promiseCallback.callback;
|
|
let context;
|
|
try {
|
|
if (typeof url === 'string') {
|
|
validators.validate(validators.isNonEmptyString(url), cb, url);
|
|
}
|
|
context = getCookieContext(url);
|
|
validators.validate(validators.isObject(options), cb, (0, utils_1.safeToString)(options));
|
|
validators.validate(typeof cb === 'function', cb);
|
|
}
|
|
catch (parameterError) {
|
|
return promiseCallback.reject(parameterError);
|
|
}
|
|
const host = (0, canonicalDomain_1.canonicalDomain)(context.hostname);
|
|
const path = context.pathname || '/';
|
|
const secure = context.protocol &&
|
|
(context.protocol == 'https:' || context.protocol == 'wss:');
|
|
let sameSiteLevel = 0;
|
|
if (options.sameSiteContext) {
|
|
const sameSiteContext = checkSameSiteContext(options.sameSiteContext);
|
|
if (sameSiteContext == null) {
|
|
return promiseCallback.reject(new Error(SAME_SITE_CONTEXT_VAL_ERR));
|
|
}
|
|
sameSiteLevel = cookie_1.Cookie.sameSiteLevel[sameSiteContext];
|
|
if (!sameSiteLevel) {
|
|
return promiseCallback.reject(new Error(SAME_SITE_CONTEXT_VAL_ERR));
|
|
}
|
|
}
|
|
const http = options.http ?? true;
|
|
const now = Date.now();
|
|
const expireCheck = options.expire ?? true;
|
|
const allPaths = options.allPaths ?? false;
|
|
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 (!(0, domainMatch_1.domainMatch)(host ?? undefined, c.domain ?? undefined, false)) {
|
|
return false;
|
|
}
|
|
}
|
|
// "The request-uri's path path-matches the cookie's path."
|
|
if (!allPaths && typeof c.path === 'string' && !(0, pathMatch_1.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) {
|
|
let cookieLevel;
|
|
if (c.sameSite === 'lax') {
|
|
cookieLevel = cookie_1.Cookie.sameSiteLevel.lax;
|
|
}
|
|
else if (c.sameSite === 'strict') {
|
|
cookieLevel = cookie_1.Cookie.sameSiteLevel.strict;
|
|
}
|
|
else {
|
|
cookieLevel = cookie_1.Cookie.sameSiteLevel.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
|
|
const expiryTime = c.expiryTime();
|
|
if (expireCheck && expiryTime && 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) {
|
|
cb(err);
|
|
return;
|
|
}
|
|
if (cookies == null) {
|
|
cb(null, []);
|
|
return;
|
|
}
|
|
cookies = cookies.filter(matchingCookie);
|
|
// sorting of S5.4 part 2
|
|
if ('sort' in options && options.sort !== false) {
|
|
cookies = cookies.sort(cookieCompare_1.cookieCompare);
|
|
}
|
|
// S5.4 part 3
|
|
const now = new Date();
|
|
for (const cookie of cookies) {
|
|
cookie.lastAccessed = now;
|
|
}
|
|
// TODO persist lastAccessed
|
|
cb(null, cookies);
|
|
});
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* Synchronously retrieve the list of cookies that can be sent in a Cookie header for the
|
|
* current URL.
|
|
*
|
|
* <strong>Note</strong>: Only works if the configured Store is also synchronous.
|
|
*
|
|
* @remarks
|
|
* - The array of cookies returned will be sorted according to {@link cookieCompare}.
|
|
*
|
|
* - The {@link Cookie.lastAccessed} property will be updated on all returned cookies.
|
|
*
|
|
* @param url - The domain to store the cookie with.
|
|
* @param options - Configuration settings to use when retrieving the cookies.
|
|
*/
|
|
getCookiesSync(url, options) {
|
|
return this.callSync(this.getCookies.bind(this, url, options)) ?? [];
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
getCookieString(url, options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
const next = function (err, cookies) {
|
|
if (err) {
|
|
promiseCallback.callback(err);
|
|
}
|
|
else {
|
|
promiseCallback.callback(null, cookies
|
|
?.sort(cookieCompare_1.cookieCompare)
|
|
.map((c) => c.cookieString())
|
|
.join('; '));
|
|
}
|
|
};
|
|
this.getCookies(url, options, next);
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* Synchronous version of `.getCookieString()`. Accepts the same options as `.getCookies()` but returns a string suitable for a
|
|
* `Cookie` header rather than an Array.
|
|
*
|
|
* <strong>Note</strong>: Only works if the configured Store is also synchronous.
|
|
*
|
|
* @param url - The domain to store the cookie with.
|
|
* @param options - Configuration settings to use when retrieving the cookies.
|
|
*/
|
|
getCookieStringSync(url, options) {
|
|
return (this.callSync(options
|
|
? this.getCookieString.bind(this, url, options)
|
|
: this.getCookieString.bind(this, url)) ?? '');
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
getSetCookieStrings(url, options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
const next = function (err, cookies) {
|
|
if (err) {
|
|
promiseCallback.callback(err);
|
|
}
|
|
else {
|
|
promiseCallback.callback(null, cookies?.map((c) => {
|
|
return c.toString();
|
|
}));
|
|
}
|
|
};
|
|
this.getCookies(url, options, next);
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* Synchronous version of `.getSetCookieStrings()`. Returns an array of strings suitable for `Set-Cookie` headers.
|
|
* Accepts the same options as `.getCookies()`.
|
|
*
|
|
* <strong>Note</strong>: Only works if the configured Store is also synchronous.
|
|
*
|
|
* @param url - The domain to store the cookie with.
|
|
* @param options - Configuration settings to use when retrieving the cookies.
|
|
*/
|
|
getSetCookieStringsSync(url, options = {}) {
|
|
return (this.callSync(this.getSetCookieStrings.bind(this, url, options)) ?? []);
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
serialize(callback) {
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
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_1.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 (typeof this.store.getAllCookies !== 'function') {
|
|
return promiseCallback.reject(new Error('store does not support getAllCookies and cannot be serialized'));
|
|
}
|
|
this.store.getAllCookies((err, cookies) => {
|
|
if (err) {
|
|
promiseCallback.callback(err);
|
|
return;
|
|
}
|
|
if (cookies == null) {
|
|
promiseCallback.callback(null, serialized);
|
|
return;
|
|
}
|
|
serialized.cookies = cookies.map((cookie) => {
|
|
// convert to serialized 'raw' cookies
|
|
const serializedCookie = cookie.toJSON();
|
|
// Remove the index so new ones get assigned during deserialization
|
|
delete serializedCookie.creationIndex;
|
|
return serializedCookie;
|
|
});
|
|
promiseCallback.callback(null, serialized);
|
|
});
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* Serialize the CookieJar if the underlying store supports `.getAllCookies`.
|
|
*
|
|
* <strong>Note</strong>: Only works if the configured Store is also synchronous.
|
|
*/
|
|
serializeSync() {
|
|
return this.callSync((callback) => {
|
|
this.serialize(callback);
|
|
});
|
|
}
|
|
/**
|
|
* Alias of {@link CookieJar.serializeSync}. Allows the cookie to be serialized
|
|
* with `JSON.stringify(cookieJar)`.
|
|
*/
|
|
toJSON() {
|
|
return this.serializeSync();
|
|
}
|
|
/**
|
|
* Use the class method CookieJar.deserialize instead of calling this directly
|
|
* @internal
|
|
*/
|
|
_importCookies(serialized, callback) {
|
|
let cookies = undefined;
|
|
if (serialized &&
|
|
typeof serialized === 'object' &&
|
|
(0, utils_1.inOperator)('cookies', serialized) &&
|
|
Array.isArray(serialized.cookies)) {
|
|
cookies = serialized.cookies;
|
|
}
|
|
if (!cookies) {
|
|
callback(new Error('serialized jar has no cookies array'), undefined);
|
|
return;
|
|
}
|
|
cookies = cookies.slice(); // do not modify the original
|
|
const putNext = (err) => {
|
|
if (err) {
|
|
callback(err, undefined);
|
|
return;
|
|
}
|
|
if (Array.isArray(cookies)) {
|
|
if (!cookies.length) {
|
|
callback(err, this);
|
|
return;
|
|
}
|
|
let cookie;
|
|
try {
|
|
cookie = cookie_1.Cookie.fromJSON(cookies.shift());
|
|
}
|
|
catch (e) {
|
|
callback(e instanceof Error ? e : new Error(), undefined);
|
|
return;
|
|
}
|
|
if (cookie === undefined) {
|
|
putNext(null); // skip this cookie
|
|
return;
|
|
}
|
|
this.store.putCookie(cookie, putNext);
|
|
}
|
|
};
|
|
putNext(null);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_importCookiesSync(serialized) {
|
|
this.callSync(this._importCookies.bind(this, serialized));
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
clone(newStore, callback) {
|
|
if (typeof newStore === 'function') {
|
|
callback = newStore;
|
|
newStore = undefined;
|
|
}
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
const cb = promiseCallback.callback;
|
|
this.serialize((err, serialized) => {
|
|
if (err) {
|
|
return promiseCallback.reject(err);
|
|
}
|
|
return CookieJar.deserialize(serialized ?? '', newStore, cb);
|
|
});
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
_cloneSync(newStore) {
|
|
const cloneFn = newStore && typeof newStore !== 'function'
|
|
? this.clone.bind(this, newStore)
|
|
: this.clone.bind(this);
|
|
return this.callSync((callback) => {
|
|
cloneFn(callback);
|
|
});
|
|
}
|
|
/**
|
|
* Produces a deep clone of this CookieJar. Modifications to the original do
|
|
* not affect the clone, and vice versa.
|
|
*
|
|
* <strong>Note</strong>: Only works if both the configured Store and destination
|
|
* Store are synchronous.
|
|
*
|
|
* @remarks
|
|
* - When no {@link Store} is provided, a new {@link MemoryCookieStore} will be used.
|
|
*
|
|
* - Transferring between store types is supported so long as the source
|
|
* implements `.getAllCookies()` and the destination implements `.putCookie()`.
|
|
*
|
|
* @param newStore - The target {@link Store} to clone cookies into.
|
|
*/
|
|
cloneSync(newStore) {
|
|
if (!newStore) {
|
|
return this._cloneSync();
|
|
}
|
|
if (!newStore.synchronous) {
|
|
throw new Error('CookieJar clone destination store is not synchronous; use async API instead.');
|
|
}
|
|
return this._cloneSync(newStore);
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
removeAllCookies(callback) {
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
const cb = promiseCallback.callback;
|
|
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_1.Store.prototype.removeAllCookies) {
|
|
// `Callback<undefined>` and `ErrorCallback` are *technically* incompatible, but for the
|
|
// standard implementation `cb = (err, result) => {}`, they're essentially the same.
|
|
store.removeAllCookies(cb);
|
|
return promiseCallback.promise;
|
|
}
|
|
store.getAllCookies((err, cookies) => {
|
|
if (err) {
|
|
cb(err);
|
|
return;
|
|
}
|
|
if (!cookies) {
|
|
cookies = [];
|
|
}
|
|
if (cookies.length === 0) {
|
|
cb(null, undefined);
|
|
return;
|
|
}
|
|
let completedCount = 0;
|
|
const removeErrors = [];
|
|
// TODO: Refactor to avoid using callback
|
|
const removeCookieCb = function removeCookieCb(removeErr) {
|
|
if (removeErr) {
|
|
removeErrors.push(removeErr);
|
|
}
|
|
completedCount++;
|
|
if (completedCount === cookies.length) {
|
|
if (removeErrors[0])
|
|
cb(removeErrors[0]);
|
|
else
|
|
cb(null, undefined);
|
|
return;
|
|
}
|
|
};
|
|
cookies.forEach((cookie) => {
|
|
store.removeCookie(cookie.domain, cookie.path, cookie.key, removeCookieCb);
|
|
});
|
|
});
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* Removes all cookies from the CookieJar.
|
|
*
|
|
* <strong>Note</strong>: Only works if the configured Store is also synchronous.
|
|
*
|
|
* @remarks
|
|
* - This is a new backwards-compatible feature of tough-cookie version 2.5,
|
|
* so not all Stores will implement it efficiently. For Stores that do not
|
|
* implement `removeAllCookies`, the fallback is to call `removeCookie` after
|
|
* `getAllCookies`.
|
|
*
|
|
* - If `getAllCookies` fails or isn't implemented in the Store, an error is returned.
|
|
*
|
|
* - If one or more of the `removeCookie` calls fail, only the first error is returned.
|
|
*/
|
|
removeAllCookiesSync() {
|
|
this.callSync((callback) => {
|
|
// `Callback<undefined>` and `ErrorCallback` are *technically* incompatible, but for the
|
|
// standard implementation `cb = (err, result) => {}`, they're essentially the same.
|
|
this.removeAllCookies(callback);
|
|
});
|
|
}
|
|
/**
|
|
* @internal No doc because this is the overload implementation
|
|
*/
|
|
static deserialize(strOrObj, store, callback) {
|
|
if (typeof store === 'function') {
|
|
callback = store;
|
|
store = undefined;
|
|
}
|
|
const promiseCallback = (0, utils_1.createPromiseCallback)(callback);
|
|
let serialized;
|
|
if (typeof strOrObj === 'string') {
|
|
try {
|
|
serialized = JSON.parse(strOrObj);
|
|
}
|
|
catch (e) {
|
|
return promiseCallback.reject(e instanceof Error ? e : new Error());
|
|
}
|
|
}
|
|
else {
|
|
serialized = strOrObj;
|
|
}
|
|
const readSerializedProperty = (property) => {
|
|
return serialized &&
|
|
typeof serialized === 'object' &&
|
|
(0, utils_1.inOperator)(property, serialized)
|
|
? serialized[property]
|
|
: undefined;
|
|
};
|
|
const readSerializedBoolean = (property) => {
|
|
const value = readSerializedProperty(property);
|
|
return typeof value === 'boolean' ? value : undefined;
|
|
};
|
|
const readSerializedString = (property) => {
|
|
const value = readSerializedProperty(property);
|
|
return typeof value === 'string' ? value : undefined;
|
|
};
|
|
const jar = new CookieJar(store, {
|
|
rejectPublicSuffixes: readSerializedBoolean('rejectPublicSuffixes'),
|
|
looseMode: readSerializedBoolean('enableLooseMode'),
|
|
allowSpecialUseDomain: readSerializedBoolean('allowSpecialUseDomain'),
|
|
prefixSecurity: getNormalizedPrefixSecurity(readSerializedString('prefixSecurity') ?? 'silent'),
|
|
});
|
|
jar._importCookies(serialized, (err) => {
|
|
if (err) {
|
|
promiseCallback.callback(err);
|
|
return;
|
|
}
|
|
promiseCallback.callback(null, jar);
|
|
});
|
|
return promiseCallback.promise;
|
|
}
|
|
/**
|
|
* A new CookieJar is created and the serialized {@link Cookie} values are added to
|
|
* the underlying store. Each {@link Cookie} is added via `store.putCookie(...)` in
|
|
* the order in which they appear in the serialization.
|
|
*
|
|
* <strong>Note</strong>: Only works if the configured Store is also synchronous.
|
|
*
|
|
* @remarks
|
|
* - When no {@link Store} is provided, a new {@link MemoryCookieStore} will be used.
|
|
*
|
|
* - As a convenience, if `strOrObj` is a string, it is passed through `JSON.parse` first.
|
|
*
|
|
* @param strOrObj - A JSON string or object representing the deserialized cookies.
|
|
* @param store - The underlying store to persist the deserialized cookies into.
|
|
*/
|
|
static deserializeSync(strOrObj, store) {
|
|
const serialized = typeof strOrObj === 'string' ? JSON.parse(strOrObj) : strOrObj;
|
|
const readSerializedProperty = (property) => {
|
|
return serialized &&
|
|
typeof serialized === 'object' &&
|
|
(0, utils_1.inOperator)(property, serialized)
|
|
? serialized[property]
|
|
: undefined;
|
|
};
|
|
const readSerializedBoolean = (property) => {
|
|
const value = readSerializedProperty(property);
|
|
return typeof value === 'boolean' ? value : undefined;
|
|
};
|
|
const readSerializedString = (property) => {
|
|
const value = readSerializedProperty(property);
|
|
return typeof value === 'string' ? value : undefined;
|
|
};
|
|
const jar = new CookieJar(store, {
|
|
rejectPublicSuffixes: readSerializedBoolean('rejectPublicSuffixes'),
|
|
looseMode: readSerializedBoolean('enableLooseMode'),
|
|
allowSpecialUseDomain: readSerializedBoolean('allowSpecialUseDomain'),
|
|
prefixSecurity: getNormalizedPrefixSecurity(readSerializedString('prefixSecurity') ?? 'silent'),
|
|
});
|
|
// 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;
|
|
}
|
|
/**
|
|
* Alias of {@link CookieJar.deserializeSync}.
|
|
*
|
|
* @remarks
|
|
* - When no {@link Store} is provided, a new {@link MemoryCookieStore} will be used.
|
|
*
|
|
* - As a convenience, if `strOrObj` is a string, it is passed through `JSON.parse` first.
|
|
*
|
|
* @param jsonString - A JSON string or object representing the deserialized cookies.
|
|
* @param store - The underlying store to persist the deserialized cookies into.
|
|
*/
|
|
static fromJSON(jsonString, store) {
|
|
return CookieJar.deserializeSync(jsonString, store);
|
|
}
|
|
}
|
|
exports.CookieJar = CookieJar;
|