@xbrowser/cli 1.11.0 → 1.13.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/dist/{browser-XNU7JQYC.js → browser-AQ2AT2YZ.js} +2 -2
- package/dist/{browser-Q5APBNF6.js → browser-OMGJMSBB.js} +1 -1
- package/dist/{browser-V3JIWSTR.js → browser-T7OVLAEC.js} +4 -3
- package/dist/{cdp-driver-XFTTZI5O.js → cdp-driver-ALDNOZYD.js} +1176 -13
- package/dist/{cdp-driver-GD6YBBGE.js → cdp-driver-HYCLE4YP.js} +2 -1
- package/dist/{cdp-driver-VEK6BNN6.js → cdp-driver-VS7Z7W7Y.js} +1 -1
- package/dist/chunk-A6LPGFAL.js +437 -0
- package/dist/{chunk-JEDP4PJW.js → chunk-BY5TT6WZ.js} +912 -688
- package/dist/{chunk-XKQVUFUS.js → chunk-CW7L53JE.js} +2 -2
- package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
- package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
- package/dist/chunk-HD4FX2N5.js +1255 -0
- package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
- package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
- package/dist/{chunk-7OFM755Z.js → chunk-R2POF2GP.js} +236 -13
- package/dist/{chunk-WHPBNUKB.js → chunk-SH6OTPXN.js} +42 -46
- package/dist/{chunk-XQ4HRPDJ.js → chunk-T2CNKWPV.js} +1177 -13
- package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
- package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
- package/dist/cli.js +1075 -569
- package/dist/{daemon-client-MMDRYCF5.js → daemon-client-7D6EZNOE.js} +42 -46
- package/dist/{daemon-client-Y2YSZCGK.js → daemon-client-HEGAXWGK.js} +1 -1
- package/dist/daemon-main.js +950 -499
- package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
- package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
- package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1086 -578
- package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
- package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
- package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
- package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
- package/dist/{session-replayer-WIUYVN5J.js → session-replayer-UVH5FGY2.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-HVPUVVSA.js +0 -2194
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
scrollIntoView,
|
|
15
15
|
waitForActionable,
|
|
16
16
|
waitForNetworkIdle
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-R2POF2GP.js";
|
|
18
18
|
import {
|
|
19
19
|
connectToCDP,
|
|
20
20
|
findChrome,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
launchChrome
|
|
24
24
|
} from "./chunk-TNEN6VQ2.js";
|
|
25
25
|
import "./chunk-GDKLH7ZY.js";
|
|
26
|
+
import "./chunk-H2A5JUK5.js";
|
|
26
27
|
import "./chunk-KFQGP6VL.js";
|
|
27
28
|
export {
|
|
28
29
|
CDPConnection,
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuleEngine
|
|
3
|
+
} from "./chunk-H2A5JUK5.js";
|
|
4
|
+
|
|
5
|
+
// src/cdp-interceptor/proxy.ts
|
|
6
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
7
|
+
|
|
8
|
+
// src/cdp-interceptor/logger.ts
|
|
9
|
+
function createLogger(config) {
|
|
10
|
+
const buffer = [];
|
|
11
|
+
const MAX_BUFFER = 2e3;
|
|
12
|
+
return {
|
|
13
|
+
info(message, meta) {
|
|
14
|
+
if (!config.enableLogging) return;
|
|
15
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
16
|
+
if (meta) {
|
|
17
|
+
console.log(`[CDPInterceptor ${ts}] ${message}`, JSON.stringify(meta));
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`[CDPInterceptor ${ts}] ${message}`);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
log(method, direction, sessionId, payload, decision) {
|
|
23
|
+
const entry = {
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
direction,
|
|
26
|
+
sessionId,
|
|
27
|
+
method,
|
|
28
|
+
payload: sanitizePayload(payload),
|
|
29
|
+
decision: decision ?? void 0
|
|
30
|
+
};
|
|
31
|
+
if (config.enableLogging) {
|
|
32
|
+
buffer.push(entry);
|
|
33
|
+
if (buffer.length > MAX_BUFFER) buffer.shift();
|
|
34
|
+
const tag = decision ? decision.action === "block" ? "\u{1F6AB}BLOCK" : decision.action === "transform" ? "\u{1F504}XFMR" : "\u2705" : " ";
|
|
35
|
+
const reason = decision ? ` [${decision.severity}] ${decision.reason}` : "";
|
|
36
|
+
console.log(`[CDP] ${tag} ${direction} ${method}${reason}`);
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
},
|
|
40
|
+
getRecent(count) {
|
|
41
|
+
return buffer.slice(-count);
|
|
42
|
+
},
|
|
43
|
+
flush() {
|
|
44
|
+
buffer.length = 0;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function sanitizePayload(payload) {
|
|
49
|
+
if (typeof payload !== "object" || payload === null) return { raw: String(payload) };
|
|
50
|
+
const obj = payload;
|
|
51
|
+
const cleaned = {};
|
|
52
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
53
|
+
if (key === "data" && typeof value === "string" && value.length > 200) {
|
|
54
|
+
cleaned[key] = `<binary: ${value.length} chars>`;
|
|
55
|
+
} else if (key === "expression" && typeof value === "string" && value.length > 500) {
|
|
56
|
+
cleaned[key] = value.substring(0, 500) + "...";
|
|
57
|
+
} else {
|
|
58
|
+
cleaned[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return cleaned;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/cdp-interceptor/advisor.ts
|
|
65
|
+
function formatBlockMessage(decision, method) {
|
|
66
|
+
const adv = advise(decision, method);
|
|
67
|
+
const lines = [
|
|
68
|
+
`[CDP-FIREWALL-BLOCK] rule=${decision.ruleId}`,
|
|
69
|
+
`method=${method}`,
|
|
70
|
+
`reason=${decision.reason}`,
|
|
71
|
+
`suggestion=${decision.suggestion ?? adv.detail}`
|
|
72
|
+
];
|
|
73
|
+
if (adv.codeExample) {
|
|
74
|
+
lines.push(`code-example=`);
|
|
75
|
+
lines.push(adv.codeExample);
|
|
76
|
+
}
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
function advise(decision, originalMethod) {
|
|
80
|
+
const baseAdvice = getBaseAdvice(decision, originalMethod);
|
|
81
|
+
return {
|
|
82
|
+
ruleId: decision.ruleId,
|
|
83
|
+
title: baseAdvice.title,
|
|
84
|
+
detail: decision.suggestion ?? baseAdvice.detail,
|
|
85
|
+
codeExample: baseAdvice.codeExample
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function getBaseAdvice(decision, method) {
|
|
89
|
+
switch (decision.ruleId) {
|
|
90
|
+
case "dom-mutation":
|
|
91
|
+
return {
|
|
92
|
+
title: "Direct DOM property mutation blocked",
|
|
93
|
+
detail: `Your ${method} call tried to set a DOM property directly. In React/Vue/Angular, this bypasses the framework's virtual DOM completely meaning onChange/onInput never fires. The website CAN detect this mismatch as automation.`,
|
|
94
|
+
codeExample: [
|
|
95
|
+
"# \u274C BLOCKED \u2014 what you tried to do:",
|
|
96
|
+
`page.evaluate(\`el.value = 'hello'\`) # triggers isTrusted=false`,
|
|
97
|
+
"",
|
|
98
|
+
"# \u2705 USE INSTEAD \u2014 proper CDP input dispatch:",
|
|
99
|
+
"page.fill('#selector', 'hello') # Playwright: dispatches input+change events",
|
|
100
|
+
"page.type('#selector', 'hello', {delay}) # Playwright: real keystrokes",
|
|
101
|
+
"page.locator('#selector').fill('hello') # Playwright: recommended API"
|
|
102
|
+
].join("\n")
|
|
103
|
+
};
|
|
104
|
+
case "mouse-trajectory":
|
|
105
|
+
return {
|
|
106
|
+
title: "Unnatural mouse trajectory blocked",
|
|
107
|
+
detail: `Your ${method} sequence formed a perfectly linear path. Human hands have micro-tremors (1-3px variation at any point along the arc), acceleration curves, and never draw straight lines between distant points.`,
|
|
108
|
+
codeExample: [
|
|
109
|
+
"# \u274C BLOCKED \u2014 linear interpolation:",
|
|
110
|
+
"for i in range(20):",
|
|
111
|
+
" page.mouse.move(x0 + (x1-x0)*(i/20), y0 + (y1-y0)*(i/20))",
|
|
112
|
+
"",
|
|
113
|
+
"# \u2705 USE INSTEAD \u2014 Bezier curves with overshoot:",
|
|
114
|
+
'# Use "ghost-cursor" or similar library',
|
|
115
|
+
"from ghost_cursor import path_to",
|
|
116
|
+
"path_to(page, (x1, y1))"
|
|
117
|
+
].join("\n")
|
|
118
|
+
};
|
|
119
|
+
case "input-keystroke":
|
|
120
|
+
if (method === "Input.insertText") {
|
|
121
|
+
return {
|
|
122
|
+
title: "Input.insertText detected (logged, not blocked)",
|
|
123
|
+
detail: "Input.insertText bypasses native keyDown\u2192keyPress\u2192input\u2192keyUp events. Playwright uses this internally for page.fill(). Logged for observability."
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
title: "Unnatural keystroke timing blocked",
|
|
128
|
+
detail: `Your ${method} calls have unnaturally constant timing (e.g., exact 50ms intervals). Human typing always has variation (CV > 0.2).`,
|
|
129
|
+
codeExample: [
|
|
130
|
+
"# \u274C BLOCKED \u2014 exact constant delay:",
|
|
131
|
+
"page.type('#input', 'hello', {delay: 50}) # every keystroke exactly 50ms apart",
|
|
132
|
+
"",
|
|
133
|
+
"# \u2705 USE INSTEAD \u2014 variable delay (human-like):",
|
|
134
|
+
"page.fill('#input', 'hello') # recommended, dispatches events properly",
|
|
135
|
+
"# OR: type with randomized delay",
|
|
136
|
+
"page.type('#input', 'hello', {delay: 50 + Math.floor(Math.random() * 80)})"
|
|
137
|
+
].join("\n")
|
|
138
|
+
};
|
|
139
|
+
case "automation-signals":
|
|
140
|
+
return {
|
|
141
|
+
title: "Browser automation marker detected",
|
|
142
|
+
detail: `Your ${method} call accessed a property/marker that anti-crawler systems check to detect automation. These markers (navigator.webdriver, window.__playwright, etc.) are the #1 detection vector.`,
|
|
143
|
+
codeExample: [
|
|
144
|
+
"# \u274C BLOCKED \u2014 don't check for automation markers:",
|
|
145
|
+
"navigator.webdriver # NEVER check this",
|
|
146
|
+
"window.__playwright # NEVER check this",
|
|
147
|
+
"chrome.runtime # NEVER check this",
|
|
148
|
+
"",
|
|
149
|
+
"# \u2705 Just go about your business normally.",
|
|
150
|
+
"# Anti-detection is handled by the CDP firewall automatically."
|
|
151
|
+
].join("\n")
|
|
152
|
+
};
|
|
153
|
+
case "fingerprinting":
|
|
154
|
+
return {
|
|
155
|
+
title: "Browser fingerprinting API access blocked",
|
|
156
|
+
detail: `Your ${method} call accessed a known fingerprinting API. These APIs (canvas.toDataURL, WebGL getParameter, AudioContext, etc.) are used by anti-crawler systems to build a unique device fingerprint.`,
|
|
157
|
+
codeExample: [
|
|
158
|
+
"# \u274C BLOCKED \u2014 fingerprinting vector:",
|
|
159
|
+
"canvas.toDataURL() # returns unique browser hash",
|
|
160
|
+
'gl.getParameter(gl.VENDOR) # returns "SwiftShader" in headless',
|
|
161
|
+
"screen.availWidth - screen.availHeight # no OS chrome in headless",
|
|
162
|
+
"",
|
|
163
|
+
"# \u2705 Avoid accessing these APIs. They are only used for fingerprinting."
|
|
164
|
+
].join("\n")
|
|
165
|
+
};
|
|
166
|
+
case "event-simulation":
|
|
167
|
+
return {
|
|
168
|
+
title: "Synthetic event simulation blocked",
|
|
169
|
+
detail: `Your ${method} call simulated user interaction via el.click() or dispatchEvent(new Event(...)). These produce isTrusted=false events, which are 100% detectable by any anti-crawler that checks isTrusted on critical events.`,
|
|
170
|
+
codeExample: [
|
|
171
|
+
"# \u274C BLOCKED \u2014 synthetic events (isTrusted=false):",
|
|
172
|
+
"el.click() # isTrusted=false",
|
|
173
|
+
'el.dispatchEvent(new Event("click")) # isTrusted=false',
|
|
174
|
+
"el.focus() # isTrusted=false",
|
|
175
|
+
"",
|
|
176
|
+
"# \u2705 USE INSTEAD \u2014 CDP-level input dispatch (isTrusted=true):",
|
|
177
|
+
"page.click(selector) # uses Input.dispatchMouseEvent",
|
|
178
|
+
"page.fill(selector, value) # dispatches real input events"
|
|
179
|
+
].join("\n")
|
|
180
|
+
};
|
|
181
|
+
case "emulation-override":
|
|
182
|
+
return {
|
|
183
|
+
title: "CDP emulation override blocked",
|
|
184
|
+
detail: `Your ${method} call overrides browser behavior in a way that creates detectable inconsistencies. Anti-crawler systems cross-check multiple sources (e.g., navigator.userAgent vs HTTP User-Agent header) to catch these mismatches.`,
|
|
185
|
+
codeExample: [
|
|
186
|
+
"# \u274C BLOCKED \u2014 detectable emulation override:",
|
|
187
|
+
"Emulation.setUserAgentOverride(...) # JS vs HTTP header mismatch",
|
|
188
|
+
"Emulation.setGeolocationOverride(...) # IP geo vs overridden geo mismatch",
|
|
189
|
+
"Emulation.setDeviceMetricsOverride(...) # matchMedia vs actual viewport",
|
|
190
|
+
"",
|
|
191
|
+
"# \u2705 These are handled automatically by the CDP firewall.",
|
|
192
|
+
"# Do NOT call them manually."
|
|
193
|
+
].join("\n")
|
|
194
|
+
};
|
|
195
|
+
case "network-anomaly":
|
|
196
|
+
return {
|
|
197
|
+
title: "Network anomaly detected",
|
|
198
|
+
detail: `Your ${method} call triggered a network pattern that is characteristic of scrapers: blocking URLs, clearing caches, or intercepting requests.`,
|
|
199
|
+
codeExample: [
|
|
200
|
+
"# \u274C BLOCKED \u2014 scraper optimization:",
|
|
201
|
+
"Network.clearBrowserCache() # natural users never do this",
|
|
202
|
+
'Network.setBlockedURLs(["*fonts*"]) # blocking resources is detectable',
|
|
203
|
+
"Fetch.enable() # MITM-style interception",
|
|
204
|
+
"",
|
|
205
|
+
"# \u2705 Let the browser manage its own cache and network normally."
|
|
206
|
+
].join("\n")
|
|
207
|
+
};
|
|
208
|
+
case "page-lifecycle":
|
|
209
|
+
return {
|
|
210
|
+
title: "Suspicious page lifecycle pattern blocked",
|
|
211
|
+
detail: `Your ${method} call reveals an unnatural page interaction sequence: navigating too fast, taking screenshots before the page renders, or generating PDFs (a telltale scraper giveaway).`,
|
|
212
|
+
codeExample: [
|
|
213
|
+
"# \u274C BLOCKED \u2014 unnatural lifecycle:",
|
|
214
|
+
"page.goto(url); page.pdf() # PDF = scraper giveaway",
|
|
215
|
+
"page.goto(url); page.screenshot() <500ms # screenshot before render",
|
|
216
|
+
"page.goto(url) 3x in <100ms # rapid navigation barrage",
|
|
217
|
+
"",
|
|
218
|
+
"# \u2705 Add proper waits between actions:",
|
|
219
|
+
'page.goto(url, {waitUntil: "networkidle"})',
|
|
220
|
+
'page.waitForSelector("body")',
|
|
221
|
+
"page.screenshot() # after rendering"
|
|
222
|
+
].join("\n")
|
|
223
|
+
};
|
|
224
|
+
default:
|
|
225
|
+
return {
|
|
226
|
+
title: decision.reason,
|
|
227
|
+
detail: decision.suggestion ?? `The CDP call "${method}" was blocked by rule "${decision.ruleId}".`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/cdp-interceptor/proxy.ts
|
|
233
|
+
function makeCompoundId(cdpSessionId, rawSessionId) {
|
|
234
|
+
return `${cdpSessionId ?? "nil"}::${rawSessionId ?? "nil"}`;
|
|
235
|
+
}
|
|
236
|
+
var CDPInterceptorProxy = class {
|
|
237
|
+
wss = null;
|
|
238
|
+
engine;
|
|
239
|
+
config;
|
|
240
|
+
logger;
|
|
241
|
+
started = false;
|
|
242
|
+
stats = {
|
|
243
|
+
totalMessages: 0,
|
|
244
|
+
blockedMessages: 0,
|
|
245
|
+
transformedMessages: 0,
|
|
246
|
+
passedMessages: 0,
|
|
247
|
+
byRule: {}
|
|
248
|
+
};
|
|
249
|
+
constructor(config) {
|
|
250
|
+
this.config = config;
|
|
251
|
+
this.engine = createRuleEngine(config.rules);
|
|
252
|
+
this.logger = createLogger({
|
|
253
|
+
enableLogging: config.enableLogging ?? true,
|
|
254
|
+
logDir: config.logDir
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** The port the proxy is listening on (only valid after start()) */
|
|
258
|
+
get port() {
|
|
259
|
+
const addr = this.wss?.address();
|
|
260
|
+
if (addr && typeof addr === "object") return addr.port;
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
/** Start the proxy server */
|
|
264
|
+
async start() {
|
|
265
|
+
if (this.started) return this.port;
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
this.wss = new WebSocketServer({ port: this.config.listenPort ?? 0 }, () => {
|
|
268
|
+
const port = this.port;
|
|
269
|
+
this.engine.start();
|
|
270
|
+
this.started = true;
|
|
271
|
+
this.logger.info("CDP interceptor proxy started", { port, endpoint: this.config.cdpEndpoint });
|
|
272
|
+
resolve(port);
|
|
273
|
+
});
|
|
274
|
+
this.wss.on("error", reject);
|
|
275
|
+
this.wss.on("connection", (clientWs, _req) => {
|
|
276
|
+
this.handleConnection(clientWs);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/** Stop the proxy server */
|
|
281
|
+
async stop() {
|
|
282
|
+
this.engine.stop();
|
|
283
|
+
this.logger.flush();
|
|
284
|
+
this.started = false;
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
if (!this.wss) return resolve();
|
|
287
|
+
this.wss.close(() => resolve());
|
|
288
|
+
this.wss = null;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** Get accumulated statistics */
|
|
292
|
+
getStats() {
|
|
293
|
+
return { ...this.stats };
|
|
294
|
+
}
|
|
295
|
+
/** Get recent log entries (for inspection) */
|
|
296
|
+
getRecentLogs(count = 50) {
|
|
297
|
+
return this.logger.getRecent(count);
|
|
298
|
+
}
|
|
299
|
+
// ── Connection handling ──────────────────────────────────────
|
|
300
|
+
handleConnection(clientWs) {
|
|
301
|
+
let browserWs = null;
|
|
302
|
+
let isAlive = true;
|
|
303
|
+
const pendingMessages = [];
|
|
304
|
+
browserWs = new WebSocket(this.config.cdpEndpoint);
|
|
305
|
+
clientWs.on("message", (raw) => {
|
|
306
|
+
if (browserWs && browserWs.readyState === WebSocket.OPEN) {
|
|
307
|
+
this.handleClientMessage(clientWs, browserWs, raw);
|
|
308
|
+
} else {
|
|
309
|
+
pendingMessages.push(raw);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
browserWs.on("open", () => {
|
|
313
|
+
for (const buf of pendingMessages) {
|
|
314
|
+
this.handleClientMessage(clientWs, browserWs, buf);
|
|
315
|
+
}
|
|
316
|
+
pendingMessages.length = 0;
|
|
317
|
+
});
|
|
318
|
+
browserWs.on("error", (err) => {
|
|
319
|
+
this.logger.info("Browser WebSocket error", { error: String(err) });
|
|
320
|
+
});
|
|
321
|
+
browserWs.on("close", (code, reason) => {
|
|
322
|
+
if (isAlive && clientWs.readyState === WebSocket.OPEN) {
|
|
323
|
+
this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
|
|
324
|
+
clientWs.close();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
browserWs.on("message", (raw) => {
|
|
328
|
+
this.handleBrowserMessage(clientWs, browserWs, raw);
|
|
329
|
+
});
|
|
330
|
+
const cleanup = () => {
|
|
331
|
+
isAlive = false;
|
|
332
|
+
if (browserWs && browserWs.readyState === WebSocket.OPEN) {
|
|
333
|
+
browserWs.close();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
clientWs.on("close", cleanup);
|
|
337
|
+
clientWs.on("error", cleanup);
|
|
338
|
+
browserWs.on("close", () => {
|
|
339
|
+
if (isAlive && clientWs.readyState === WebSocket.OPEN) {
|
|
340
|
+
clientWs.close();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
browserWs.on("error", cleanup);
|
|
344
|
+
}
|
|
345
|
+
// ── Message processing ───────────────────────────────────────
|
|
346
|
+
handleClientMessage(clientWs, browserWs, raw) {
|
|
347
|
+
const msg = this.parseMessage(raw);
|
|
348
|
+
if (!msg) return;
|
|
349
|
+
this.stats.totalMessages++;
|
|
350
|
+
if (!("method" in msg)) {
|
|
351
|
+
browserWs.send(raw.toString());
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const request = msg;
|
|
355
|
+
const ctx = {
|
|
356
|
+
method: request.method,
|
|
357
|
+
params: request.params ?? {},
|
|
358
|
+
sessionId: makeCompoundId("_cdpSession" in browserWs ? browserWs._cdpSession : void 0, request.sessionId),
|
|
359
|
+
direction: "client\u2192browser"
|
|
360
|
+
};
|
|
361
|
+
const decision = this.engine.evaluate(ctx);
|
|
362
|
+
this.logger.log(ctx.method, "client\u2192browser", ctx.sessionId, { method: ctx.method, params: ctx.params }, decision);
|
|
363
|
+
if (decision) {
|
|
364
|
+
this.recordDecision(decision);
|
|
365
|
+
}
|
|
366
|
+
if (decision?.action === "block") {
|
|
367
|
+
const blockMsg = formatBlockMessage(decision, ctx.method);
|
|
368
|
+
const errorResponse = {
|
|
369
|
+
id: request.id,
|
|
370
|
+
error: {
|
|
371
|
+
code: decision.errorCode ?? -32e3,
|
|
372
|
+
message: blockMsg
|
|
373
|
+
},
|
|
374
|
+
sessionId: request.sessionId
|
|
375
|
+
};
|
|
376
|
+
this.stats.blockedMessages++;
|
|
377
|
+
console.error(`
|
|
378
|
+
${blockMsg}
|
|
379
|
+
`);
|
|
380
|
+
clientWs.send(JSON.stringify(errorResponse));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (decision?.action === "transform" && decision.transformedParams) {
|
|
384
|
+
const transformed = { ...request, params: decision.transformedParams };
|
|
385
|
+
this.stats.transformedMessages++;
|
|
386
|
+
browserWs.send(JSON.stringify(transformed));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.stats.passedMessages++;
|
|
390
|
+
browserWs.send(raw.toString());
|
|
391
|
+
}
|
|
392
|
+
handleBrowserMessage(clientWs, _browserWs, raw) {
|
|
393
|
+
const msg = this.parseMessage(raw);
|
|
394
|
+
if (!msg) {
|
|
395
|
+
clientWs.send(raw.toString());
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if ("method" in msg) {
|
|
399
|
+
const event = msg;
|
|
400
|
+
const ctx = {
|
|
401
|
+
method: event.method,
|
|
402
|
+
params: event.params ?? {},
|
|
403
|
+
sessionId: event.sessionId ?? "browser",
|
|
404
|
+
direction: "browser\u2192client"
|
|
405
|
+
};
|
|
406
|
+
const decision = this.engine.evaluate(ctx);
|
|
407
|
+
if (decision?.action === "block") {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
clientWs.send(raw.toString());
|
|
412
|
+
}
|
|
413
|
+
// ── Utilities ────────────────────────────────────────────────
|
|
414
|
+
parseMessage(raw) {
|
|
415
|
+
try {
|
|
416
|
+
return JSON.parse(raw.toString());
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
recordDecision(decision) {
|
|
422
|
+
if (!this.stats.byRule[decision.ruleId]) {
|
|
423
|
+
this.stats.byRule[decision.ruleId] = { matched: 0, blocked: 0, transformed: 0 };
|
|
424
|
+
}
|
|
425
|
+
this.stats.byRule[decision.ruleId].matched++;
|
|
426
|
+
if (decision.action === "block") {
|
|
427
|
+
this.stats.byRule[decision.ruleId].blocked++;
|
|
428
|
+
} else if (decision.action === "transform") {
|
|
429
|
+
this.stats.byRule[decision.ruleId].transformed++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export {
|
|
435
|
+
advise,
|
|
436
|
+
CDPInterceptorProxy
|
|
437
|
+
};
|