browser-commander 0.19.0 → 0.21.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/CHANGELOG.md +156 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +25 -6
- package/scripts/run-tests.mjs +4 -2
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/downloads/manager.js +9 -0
- package/src/downloads/sources.js +71 -0
- package/src/downloads/staging.js +21 -9
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/traces/bundle.js +10 -2
- package/src/traces/recorder.js +16 -3
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/downloads.e2e.test.js +79 -2
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/trace-fixtures.js +18 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/downloads/manager.test.js +82 -9
- package/tests/unit/downloads/staging.test.js +24 -2
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
- package/tests/unit/scripts/pre-commit-config.test.js +2 -1
- package/tests/unit/traces/bundle.test.js +24 -3
- package/tests/unit/traces/links.test.js +7 -0
- package/tests/unit/traces/reader.test.js +11 -4
- package/tests/unit/traces/recorder.test.js +4 -0
- package/tests/unit/traces/viewer.test.js +4 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DBSC_BOUND_COOKIE_NAMES,
|
|
8
|
+
isDbscBoundCookie,
|
|
9
|
+
migrateCookies,
|
|
10
|
+
} from '../../../../src/browser/migration/cookies.js';
|
|
11
|
+
|
|
12
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
13
|
+
|
|
14
|
+
const makeTempDir = useTempDirectories('bc-cookies-');
|
|
15
|
+
|
|
16
|
+
describe('isDbscBoundCookie', () => {
|
|
17
|
+
it('flags rotating Google session-token cookies', () => {
|
|
18
|
+
for (const name of DBSC_BOUND_COOKIE_NAMES) {
|
|
19
|
+
assert.equal(
|
|
20
|
+
isDbscBoundCookie({ name, domain: '.google.com' }),
|
|
21
|
+
true,
|
|
22
|
+
name
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
assert.equal(
|
|
26
|
+
isDbscBoundCookie({ name: 'SID', domain: '.google.com' }),
|
|
27
|
+
false
|
|
28
|
+
);
|
|
29
|
+
assert.equal(
|
|
30
|
+
isDbscBoundCookie({ name: '__Secure-1PSIDTS', domain: '.example.com' }),
|
|
31
|
+
false
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('migrateCookies', () => {
|
|
37
|
+
it('dedupes across domain filters and tags DBSC-bound cookies', async () => {
|
|
38
|
+
const calls = [];
|
|
39
|
+
const readCookies = async ({ domainFilter }) => {
|
|
40
|
+
calls.push(domainFilter);
|
|
41
|
+
if (domainFilter === 'google.com') {
|
|
42
|
+
return [
|
|
43
|
+
{ name: '__Secure-1PSIDTS', domain: '.google.com', path: '/' },
|
|
44
|
+
{ name: 'SID', domain: '.google.com', path: '/' },
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
return [{ name: 'session', domain: '.example.com', path: '/' }];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const report = await migrateCookies({
|
|
51
|
+
browser: 'chrome',
|
|
52
|
+
domains: ['google.com', 'example.com'],
|
|
53
|
+
readCookies,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(calls, ['google.com', 'example.com']);
|
|
57
|
+
assert.equal(report.cookies.length, 3);
|
|
58
|
+
assert.equal(report.migrated, 3);
|
|
59
|
+
assert.equal(report.skipped.length, 1);
|
|
60
|
+
assert.equal(report.skipped[0].reason, 'dbsc-bound');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('warns when the source has a DBSC registration database', async () => {
|
|
64
|
+
const sourceProfileDir = await makeTempDir();
|
|
65
|
+
await mkdir(path.join(sourceProfileDir, 'Network'), { recursive: true });
|
|
66
|
+
await writeFile(
|
|
67
|
+
path.join(sourceProfileDir, 'Network', 'DeviceBoundSessions'),
|
|
68
|
+
'x'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const report = await migrateCookies({
|
|
72
|
+
browser: 'chrome',
|
|
73
|
+
sourceProfileDir,
|
|
74
|
+
readCookies: async () => [
|
|
75
|
+
{ name: 'a', domain: '.example.com', path: '/' },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.equal(report.warnings.length, 1);
|
|
80
|
+
assert.equal(report.warnings[0].reason, 'dbsc-registration-present');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('passes ignoreDecryptionErrors to the reader', async () => {
|
|
84
|
+
let seen;
|
|
85
|
+
await migrateCookies({
|
|
86
|
+
browser: 'chrome',
|
|
87
|
+
readCookies: async (options) => {
|
|
88
|
+
seen = options;
|
|
89
|
+
return [];
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
assert.equal(seen.ignoreDecryptionErrors, true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
migrateExtensions,
|
|
8
|
+
selectMigratableExtensions,
|
|
9
|
+
} from '../../../../src/browser/migration/extensions.js';
|
|
10
|
+
import {
|
|
11
|
+
migrateBetween,
|
|
12
|
+
readProfileJson,
|
|
13
|
+
writeProfileJson,
|
|
14
|
+
} from '../../../helpers/migration-fixtures.js';
|
|
15
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
16
|
+
|
|
17
|
+
const makeTempDir = useTempDirectories('bc-ext-');
|
|
18
|
+
|
|
19
|
+
async function writeExtension(profileDir, id, version) {
|
|
20
|
+
const dir = path.join(profileDir, 'Extensions', id, version);
|
|
21
|
+
await mkdir(dir, { recursive: true });
|
|
22
|
+
await writeFile(
|
|
23
|
+
path.join(dir, 'manifest.json'),
|
|
24
|
+
JSON.stringify({ name: id, version, manifest_version: 3 })
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('selectMigratableExtensions', () => {
|
|
29
|
+
it('excludes policy-installed and component extensions', () => {
|
|
30
|
+
const { eligible, excluded } = selectMigratableExtensions({
|
|
31
|
+
user: { location: 1 },
|
|
32
|
+
component: { location: 5 },
|
|
33
|
+
policy: { location: 7 },
|
|
34
|
+
external: { location: 10 },
|
|
35
|
+
});
|
|
36
|
+
assert.deepEqual(eligible, ['user']);
|
|
37
|
+
assert.equal(excluded.length, 3);
|
|
38
|
+
assert.ok(
|
|
39
|
+
excluded.every((e) => e.reason === 'policy-or-component-extension')
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('migrateExtensions', () => {
|
|
45
|
+
it('copies eligible extension files and warns about the MAC', async () => {
|
|
46
|
+
const source = await makeTempDir();
|
|
47
|
+
const target = await makeTempDir();
|
|
48
|
+
await writeExtension(source, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', '1.0');
|
|
49
|
+
await writeExtension(source, 'cccccccccccccccccccccccccccccccc', '2.0');
|
|
50
|
+
await writeProfileJson(source, 'Secure Preferences', {
|
|
51
|
+
extensions: {
|
|
52
|
+
settings: {
|
|
53
|
+
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: { location: 1, manifest: {} },
|
|
54
|
+
cccccccccccccccccccccccccccccccc: { location: 5 },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const report = await migrateBetween(migrateExtensions, source, target);
|
|
60
|
+
|
|
61
|
+
assert.equal(report.migrated, 1);
|
|
62
|
+
assert.ok(
|
|
63
|
+
report.skipped.some(
|
|
64
|
+
(s) =>
|
|
65
|
+
s.item === 'cccccccccccccccccccccccccccccccc' &&
|
|
66
|
+
s.reason === 'policy-or-component-extension'
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
assert.equal(report.warnings[0].reason, 'mac-will-not-validate');
|
|
70
|
+
|
|
71
|
+
const copied = await readFile(
|
|
72
|
+
path.join(
|
|
73
|
+
target,
|
|
74
|
+
'Extensions',
|
|
75
|
+
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
76
|
+
'1.0',
|
|
77
|
+
'manifest.json'
|
|
78
|
+
),
|
|
79
|
+
'utf8'
|
|
80
|
+
);
|
|
81
|
+
assert.match(copied, /manifest_version/);
|
|
82
|
+
|
|
83
|
+
const targetSecure = await readProfileJson(target, 'Secure Preferences');
|
|
84
|
+
assert.ok(
|
|
85
|
+
targetSecure.extensions.settings.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('falls back to the directory listing without Secure Preferences', async () => {
|
|
90
|
+
const source = await makeTempDir();
|
|
91
|
+
const target = await makeTempDir();
|
|
92
|
+
await writeExtension(source, 'dddddddddddddddddddddddddddddddd', '1.0');
|
|
93
|
+
|
|
94
|
+
const report = await migrateBetween(migrateExtensions, source, target);
|
|
95
|
+
assert.equal(report.migrated, 1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('reports nothing when there is no Extensions directory', async () => {
|
|
99
|
+
const source = await makeTempDir();
|
|
100
|
+
const target = await makeTempDir();
|
|
101
|
+
const report = await migrateBetween(migrateExtensions, source, target);
|
|
102
|
+
assert.deepEqual(report, { migrated: 0, skipped: [], warnings: [] });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
|
|
6
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
decodeDerElement,
|
|
10
|
+
decodeDerSequence,
|
|
11
|
+
oidToString,
|
|
12
|
+
} from '../../../../src/browser/migration/firefox-der.js';
|
|
13
|
+
import {
|
|
14
|
+
PrimaryPasswordError,
|
|
15
|
+
decryptFirefoxField,
|
|
16
|
+
recoverFirefoxKeyFromDatabase,
|
|
17
|
+
} from '../../../../src/browser/migration/firefox-nss.js';
|
|
18
|
+
import {
|
|
19
|
+
buildKey4Database,
|
|
20
|
+
encodeLoginField,
|
|
21
|
+
} from '../../../fixtures/firefox-nss-fixtures.mjs';
|
|
22
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
23
|
+
|
|
24
|
+
const makeTempDir = useTempDirectories('bc-nss-');
|
|
25
|
+
|
|
26
|
+
// Build a key4.db, open it read-only as the migration does, and hand it to
|
|
27
|
+
// `check` together with the fixture's `loginKey`.
|
|
28
|
+
async function withKey4Database(options, check) {
|
|
29
|
+
const fixture = buildKey4Database({ dir: await makeTempDir(), ...options });
|
|
30
|
+
const db = new BetterSqlite3(fixture.key4Path, { readonly: true });
|
|
31
|
+
try {
|
|
32
|
+
check(db, fixture);
|
|
33
|
+
} finally {
|
|
34
|
+
db.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('firefox-der', () => {
|
|
39
|
+
it('decodes a nested SEQUENCE and OID', () => {
|
|
40
|
+
// SEQUENCE { OID 1.2.840.113549.1.5.13, OCTET STRING 0x01 0x02 }
|
|
41
|
+
const oid = Buffer.from([
|
|
42
|
+
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0d,
|
|
43
|
+
]);
|
|
44
|
+
const octet = Buffer.from([0x04, 0x02, 0x01, 0x02]);
|
|
45
|
+
const seq = Buffer.concat([
|
|
46
|
+
Buffer.from([0x30, oid.length + octet.length]),
|
|
47
|
+
oid,
|
|
48
|
+
octet,
|
|
49
|
+
]);
|
|
50
|
+
const decoded = decodeDerElement(seq);
|
|
51
|
+
const [oidElement, octetElement] = decodeDerSequence(decoded.content);
|
|
52
|
+
assert.equal(oidToString(oidElement.content), '1.2.840.113549.1.5.13');
|
|
53
|
+
assert.deepEqual([...octetElement.content], [0x01, 0x02]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('recoverFirefoxKeyFromDatabase', () => {
|
|
58
|
+
it('recovers the 3DES login key via PBES2 with no primary password', async () => {
|
|
59
|
+
await withKey4Database({}, (db, { loginKey }) => {
|
|
60
|
+
const recovered = recoverFirefoxKeyFromDatabase(db);
|
|
61
|
+
assert.deepEqual(recovered, loginKey);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('round-trips a login field decryption', async () => {
|
|
66
|
+
await withKey4Database({}, (db) => {
|
|
67
|
+
const recovered = recoverFirefoxKeyFromDatabase(db);
|
|
68
|
+
const encoded = encodeLoginField({
|
|
69
|
+
key: recovered,
|
|
70
|
+
plaintext: 'super-secret',
|
|
71
|
+
});
|
|
72
|
+
assert.equal(decryptFirefoxField(encoded, recovered), 'super-secret');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('throws PrimaryPasswordError when the wrong password is supplied', async () => {
|
|
77
|
+
const primaryPassword = Buffer.from('correct horse');
|
|
78
|
+
await withKey4Database({ primaryPassword }, (db) => {
|
|
79
|
+
assert.throws(
|
|
80
|
+
() => recoverFirefoxKeyFromDatabase(db, Buffer.from('wrong')),
|
|
81
|
+
PrimaryPasswordError
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('recovers with the correct primary password', async () => {
|
|
87
|
+
const primaryPassword = Buffer.from('correct horse');
|
|
88
|
+
await withKey4Database({ primaryPassword }, (db, { loginKey }) => {
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
recoverFirefoxKeyFromDatabase(db, primaryPassword),
|
|
91
|
+
loginKey
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('reports a helpful error when metadata is missing', async () => {
|
|
97
|
+
const dir = await makeTempDir();
|
|
98
|
+
const key4Path = path.join(dir, 'key4.db');
|
|
99
|
+
const db = new BetterSqlite3(key4Path);
|
|
100
|
+
db.exec('CREATE TABLE metadata (id TEXT, item1 BLOB, item2 BLOB)');
|
|
101
|
+
db.close();
|
|
102
|
+
await writeFile(path.join(dir, 'marker'), 'x');
|
|
103
|
+
const readonly = new BetterSqlite3(key4Path, { readonly: true });
|
|
104
|
+
try {
|
|
105
|
+
assert.throws(
|
|
106
|
+
() => recoverFirefoxKeyFromDatabase(readonly),
|
|
107
|
+
/no password metadata/
|
|
108
|
+
);
|
|
109
|
+
} finally {
|
|
110
|
+
readonly.close();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
migrateFirefoxBookmarks,
|
|
7
|
+
migrateFirefoxPasswords,
|
|
8
|
+
readFirefoxCookies,
|
|
9
|
+
reportFirefoxHistory,
|
|
10
|
+
} from '../../../../src/browser/migration/firefox.js';
|
|
11
|
+
import { deriveChromiumCookieKey } from '../../../../src/browser/browser-cookie-crypto.js';
|
|
12
|
+
import {
|
|
13
|
+
assertNothingMigrated,
|
|
14
|
+
readMigratedLogins,
|
|
15
|
+
readProfileJson,
|
|
16
|
+
writeFirefoxCookies,
|
|
17
|
+
writeFirefoxLogins,
|
|
18
|
+
writeFirefoxPlaces,
|
|
19
|
+
} from '../../../helpers/migration-fixtures.js';
|
|
20
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
21
|
+
|
|
22
|
+
const makeTempDir = useTempDirectories('bc-ff-');
|
|
23
|
+
|
|
24
|
+
describe('readFirefoxCookies', () => {
|
|
25
|
+
it('reads and maps cookies, applying the domain filter', async () => {
|
|
26
|
+
const dir = await makeTempDir();
|
|
27
|
+
await writeFirefoxCookies(dir, [
|
|
28
|
+
{ name: 'a', value: '1', host: '.example.com', secure: true },
|
|
29
|
+
{ name: 'b', value: '2', host: '.other.com' },
|
|
30
|
+
]);
|
|
31
|
+
const all = await readFirefoxCookies({ profileDir: dir });
|
|
32
|
+
assert.equal(all.length, 2);
|
|
33
|
+
const filtered = await readFirefoxCookies({
|
|
34
|
+
profileDir: dir,
|
|
35
|
+
domains: ['example.com'],
|
|
36
|
+
});
|
|
37
|
+
assert.equal(filtered.length, 1);
|
|
38
|
+
assert.equal(filtered[0].domain, '.example.com');
|
|
39
|
+
assert.equal(filtered[0].secure, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns an empty list when there is no cookies.sqlite', async () => {
|
|
43
|
+
const dir = await makeTempDir();
|
|
44
|
+
assert.deepEqual(await readFirefoxCookies({ profileDir: dir }), []);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('migrateFirefoxBookmarks', () => {
|
|
49
|
+
it('converts places bookmarks into a Chrome Bookmarks document', async () => {
|
|
50
|
+
const source = await makeTempDir();
|
|
51
|
+
const target = await makeTempDir();
|
|
52
|
+
writeFirefoxPlaces(source);
|
|
53
|
+
const report = await migrateFirefoxBookmarks({
|
|
54
|
+
profileDir: source,
|
|
55
|
+
targetProfileDir: target,
|
|
56
|
+
});
|
|
57
|
+
assert.equal(report.migrated, 2);
|
|
58
|
+
const doc = await readProfileJson(target, 'Bookmarks');
|
|
59
|
+
assert.equal(
|
|
60
|
+
doc.roots.bookmark_bar.children[0].url,
|
|
61
|
+
'https://toolbar.example/'
|
|
62
|
+
);
|
|
63
|
+
assert.equal(doc.roots.other.children[0].url, 'https://menu.example/');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('reportFirefoxHistory', () => {
|
|
68
|
+
it('counts places history and reports it as not migrated', async () => {
|
|
69
|
+
const source = await makeTempDir();
|
|
70
|
+
writeFirefoxPlaces(source);
|
|
71
|
+
const report = await reportFirefoxHistory({ profileDir: source });
|
|
72
|
+
assertNothingMigrated(report, 'firefox-history-schema-incompatible');
|
|
73
|
+
assert.match(report.warnings[0].detail, /2 history entries/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('migrateFirefoxPasswords', () => {
|
|
78
|
+
it('decrypts logins and re-encrypts them into a Chrome Login Data', async () => {
|
|
79
|
+
const source = await makeTempDir();
|
|
80
|
+
const target = await makeTempDir();
|
|
81
|
+
const { key4Path } = await writeFirefoxLogins(source, {
|
|
82
|
+
entries: [
|
|
83
|
+
{
|
|
84
|
+
hostname: 'https://a.example',
|
|
85
|
+
username: 'alice',
|
|
86
|
+
password: 'secret-A',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
assert.equal(key4Path, path.join(source, 'key4.db'));
|
|
91
|
+
|
|
92
|
+
const targetKey = deriveChromiumCookieKey('target-pass', 'linux');
|
|
93
|
+
const report = await migrateFirefoxPasswords({
|
|
94
|
+
profileDir: source,
|
|
95
|
+
targetProfileDir: target,
|
|
96
|
+
platform: 'linux',
|
|
97
|
+
targetKey,
|
|
98
|
+
targetPrefix: 'v11',
|
|
99
|
+
});
|
|
100
|
+
assert.equal(report.migrated, 1);
|
|
101
|
+
|
|
102
|
+
const [login] = readMigratedLogins(target, targetKey);
|
|
103
|
+
assert.equal(login.username, 'alice');
|
|
104
|
+
assert.equal(login.password, 'secret-A');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Each case leaves the source unable to yield passwords in a different way.
|
|
108
|
+
for (const { title, reason, writeSource } of [
|
|
109
|
+
{
|
|
110
|
+
title: 'reports primary-password-set when the key is locked',
|
|
111
|
+
reason: 'primary-password-set',
|
|
112
|
+
writeSource: (source) =>
|
|
113
|
+
writeFirefoxLogins(source, {
|
|
114
|
+
primaryPassword: Buffer.from('locked'),
|
|
115
|
+
entries: [
|
|
116
|
+
{ hostname: 'https://a.example', username: 'a', password: 'b' },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
title: 'reports source-missing when there is no logins.json',
|
|
122
|
+
reason: 'source-missing',
|
|
123
|
+
writeSource: async () => {},
|
|
124
|
+
},
|
|
125
|
+
]) {
|
|
126
|
+
it(title, async () => {
|
|
127
|
+
const source = await makeTempDir();
|
|
128
|
+
const target = await makeTempDir();
|
|
129
|
+
await writeSource(source);
|
|
130
|
+
const report = await migrateFirefoxPasswords({
|
|
131
|
+
profileDir: source,
|
|
132
|
+
targetProfileDir: target,
|
|
133
|
+
platform: 'linux',
|
|
134
|
+
targetKey: deriveChromiumCookieKey('t', 'linux'),
|
|
135
|
+
});
|
|
136
|
+
assertNothingMigrated(report, reason);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
|
|
6
|
+
import { migrateHistory } from '../../../../src/browser/migration/history.js';
|
|
7
|
+
import {
|
|
8
|
+
assertNothingMigrated,
|
|
9
|
+
assertSourceUnchanged,
|
|
10
|
+
migrateBetween,
|
|
11
|
+
writeChromiumHistory,
|
|
12
|
+
} from '../../../helpers/migration-fixtures.js';
|
|
13
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
14
|
+
|
|
15
|
+
const makeTempDir = useTempDirectories('bc-history-');
|
|
16
|
+
|
|
17
|
+
describe('migrateHistory', () => {
|
|
18
|
+
it('snapshots History into the target and reports the URL count', async () => {
|
|
19
|
+
const source = await makeTempDir();
|
|
20
|
+
const target = await makeTempDir();
|
|
21
|
+
writeChromiumHistory(source, 5);
|
|
22
|
+
|
|
23
|
+
const report = await migrateBetween(migrateHistory, source, target);
|
|
24
|
+
|
|
25
|
+
assert.equal(report.migrated, 1);
|
|
26
|
+
await stat(path.join(target, 'History'));
|
|
27
|
+
const warning = report.warnings.find((w) => w.reason === 'snapshot-copied');
|
|
28
|
+
assert.ok(warning);
|
|
29
|
+
assert.match(warning.detail, /5 history URLs/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('never writes to the source database', async () => {
|
|
33
|
+
const source = await makeTempDir();
|
|
34
|
+
const target = await makeTempDir();
|
|
35
|
+
const historyPath = writeChromiumHistory(source, 2);
|
|
36
|
+
|
|
37
|
+
await assertSourceUnchanged(historyPath, () =>
|
|
38
|
+
migrateBetween(migrateHistory, source, target)
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('reports a skip when there is no History database', async () => {
|
|
43
|
+
const source = await makeTempDir();
|
|
44
|
+
const target = await makeTempDir();
|
|
45
|
+
const report = await migrateBetween(migrateHistory, source, target);
|
|
46
|
+
assertNothingMigrated(report, 'source-missing');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// feature-parity: migration.profile
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ALL_DATA_CLASSES,
|
|
9
|
+
migrateProfile,
|
|
10
|
+
} from '../../../../src/browser/migration/index.js';
|
|
11
|
+
import { deriveChromiumCookieKey } from '../../../../src/browser/browser-cookie-crypto.js';
|
|
12
|
+
import {
|
|
13
|
+
readMigratedLogins,
|
|
14
|
+
writeFirefoxCookies,
|
|
15
|
+
writeFirefoxLogins,
|
|
16
|
+
writeFirefoxPlaces,
|
|
17
|
+
} from '../../../helpers/migration-fixtures.js';
|
|
18
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
19
|
+
|
|
20
|
+
const makeTempDir = useTempDirectories('bc-orch-');
|
|
21
|
+
|
|
22
|
+
describe('migrateProfile', () => {
|
|
23
|
+
it('validates required options', async () => {
|
|
24
|
+
await assert.rejects(
|
|
25
|
+
() => migrateProfile({ to: '/tmp/x' }),
|
|
26
|
+
/from\.browser/
|
|
27
|
+
);
|
|
28
|
+
await assert.rejects(
|
|
29
|
+
() => migrateProfile({ from: { browser: 'chrome' } }),
|
|
30
|
+
/target directory/
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('migrates a Firefox source and returns the documented report shape', async () => {
|
|
35
|
+
const source = await makeTempDir();
|
|
36
|
+
const target = await makeTempDir();
|
|
37
|
+
await writeFirefoxCookies(source, [
|
|
38
|
+
{ name: 'a', value: '1', host: '.example.com' },
|
|
39
|
+
]);
|
|
40
|
+
writeFirefoxPlaces(source, { withMenuBookmark: false });
|
|
41
|
+
await writeFirefoxLogins(source, {
|
|
42
|
+
entries: [
|
|
43
|
+
{ hostname: 'https://a.example', username: 'alice', password: 'pw' },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const targetKey = deriveChromiumCookieKey('target-pass', 'linux');
|
|
48
|
+
const report = await migrateProfile({
|
|
49
|
+
from: { browser: 'firefox', userDataDir: source },
|
|
50
|
+
to: target,
|
|
51
|
+
platform: 'linux',
|
|
52
|
+
keys: { targetKey, targetPrefix: 'v11' },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Exact report shape.
|
|
56
|
+
assert.deepEqual(
|
|
57
|
+
Object.keys(report.migrated).sort(),
|
|
58
|
+
[...ALL_DATA_CLASSES].sort()
|
|
59
|
+
);
|
|
60
|
+
assert.equal(report.source.browser, 'firefox');
|
|
61
|
+
assert.equal(report.target, target);
|
|
62
|
+
assert.ok(Array.isArray(report.skipped));
|
|
63
|
+
assert.ok(Array.isArray(report.warnings));
|
|
64
|
+
|
|
65
|
+
assert.equal(report.migrated.cookies, 1);
|
|
66
|
+
assert.equal(report.migrated.bookmarks, 1);
|
|
67
|
+
assert.equal(report.migrated.passwords, 1);
|
|
68
|
+
// Firefox history cannot be migrated into Chrome's schema.
|
|
69
|
+
assert.equal(report.migrated.history, 0);
|
|
70
|
+
assert.equal(report.migrated.preferences, 0);
|
|
71
|
+
assert.equal(report.migrated.extensions, 0);
|
|
72
|
+
|
|
73
|
+
// Cookies are returned for CDP seeding, not written to disk.
|
|
74
|
+
assert.equal(report.cookies[0].domain, '.example.com');
|
|
75
|
+
|
|
76
|
+
assert.equal(readMigratedLogins(target, targetKey)[0].password, 'pw');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('honours the include filter', async () => {
|
|
80
|
+
const source = await makeTempDir();
|
|
81
|
+
const target = await makeTempDir();
|
|
82
|
+
writeFirefoxPlaces(source, { withMenuBookmark: false });
|
|
83
|
+
await writeFirefoxCookies(source, [
|
|
84
|
+
{ name: 'a', value: '1', host: '.example.com' },
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const report = await migrateProfile({
|
|
88
|
+
from: { browser: 'firefox', userDataDir: source },
|
|
89
|
+
to: target,
|
|
90
|
+
include: ['bookmarks'],
|
|
91
|
+
platform: 'linux',
|
|
92
|
+
});
|
|
93
|
+
assert.equal(report.migrated.bookmarks, 1);
|
|
94
|
+
assert.equal(report.migrated.cookies, 0);
|
|
95
|
+
await assert.rejects(() => readFile(path.join(target, 'Login Data')));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('skips passwords with a warning when no target key is available on Windows', async () => {
|
|
99
|
+
const source = await makeTempDir();
|
|
100
|
+
const target = await makeTempDir();
|
|
101
|
+
writeFirefoxPlaces(source, { withMenuBookmark: false });
|
|
102
|
+
|
|
103
|
+
const report = await migrateProfile({
|
|
104
|
+
from: { browser: 'firefox', userDataDir: source },
|
|
105
|
+
to: target,
|
|
106
|
+
include: ['passwords'],
|
|
107
|
+
platform: 'win32',
|
|
108
|
+
});
|
|
109
|
+
assert.equal(report.migrated.passwords, 0);
|
|
110
|
+
assert.ok(
|
|
111
|
+
report.skipped.some(
|
|
112
|
+
(s) => s.type === 'passwords' && s.reason === 'target-key-unavailable'
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
assert.equal(report.warnings[0].reason, 'target-key-unavailable');
|
|
116
|
+
});
|
|
117
|
+
});
|