tough-cookie-file-store
Version:
A JSON file store for tough-cookie module
889 lines • 33.6 kB
JavaScript
import * as tough from 'tough-cookie';
import * as fs from 'fs';
import * as util from 'util';
import { parseCookiesJson, parseCookiesJsonObject, stringifyCookiesJson } from './formats/cookies-json.js';
import { parseNetscapeCookiesTxtToIndex, stringifyNetscapeCookiesTxt } from './formats/netscape-cookies-txt.js';
export var FileFormat;
(function (FileFormat) {
FileFormat["json"] = "json";
FileFormat["txt"] = "txt";
})(FileFormat || (FileFormat = {}));
export const DefaultFileFormat = FileFormat.json;
/**
* Class representing a JSON file store.
*
* @augments Store
*/
class FileCookieStore extends tough.Store {
/**
* Creates a new JSON file store in the specified file.
*
* @param {string} filePath - The file in which the store will be created.
* @param {object} options - Options for initializing the store.
* @param {boolean} options.async - Whether to write the file asynchronously.
* @param {boolean} options.loadAsync - Whether to read the file asynchronously.
* @param {FileFormat} options.fileFormat - The format of the file. If not specified, the format will be auto-detected. If not specified and the file doesn't exist, the format will default to txt.
* @param {boolean} options.forceParse - Continue parse file and don't throw exception if a bad line was found. This only applies to the txt format.
* @param {boolean} options.httpOnlyExtension - Detect the #HttpOnly_ prefix for http only cookies. Defaults to true.
* @param {Function} options.onLoad - Called when the file successfuly loads asynchronously. Unused if `loadAsync` is false.
* @param {Function} options.onLoadError - Optional callback for any async file-load error. Unused if `loadAsync` is false.
*/
constructor(filePath, options) {
var _a, _b;
super();
this.idx = {};
this.synchronous = !(options === null || options === void 0 ? void 0 : options.async);
this.filePath = filePath;
this.fileFormat = (options === null || options === void 0 ? void 0 : options.fileFormat) || undefined;
this.idx = {};
this.httpOnlyExtension = (_a = options === null || options === void 0 ? void 0 : options.httpOnlyExtension) !== null && _a !== void 0 ? _a : true;
if (util.inspect.custom) {
this[util.inspect.custom] = this._inspect;
}
// load from file
if (!filePath) {
throw new Error('Unknown file for read/write cookies');
}
const loadOptions = {
forceParse: (_b = options === null || options === void 0 ? void 0 : options.forceParse) !== null && _b !== void 0 ? _b : false,
onLineError: options === null || options === void 0 ? void 0 : options.onLoadLineError
};
if (options === null || options === void 0 ? void 0 : options.loadAsync) {
const promise = this._loadFromFileAsync(this.filePath, loadOptions);
this._readPromise = promise;
promise.then((exists) => {
var _a;
delete this._readPromise;
// istanbul ignore next
(_a = options === null || options === void 0 ? void 0 : options.onLoad) === null || _a === void 0 ? void 0 : _a.call(options, exists);
}, (error) => {
delete this._readPromise;
// istanbul ignore next
if (options === null || options === void 0 ? void 0 : options.onLoadError) {
options.onLoadError(error);
}
else {
// istanbul ignore next
console.error(error);
}
}).catch(
// istanbul ignore next
(error) => {
// istanbul ignore next
console.error(error);
});
}
else {
this._loadFromFileSync(this.filePath, loadOptions);
}
}
/**
* Waits for the initial load to finish if unfinished, and then performs the given synchronous read action.
* Afterwards, the callback will be called with an error or a result. If no callback is passed, a promise
* will be returned instead.
* @param {Function} action - The synchronous read action to execute
* @param {Function} cb - The callback to call with the error or result
* @returns {Promise} a promise if no callback was passed.
*/
_doSyncReadAsAsync(action, cb) {
if (this._readPromise) {
// wait for read promise to finish
const promise = this._readPromise;
if (typeof cb === 'function') {
// handle with callback
const continueFunc = () => {
try {
let result;
try {
result = action();
}
catch (error) /* istanbul ignore next */ {
cb(error, undefined);
return;
}
cb(null, result);
}
catch (error) {
// istanbul ignore next
console.error(error);
}
};
promise.then(continueFunc, continueFunc);
}
else {
// handle with promise
const continueFunc = () => action();
return promise.then(continueFunc, continueFunc);
}
}
else {
// do action immediately
if (typeof cb === 'function') {
let result;
try {
result = action();
}
catch (error) /* istanbul ignore next */ {
cb(error, undefined);
return;
}
cb(null, result);
}
else {
return (async () => action())();
}
}
}
/**
* Waits for the initial load to finish if unfinished, and then performs the given synchronous write action.
* Afterwards, if the store has changed, then changes to the store will be saved to its file, and then
* the callback will be called with an error if any, or `null` if no error. If no callback is passed, a
* promise will be returned instead.
* @param {Function} action - The synchronous write action to execute. This should return a boolean indicating whether the store has changed.
* @param {Function} cb - The callback to call with the error or result
* @returns {Promise} a promise if no callback was passed.
*/
_doSyncWriteAsAsync(action, cb) {
if (this._readPromise) {
// wait for read promise to finish
const promise = this._readPromise;
if (typeof cb === 'function') {
// handle with callback
const continueFunc = () => {
let done = false;
try {
// perform write action
if (action()) {
// save to file
this._saveAsync((error) => {
// done
// istanbul ignore next
if (!done) {
done = true;
cb(error);
}
else {
// istanbul ignore next
console.error(error);
}
});
}
else {
// no need to save to file, so done
done = true;
cb(null);
}
}
catch (error) /* istanbul ignore next */ {
// only pass error to callback if it hasnt been called yet
if (!done) {
done = true;
cb(error);
}
else {
console.error(error);
}
}
};
promise.then(continueFunc, continueFunc);
}
else {
// handle with promise
const continueFunc = () => {
if (action()) {
return this._saveAsync();
}
};
return promise.then(continueFunc, continueFunc);
}
}
else {
// do action immediately
let changed;
try {
changed = action();
}
catch (error) /* istanbul ignore next */ {
if (typeof cb === 'function') {
cb(error);
return;
}
else {
return Promise.reject(error);
}
}
if (changed) {
return this._saveAsync(cb);
}
else {
if (typeof cb === 'function') {
cb(null);
}
else {
return Promise.resolve();
}
}
}
}
/** @inheritdoc */
findCookie(domain, path, key, cb) {
if (this.synchronous) {
if (typeof cb === 'function') {
let cookie;
try {
cookie = this._findCookieSync(domain, path, key);
}
catch (error) /* istanbul ignore next */ {
cb(error, undefined);
return;
}
cb(null, cookie);
}
else {
return (async () => this._findCookieSync(domain, path, key))();
}
}
else {
return this._findCookieAsync(domain, path, key, cb);
}
}
/**
* Searches for a cookie after waiting for the initial read to finish.
* @see _doSyncReadAsAsync
* @param {string} domain - The cookie domain.
* @param {string} path - The cookie path.
* @param {string} key - The cookie key.
* @param {Function} cb - The callback that will be called with the result.
* @returns {Promise<tough.Cookie>} a promise if no callback was passed.
*/
_findCookieAsync(domain, path, key, cb) {
return this._doSyncReadAsAsync(() => this._findCookieSync(domain, path, key), cb);
}
/**
* Searches for a cookie and returns it or null.
* @param {string} domain - The cookie domain.
* @param {string} path - The cookie path.
* @param {string} key - The cookie key.
* @returns {Cookie} the matching cookie if found.
*/
_findCookieSync(domain, path, key) {
var _a, _b;
// istanbul ignore next
if (domain == null || path == null || key == null) {
return undefined;
}
return (_b = (_a = this.idx[domain]) === null || _a === void 0 ? void 0 : _a[path]) === null || _b === void 0 ? void 0 : _b[key];
}
/** @inheritdoc */
findCookies(domain, path, allowSpecialUseDomain, cb) {
if (typeof allowSpecialUseDomain === 'function') {
cb = allowSpecialUseDomain;
allowSpecialUseDomain = undefined;
}
// istanbul ignore next
allowSpecialUseDomain !== null && allowSpecialUseDomain !== void 0 ? allowSpecialUseDomain : (allowSpecialUseDomain = false);
if (this.synchronous) {
if (typeof cb === 'function') {
let cookies;
try {
cookies = this._findCookiesSync(domain, path, allowSpecialUseDomain);
}
catch (error) /* istanbul ignore next */ {
cb(error, undefined);
return;
}
cb(null, cookies);
}
else {
return (async () => this._findCookiesSync(domain, path, allowSpecialUseDomain))();
}
}
else {
return this._findCookiesAsync(domain, path, allowSpecialUseDomain, cb);
}
}
/**
* Searches for cookies after waiting for the initial read to finish
* @see _doSyncReadAsAsync
* @param {string} domain - The cookies domain.
* @param {string} path - The cookies path.
* @param {boolean} allowSpecialUseDomain - If `true` then special-use domain suffixes will be allowed in matches. Defaults to `false`.
* @param {Function} cb - The callback that will be called with the result.
* @returns {Promise<tough.Cookie[]>} a promise if no callback was passed.
*/
_findCookiesAsync(domain, path, allowSpecialUseDomain, cb) {
return this._doSyncReadAsAsync(() => this._findCookiesSync(domain, path, allowSpecialUseDomain), cb);
}
/**
* Searches for matching cookies and returns them.
* @param {string} domain - The cookies domain.
* @param {string} path - The cookies path.
* @param {boolean} allowSpecialUseDomain - If `true` then special-use domain suffixes will be allowed in matches. Defaults to `false`.
* @returns {Cookie[]} the matching cookies if any were found.
*/
_findCookiesSync(domain, path, allowSpecialUseDomain) {
const results = [];
if (!domain) {
return results;
}
let pathMatcher;
if (!path) {
pathMatcher = function matchAll(domainIndex) {
for (const curPath of Object.keys(domainIndex)) {
const pathIndex = domainIndex[curPath];
for (const key of Object.keys(pathIndex)) {
results.push(pathIndex[key]);
}
}
};
}
else {
pathMatcher = function matchRFC(domainIndex) {
for (const cookiePath of Object.keys(domainIndex)) {
if (tough.pathMatch(path, cookiePath)) {
const pathIndex = domainIndex[cookiePath];
for (const key of Object.keys(pathIndex)) {
results.push(pathIndex[key]);
}
}
}
};
}
const domains = tough.permuteDomain(domain, allowSpecialUseDomain) || [domain];
const idx = this.idx;
for (const curDomain of domains) {
const domainIndex = idx[curDomain];
if (!domainIndex) {
continue;
}
pathMatcher(domainIndex);
}
return results;
}
/** @inheritdoc */
putCookie(cookie, cb) {
if (this.synchronous) {
if (typeof cb === 'function') {
try {
this._putCookieSync(cookie);
}
catch (error) /* istanbul ignore next */ {
cb(error);
return;
}
cb(null);
}
else {
return (async () => this._putCookieSync(cookie))();
}
}
else {
return this._putCookieAsync(cookie, cb);
}
}
/**
* Puts a cookie in the store after waiting for the initial read to finish, then saves the store to its file.
* @see _doSyncReadAsAsync
* @param {Cookie} cookie - The cookie to add to the store.
* @param {Function} cb - The callback to be called when finished.
* @returns {Promise} a promise if no callback was passed.
*/
_putCookieAsync(cookie, cb) {
return this._doSyncWriteAsAsync(() => {
return this._putCookieSyncInternal(cookie);
}, cb);
}
/**
* Puts a cookie in the store without saving to a file.
* @param {Cookie} cookie - The cookie to add to the store.
* @returns {boolean} true if the store was changed, or false if the store was not changed.
*/
_putCookieSyncInternal(cookie) {
const { domain, path, key } = cookie;
const canDomain = tough.canonicalDomain(domain);
// Guarding against invalid input
// istanbul ignore next
if (canDomain == null || path == null || key == null) {
return false;
}
let domainVal = this.idx[canDomain];
if (!domainVal) {
domainVal = {};
this.idx[canDomain] = domainVal;
}
let pathVal = domainVal[path];
if (!pathVal) {
pathVal = {};
domainVal[path] = pathVal;
}
pathVal[key] = cookie;
return true;
}
/**
* Puts a cookie in the store, then saves synchronously.
* @param {Cookie} cookie - The cookie to add to the store.
*/
_putCookieSync(cookie) {
if (this._putCookieSyncInternal(cookie)) {
this._saveSync();
}
}
/** @inheritdoc */
updateCookie(oldCookie, newCookie, cb) {
// TODO delete old cookie?
if (cb) {
return this.putCookie(newCookie, cb);
}
else {
return this.putCookie(newCookie);
}
}
/** @inheritdoc */
removeCookie(domain, path, key, cb) {
if (this.synchronous) {
if (typeof cb === 'function') {
try {
this._removeCookieSync(domain, path, key);
}
catch (error) /* istanbul ignore next */ {
cb(error);
return;
}
cb(null);
}
else {
return (async () => this._removeCookieSync(domain, path, key))();
}
}
else {
return this._removeCookieAsync(domain, path, key, cb);
}
}
/**
* Removes a cookie from the store after waiting for the initial read to finish, then saves the store to its file if removed.
* @see _doSyncReadAsAsync
* @param {string} domain - The domain of the cookie to remove.
* @param {string} path - The path of the cookie to remove.
* @param {string} key - The key of the cookie to remove.
* @param {Function} cb - The callback to be called when finished.
* @returns {Promise} a promise if no callback was passed.
*/
_removeCookieAsync(domain, path, key, cb) {
return this._doSyncWriteAsAsync(() => {
return this._removeCookieSyncInternal(domain, path, key);
}, cb);
}
/**
* Removes a cookie from the store without saving to a file.
* @param {string} domain - The domain of the cookie to remove.
* @param {string} path - The path of the cookie to remove.
* @param {string} key - The key of the cookie to remove.
* @returns {boolean} true if a cookie was removed, or false if no change occured.
*/
_removeCookieSyncInternal(domain, path, key) {
const domainVal = this.idx[domain];
if (!domainVal) {
return false;
}
const pathVal = domainVal[path];
if (!pathVal) {
return false;
}
const deleted = (delete pathVal[key]);
// clean up entries if empty
if (deleted && Object.keys(pathVal).length === 0) {
delete domainVal[path];
if (Object.keys(domainVal).length === 0) {
delete this.idx[domain];
}
}
return deleted;
}
/**
* Removes a cookie from the store, then saves synchronously if removed.
* @param {string} domain - The domain of the cookie to remove.
* @param {string} path - The path of the cookie to remove.
* @param {string} key - The key of the cookie to remove.
*/
_removeCookieSync(domain, path, key) {
if (this._removeCookieSyncInternal(domain, path, key)) {
this._saveSync();
}
}
/** @inheritdoc */
removeCookies(domain, path, cb) {
if (this.synchronous) {
if (typeof cb === 'function') {
try {
this._removeCookiesSync(domain, path);
}
catch (error) /* istanbul ignore next */ {
cb(error);
return;
}
cb(null);
}
else {
return (async () => this._removeCookiesSync(domain, path))();
}
}
else {
return this._removeCookiesAsync(domain, path, cb);
}
}
/**
* Removes cookies from the store after waiting for the initial read to finish, then saves the store to its file if any were removed.
* @see _doSyncReadAsAsync
* @param {string} domain - The domain of the cookies to remove.
* @param {string} path - The path of the cookies to remove.
* @param {Function} cb - The callback to be called when finished.
* @returns {Promise} a promise if no callback was passed.
*/
_removeCookiesAsync(domain, path, cb) {
return this._doSyncWriteAsAsync(() => {
return this._removeCookiesSyncInternal(domain, path);
}, cb);
}
/**
* Removes cookies from the store without saving to a file.
* @param {string} domain - The domain of the cookies to remove.
* @param {string} path - The path of the cookies to remove.
* @returns {boolean} true if any cookies were removed, or false if no change occured
*/
_removeCookiesSyncInternal(domain, path) {
if (path != null) {
const domainVal = this.idx[domain];
if (domainVal) {
const deleted = (delete domainVal[path]);
// clean up entries if empty
if (deleted && Object.keys(domainVal).length === 0) {
delete this.idx[domain];
}
return deleted;
}
return false;
}
else {
const deleted = (delete this.idx[domain]);
return deleted;
}
}
/**
* Removes cookies from the store, then saves synchronously if any were removed.
* @param {string} domain - The domain of the cookies to remove.
* @param {string} path - The path of the cookies to remove.
*/
_removeCookiesSync(domain, path) {
if (this._removeCookiesSyncInternal(domain, path)) {
this._saveSync();
}
}
/** @inheritdoc */
removeAllCookies(cb) {
if (this.synchronous) {
if (typeof cb === 'function') {
try {
this._removeAllCookiesSync();
}
catch (error) /* istanbul ignore next */ {
cb(error);
return;
}
cb(null);
}
else {
return (async () => this._removeAllCookiesSync())();
}
}
else {
return this._removeAllCookiesAsync(cb);
}
}
/**
* Removes all cookies after waiting for the initial read to finish, then saves the store to its file if any were removed.
* @param {Function} cb - The callback to be called when finished.
* @returns {Promise} a promise if no callback was passed.
*/
_removeAllCookiesAsync(cb) {
return this._doSyncWriteAsAsync(() => {
return this._removeAllCookiesSyncInternal();
}, cb);
}
/**
* Removes all cookies from the store without saving to a file.
* @returns {boolean} true if any cookies were removed, or false if no change occured
*/
_removeAllCookiesSyncInternal() {
if (Object.keys(this.idx).length === 0) {
return false;
}
this.idx = {};
return true;
}
/**
* Removes all cookies from the store, then saves synchronously if any were removed.
*/
_removeAllCookiesSync() {
if (this._removeAllCookiesSyncInternal()) {
this._saveSync();
}
}
/** @inheritdoc */
getAllCookies(cb) {
if (this.synchronous) {
if (typeof cb === 'function') {
let cookies;
try {
cookies = this._getAllCookiesSync();
}
catch (error) /* istanbul ignore next */ {
cb(error, undefined);
return;
}
cb(null, cookies);
}
else {
return (async () => this._getAllCookiesSync())();
}
}
else {
return this._getAllCookiesAsync(cb);
}
}
/**
* Gets all the cookies after waiting for the initial read to finish.
* @param {Function} cb - The callback to be called with the results.
* @returns {Promise<tough.Cookie[]>} a promise if no callback was passed.
*/
_getAllCookiesAsync(cb) {
return this._doSyncReadAsAsync(() => this._getAllCookiesSync(), cb);
}
/**
* Gets all the cookies in the store and returns them.
* @returns {Cookie[]} an array of all the cookies in the store.
*/
_getAllCookiesSync() {
const cookies = [];
for (const domain of Object.keys(this.idx)) {
const domainVal = this.idx[domain];
for (const p of Object.keys(domainVal)) {
const pVal = domainVal[p];
for (const key of Object.keys(pVal)) {
const cookie = pVal[key];
if (key != null) {
cookies.push(cookie);
}
}
}
}
cookies.sort((a, b) => {
return (a.creationIndex || 0) - (b.creationIndex || 0);
});
return cookies;
}
/**
* Returns a string representation of the store object for debugging purposes.
*
* @returns {string} - The string representation of the store.
*/
_inspect() {
return `{ idx: ${util.inspect(this.idx, false, 2)} }`;
}
/**
* Load the store from a file asynchronously.
*
* @param {string} filePath - The file to load the store from.
* @param {object} options - Options for loading the file
* @returns {Promise<CookiesIndex>} a promise that resolves with the parsed data from the file.
*/
async _loadFromFileAsync(filePath, options) {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
}
catch (_a) {
if (!this.fileFormat) {
this.fileFormat = DefaultFileFormat;
}
return false;
}
const data = await fs.promises.readFile(filePath, 'utf8');
return this._loadFromStringSync(data, filePath, options);
}
/**
* Load the store from a file synchronously.
*
* @param {string} filePath - The file to load the store from.
* @param {object} options - Options for loading the file
* @returns {CookiesIndex} the parsed data from the file
*/
_loadFromFileSync(filePath, options) {
if (!fs.existsSync(this.filePath)) {
if (!this.fileFormat) {
this.fileFormat = DefaultFileFormat;
}
return false;
}
const data = fs.readFileSync(filePath, 'utf8');
return this._loadFromStringSync(data, filePath, options);
}
/**
* Loads the store from a json string.
* @param {string} data - The string data that was loaded from a file.
* @param {string} filePath - The path of the file that the string data was loaded from.
* @param {object} options - Options for loading the file.
* @returns {CookiesIndex} the parsed data
*/
_loadFromStringSync(data, filePath, options) {
// determine file format if not specified
let jsonData;
let fileFormat = this.fileFormat;
if (!fileFormat) {
// try to parse json initially
try {
jsonData = JSON.parse(data);
fileFormat = FileFormat.json;
}
catch (_a) {
fileFormat = FileFormat.txt;
}
}
// load depending on format
let cookies;
switch (fileFormat) {
case FileFormat.json:
if (jsonData !== undefined) {
cookies = parseCookiesJsonObject(jsonData);
}
else {
cookies = parseCookiesJson(data, Object.assign(Object.assign({}, options), { filePath }));
}
break;
case FileFormat.txt:
cookies = parseNetscapeCookiesTxtToIndex(data, Object.assign(Object.assign({}, options), { filePath, httpOnlyExtension: this.httpOnlyExtension }));
break;
default:
throw new Error(`Unknown cookies file format ${fileFormat}`);
}
// apply loaded file
this.fileFormat = fileFormat;
if (cookies) {
this.idx = cookies;
}
return (cookies !== undefined);
}
/**
* Saves the store to its file asynchronously.
* @param {Function} cb - The callback to be called when finished.
* @returns {Promise} a promise if no callback was passed.
*/
_saveAsync(cb) {
if (!this._nextWritePromise) {
// create next write promise
this._nextWritePromise = (async () => {
// wait for active write to finish if any
if (this._writePromise) {
// wait for write to finish
try {
await this._writePromise;
}
catch (_a) {
// ignore error
}
}
else {
// delay atleast 1 tick, in case of multiple writes
await Promise.resolve();
}
// this is now the active write, so update the write promises
this._writePromise = this._nextWritePromise;
this._nextWritePromise = undefined;
// save to the file
try {
await this._saveToFileAsync(this.filePath, this.idx);
}
finally {
// clear write promise
this._writePromise = undefined;
}
})();
}
// wait for next write promise
if (typeof cb === 'function') {
this._nextWritePromise
.then(() => {
cb(null);
},
// istanbul ignore next
(error) => {
cb(error);
})
.catch(
// istanbul ignore next
(error) => {
// istanbul ignore next
console.error(error);
});
}
else {
return this._nextWritePromise;
}
}
/**
* Saves the store to its file synchronously.
*/
_saveSync() {
this._saveToFileSync(this.filePath, this.idx);
if (this._writePromise) {
// since we're actively writing, also save async to ensure file gets written correctly
this._saveAsync((error) => {
// istanbul ignore next
if (error) {
// istanbul ignore next
console.error(error);
}
});
}
}
/**
* Saves the store to a file asynchronously.
* @param {string} filePath - The file path to save the store to.
* @param {CookiesIndex} data - The cookies to save to the file.
* @returns {Promise} a promise for the write task
*/
_saveToFileAsync(filePath, data) {
const dataString = this._saveToStringSync(data);
return fs.promises.writeFile(filePath, dataString);
}
/**
* Saves the store to a file synchronously.
* @param {string} filePath - The file path to save the store to.
* @param {CookiesIndex} data - The cookies to save to the file.
*/
_saveToFileSync(filePath, data) {
const dataString = this._saveToStringSync(data);
fs.writeFileSync(filePath, dataString);
}
/**
* Serializes the cookies data to a string
* @param {CookiesIndex} data - The cookies data to serialize
* @returns {string} the serialized cookies data
*/
_saveToStringSync(data) {
if (!this.fileFormat) {
throw new Error('File format has not been determined');
}
switch (this.fileFormat) {
case FileFormat.json:
return stringifyCookiesJson(data);
case FileFormat.txt:
return stringifyNetscapeCookiesTxt(data, {
httpOnlyExtension: this.httpOnlyExtension
});
default:
throw new Error(`Unknown cookies file format ${this.fileFormat}`);
}
}
}
util.inspect.custom;
export default FileCookieStore;
//# sourceMappingURL=cookie-file-store.js.map