diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..fbcc767 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/pages/browser_profiles.md b/docs/pages/browser_profiles.md new file mode 100644 index 0000000..3ea31aa --- /dev/null +++ b/docs/pages/browser_profiles.md @@ -0,0 +1,50 @@ +--- +layout: default +title: Browser profiles +nav_order: 4 +description: Specifying browser profiles +permalink: /browser-profiles +--- + +# Browser profiles + +## Choosing browser profiles + +If you are utilising multiple browser profiles, you can specify which one to use in the function options. + +```js +import { getCookie, listCookies, Browser } from 'cookie-thief'; + +await getCookie({ + browser: Browser.Chrome, + cookieName: 'foo', + domain: '.github.com', + options: { + profile: 'SomeProfile', + }, +}); + +await listCookies({ + browser: Browser.Firefox, + options: { + profile: 'SomeProfile', + }, +}); +``` + +## Listing browser profiles + +If you want to programmatically list browser profiles, you can with `listProfiles`. + +```js +import { listProfiles, Browser } from 'cookie-thief'; + +const profiles: string[] = await listProfiles(Browser.Chrome); +``` + +## Default browser profiles + +If you do not specify a profile, a default profile name will be used. + +* Firefox: `default-release` +* Chrome: `Default` diff --git a/docs/pages/compatibility.md b/docs/pages/compatibility.md new file mode 100644 index 0000000..961d119 --- /dev/null +++ b/docs/pages/compatibility.md @@ -0,0 +1,28 @@ +--- +layout: default +title: Compatibility +nav_order: 2 +description: Compatible browsers and operation systems +permalink: /compatibility +--- + +## Compatibility + +### Supported Browsers + +* Google Chrome +* Firefox + +### Supported Operating Systems + +* MacOS +* Linux +* Windows + +## Limitations + +### MacOS + +On macOS, this package requires keychain access to access the Google Chrome encryption key. +You will get a dialogue popup requesting access. +Due to this popup, you cannot use this library completely headlessly to fetch cookies unless you run it once and click `Always Allow`. diff --git a/docs/pages/fetching_cookies.md b/docs/pages/fetching_cookies.md new file mode 100644 index 0000000..b66f2e7 --- /dev/null +++ b/docs/pages/fetching_cookies.md @@ -0,0 +1,55 @@ +--- +layout: default +title: Fetching Cookies +nav_order: 3 +description: Getting and listing cookies +permalink: /fetching-cookies +--- + +# Get Cookie + +`getCookie` can be used to try and find a cookie based on the domain and cookie name. +If a cookie is not found, the result will be `undefined`. + +```js +import { getCookie, Browser } from 'cookie-thief'; + +const cookie = await getCookie({ + browser: Browser.Chrome, // or Browser.Firefox + domain: '.reddit.com', + cookieName: 'loid', +}); + +/* +{ + name: 'loid', + value: 'the decrypted cookie value here', + domain: '.reddit.com', + path: '/', +} +*/ +``` + +# List Cookies + +`listCookies` can be used to list all cookies for a browser. +If no cookies are found you will get `[]`. + +```js +import { listCookies, Browser } from 'cookie-thief'; + +const cookies = await listCookies({ + browser: Browser.Chrome, // or Browser.Firefox +}) + +/* +[ + { + name: 'loid', + value: 'the decrypted cookie value here', + domain: '.reddit.com', + path: '/', + } +] +*/ +``` diff --git a/docs/pages/index.md b/docs/pages/index.md index 03bb8da..553a021 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -7,16 +7,15 @@ permalink: / --- # Cookie Thief -{: .no_toc} -1. TOC -{:toc} +![npm](https://img.shields.io/npm/v/cookie-thief) +![npm](https://img.shields.io/npm/dw/cookie-thief) +![npm bundle size](https://img.shields.io/bundlephobia/min/cookie-thief) +![npm bundle size](https://img.shields.io/bundlephobia/minzip/cookie-thief) +[![codecov](https://codecov.io/gh/Kalininator/cookie-thief/branch/master/graph/badge.svg?token=H0F1TIE0CY)](https://codecov.io/gh/Kalininator/cookie-thief) -## Compatibility - -Currently supports only Google Chrome and Firefox on MacOS, Linux, and Windows. - -In the future will hopefully expand to support other browsers. +A node.js library for extracting cookies from a browser installed on your system. +Inspired by [chrome-cookies-secure](https://github.com/bertrandom/chrome-cookies-secure). ## Installation @@ -31,23 +30,24 @@ yarn add cookie-thief ## Usage -### Google Chrome - ```javascript -const { getCookie, listCookies, Browser } = require('cookie-thief') +const { getCookie, listCookies, Browser, listBrowsers } = require('cookie-thief') // Get a cookie from chrome browser for domain .github.com, searching for cookie named 'dotcom_user' const cookie = await getCookie({ browser: Browser.Chrome, - url: 'https://github.com', + domain: '.github.com', cookieName: 'dotcom_user', - options: { - profile: 'Default', - }, }); console.log(cookie); -// Will be a string if cookie is successfully found +// Will be a Cookie if cookie is successfully found // Will be undefined if not found +//{ +// name: 'cookie name here', +// value: 'decrypted cookie content here', +// host: 'hostname of cookie here', +// path: 'path of cookie here' +//} const cookies = await listCookies({ browser: Browser.Chrome, @@ -63,42 +63,8 @@ console.log(cookies); // } //] -``` - -### Firefox - -```javascript -const { getCookie, Browser } = require('cookie-thief') - -// Get a cookie from chrome browser for domain .github.com, searching for cookie named 'dotcom_user' -const cookie = await getCookie({ - browser: Browser.Firefox, - url: 'https://github.com', - cookieName: 'dotcom_user', - options: { - profile: 'default-release', - }, -}); -console.log(cookie); -// Will be a string if cookie is successfully found -// Will be undefined if not found +const browsers = listBrowsers(); +console.log(browsers); +// [ Browser.Chrome, Browser.Firefox ] -const cookies = await listCookies({ - browser: Browser.Firefox, -}); -console.log(cookies); -// Array of cookies -//[ -// { -// name: 'cookie name here', -// value: 'decrypted cookie content here', -// host: 'hostname of cookie here', -// path: 'path of cookie here' -// } -//] ``` - -## Limitations - -### macOS -On macOS, this package requires keychain access to access the Google Chrome encryption key. You will get a dialogue popup requesting access. diff --git a/package.json b/package.json index 464f0d8..a737aec 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { "name": "cookie-thief", - "version": "0.7.0", + "version": "1.0.0", "description": "Steal browser cookies", "main": "./lib/index.js", "author": "Alex Kalinin (https://kalinin.uk)", + "homepage": "https://kalininator.github.io/cookie-thief/", + "bugs": { + "url": "https://github.com/Kalininator/cookie-thief/issues" + }, "license": "MIT", "files": [ "lib/", @@ -33,8 +37,8 @@ "publish-package": "npm run build && npm publish", "test": "jest", "test:watch": "jest --watch", - "clean:some": "rm -rf ./lib ./docs", - "clean:all": "rm -rf ./node_modules ./package-lock.json ./lib ./docs", + "clean:some": "rm -rf ./lib", + "clean:all": "rm -rf ./node_modules ./package-lock.json ./lib", "pr:lint": "./node_modules/eslint/bin/eslint.js 'src/**/*.ts'", "pr:test": "jest", "t": "ts-node testFile.ts" @@ -67,8 +71,7 @@ }, "dependencies": { "better-sqlite3": "^7.4.3", - "ini": "^2.0.0", - "tldjs": "^2.3.1" + "ini": "^2.0.0" }, "optionalDependencies": { "keytar": "^7.7.0", diff --git a/src/CookieProvider.ts b/src/CookieProvider.ts new file mode 100644 index 0000000..b39e71a --- /dev/null +++ b/src/CookieProvider.ts @@ -0,0 +1,6 @@ +import { Cookie } from './types'; + +export interface CookieProvider { + getCookie(domain: string, cookieName: string): Promise; + listCookies(): Promise; +} diff --git a/src/chrome/ChromeCookieDatabase.test.ts b/src/chrome/ChromeCookieDatabase.test.ts index beed1e3..d40c031 100644 --- a/src/chrome/ChromeCookieDatabase.test.ts +++ b/src/chrome/ChromeCookieDatabase.test.ts @@ -30,7 +30,7 @@ describe('ChromeCookieDatabase', () => { }); expect(getFn).toHaveBeenCalled(); expect(prepareFn).toHaveBeenCalledWith( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies where host_key like '%.domain.com' and name like '%someCookie' ORDER BY LENGTH(path) DESC, creation_utc ASC`, + `SELECT host_key, path, name, encrypted_value FROM cookies where host_key like '%.domain.com' and name like '%someCookie' ORDER BY LENGTH(path) DESC, creation_utc ASC`, ); expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, { readonly: true, @@ -68,7 +68,7 @@ describe('ChromeCookieDatabase', () => { ]); expect(allFn).toHaveBeenCalled(); expect(prepareFn).toHaveBeenCalledWith( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies`, + `SELECT host_key, path, name, encrypted_value FROM cookies`, ); expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, { readonly: true, diff --git a/src/chrome/ChromeCookieDatabase.ts b/src/chrome/ChromeCookieDatabase.ts index bba3240..2c7ad08 100644 --- a/src/chrome/ChromeCookieDatabase.ts +++ b/src/chrome/ChromeCookieDatabase.ts @@ -1,32 +1,13 @@ import sqlite from 'better-sqlite3'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; -type BooleanNumber = 0 | 1; +export class ChromeCookieDatabase implements ChromeCookieRepository { + constructor(private path: string) {} -export type ChromeCookie = { - host_key: string; - path: string; - is_secure: BooleanNumber; - expires_utc: number; - name: string; - value: string; - encrypted_value: Buffer; - creation_utc: number; - is_httponly: BooleanNumber; - has_expires: BooleanNumber; - is_persistent: BooleanNumber; -}; - -export class ChromeCookieDatabase { - path: string; - - constructor(path: string) { - this.path = path; - } - - findCookie(cookieName: string, domain: string): ChromeCookie { + findCookie(cookieName: string, domain: string): ChromeCookie | undefined { const db = sqlite(this.path, { readonly: true, fileMustExist: true }); const statement = db.prepare( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies where host_key like '%${domain}' and name like '%${cookieName}' ORDER BY LENGTH(path) DESC, creation_utc ASC`, + `SELECT host_key, path, name, encrypted_value FROM cookies where host_key like '%${domain}' and name like '%${cookieName}' ORDER BY LENGTH(path) DESC, creation_utc ASC`, ); return statement.get(); } @@ -34,7 +15,7 @@ export class ChromeCookieDatabase { listCookies(): ChromeCookie[] { const db = sqlite(this.path, { readonly: true, fileMustExist: true }); const statement = db.prepare( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies`, + `SELECT host_key, path, name, encrypted_value FROM cookies`, ); const cookies: ChromeCookie[] = statement.all(); return cookies; diff --git a/src/chrome/ChromeCookieRepository.ts b/src/chrome/ChromeCookieRepository.ts new file mode 100644 index 0000000..e460dcc --- /dev/null +++ b/src/chrome/ChromeCookieRepository.ts @@ -0,0 +1,11 @@ +export type ChromeCookie = { + host_key: string; + path: string; + name: string; + encrypted_value: Buffer; +}; + +export interface ChromeCookieRepository { + findCookie(cookieName: string, domain: string): ChromeCookie | undefined; + listCookies(): ChromeCookie[]; +} diff --git a/src/chrome/ChromeLinuxCookieProvider.ts b/src/chrome/ChromeLinuxCookieProvider.ts new file mode 100644 index 0000000..62ddb2d --- /dev/null +++ b/src/chrome/ChromeLinuxCookieProvider.ts @@ -0,0 +1,39 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { decrypt } from './decrypt'; +import { getLinuxDerivedKey } from './getDerivedKey'; + +const KEYLENGTH = 16; +const ITERATIONS = 1; + +async function decryptCookie(cookie: ChromeCookie): Promise { + const derivedKey = await getLinuxDerivedKey(KEYLENGTH, ITERATIONS); + return decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); +} + +async function toCookie(chromeCookie: ChromeCookie): Promise { + return { + value: await decryptCookie(chromeCookie), + host: chromeCookie.host_key, + path: chromeCookie.path, + name: chromeCookie.name, + }; +} + +export class ChromeLinuxCookieProvider implements CookieProvider { + constructor(private db: ChromeCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + const chromeCookie = this.db.findCookie(cookieName, domain); + if (!chromeCookie) return undefined; + return toCookie(chromeCookie); + } + + async listCookies(): Promise { + return Promise.all(this.db.listCookies().map(toCookie)); + } +} diff --git a/src/chrome/ChromeMacosCookieProvider.ts b/src/chrome/ChromeMacosCookieProvider.ts new file mode 100644 index 0000000..fa2523f --- /dev/null +++ b/src/chrome/ChromeMacosCookieProvider.ts @@ -0,0 +1,39 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { decrypt } from './decrypt'; +import { getMacDerivedKey } from './getDerivedKey'; + +const KEYLENGTH = 16; +const ITERATIONS = 1003; + +async function decryptCookie(cookie: ChromeCookie): Promise { + const derivedKey = await getMacDerivedKey(KEYLENGTH, ITERATIONS); + return decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); +} + +async function toCookie(chromeCookie: ChromeCookie): Promise { + return { + value: await decryptCookie(chromeCookie), + host: chromeCookie.host_key, + path: chromeCookie.path, + name: chromeCookie.name, + }; +} + +export class ChromeMacosCookieProvider implements CookieProvider { + constructor(private db: ChromeCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + const chromeCookie = this.db.findCookie(cookieName, domain); + if (!chromeCookie) return undefined; + return toCookie(chromeCookie); + } + + async listCookies(): Promise { + return Promise.all(this.db.listCookies().map(toCookie)); + } +} diff --git a/src/chrome/ChromeMacosCookieProvider.unit.test.ts b/src/chrome/ChromeMacosCookieProvider.unit.test.ts new file mode 100644 index 0000000..3107fb1 --- /dev/null +++ b/src/chrome/ChromeMacosCookieProvider.unit.test.ts @@ -0,0 +1,48 @@ +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { ChromeMacosCookieProvider } from './ChromeMacosCookieProvider'; + +jest.mock('./decrypt', () => ({ + decrypt: jest.fn().mockResolvedValue('foo'), +})); + +jest.mock('./getDerivedKey', () => ({ + getMacDerivedKey: jest.fn().mockResolvedValue(Buffer.from('some_key')), +})); + +const fakeBuffer = Buffer.from('encrypted'); + +describe('chrome macos cookie provider', () => { + it('should fetch and decrypt cookie', async () => { + const mockFindCookie = jest + .fn() + .mockImplementation( + (cookieName: string, domain: string): ChromeCookie => { + return { + host_key: domain, + path: '/', + name: cookieName, + encrypted_value: fakeBuffer, + }; + }, + ); + const mockDb: ChromeCookieRepository = { + findCookie: mockFindCookie, + listCookies(): ChromeCookie[] { + throw new Error('Function not implemented.'); + }, + }; + + const provider = new ChromeMacosCookieProvider(mockDb); + + const cookie = await provider.getCookie('.test.com', 'some_cookie'); + + expect(cookie).toEqual({ + value: 'foo', + host: '.test.com', + path: '/', + name: 'some_cookie', + }); + + expect(mockFindCookie).toHaveBeenCalledWith('some_cookie', '.test.com'); + }); +}); diff --git a/src/chrome/ChromeWindowsCookieProvider.ts b/src/chrome/ChromeWindowsCookieProvider.ts new file mode 100644 index 0000000..d490cee --- /dev/null +++ b/src/chrome/ChromeWindowsCookieProvider.ts @@ -0,0 +1,34 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { decryptWindows } from './decrypt'; + +async function decryptCookie(cookie: ChromeCookie): Promise { + return decryptWindows(cookie.encrypted_value); +} + +async function toCookie(chromeCookie: ChromeCookie): Promise { + return { + value: await decryptCookie(chromeCookie), + host: chromeCookie.host_key, + path: chromeCookie.path, + name: chromeCookie.name, + }; +} + +export class ChromeWindowsCookieProvider implements CookieProvider { + constructor(private db: ChromeCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + const chromeCookie = this.db.findCookie(cookieName, domain); + if (!chromeCookie) return undefined; + return toCookie(chromeCookie); + } + + async listCookies(): Promise { + return Promise.all(this.db.listCookies().map(toCookie)); + } +} diff --git a/src/chrome/ChromeWindowsCookieProvider.unit.test.ts b/src/chrome/ChromeWindowsCookieProvider.unit.test.ts new file mode 100644 index 0000000..94450d9 --- /dev/null +++ b/src/chrome/ChromeWindowsCookieProvider.unit.test.ts @@ -0,0 +1,44 @@ +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { ChromeWindowsCookieProvider } from './ChromeWindowsCookieProvider'; + +jest.mock('./decrypt', () => ({ + decryptWindows: jest.fn().mockResolvedValue('foo'), +})); + +const fakeBuffer = Buffer.from('encrypted'); + +describe('chrome windows cookie provider', () => { + it('should fetch and decrypt cookie', async () => { + const mockFindCookie = jest + .fn() + .mockImplementation( + (cookieName: string, domain: string): ChromeCookie => { + return { + host_key: domain, + path: '/', + name: cookieName, + encrypted_value: fakeBuffer, + }; + }, + ); + const mockDb: ChromeCookieRepository = { + findCookie: mockFindCookie, + listCookies(): ChromeCookie[] { + throw new Error('Function not implemented.'); + }, + }; + + const provider = new ChromeWindowsCookieProvider(mockDb); + + const cookie = await provider.getCookie('.test.com', 'some_cookie'); + + expect(cookie).toEqual({ + value: 'foo', + host: '.test.com', + path: '/', + name: 'some_cookie', + }); + + expect(mockFindCookie).toHaveBeenCalledWith('some_cookie', '.test.com'); + }); +}); diff --git a/src/chrome/getDerivedKey.ts b/src/chrome/getDerivedKey.ts index 537a273..ae38aef 100644 --- a/src/chrome/getDerivedKey.ts +++ b/src/chrome/getDerivedKey.ts @@ -6,7 +6,7 @@ const promisedPbkdf2 = promisify(crypto.pbkdf2); const SALT = 'saltysalt'; -async function getMacDerivedKey( +export async function getMacDerivedKey( keyLength: number, iterations: number, ): Promise { diff --git a/src/chrome/index.ts b/src/chrome/index.ts index c6799a5..13d6394 100644 --- a/src/chrome/index.ts +++ b/src/chrome/index.ts @@ -1,14 +1,14 @@ import { existsSync, readdirSync } from 'fs'; import { join } from 'path'; +import { CookieProvider } from '../CookieProvider'; import { Cookie } from '../types'; import { mergeDefaults } from '../utils'; import { ChromeCookieDatabase } from './ChromeCookieDatabase'; +import { ChromeLinuxCookieProvider } from './ChromeLinuxCookieProvider'; +import { ChromeMacosCookieProvider } from './ChromeMacosCookieProvider'; +import { ChromeWindowsCookieProvider } from './ChromeWindowsCookieProvider'; -import { decrypt, decryptWindows } from './decrypt'; -import { getDerivedKey } from './getDerivedKey'; -import { getDomain, getIterations, getCookiesPath, getPath } from './util'; - -const KEYLENGTH = 16; +import { getCookiesPath, getPath } from './util'; export interface GetChromeCookiesOptions { profile: string; @@ -18,36 +18,24 @@ const defaultOptions: GetChromeCookiesOptions = { profile: 'Default', }; -/** - * @deprecated Replaced by getCookie - */ +function getChromeCookieProvider(profile: string): CookieProvider { + const path = getCookiesPath(profile); + const db = new ChromeCookieDatabase(path); + if (process.platform === 'darwin') return new ChromeMacosCookieProvider(db); + if (process.platform === 'linux') return new ChromeLinuxCookieProvider(db); + if (process.platform === 'win32') return new ChromeWindowsCookieProvider(db); + + throw new Error(`Platform ${process.platform} is not supported`); +} + export async function getChromeCookie( - url: string, + domain: string, cookieName: string, options?: Partial, -): Promise { +): Promise { const config = mergeDefaults(defaultOptions, options); - const path = getCookiesPath(config.profile); - const domain = getDomain(url); - - const db = new ChromeCookieDatabase(path); - - // const cookie = tryGetCookie(path, domain, cookieName); - const cookie = db.findCookie(cookieName, domain); - - if (!cookie) return undefined; - - if (process.platform === 'darwin' || process.platform === 'linux') { - const iterations = getIterations(); - const derivedKey = await getDerivedKey(KEYLENGTH, iterations); - return decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); - } - - if (process.platform === 'win32') { - return decryptWindows(cookie.encrypted_value); - } - - throw new Error(`Platform ${process.platform} is not supported`); + const provider = getChromeCookieProvider(config.profile); + return provider.getCookie(domain, cookieName); } export async function listChromeProfiles(): Promise { @@ -61,42 +49,6 @@ export async function listChromeCookies( options?: Partial, ): Promise { const config = mergeDefaults(defaultOptions, options); - const path = getCookiesPath(config.profile); - const db = new ChromeCookieDatabase(path); - const cookies = db.listCookies(); - const decryptedCookies = await Promise.all( - cookies.map(async (cookie): Promise => { - if (cookie.value) - return { - name: cookie.name, - host: cookie.host_key, - path: cookie.path, - value: cookie.value, - }; - if (process.platform === 'darwin' || process.platform === 'linux') { - const iterations = getIterations(); - const derivedKey = await getDerivedKey(KEYLENGTH, iterations); - const value = decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); - return { - name: cookie.name, - host: cookie.host_key, - path: cookie.path, - value, - }; - } - - if (process.platform === 'win32') { - const value = decryptWindows(cookie.encrypted_value); - return { - name: cookie.name, - host: cookie.host_key, - path: cookie.path, - value, - }; - } - throw new Error('Failed to decrypt cookie'); - }), - ); - - return decryptedCookies; + const provider = getChromeCookieProvider(config.profile); + return provider.listCookies(); } diff --git a/src/chrome/util.ts b/src/chrome/util.ts index e3c91fd..c9ffc86 100644 --- a/src/chrome/util.ts +++ b/src/chrome/util.ts @@ -1,11 +1,4 @@ import { homedir } from 'os'; -import tld from 'tldjs'; - -export function getDomain(url: string): string { - const domain = tld.getDomain(url); - if (domain) return domain; - throw new Error(`Failed to extract domain from URL ${url}`); -} export function getPath(): string { if (process.platform === 'darwin') @@ -27,10 +20,3 @@ export function getCookiesPath(profile: string): string { throw new Error(`Platform ${process.platform} is not supported`); } - -export function getIterations(): number { - if (process.platform === 'darwin') return 1003; - if (process.platform === 'linux') return 1; - - throw new Error(`Platform ${process.platform} is not supported`); -} diff --git a/src/chrome/util.unit.test.ts b/src/chrome/util.unit.test.ts index 28221a7..1024789 100644 --- a/src/chrome/util.unit.test.ts +++ b/src/chrome/util.unit.test.ts @@ -1,25 +1,7 @@ import { homedir } from 'os'; -import { getDomain, getIterations, getCookiesPath, getPath } from './util'; +import { getCookiesPath, getPath } from './util'; import { mockPlatform, restorePlatform } from '../../test/util'; -describe('getDomain', () => { - it('should extract domain from github.com', () => { - expect(getDomain('https://github.com')).toEqual('github.com'); - }); - - it('should extract domain from https://some.web.site.com/some/page', () => { - expect(getDomain('https://some.web.site.com/some/page')).toEqual( - 'site.com', - ); - }); - - it('should fail to extract domain', () => { - expect(() => getDomain('foo')).toThrowError( - 'Failed to extract domain from URL foo', - ); - }); -}); - describe('getPath', () => { afterEach(() => { restorePlatform(); @@ -91,25 +73,3 @@ describe('getCookiesPath', () => { ); }); }); - -describe('getIterations', () => { - afterEach(restorePlatform); - - it('should get correct macos iterations', async () => { - mockPlatform('darwin'); - - expect(getIterations()).toEqual(1003); - }); - - it('should get correct linux iterations', async () => { - mockPlatform('linux'); - - expect(getIterations()).toEqual(1); - }); - - it('should throw if invalid os', () => { - mockPlatform('win32'); - - expect(() => getIterations()).toThrow('Platform win32 is not supported'); - }); -}); diff --git a/src/firefox/FirefoxCookieDatabase.test.ts b/src/firefox/FirefoxCookieDatabase.test.ts index d5395a3..8f43c86 100644 --- a/src/firefox/FirefoxCookieDatabase.test.ts +++ b/src/firefox/FirefoxCookieDatabase.test.ts @@ -17,10 +17,10 @@ describe('FirefoxCookieDatabase', () => { const cookie = db.findCookie('someCookie', '.domain.com'); - expect(cookie).toEqual('foo'); + expect(cookie).toEqual({ value: 'foo' }); expect(getFn).toHaveBeenCalled(); expect(prepareFn).toHaveBeenCalledWith( - "SELECT value from moz_cookies WHERE name like 'someCookie' AND host like '%.domain.com'", + "SELECT name, value, host, path from moz_cookies WHERE name like 'someCookie' AND host like '%.domain.com'", ); expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, { readonly: true, diff --git a/src/firefox/FirefoxCookieDatabase.ts b/src/firefox/FirefoxCookieDatabase.ts index 80c6b2f..d4da69c 100644 --- a/src/firefox/FirefoxCookieDatabase.ts +++ b/src/firefox/FirefoxCookieDatabase.ts @@ -1,27 +1,16 @@ import sqlite from 'better-sqlite3'; +import { FirefoxCookieRepository, MozCookie } from './FirefoxCookieRepository'; -type MozCookie = { - name: string; - value: string; - host: string; - path: string; -}; +export class FirefoxCookieDatabase implements FirefoxCookieRepository { + constructor(private path: string) {} -export class FirefoxCookieDatabase { - path: string; - - constructor(path: string) { - this.path = path; - } - - findCookie(cookieName: string, domain: string): string | undefined { + findCookie(cookieName: string, domain: string): MozCookie | undefined { const db = sqlite(this.path, { readonly: true, fileMustExist: true }); const statement = db.prepare( - `SELECT value from moz_cookies WHERE name like '${cookieName}' AND host like '%${domain}'`, + `SELECT name, value, host, path from moz_cookies WHERE name like '${cookieName}' AND host like '%${domain}'`, ); - const res = statement.get(); - return res?.value; + return statement.get(); } listCookies(): MozCookie[] { diff --git a/src/firefox/FirefoxCookieProvider.ts b/src/firefox/FirefoxCookieProvider.ts new file mode 100644 index 0000000..135a5d4 --- /dev/null +++ b/src/firefox/FirefoxCookieProvider.ts @@ -0,0 +1,18 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { FirefoxCookieRepository } from './FirefoxCookieRepository'; + +export class FirefoxCookieProvider implements CookieProvider { + constructor(private db: FirefoxCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + return this.db.findCookie(cookieName, domain); + } + + async listCookies(): Promise { + return this.db.listCookies(); + } +} diff --git a/src/firefox/FirefoxCookieRepository.ts b/src/firefox/FirefoxCookieRepository.ts new file mode 100644 index 0000000..ec88a41 --- /dev/null +++ b/src/firefox/FirefoxCookieRepository.ts @@ -0,0 +1,11 @@ +export type MozCookie = { + name: string; + value: string; + host: string; + path: string; +}; + +export interface FirefoxCookieRepository { + findCookie(cookieName: string, domain: string): MozCookie | undefined; + listCookies(): MozCookie[]; +} diff --git a/src/firefox/index.ts b/src/firefox/index.ts index f9c7b84..6e20829 100644 --- a/src/firefox/index.ts +++ b/src/firefox/index.ts @@ -1,45 +1,9 @@ -import { readFileSync } from 'fs'; -import { homedir } from 'os'; import { join } from 'path'; -import * as ini from 'ini'; -import { getDomain } from 'tldjs'; import { mergeDefaults } from '../utils'; import { FirefoxCookieDatabase } from './FirefoxCookieDatabase'; import { Cookie } from '../types'; - -function getUserDirectory(): string { - switch (process.platform) { - case 'darwin': - return join(homedir(), '/Library/Application Support/Firefox'); - case 'linux': - return join(homedir(), '/.mozilla/firefox'); - case 'win32': - return join(process.env.APPDATA!, '/Mozilla/Firefox'); - default: - throw new Error(`Platform ${process.platform} is not supported`); - } -} - -type FirefoxProfile = { - Name: string; - IsRelative: number; - Path: string; - Default?: number; -}; - -function getProfiles(): FirefoxProfile[] { - const userDirectory = getUserDirectory(); - const fileData = readFileSync(join(userDirectory, 'profiles.ini'), { - encoding: 'utf8', - }); - - const iniData = ini.parse(fileData); - return Object.keys(iniData) - .filter((key) => { - return typeof key === 'string' && key.match(/^Profile/); - }) - .map((key) => iniData[key]); -} +import { getProfiles, getUserDirectory } from './profiles'; +import { FirefoxCookieProvider } from './FirefoxCookieProvider'; function getCookieFilePath(profile: string): string { const profiles = getProfiles(); @@ -60,28 +24,26 @@ export async function listFirefoxProfiles(): Promise { return getProfiles().map((p) => p.Name); } -/** - * @deprecated Replaced by getCookie - */ +function getFirefoxCookieProvider(profile: string): FirefoxCookieProvider { + const cookieFilePath = getCookieFilePath(profile); + const db = new FirefoxCookieDatabase(cookieFilePath); + return new FirefoxCookieProvider(db); +} export async function getFirefoxCookie( - url: string, + domain: string, cookieName: string, options?: Partial, -): Promise { +): Promise { const config = mergeDefaults(defaultOptions, options); - const domain = getDomain(url); - if (!domain) throw new Error('Could not extract domain from URL'); - const cookieFilePath = getCookieFilePath(config.profile); - const db = new FirefoxCookieDatabase(cookieFilePath); - return db.findCookie(cookieName, domain); + const cookieRepo = getFirefoxCookieProvider(config.profile); + return cookieRepo.getCookie(domain, cookieName); } export async function listFirefoxCookies( options?: Partial, ): Promise { const config = mergeDefaults(defaultOptions, options); - const cookieFilePath = getCookieFilePath(config.profile); - const db = new FirefoxCookieDatabase(cookieFilePath); - return db.listCookies(); + const cookieRepo = getFirefoxCookieProvider(config.profile); + return cookieRepo.listCookies(); } diff --git a/src/firefox/profiles.ts b/src/firefox/profiles.ts new file mode 100644 index 0000000..838c597 --- /dev/null +++ b/src/firefox/profiles.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'fs'; +import * as ini from 'ini'; +import { homedir } from 'os'; +import { join } from 'path'; + +export function getUserDirectory(): string { + switch (process.platform) { + case 'darwin': + return join(homedir(), '/Library/Application Support/Firefox'); + case 'linux': + return join(homedir(), '/.mozilla/firefox'); + case 'win32': + return join(process.env.APPDATA!, '/Mozilla/Firefox'); + default: + throw new Error(`Platform ${process.platform} is not supported`); + } +} +export type FirefoxProfile = { + Name: string; + IsRelative: number; + Path: string; + Default?: number; +}; + +export function getProfiles(): FirefoxProfile[] { + const userDirectory = getUserDirectory(); + const fileData = readFileSync(join(userDirectory, 'profiles.ini'), { + encoding: 'utf8', + }); + + const iniData = ini.parse(fileData); + return Object.keys(iniData) + .filter((key) => { + return typeof key === 'string' && key.match(/^Profile/); + }) + .map((key) => iniData[key]); +} diff --git a/src/index.ts b/src/index.ts index 74ce5f7..2a6d804 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,16 +5,16 @@ import { listChromeProfiles, } from './chrome'; import { - GetFirefoxCookieOptions, getFirefoxCookie, + GetFirefoxCookieOptions, listFirefoxCookies, listFirefoxProfiles, } from './firefox'; import { Cookie } from './types'; import { assertUnreachable } from './utils'; -export * from './chrome'; -export * from './firefox'; +// export * from './chrome'; +// export * from './firefox'; export * from './types'; export enum Browser { @@ -24,7 +24,7 @@ export enum Browser { interface BaseGetCookieConfig { browser: Browser; - url: string; + domain: string; cookieName: string; } @@ -44,12 +44,12 @@ export function listSupportedBrowsers(): Browser[] { export async function getCookie( config: GetFirefoxCookieConfig | GetChromeCookieConfig, -): Promise { +): Promise { switch (config.browser) { case Browser.Firefox: - return getFirefoxCookie(config.url, config.cookieName, config.options); + return getFirefoxCookie(config.domain, config.cookieName, config.options); case Browser.Chrome: - return getChromeCookie(config.url, config.cookieName, config.options); + return getChromeCookie(config.domain, config.cookieName, config.options); default: return assertUnreachable(config); } diff --git a/test/chrome/linux.unit.test.ts b/test/chrome/linux.unit.test.ts index d98bb86..3e6b76f 100644 --- a/test/chrome/linux.unit.test.ts +++ b/test/chrome/linux.unit.test.ts @@ -6,6 +6,9 @@ jest.mock('better-sqlite3', () => prepare: jest.fn().mockReturnValue({ get: jest.fn().mockReturnValue({ encrypted_value: Buffer.from('1MNujnd6tlf09xoB4tvBLQ==', 'base64'), + name: 'foo', + path: '/', + host_key: '.someUrl.com', }), all: jest.fn().mockReturnValue([ { @@ -35,11 +38,16 @@ describe('chrome - linux', () => { it('gets and decrypts linux cookie', async () => { const res = await getCookie({ browser: Browser.Chrome, - url: 'https://someUrl.com', + domain: '.someUrl.com', cookieName: 'foo', }); - expect(res).toEqual('bar'); + expect(res).toEqual({ + value: 'bar', + host: '.someUrl.com', + path: '/', + name: 'foo', + }); }); it('lists and decrypts linux cookies', async () => { diff --git a/test/firefox.unit.test.ts b/test/firefox.unit.test.ts index 3e9a4d6..3194636 100644 --- a/test/firefox.unit.test.ts +++ b/test/firefox.unit.test.ts @@ -66,10 +66,10 @@ describe('firefox get cookie', () => { expect( await getCookie({ browser: Browser.Firefox, - url: 'https://some.url', + domain: '.some.url', cookieName: 'some-cookie', }), - ).toEqual('foo'); + ).toEqual({ value: 'foo' }); expect(sqlite).toHaveBeenCalledWith( `${homedir()}/Library/Application Support/Firefox/Profiles/tfhz7h6q.default-release/cookies.sqlite`, { fileMustExist: true, readonly: true }, @@ -116,10 +116,10 @@ describe('firefox get cookie', () => { expect( await getCookie({ browser: Browser.Firefox, - url: 'https://some.url', + domain: '.some.url', cookieName: 'some-cookie', }), - ).toEqual('foo'); + ).toEqual({ value: 'foo' }); expect(sqlite).toHaveBeenCalledWith( `${homedir()}/.mozilla/firefox/Profiles/tfhz7h6q.default-release/cookies.sqlite`, { fileMustExist: true, readonly: true }, @@ -151,10 +151,10 @@ describe('firefox get cookie', () => { expect( await getCookie({ browser: Browser.Firefox, - url: 'https://some.url', + domain: '.some.url', cookieName: 'some-cookie', }), - ).toEqual('foo'); + ).toEqual({ value: 'foo' }); expect(sqlite).toHaveBeenCalledWith( `C:/foo/Mozilla/Firefox/Profiles/tfhz7h6q.default-release/cookies.sqlite`, { fileMustExist: true, readonly: true }, @@ -182,7 +182,7 @@ describe('firefox get cookie', () => { await expect( getCookie({ browser: Browser.Firefox, - url: 'https://someurl.com', + domain: '.some.url', cookieName: 'some-cookie', }), ).rejects.toThrow('Platform freebsd is not supported'); diff --git a/yarn.lock b/yarn.lock index 9e46821..d8b1f84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3522,11 +3522,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -4030,13 +4025,6 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== -tldjs@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039" - integrity sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw== - dependencies: - punycode "^1.4.1" - tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"