browser-commander 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jscpd-baseline.json +48 -30
- package/CHANGELOG.md +32 -0
- package/README.md +19 -7
- package/eslint.config.js +1 -1
- package/examples/react-test-app/package-lock.json +890 -0
- package/examples/react-test-app/package.json +4 -4
- package/package.json +20 -17
- package/src/browser/attach/snapshot.js +4 -28
- package/src/browser/browser-cookie-credentials.js +15 -8
- package/src/browser/browser-cookies.js +36 -30
- package/src/browser/browser-profile-files.js +31 -0
- package/src/browser/browser-profiles.js +11 -13
- package/src/browser/browser-sources.js +39 -9
- package/src/browser/browser-sources.json +600 -23
- package/src/browser/connector.js +46 -4
- package/src/browser/launcher.js +26 -1
- package/src/browser/migration/capabilities.json +117 -0
- package/src/browser/migration/chromium-history.sql +22 -0
- package/src/browser/migration/chromium-writers.js +85 -0
- package/src/browser/migration/cookies.js +4 -1
- package/src/browser/migration/data-classes.js +68 -0
- package/src/browser/migration/domains.js +32 -0
- package/src/browser/migration/firefox-history.js +106 -0
- package/src/browser/migration/firefox.js +39 -25
- package/src/browser/migration/history.js +161 -2
- package/src/browser/migration/index.js +229 -83
- package/src/browser/migration/os-crypt-keys.js +1 -5
- package/src/browser/migration/password-metadata.js +95 -0
- package/src/browser/migration/passwords.js +78 -41
- package/src/browser/migration/safari-import.js +100 -0
- package/src/browser/migration/safari.js +110 -0
- package/src/browser/migration/sqlite-snapshot.js +31 -65
- package/src/browser/migration/validation.js +48 -0
- package/src/browser/real-browser.js +21 -3
- package/src/browser/safari-access.js +45 -0
- package/src/browser/safari-cookies.js +39 -36
- package/src/browser/safari-profiles.js +83 -0
- package/src/browser/storage-state.js +27 -0
- package/src/browser/system-browser.js +31 -143
- package/src/browser/webdriver.js +18 -2
- package/src/cli/args.js +7 -0
- package/src/cli/commands.js +12 -6
- package/src/cli/handle-methods.js +19 -2
- package/src/cli/high-level-methods.js +22 -3
- package/src/cli/modules.js +5 -1
- package/src/cli/sessions.js +14 -2
- package/src/tests/index.js +1 -0
- package/tests/e2e/engine-matrix.e2e.test.js +107 -0
- package/tests/e2e/playwright.e2e.test.js +11 -17
- package/tests/e2e/puppeteer.e2e.test.js +13 -17
- package/tests/unit/browser/attach/snapshot.test.js +30 -0
- package/tests/unit/browser/browser-cookies.test.js +35 -0
- package/tests/unit/browser/catalogue-launch.test.js +93 -0
- package/tests/unit/browser/cookie-sources.test.js +92 -17
- package/tests/unit/browser/credential-catalogue.test.js +50 -0
- package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
- package/tests/unit/browser/migration/data-classes.test.js +89 -0
- package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
- package/tests/unit/browser/migration/firefox-history.test.js +130 -0
- package/tests/unit/browser/migration/history-metadata.test.js +74 -0
- package/tests/unit/browser/migration/index.test.js +16 -1
- package/tests/unit/browser/migration/opera-keys.test.js +22 -0
- package/tests/unit/browser/migration/safari-data.test.js +143 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
- package/tests/unit/browser/migration/validation.test.js +63 -0
- package/tests/unit/browser/migration/yandex.test.js +84 -0
- package/tests/unit/browser/real-browser.test.js +19 -0
- package/tests/unit/browser/safari-cookies.test.js +26 -1
- package/tests/unit/browser/safari-profiles.test.js +111 -0
- package/tests/unit/cli/dispatcher.test.js +6 -2
- package/tests/unit/cli/main.test.js +5 -0
- package/tests/unit/cli/selenium-parity.test.js +186 -0
- package/tests/unit/scripts/browser-support.test.js +14 -7
- package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
- package/tests/unit/scripts/migration-support.test.js +31 -0
- package/tests/unit/tests/index.test.js +2 -1
- package/tests/unit/traces/links.test.js +2 -2
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Native common launch and commander contracts for every JS engine (#124). */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import { launchBrowser } from '../../src/browser/launcher.js';
|
|
7
|
+
import { makeBrowserCommander } from '../../src/factory.js';
|
|
8
|
+
import {
|
|
9
|
+
CHROME_LAUNCH_OPTIONS,
|
|
10
|
+
PARITY_CHROME,
|
|
11
|
+
} from '../helpers/e2e-browser.js';
|
|
12
|
+
|
|
13
|
+
// feature-parity: engines.webdriver@native-typed engines.cli-matrix@native-typed
|
|
14
|
+
describe('Common native engine API', { skip: !process.env.RUN_E2E }, () => {
|
|
15
|
+
it(
|
|
16
|
+
'selenium restores portable cookies and BiDi origin state',
|
|
17
|
+
{ timeout: 60000 },
|
|
18
|
+
async () => {
|
|
19
|
+
const server = createServer((_request, response) => {
|
|
20
|
+
response.end('<title>Portable state</title>');
|
|
21
|
+
});
|
|
22
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
23
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
24
|
+
let session;
|
|
25
|
+
try {
|
|
26
|
+
session = await launchBrowser({
|
|
27
|
+
engine: 'selenium',
|
|
28
|
+
launch: 'engine',
|
|
29
|
+
bidi: true,
|
|
30
|
+
headless: true,
|
|
31
|
+
executablePath: PARITY_CHROME,
|
|
32
|
+
...CHROME_LAUNCH_OPTIONS,
|
|
33
|
+
storageState: {
|
|
34
|
+
cookies: [{ name: 'session', value: 'restored', url: origin }],
|
|
35
|
+
origins: [
|
|
36
|
+
{ origin, localStorage: [{ name: 'theme', value: 'dark' }] },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
await session.page.goto(origin);
|
|
41
|
+
assert.equal(
|
|
42
|
+
await session.page.evaluate(() =>
|
|
43
|
+
globalThis.localStorage.getItem('theme')
|
|
44
|
+
),
|
|
45
|
+
'dark'
|
|
46
|
+
);
|
|
47
|
+
assert.match(
|
|
48
|
+
await session.page.evaluate(() => document.cookie),
|
|
49
|
+
/session=restored/u
|
|
50
|
+
);
|
|
51
|
+
} finally {
|
|
52
|
+
await session?.close();
|
|
53
|
+
await new Promise((resolve) => server.close(resolve));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
for (const engine of ['playwright', 'puppeteer', 'selenium']) {
|
|
58
|
+
for (const launch of ['real', 'engine']) {
|
|
59
|
+
it(
|
|
60
|
+
`${engine}/${launch}: input, click, native handles, PDF, cleanup`,
|
|
61
|
+
{ timeout: 120000 },
|
|
62
|
+
async () => {
|
|
63
|
+
const session = await launchBrowser({
|
|
64
|
+
engine,
|
|
65
|
+
launch,
|
|
66
|
+
headless: true,
|
|
67
|
+
executablePath: PARITY_CHROME,
|
|
68
|
+
...CHROME_LAUNCH_OPTIONS,
|
|
69
|
+
});
|
|
70
|
+
const commander = makeBrowserCommander({
|
|
71
|
+
page: session.page,
|
|
72
|
+
enableNetworkTracking: false,
|
|
73
|
+
enableNavigationManager: false,
|
|
74
|
+
});
|
|
75
|
+
const profile = session.userDataDir;
|
|
76
|
+
try {
|
|
77
|
+
assert.equal(commander.engine, engine);
|
|
78
|
+
await commander.goto({
|
|
79
|
+
url: `data:text/html,${encodeURIComponent('<input id="name"><button id="go" onclick="document.title=document.querySelector(\'#name\').value">Go</button>')}`,
|
|
80
|
+
});
|
|
81
|
+
await commander.fill({ selector: '#name', text: 'Ada' });
|
|
82
|
+
assert.equal(
|
|
83
|
+
await commander.inputValue({ selector: '#name' }),
|
|
84
|
+
'Ada'
|
|
85
|
+
);
|
|
86
|
+
await commander.click({ selector: '#go' });
|
|
87
|
+
assert.equal(await session.page.title(), 'Ada');
|
|
88
|
+
assert.ok(
|
|
89
|
+
Buffer.from(await session.page.pdf({ format: 'A4' }))
|
|
90
|
+
.subarray(0, 5)
|
|
91
|
+
.equals(Buffer.from('%PDF-'))
|
|
92
|
+
);
|
|
93
|
+
if (engine === 'selenium') {
|
|
94
|
+
assert.equal(await session.driver.getTitle(), 'Ada');
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
await commander.destroy();
|
|
98
|
+
await session.close();
|
|
99
|
+
}
|
|
100
|
+
if (session.temporaryProfile) {
|
|
101
|
+
assert.equal(existsSync(profile), false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, before, after } from 'node:test';
|
|
11
11
|
import assert from 'node:assert';
|
|
12
|
+
import { CHROME_LAUNCH_OPTIONS } from '../helpers/e2e-browser.js';
|
|
12
13
|
|
|
13
14
|
// Dynamic import for playwright since it may not be installed
|
|
14
15
|
let playwright;
|
|
@@ -25,23 +26,16 @@ describe(
|
|
|
25
26
|
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
|
|
26
27
|
|
|
27
28
|
before(async () => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
commander = createCommander({ page, verbose: true });
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.log(
|
|
41
|
-
'Skipping E2E tests - playwright not available or test app not running'
|
|
42
|
-
);
|
|
43
|
-
console.log('Error:', error.message);
|
|
44
|
-
}
|
|
29
|
+
playwright = await import('playwright');
|
|
30
|
+
const module = await import('../../src/index.js');
|
|
31
|
+
createCommander = module.createCommander;
|
|
32
|
+
browser = await playwright.chromium.launch({
|
|
33
|
+
headless: process.env.HEADLESS !== 'false',
|
|
34
|
+
...CHROME_LAUNCH_OPTIONS,
|
|
35
|
+
});
|
|
36
|
+
const context = await browser.newContext();
|
|
37
|
+
page = await context.newPage();
|
|
38
|
+
commander = createCommander({ page, verbose: true });
|
|
45
39
|
});
|
|
46
40
|
|
|
47
41
|
after(async () => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it, before, after } from 'node:test';
|
|
11
11
|
import assert from 'node:assert';
|
|
12
|
+
import { CHROME_LAUNCH_OPTIONS } from '../helpers/e2e-browser.js';
|
|
12
13
|
|
|
13
14
|
// Dynamic import for puppeteer since it may not be installed
|
|
14
15
|
let puppeteer;
|
|
@@ -22,22 +23,15 @@ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
|
|
|
22
23
|
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
|
|
23
24
|
|
|
24
25
|
before(async () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
commander = createCommander({ page, verbose: true });
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.log(
|
|
37
|
-
'Skipping E2E tests - puppeteer not available or test app not running'
|
|
38
|
-
);
|
|
39
|
-
console.log('Error:', error.message);
|
|
40
|
-
}
|
|
26
|
+
puppeteer = await import('puppeteer');
|
|
27
|
+
const module = await import('../../src/index.js');
|
|
28
|
+
createCommander = module.createCommander;
|
|
29
|
+
browser = await puppeteer.default.launch({
|
|
30
|
+
headless: process.env.HEADLESS !== 'false',
|
|
31
|
+
...CHROME_LAUNCH_OPTIONS,
|
|
32
|
+
});
|
|
33
|
+
page = await browser.newPage();
|
|
34
|
+
commander = createCommander({ page, verbose: true });
|
|
41
35
|
});
|
|
42
36
|
|
|
43
37
|
after(async () => {
|
|
@@ -352,7 +346,9 @@ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
|
|
|
352
346
|
}
|
|
353
347
|
|
|
354
348
|
// Scroll to specific item
|
|
355
|
-
await commander.
|
|
349
|
+
await commander.scrollIntoView({
|
|
350
|
+
locatorOrElement: await page.$('[data-testid="scroll-item-15"]'),
|
|
351
|
+
});
|
|
356
352
|
await commander.wait({ ms: 500 });
|
|
357
353
|
|
|
358
354
|
// Target should be visible
|
|
@@ -34,6 +34,36 @@ function snapshotChrome(root, to) {
|
|
|
34
34
|
return snapshotUserDataDir({ browser: 'chrome', userDataDir: root, to });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
it(
|
|
38
|
+
'reports a locked database with snapshot guidance and leaves no target database',
|
|
39
|
+
{ timeout: 5000 },
|
|
40
|
+
async () => {
|
|
41
|
+
const root = await makeTempDir();
|
|
42
|
+
const target = await makeTempDir();
|
|
43
|
+
const profile = path.join(root, 'Default');
|
|
44
|
+
await mkdir(profile);
|
|
45
|
+
const filename = await writeChromiumHistory(profile, 1);
|
|
46
|
+
const writer = new BetterSqlite3(filename);
|
|
47
|
+
writer.exec('BEGIN EXCLUSIVE');
|
|
48
|
+
try {
|
|
49
|
+
const report = await snapshotChrome(root, target);
|
|
50
|
+
assert.equal(report.copied.databases, 0);
|
|
51
|
+
assert.ok(
|
|
52
|
+
report.skipped.some(
|
|
53
|
+
({ item, reason, detail }) =>
|
|
54
|
+
item === 'Default/History' &&
|
|
55
|
+
reason === 'unreadable' &&
|
|
56
|
+
detail.includes('Consistent SQLite snapshot unavailable')
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
await assert.rejects(stat(path.join(target, 'Default/History')));
|
|
60
|
+
} finally {
|
|
61
|
+
writer.exec('ROLLBACK');
|
|
62
|
+
writer.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
37
67
|
/** Write `content` to `root/relativePath`, creating the parent folders. */
|
|
38
68
|
async function put(root, relativePath, content = relativePath) {
|
|
39
69
|
const filePath = path.join(root, ...relativePath.split('/'));
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
mkdtemp,
|
|
13
13
|
readFile,
|
|
14
14
|
readdir,
|
|
15
|
+
rename,
|
|
15
16
|
rm,
|
|
16
17
|
stat,
|
|
17
18
|
writeFile,
|
|
@@ -149,6 +150,40 @@ describe('installed browser cookie import', () => {
|
|
|
149
150
|
}
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
it('discovers and decrypts Yandex Cookies through its catalogue alias', async () => {
|
|
154
|
+
temporaryDirectory = await mkdtemp(
|
|
155
|
+
path.join(os.tmpdir(), 'bc-yandex-cookies-')
|
|
156
|
+
);
|
|
157
|
+
const host = '.yandex.ru';
|
|
158
|
+
const { root } = await createChromiumProfile({
|
|
159
|
+
homeDir: temporaryDirectory,
|
|
160
|
+
rows: [
|
|
161
|
+
{
|
|
162
|
+
host,
|
|
163
|
+
name: 'SID',
|
|
164
|
+
encryptedValue: encryptCbcCookie({
|
|
165
|
+
host,
|
|
166
|
+
value: 'yandex-session',
|
|
167
|
+
password: 'peanuts',
|
|
168
|
+
prefix: 'v10',
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
const yandexRoot = path.join(temporaryDirectory, '.config/yandex-browser');
|
|
174
|
+
await rename(root, yandexRoot);
|
|
175
|
+
const file = path.join(yandexRoot, 'Default/Network/Cookies');
|
|
176
|
+
const before = await readFile(file);
|
|
177
|
+
const cookies = await readBrowserCookiesWithDependencies(
|
|
178
|
+
{ browser: 'yandex-browser', domainFilter: 'yandex.ru', cache: false },
|
|
179
|
+
{ platform: 'linux', homeDir: temporaryDirectory, environment: {} }
|
|
180
|
+
);
|
|
181
|
+
assert.equal(cookies.length, 1);
|
|
182
|
+
assert.equal(cookies[0].domain, host);
|
|
183
|
+
assert.equal(cookies[0].value, 'yandex-session');
|
|
184
|
+
assert.deepEqual(await readFile(file), before);
|
|
185
|
+
});
|
|
186
|
+
|
|
152
187
|
it('exports both public helpers', () => {
|
|
153
188
|
assert.equal(publicListBrowserProfiles, listBrowserProfiles);
|
|
154
189
|
assert.equal(publicReadBrowserCookies, readBrowserCookies);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { mkdir, symlink } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { useTempDirectories } from '../../helpers/temp-directory.js';
|
|
6
|
+
import {
|
|
7
|
+
BROWSER_SOURCES,
|
|
8
|
+
resolveBrowserRoots,
|
|
9
|
+
} from '../../../src/browser/browser-sources.js';
|
|
10
|
+
import {
|
|
11
|
+
assertDedicatedUserDataDir,
|
|
12
|
+
CHANNEL_EXECUTABLE_NAMES,
|
|
13
|
+
} from '../../../src/browser/system-browser.js';
|
|
14
|
+
|
|
15
|
+
const temporary = useTempDirectories('bc-catalogue-protection-');
|
|
16
|
+
|
|
17
|
+
describe('catalogue launch and protection (#121)', () => {
|
|
18
|
+
it('protects a new profile beneath a symlink to a default root', async () => {
|
|
19
|
+
const homeDir = await temporary();
|
|
20
|
+
const options = { homeDir, environment: {} };
|
|
21
|
+
const root = resolveBrowserRoots('chrome', options)[0];
|
|
22
|
+
await mkdir(root, { recursive: true });
|
|
23
|
+
const alias = path.join(homeDir, 'profile-alias');
|
|
24
|
+
await symlink(
|
|
25
|
+
root,
|
|
26
|
+
alias,
|
|
27
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
28
|
+
);
|
|
29
|
+
assert.throws(
|
|
30
|
+
() =>
|
|
31
|
+
assertDedicatedUserDataDir(path.join(alias, 'new-profile'), options),
|
|
32
|
+
/dedicated.*default profile/
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('offers launch names for every supported Chromium source and alias', () => {
|
|
37
|
+
for (const browser of BROWSER_SOURCES.filter(
|
|
38
|
+
(entry) => entry.family === 'chromium'
|
|
39
|
+
)) {
|
|
40
|
+
assert.ok(CHANNEL_EXECUTABLE_NAMES[browser.id]?.length, browser.id);
|
|
41
|
+
for (const alias of browser.aliases ?? []) {
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
CHANNEL_EXECUTABLE_NAMES[alias],
|
|
44
|
+
CHANNEL_EXECUTABLE_NAMES[browser.id]
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('protects every catalogue root and descendant profile on every OS', () => {
|
|
51
|
+
for (const platform of ['linux', 'darwin', 'win32']) {
|
|
52
|
+
const options = {
|
|
53
|
+
platform,
|
|
54
|
+
homeDir: platform === 'win32' ? 'C:\\Users\\test' : '/users/test',
|
|
55
|
+
environment: {},
|
|
56
|
+
};
|
|
57
|
+
for (const browser of BROWSER_SOURCES) {
|
|
58
|
+
for (const root of resolveBrowserRoots(browser.id, options)) {
|
|
59
|
+
assert.throws(
|
|
60
|
+
() => assertDedicatedUserDataDir(root, options),
|
|
61
|
+
/dedicated.*default profile/,
|
|
62
|
+
`${browser.id}: ${root}`
|
|
63
|
+
);
|
|
64
|
+
assert.throws(
|
|
65
|
+
() =>
|
|
66
|
+
assertDedicatedUserDataDir(
|
|
67
|
+
`${root}${platform === 'win32' ? '\\' : '/'}Profile 1`,
|
|
68
|
+
options
|
|
69
|
+
),
|
|
70
|
+
/dedicated.*default profile/
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('adds the requested desktop catalogue entries', () => {
|
|
78
|
+
for (const id of [
|
|
79
|
+
'whale',
|
|
80
|
+
'360se',
|
|
81
|
+
'360chrome',
|
|
82
|
+
'qq',
|
|
83
|
+
'sogou',
|
|
84
|
+
'duckduckgo',
|
|
85
|
+
'tor',
|
|
86
|
+
]) {
|
|
87
|
+
assert.ok(
|
|
88
|
+
BROWSER_SOURCES.some((entry) => entry.id === id),
|
|
89
|
+
id
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -1,17 +1,78 @@
|
|
|
1
1
|
// feature-parity: sources.cookie-listing@native-typed
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
4
7
|
|
|
5
8
|
import {
|
|
6
9
|
listCookieSources,
|
|
7
10
|
resolveImportSource,
|
|
8
11
|
} from '../../../src/browser/browser-cookies.js';
|
|
12
|
+
import { migrateProfile } from '../../../src/browser/migration/index.js';
|
|
9
13
|
import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
|
|
10
14
|
import { useTempDirectories } from '../../helpers/temp-directory.js';
|
|
11
15
|
|
|
12
16
|
const makeDirectory = useTempDirectories();
|
|
13
17
|
|
|
14
18
|
describe('listCookieSources', () => {
|
|
19
|
+
it('matches only whole hosts and subdomains in both SQLite families', async () => {
|
|
20
|
+
const hosts = [
|
|
21
|
+
'.github.com',
|
|
22
|
+
'api.GITHUB.COM.',
|
|
23
|
+
'notgithub.com',
|
|
24
|
+
'.github.com.attacker.test',
|
|
25
|
+
'.gitXhub.com',
|
|
26
|
+
];
|
|
27
|
+
for (const browser of ['firefox', 'chrome']) {
|
|
28
|
+
const homeDir = await makeDirectory('bc-source-domains-');
|
|
29
|
+
let file;
|
|
30
|
+
if (browser === 'firefox') {
|
|
31
|
+
const profile = await writeFirefoxProfile(
|
|
32
|
+
homeDir,
|
|
33
|
+
hosts.map((host, index) => ({
|
|
34
|
+
host,
|
|
35
|
+
name: `${index}`,
|
|
36
|
+
value: 'secret',
|
|
37
|
+
}))
|
|
38
|
+
);
|
|
39
|
+
file = path.join(profile, 'cookies.sqlite');
|
|
40
|
+
} else {
|
|
41
|
+
const profile = path.join(homeDir, '.config/google-chrome/Default');
|
|
42
|
+
await mkdir(profile, { recursive: true });
|
|
43
|
+
file = path.join(profile, 'Cookies');
|
|
44
|
+
const database = new BetterSqlite3(file);
|
|
45
|
+
try {
|
|
46
|
+
database.exec('CREATE TABLE cookies (host_key TEXT, value TEXT)');
|
|
47
|
+
for (const host of hosts) {
|
|
48
|
+
database
|
|
49
|
+
.prepare('INSERT INTO cookies VALUES (?, ?)')
|
|
50
|
+
.run(host, 'secret');
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
database.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const before = await readFile(file);
|
|
57
|
+
const sources = await listCookieSources({
|
|
58
|
+
domains: ['GITHUB.COM.', 'git_hub.com', '%github.com'],
|
|
59
|
+
platform: 'linux',
|
|
60
|
+
homeDir,
|
|
61
|
+
environment: {},
|
|
62
|
+
});
|
|
63
|
+
assert.equal(sources.length, 1);
|
|
64
|
+
assert.equal(sources[0].browser, browser);
|
|
65
|
+
assert.equal(sources[0].cookies, hosts.length);
|
|
66
|
+
assert.deepEqual(sources[0].byDomain, {
|
|
67
|
+
'GITHUB.COM.': 2,
|
|
68
|
+
'git_hub.com': 0,
|
|
69
|
+
'%github.com': 0,
|
|
70
|
+
});
|
|
71
|
+
assert.ok(!JSON.stringify(sources).includes('secret'));
|
|
72
|
+
assert.deepEqual(await readFile(file), before);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
15
76
|
it('reports cookie counts per profile without reading values', async () => {
|
|
16
77
|
const temporaryDirectory = await makeDirectory('bc-src-');
|
|
17
78
|
const profilePath = await writeFirefoxProfile(temporaryDirectory, [
|
|
@@ -92,23 +153,37 @@ describe('resolveImportSource', () => {
|
|
|
92
153
|
});
|
|
93
154
|
});
|
|
94
155
|
|
|
95
|
-
it('falls back
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
156
|
+
it('falls back when the default holds unrelated or lookalike domains', async () => {
|
|
157
|
+
for (const host of ['.other.test', '.notgithub.com']) {
|
|
158
|
+
const homeDir = await makeDirectory('bc-import-fallback-');
|
|
159
|
+
await writeFirefoxProfile(homeDir, [{ ...otherCookie[0], host }]);
|
|
160
|
+
await writeFirefoxProfile(homeDir, githubCookie, {
|
|
161
|
+
root: '.librewolf',
|
|
162
|
+
name: 'default',
|
|
163
|
+
});
|
|
164
|
+
const source = await resolve(homeDir);
|
|
165
|
+
assert.equal(source.browser, 'librewolf');
|
|
166
|
+
assert.equal(source.profile, 'default');
|
|
167
|
+
assert.equal(source.warning.reason, 'default-browser-fallback');
|
|
168
|
+
assert.equal(source.warning.item, 'librewolf');
|
|
169
|
+
assert.match(
|
|
170
|
+
source.warning.detail,
|
|
171
|
+
/default browser \(firefox\) holds no cookies for github\.com; imported from librewolf/
|
|
172
|
+
);
|
|
173
|
+
const report = await migrateProfile({
|
|
174
|
+
from: { browser: 'auto' },
|
|
175
|
+
to: path.join(homeDir, 'target'),
|
|
176
|
+
include: ['cookies'],
|
|
177
|
+
domains: ['github.com'],
|
|
178
|
+
platform: 'linux',
|
|
179
|
+
homeDir,
|
|
180
|
+
environment: {},
|
|
181
|
+
runCommand: firefoxIsDefault,
|
|
182
|
+
});
|
|
183
|
+
assert.equal(report.source.browser, 'librewolf');
|
|
184
|
+
assert.equal(report.migrated.cookies, 1);
|
|
185
|
+
assert.equal(report.cookies[0].domain, '.github.com');
|
|
186
|
+
}
|
|
112
187
|
});
|
|
113
188
|
|
|
114
189
|
it('falls back when the system default cannot be determined', async () => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { BROWSER_SOURCES } from '../../../src/browser/browser-sources.js';
|
|
4
|
+
import { readSafeStoragePassword } from '../../../src/browser/browser-cookie-credentials.js';
|
|
5
|
+
|
|
6
|
+
describe('catalogue credential identities and Keychain diagnostics', () => {
|
|
7
|
+
it('requests every declared browser service rather than a four-browser table', async () => {
|
|
8
|
+
for (const browser of BROWSER_SOURCES.filter(
|
|
9
|
+
(entry) => entry.safeStorage
|
|
10
|
+
)) {
|
|
11
|
+
for (const name of [browser.id, ...(browser.aliases ?? [])]) {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const password = await readSafeStoragePassword({
|
|
14
|
+
browser: name,
|
|
15
|
+
platform: 'darwin',
|
|
16
|
+
environment: {},
|
|
17
|
+
runCredentialCommand: async (command, args) => {
|
|
18
|
+
calls.push([command, args]);
|
|
19
|
+
return 'synthetic-password';
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
assert.equal(password, 'synthetic-password');
|
|
23
|
+
assert.deepEqual(calls, [
|
|
24
|
+
[
|
|
25
|
+
'security',
|
|
26
|
+
['find-generic-password', '-w', '-s', browser.safeStorage.service],
|
|
27
|
+
],
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const outcome of ['denied', 'empty']) {
|
|
34
|
+
it(`identifies the item and retry path when Keychain returns ${outcome}`, async () => {
|
|
35
|
+
await assert.rejects(
|
|
36
|
+
readSafeStoragePassword({
|
|
37
|
+
browser: 'chrome',
|
|
38
|
+
platform: 'darwin',
|
|
39
|
+
runCredentialCommand: async () => {
|
|
40
|
+
if (outcome === 'denied') {
|
|
41
|
+
throw new Error('security exited with code 36');
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
/Chrome Safe Storage.*Keychain.*allow.*retry.*refresh=true/s
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { it } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { openSqliteDatabase } from '../../../src/browser/browser-cookie-database.js';
|
|
8
|
+
import { readBrowserCookiesWithDependencies } from '../../../src/browser/browser-cookies.js';
|
|
9
|
+
import { migrateProfile } from '../../../src/browser/migration/index.js';
|
|
10
|
+
|
|
11
|
+
const fixture = await readFile(
|
|
12
|
+
new URL(
|
|
13
|
+
'../../../../tests/fixtures/firefox-cookie-expiry.sql',
|
|
14
|
+
import.meta.url
|
|
15
|
+
),
|
|
16
|
+
'utf8'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
for (const version of [0, 15, 16, 17]) {
|
|
20
|
+
for (const migration of [false, true]) {
|
|
21
|
+
it(`normalizes Firefox schema ${version} expiry through ${migration ? 'migration' : 'installed reading'}`, async () => {
|
|
22
|
+
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'bc-ff-expiry-'));
|
|
23
|
+
try {
|
|
24
|
+
const root = path.join(homeDir, '.mozilla', 'firefox');
|
|
25
|
+
const source = path.join(root, 'expiry.default-release');
|
|
26
|
+
await mkdir(source, { recursive: true });
|
|
27
|
+
await writeFile(
|
|
28
|
+
path.join(root, 'profiles.ini'),
|
|
29
|
+
'[Profile0]\nName=default-release\nIsRelative=1\nPath=expiry.default-release\nDefault=1\n'
|
|
30
|
+
);
|
|
31
|
+
const file = path.join(source, 'cookies.sqlite');
|
|
32
|
+
const database = await openSqliteDatabase(file);
|
|
33
|
+
try {
|
|
34
|
+
database.exec(fixture);
|
|
35
|
+
database.exec(`PRAGMA user_version = ${version}`);
|
|
36
|
+
if (version >= 16) {
|
|
37
|
+
database.exec(
|
|
38
|
+
'UPDATE moz_cookies SET expiry = expiry * 1000 + 999 WHERE expiry > 0'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
database.close();
|
|
43
|
+
}
|
|
44
|
+
const before = await readFile(file);
|
|
45
|
+
const options = { platform: 'linux', homeDir, environment: {} };
|
|
46
|
+
const cookies = migration
|
|
47
|
+
? (
|
|
48
|
+
await migrateProfile({
|
|
49
|
+
from: { browser: 'firefox', userDataDir: source },
|
|
50
|
+
to: path.join(homeDir, 'target'),
|
|
51
|
+
include: ['cookies'],
|
|
52
|
+
domains: ['expiry.example'],
|
|
53
|
+
...options,
|
|
54
|
+
})
|
|
55
|
+
).cookies
|
|
56
|
+
: await readBrowserCookiesWithDependencies(
|
|
57
|
+
{
|
|
58
|
+
browser: 'firefox',
|
|
59
|
+
domainFilter: 'expiry.example',
|
|
60
|
+
cache: false,
|
|
61
|
+
},
|
|
62
|
+
options
|
|
63
|
+
);
|
|
64
|
+
assert.equal(cookies.length, 3);
|
|
65
|
+
const expiry = Object.fromEntries(
|
|
66
|
+
cookies.map((cookie) => [cookie.name, cookie.expires])
|
|
67
|
+
);
|
|
68
|
+
assert.deepEqual(expiry, {
|
|
69
|
+
persistent: 2_000_000_001,
|
|
70
|
+
'session-negative': -1,
|
|
71
|
+
'session-zero': -1,
|
|
72
|
+
});
|
|
73
|
+
assert.deepEqual(await readFile(file), before);
|
|
74
|
+
} finally {
|
|
75
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|