1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
// Console capture must not trip anti-DevTools checks (issue #9) and must only touch Pi's tabs.
// Sites detect DevTools by logging an object whose getter/toString fires when something inspects
// it. Closed DevTools never does; our capture used to serialize every argument immediately.
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
import { test } from "node:test";
const workerSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/browser-extension/service_worker.js", import.meta.url), "utf8");
const snapshotSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/browser-extension/snapshot_injected.js", import.meta.url), "utf8");
function loadWorker() {
const navListeners = [];
const injected = [];
const noop = { addListener() {}, removeListener() {} };
const chrome = {
runtime: { id: "test", getManifest: () => ({ version: "0.0.0" }), onInstalled: noop, onStartup: noop },
alarms: { create() {}, onAlarm: noop }, action: { onClicked: noop },
webNavigation: { onCommitted: { addListener: (fn) => navListeners.push(fn) } },
debugger: { onDetach: noop, onEvent: noop },
storage: { session: { get: async () => ({}), set: async () => {} } },
scripting: { executeScript: async (options) => { injected.push(options); return []; } },
tabs: { get: async () => null },
};
const w = { chrome, console, setTimeout, clearTimeout, setInterval: () => 0, navigator: { userAgent: "unit-test" }, fetch: async () => { throw new Error("offline"); } };
w.self = w;
vm.runInNewContext(workerSource, w);
const commit = async (tabId, frameId = 0) => {
for (const fn of navListeners) await fn({ tabId, frameId, url: "https://site.test/" });
};
return { w, injected, commit };
}
// A page world whose console records native calls and a DevTools trap object.
function pageWorld() {
const nativeCalls = [];
const trap = { tripped: 0 };
const page = vm.createContext({
console: Object.fromEntries(["debug", "log", "info", "warn", "error"].map((l) => [l, (...a) => nativeCalls.push([l, ...a])])),
location: { href: "https://site.test/" },
addEventListener() {},
fetch: undefined, XMLHttpRequest: undefined,
});
page.window = page;
page.trip = () => { trap.tripped++; };
vm.runInContext(`
var bait = { name: "bait" };
Object.defineProperty(bait, "id", { enumerable: true, get() { trip(); return "x"; } });
bait.toString = function toString() { trip(); return "bait"; };
var trapError = new Error("trap");
Object.defineProperty(trapError, "message", { get() { trip(); return "m"; } });
var trapRegex = /a+/g;
trapRegex.toString = function () { trip(); return "re"; };
Object.defineProperty(trapRegex, "source", { get() { trip(); return "x"; } });
var trapDate = new Date(0);
trapDate.toString = function () { trip(); return "d"; };
`, page);
return { page, trap, nativeCalls };
}
const fnSource = (name) => {
const start = workerSource.indexOf(`function ${name}(`);
let depth = 0;
for (let i = workerSource.indexOf("{", start); i < workerSource.length; i++) {
if (workerSource[i] === "{") depth++;
else if (workerSource[i] === "}" && --depth === 0) return workerSource.slice(start, i + 1);
}
throw new Error(`missing ${name}`);
};
for (const [label, install] of [
["early capture", (page) => vm.runInContext(`(${fnSource("installEarlyCapture")})()`, page)],
["post-hoc instrumentation", (page) => vm.runInContext(`${fnSource("getPiChromeState")}\n(${fnSource("installPiChromeInstrumentation")})()`, page)],
["snapshot_injected instrumentation", (page) => { vm.runInContext(snapshotSource, page); }],
]) {
test(`${label}: logging an object does not read it until Pi lists messages`, () => {
const { page, trap, nativeCalls } = pageWorld();
install(page);
if (label.startsWith("snapshot")) {
// snapshot_injected installs lazily through its snapshot entry point; force the install.
assert.equal(typeof page.__piChromeSnapshotPage, "function");
try { page.__piChromeSnapshotPage({}); } catch {}
}
vm.runInContext("console.log('hello', bait); console.error(new Error('boom')); console.log(trapError, trapRegex, trapDate)", page);
assert.equal(trap.tripped, 0, "no getter/toString fired while the page runs");
assert.equal(nativeCalls.length, 3, "native console still receives the calls");
const listed = vm.runInContext(`${fnSource("getPiChromeState")}\n${fnSource("installPiChromeInstrumentation")}\n(${fnSource("listConsoleMessages")})(false)`, page);
assert.equal(trap.tripped, 0, "listing does not fire page getters or toString either");
assert.equal(listed.count, 3);
assert.equal(listed.messages[0].args[0], "hello");
assert.deepEqual(JSON.parse(JSON.stringify(listed.messages[0].args[1])), { name: "bait", id: "[getter]", toString: "[function toString]" });
assert.equal(listed.messages[1].args[0].message, "boom");
assert.match(listed.messages[1].args[0].stack, /boom/);
const [err, re, date] = listed.messages[2].args;
assert.equal(err.message, "[getter]");
assert.equal(re, "/a+/g", "regex described through native getters");
assert.equal(date, "1970-01-01T00:00:00.000Z");
});
}
test("listing survives values that cannot be serialized", () => {
const { page } = pageWorld();
vm.runInContext(`(${fnSource("installEarlyCapture")})()`, page);
vm.runInContext("const loop = { a: [1, 2n, undefined, Symbol('s')] }; loop.self = loop; console.log(loop); console.log(new Proxy({}, { ownKeys() { throw new Error('no'); } }), new Map([[1, 2]]), function named() {})", page);
const listed = vm.runInContext(`${fnSource("getPiChromeState")}\n${fnSource("installPiChromeInstrumentation")}\n(${fnSource("listConsoleMessages")})(true)`, page);
assert.equal(listed.count, 2);
assert.deepEqual(JSON.parse(JSON.stringify(listed.messages[0].args[0])), { a: [1, "2n", "undefined", "Symbol(s)"], self: "[circular]" });
assert.deepEqual(JSON.parse(JSON.stringify(listed.messages[1].args)), ["[unserializable]", "[Map(1)]", "[function named]"]);
assert.equal(vm.runInContext("window.__PI_CHROME_STATE__.console.length", page), 0, "clear still empties the buffer");
});
test("early capture is injected only into tabs Pi owns or tracks", async () => {
const h = loadWorker();
await h.commit(10);
assert.equal(h.injected.length, 0, "a user tab is never patched");
vm.runInContext("automationTargets.set('s', { tabId: 11 }); sessionTabs.set('s', new Map([[12, { created: true }]]));", h.w);
await h.commit(11);
await h.commit(12);
await h.commit(11, 3); // subframe
assert.deepEqual(h.injected.map((o) => o.target.tabId), [11, 12]);
assert.ok(h.injected.every((o) => o.func.name === "installEarlyCapture" && o.injectImmediately === true));
});
|