UNPKG

tough-cookie-file-store

Version:
889 lines 33.6 kB
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