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
123
124
125
126
127
128
|
// puppeteer-core for the browser tests, with one difference: a page's
// sockets to public relays never leave the machine. Throwaway test wallets
// boot, subscribe and publish like real ones, and a suite run from one home
// address gets that address banned (nos.lol, relay.damus.io, 2026-10-07).
// So every socket to a host that isn't local is answered in the page by a
// stand-in relay that has nothing stored: EVENT gets OK, REQ gets EOSE,
// COUNT gets 0. Local relays (ws://127.0.0.1:…, a test's own) are untouched,
// and a test's own WebSocket stubs still see every send, as before.
//
// A test that reads real content off the public relays launches with
// { relays: 'read' }: its sockets are real, but nothing it publishes leaves
// the page (each EVENT is answered OK in place). LIVE_RELAYS=read does the
// same for any test. { relays: 'live' } (a test of a public service, like
// the Cordn coordinator) or LIVE_RELAYS=1 turns the guard off, and
// LIVE_RELAYS=relay.example.com,other.example lets just those hosts through.
// puppeteer.connect (driving a real Chrome) is never guarded.
//
// Use: import puppeteer from './puppeteer.js'; (in place of 'puppeteer-core')
import real from 'puppeteer-core';
const live = (process.env.LIVE_RELAYS || '').trim();
const allow = live && live !== '1' && live !== 'read' ? live.split(',').map((h) => h.trim()).filter(Boolean) : [];
// Runs in the page before anything else.
function relayGuard(allow, readOnly) {
if (window.__relayGuard) return;
window.__relayGuard = { blocked: [], sent: 0, events: [] }; // events: what didn't go out
const Real = window.WebSocket;
const nativeSend = Real.prototype.send;
const nativeClose = Real.prototype.close;
const fakes = new WeakSet();
const reading = new WeakSet(); // real public sockets in read mode
const local = (h) => h === 'localhost' || h === '[::1]' || h === '0.0.0.0' || /^127\./.test(h) || h.endsWith('.localhost');
const answer = (ws, msg) => setTimeout(() => {
if (ws.readyState !== 1) return;
ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(msg) }));
}, 5);
const relay = (ws, d) => {
let m;
try { m = JSON.parse(d); } catch { return; }
if (!Array.isArray(m)) return;
window.__relayGuard.sent++;
if (m[0] === 'EVENT' && m[1]?.id) { window.__relayGuard.events.push(m[1]); answer(ws, ['OK', m[1].id, true, '']); }
else if (m[0] === 'REQ') answer(ws, ['EOSE', m[1]]);
else if (m[0] === 'COUNT') answer(ws, ['COUNT', m[1], { count: 0 }]);
else if (m[0] === 'AUTH' && m[1]?.id) answer(ws, ['OK', m[1].id, true, '']);
};
// Every send goes through here, so a test that wraps
// WebSocket.prototype.send (before or after) still sees a stand-in's.
Real.prototype.send = function (d) {
if (fakes.has(this)) { if (this.readyState !== 1) throw new DOMException('not open', 'InvalidStateError'); return relay(this, d); }
if (reading.has(this) && typeof d === 'string' && /^\s*\[\s*"EVENT"/.test(d)) return relay(this, d);
return nativeSend.call(this, d);
};
Real.prototype.close = function (code, reason) {
if (!fakes.has(this)) return nativeClose.call(this, code, reason);
if (this.readyState >= 2) return;
this.readyState = 2;
setTimeout(() => { this.readyState = 3; this.dispatchEvent(new CloseEvent('close', { code: code || 1000, reason: reason || '', wasClean: true })); }, 0);
};
const handlerProp = (ws, type) => {
let fn = null;
const call = (e) => fn && fn.call(ws, e);
ws.addEventListener(type, call);
Object.defineProperty(ws, 'on' + type, { get: () => fn, set: (v) => { fn = typeof v === 'function' ? v : null; }, configurable: true });
};
function WebSocket(url, protocols) {
if (!new.target) throw new TypeError("Failed to construct 'WebSocket': Please use the 'new' operator");
let u;
try { u = new URL(url, location.href); } catch { return new Real(url, protocols); }
if (u.protocol !== 'wss:' && u.protocol !== 'ws:') return new Real(url, protocols);
if (local(u.hostname) || allow.includes(u.hostname)) return new Real(url, protocols);
if (readOnly) { const ws = new Real(url, protocols); reading.add(ws); return ws; }
window.__relayGuard.blocked.push(u.href);
// a genuine EventTarget (so dispatchEvent works) that passes instanceof
const ws = Object.setPrototypeOf(new EventTarget(), Real.prototype);
fakes.add(ws);
Object.defineProperties(ws, {
url: { value: u.href }, protocol: { value: '' }, extensions: { value: '' },
bufferedAmount: { value: 0 }, binaryType: { value: 'blob', writable: true },
readyState: { value: 0, writable: true },
});
for (const t of ['open', 'message', 'close', 'error']) handlerProp(ws, t);
setTimeout(() => { if (ws.readyState !== 0) return; ws.readyState = 1; ws.dispatchEvent(new Event('open')); }, 0);
return ws;
}
WebSocket.prototype = Real.prototype;
for (const k of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) WebSocket[k] = Real[k];
window.WebSocket = WebSocket;
}
const guarded = new WeakSet();
const guardPage = async (page, ro) => {
if (!page || guarded.has(page)) return;
guarded.add(page);
await page.evaluateOnNewDocument(relayGuard, allow, ro).catch(() => {});
};
const guardContext = (ctx, ro) => {
const newPage = ctx.newPage.bind(ctx);
ctx.newPage = async (...a) => { const p = await newPage(...a); await guardPage(p, ro); return p; };
};
const guardBrowser = async (browser, ro) => {
guardContext(browser, ro);
for (const name of ['createBrowserContext', 'createIncognitoBrowserContext']) {
if (typeof browser[name] !== 'function') continue;
const make = browser[name].bind(browser);
browser[name] = async (...a) => { const c = await make(...a); guardContext(c, ro); return c; };
}
// pages it opened itself (popups, the first tab)
browser.on('targetcreated', async (t) => { if (t.type() === 'page') await guardPage(await t.page().catch(() => null), ro); });
for (const p of await browser.pages()) await guardPage(p, ro);
};
const launch = async ({ relays, ...opts } = {}) => {
// a machine without /usr/bin/google-chrome names its browser: CHROME=/path/to/chrome
if (process.env.CHROME) opts.executablePath = process.env.CHROME;
const browser = await real.launch(opts);
if (live !== '1' && relays !== 'live') await guardBrowser(browser, live === 'read' || relays === 'read');
return browser;
};
export default new Proxy(real, {
get(t, k) {
if (k === 'launch') return launch;
const v = t[k];
return typeof v === 'function' ? v.bind(t) : v;
},
});
|