@wordbricks/playwright-mcp 0.1.20 → 0.1.22
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/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/lib/browserContextFactory.js +131 -58
- package/lib/browserServerBackend.js +14 -12
- package/lib/config.js +60 -46
- package/lib/context.js +41 -39
- package/lib/extension/cdpRelay.js +67 -61
- package/lib/extension/extensionContextFactory.js +10 -10
- package/lib/frameworkPatterns.js +21 -21
- package/lib/hooks/antiBotDetectionHook.js +59 -52
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +21 -21
- package/lib/hooks/events.js +3 -3
- package/lib/hooks/formatToolCallEvent.js +3 -7
- package/lib/hooks/frameworkStateHook.js +40 -40
- package/lib/hooks/grouping.js +3 -3
- package/lib/hooks/jsonLdDetectionHook.js +44 -37
- package/lib/hooks/networkFilters.js +17 -17
- package/lib/hooks/networkSetup.js +9 -7
- package/lib/hooks/networkTrackingHook.js +21 -21
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +15 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +38 -38
- package/lib/hooks/waitHook.js +7 -7
- package/lib/index.js +12 -10
- package/lib/mcp/inProcessTransport.js +3 -4
- package/lib/mcp/proxyBackend.js +43 -28
- package/lib/mcp/server.js +24 -19
- package/lib/mcp/tool.js +14 -8
- package/lib/mcp/transport.js +60 -53
- package/lib/playwrightTransformer.js +129 -106
- package/lib/program.js +54 -52
- package/lib/response.js +36 -30
- package/lib/sessionLog.js +19 -17
- package/lib/tab.js +41 -39
- package/lib/tools/common.js +19 -19
- package/lib/tools/console.js +11 -11
- package/lib/tools/dialogs.js +18 -15
- package/lib/tools/evaluate.js +26 -17
- package/lib/tools/extractFrameworkState.js +48 -37
- package/lib/tools/files.js +17 -14
- package/lib/tools/form.js +32 -23
- package/lib/tools/getSnapshot.js +14 -15
- package/lib/tools/getVisibleHtml.js +33 -17
- package/lib/tools/install.js +20 -20
- package/lib/tools/keyboard.js +29 -24
- package/lib/tools/mouse.js +29 -31
- package/lib/tools/navigate.js +19 -23
- package/lib/tools/network.js +12 -14
- package/lib/tools/networkDetail.js +58 -49
- package/lib/tools/networkSearch/bodySearch.js +46 -32
- package/lib/tools/networkSearch/grouping.js +15 -6
- package/lib/tools/networkSearch/helpers.js +4 -4
- package/lib/tools/networkSearch/searchHtml.js +25 -16
- package/lib/tools/networkSearch/urlSearch.js +56 -14
- package/lib/tools/networkSearch.js +46 -36
- package/lib/tools/pdf.js +13 -12
- package/lib/tools/repl.js +66 -54
- package/lib/tools/screenshot.js +57 -33
- package/lib/tools/scroll.js +29 -24
- package/lib/tools/snapshot.js +66 -49
- package/lib/tools/tabs.js +22 -19
- package/lib/tools/tool.js +5 -3
- package/lib/tools/utils.js +17 -13
- package/lib/tools/wait.js +24 -19
- package/lib/tools.js +21 -20
- package/lib/utils/adBlockFilter.js +29 -26
- package/lib/utils/codegen.js +20 -16
- package/lib/utils/extensionPath.js +4 -4
- package/lib/utils/fileUtils.js +17 -13
- package/lib/utils/graphql.js +69 -58
- package/lib/utils/guid.js +3 -3
- package/lib/utils/httpServer.js +9 -9
- package/lib/utils/log.js +3 -3
- package/lib/utils/manualPromise.js +7 -7
- package/lib/utils/networkFormat.js +7 -5
- package/lib/utils/package.js +4 -4
- package/lib/utils/sanitizeHtml.js +66 -34
- package/lib/utils/truncate.js +25 -25
- package/lib/utils/withTimeout.js +1 -1
- package/package.json +34 -57
- package/src/index.ts +27 -17
- package/LICENSE +0 -202
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
* - /cdp/guid - Full CDP interface for Playwright MCP
|
|
21
21
|
* - /extension/guid - Extension connection for chrome.debugger forwarding
|
|
22
22
|
*/
|
|
23
|
-
import { spawn } from
|
|
24
|
-
import debug from
|
|
25
|
-
import { WebSocket, WebSocketServer } from
|
|
26
|
-
import { httpAddressToString } from
|
|
27
|
-
import { logUnhandledError } from
|
|
28
|
-
import { ManualPromise } from
|
|
29
|
-
// @ts-
|
|
30
|
-
const { registry } = await import(
|
|
31
|
-
const debugLogger = debug(
|
|
23
|
+
import { spawn } from "child_process";
|
|
24
|
+
import debug from "debug";
|
|
25
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
26
|
+
import { httpAddressToString } from "../utils/httpServer.js";
|
|
27
|
+
import { logUnhandledError } from "../utils/log.js";
|
|
28
|
+
import { ManualPromise } from "../utils/manualPromise.js";
|
|
29
|
+
// @ts-expect-error
|
|
30
|
+
const { registry } = await import("playwright-core/lib/server/registry/index");
|
|
31
|
+
const debugLogger = debug("pw:mcp:relay");
|
|
32
32
|
export class CDPRelayServer {
|
|
33
33
|
_wsHost;
|
|
34
34
|
_browserChannel;
|
|
@@ -42,7 +42,7 @@ export class CDPRelayServer {
|
|
|
42
42
|
_nextSessionId = 1;
|
|
43
43
|
_extensionConnectionPromise;
|
|
44
44
|
constructor(server, browserChannel, userDataDir) {
|
|
45
|
-
this._wsHost = httpAddressToString(server.address()).replace(/^http/,
|
|
45
|
+
this._wsHost = httpAddressToString(server.address()).replace(/^http/, "ws");
|
|
46
46
|
this._browserChannel = browserChannel;
|
|
47
47
|
this._userDataDir = userDataDir;
|
|
48
48
|
const uuid = crypto.randomUUID();
|
|
@@ -50,7 +50,7 @@ export class CDPRelayServer {
|
|
|
50
50
|
this._extensionPath = `/extension/${uuid}`;
|
|
51
51
|
this._resetExtensionConnection();
|
|
52
52
|
this._wss = new WebSocketServer({ server });
|
|
53
|
-
this._wss.on(
|
|
53
|
+
this._wss.on("connection", this._onConnection.bind(this));
|
|
54
54
|
}
|
|
55
55
|
cdpEndpoint() {
|
|
56
56
|
return `${this._wsHost}${this._cdpPath}`;
|
|
@@ -59,23 +59,23 @@ export class CDPRelayServer {
|
|
|
59
59
|
return `${this._wsHost}${this._extensionPath}`;
|
|
60
60
|
}
|
|
61
61
|
async ensureExtensionConnectionForMCPContext(clientInfo, abortSignal) {
|
|
62
|
-
debugLogger(
|
|
62
|
+
debugLogger("Ensuring extension connection for MCP context");
|
|
63
63
|
if (this._extensionConnection)
|
|
64
64
|
return;
|
|
65
65
|
this._connectBrowser(clientInfo);
|
|
66
|
-
debugLogger(
|
|
66
|
+
debugLogger("Waiting for incoming extension connection");
|
|
67
67
|
await Promise.race([
|
|
68
68
|
this._extensionConnectionPromise,
|
|
69
|
-
new Promise((_, reject) => abortSignal.addEventListener(
|
|
69
|
+
new Promise((_, reject) => abortSignal.addEventListener("abort", reject)),
|
|
70
70
|
]);
|
|
71
|
-
debugLogger(
|
|
71
|
+
debugLogger("Extension connection established");
|
|
72
72
|
}
|
|
73
73
|
_connectBrowser(clientInfo) {
|
|
74
74
|
const mcpRelayEndpoint = `${this._wsHost}${this._extensionPath}`;
|
|
75
75
|
// Need to specify "key" in the manifest.json to make the id stable when loading from file.
|
|
76
|
-
const url = new URL(
|
|
77
|
-
url.searchParams.set(
|
|
78
|
-
url.searchParams.set(
|
|
76
|
+
const url = new URL("chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html");
|
|
77
|
+
url.searchParams.set("mcpRelayUrl", mcpRelayEndpoint);
|
|
78
|
+
url.searchParams.set("client", JSON.stringify(clientInfo));
|
|
79
79
|
const href = url.toString();
|
|
80
80
|
const executableInfo = registry.findExecutable(this._browserChannel);
|
|
81
81
|
if (!executableInfo)
|
|
@@ -91,11 +91,11 @@ export class CDPRelayServer {
|
|
|
91
91
|
windowsHide: true,
|
|
92
92
|
detached: true,
|
|
93
93
|
shell: false,
|
|
94
|
-
stdio:
|
|
94
|
+
stdio: "ignore",
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
stop() {
|
|
98
|
-
this.closeConnections(
|
|
98
|
+
this.closeConnections("Server stopped");
|
|
99
99
|
this._wss.close();
|
|
100
100
|
}
|
|
101
101
|
closeConnections(reason) {
|
|
@@ -113,17 +113,17 @@ export class CDPRelayServer {
|
|
|
113
113
|
}
|
|
114
114
|
else {
|
|
115
115
|
debugLogger(`Invalid path: ${url.pathname}`);
|
|
116
|
-
ws.close(4004,
|
|
116
|
+
ws.close(4004, "Invalid path");
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
_handlePlaywrightConnection(ws) {
|
|
120
120
|
if (this._playwrightConnection) {
|
|
121
|
-
debugLogger(
|
|
122
|
-
ws.close(1000,
|
|
121
|
+
debugLogger("Rejecting second Playwright connection");
|
|
122
|
+
ws.close(1000, "Another CDP client already connected");
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
this._playwrightConnection = ws;
|
|
126
|
-
ws.on(
|
|
126
|
+
ws.on("message", async (data) => {
|
|
127
127
|
try {
|
|
128
128
|
const message = JSON.parse(data.toString());
|
|
129
129
|
await this._handlePlaywrightMessage(message);
|
|
@@ -132,17 +132,17 @@ export class CDPRelayServer {
|
|
|
132
132
|
debugLogger(`Error while handling Playwright message\n${data.toString()}\n`, error);
|
|
133
133
|
}
|
|
134
134
|
});
|
|
135
|
-
ws.on(
|
|
135
|
+
ws.on("close", () => {
|
|
136
136
|
if (this._playwrightConnection !== ws)
|
|
137
137
|
return;
|
|
138
138
|
this._playwrightConnection = null;
|
|
139
|
-
this._closeExtensionConnection(
|
|
140
|
-
debugLogger(
|
|
139
|
+
this._closeExtensionConnection("Playwright client disconnected");
|
|
140
|
+
debugLogger("Playwright WebSocket closed");
|
|
141
141
|
});
|
|
142
|
-
ws.on(
|
|
143
|
-
debugLogger(
|
|
142
|
+
ws.on("error", (error) => {
|
|
143
|
+
debugLogger("Playwright WebSocket error:", error);
|
|
144
144
|
});
|
|
145
|
-
debugLogger(
|
|
145
|
+
debugLogger("Playwright MCP connected");
|
|
146
146
|
}
|
|
147
147
|
_closeExtensionConnection(reason) {
|
|
148
148
|
this._extensionConnection?.close(reason);
|
|
@@ -162,89 +162,91 @@ export class CDPRelayServer {
|
|
|
162
162
|
}
|
|
163
163
|
_handleExtensionConnection(ws) {
|
|
164
164
|
if (this._extensionConnection) {
|
|
165
|
-
ws.close(1000,
|
|
165
|
+
ws.close(1000, "Another extension connection already established");
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
this._extensionConnection = new ExtensionConnection(ws);
|
|
169
169
|
this._extensionConnection.onclose = (c, reason) => {
|
|
170
|
-
debugLogger(
|
|
170
|
+
debugLogger("Extension WebSocket closed:", reason, c === this._extensionConnection);
|
|
171
171
|
if (this._extensionConnection !== c)
|
|
172
172
|
return;
|
|
173
173
|
this._resetExtensionConnection();
|
|
174
174
|
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
|
|
175
175
|
};
|
|
176
|
-
this._extensionConnection.onmessage =
|
|
176
|
+
this._extensionConnection.onmessage =
|
|
177
|
+
this._handleExtensionMessage.bind(this);
|
|
177
178
|
this._extensionConnectionPromise.resolve();
|
|
178
179
|
}
|
|
179
180
|
_handleExtensionMessage(method, params) {
|
|
180
181
|
switch (method) {
|
|
181
|
-
case
|
|
182
|
+
case "forwardCDPEvent": {
|
|
182
183
|
const sessionId = params.sessionId || this._connectedTabInfo?.sessionId;
|
|
183
184
|
this._sendToPlaywright({
|
|
184
185
|
sessionId,
|
|
185
186
|
method: params.method,
|
|
186
|
-
params: params.params
|
|
187
|
+
params: params.params,
|
|
187
188
|
});
|
|
188
189
|
break;
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
}
|
|
191
|
+
case "detachedFromTab":
|
|
192
|
+
debugLogger("← Debugger detached from tab:", params);
|
|
191
193
|
this._connectedTabInfo = undefined;
|
|
192
194
|
break;
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
async _handlePlaywrightMessage(message) {
|
|
196
|
-
debugLogger(
|
|
198
|
+
debugLogger("← Playwright:", `${message.method} (id=${message.id})`);
|
|
197
199
|
const { id, sessionId, method, params } = message;
|
|
198
200
|
try {
|
|
199
201
|
const result = await this._handleCDPCommand(method, params, sessionId);
|
|
200
202
|
this._sendToPlaywright({ id, sessionId, result });
|
|
201
203
|
}
|
|
202
204
|
catch (e) {
|
|
203
|
-
debugLogger(
|
|
205
|
+
debugLogger("Error in the extension:", e);
|
|
204
206
|
this._sendToPlaywright({
|
|
205
207
|
id,
|
|
206
208
|
sessionId,
|
|
207
|
-
error: { message: e.message }
|
|
209
|
+
error: { message: e.message },
|
|
208
210
|
});
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
async _handleCDPCommand(method, params, sessionId) {
|
|
212
214
|
switch (method) {
|
|
213
|
-
case
|
|
215
|
+
case "Browser.getVersion": {
|
|
214
216
|
return {
|
|
215
|
-
protocolVersion:
|
|
216
|
-
product:
|
|
217
|
-
userAgent:
|
|
217
|
+
protocolVersion: "1.3",
|
|
218
|
+
product: "Chrome/Extension-Bridge",
|
|
219
|
+
userAgent: "CDP-Bridge-Server/1.0.0",
|
|
218
220
|
};
|
|
219
221
|
}
|
|
220
|
-
case
|
|
222
|
+
case "Browser.setDownloadBehavior": {
|
|
221
223
|
return {};
|
|
222
224
|
}
|
|
223
|
-
case
|
|
225
|
+
case "Target.setAutoAttach": {
|
|
224
226
|
// Forward child session handling.
|
|
225
227
|
if (sessionId)
|
|
226
228
|
break;
|
|
227
229
|
// Simulate auto-attach behavior with real target info
|
|
228
|
-
const { targetInfo } = await this._extensionConnection.send(
|
|
230
|
+
const { targetInfo } = await this._extensionConnection.send("attachToTab");
|
|
229
231
|
this._connectedTabInfo = {
|
|
230
232
|
targetInfo,
|
|
231
233
|
sessionId: `pw-tab-${this._nextSessionId++}`,
|
|
232
234
|
};
|
|
233
|
-
debugLogger(
|
|
235
|
+
debugLogger("Simulating auto-attach");
|
|
234
236
|
this._sendToPlaywright({
|
|
235
|
-
method:
|
|
237
|
+
method: "Target.attachedToTarget",
|
|
236
238
|
params: {
|
|
237
239
|
sessionId: this._connectedTabInfo.sessionId,
|
|
238
240
|
targetInfo: {
|
|
239
241
|
...this._connectedTabInfo.targetInfo,
|
|
240
242
|
attached: true,
|
|
241
243
|
},
|
|
242
|
-
waitingForDebugger: false
|
|
243
|
-
}
|
|
244
|
+
waitingForDebugger: false,
|
|
245
|
+
},
|
|
244
246
|
});
|
|
245
247
|
return {};
|
|
246
248
|
}
|
|
247
|
-
case
|
|
249
|
+
case "Target.getTargetInfo": {
|
|
248
250
|
return this._connectedTabInfo?.targetInfo;
|
|
249
251
|
}
|
|
250
252
|
}
|
|
@@ -252,14 +254,18 @@ export class CDPRelayServer {
|
|
|
252
254
|
}
|
|
253
255
|
async _forwardToExtension(method, params, sessionId) {
|
|
254
256
|
if (!this._extensionConnection)
|
|
255
|
-
throw new Error(
|
|
257
|
+
throw new Error("Extension not connected");
|
|
256
258
|
// Top level sessionId is only passed between the relay and the client.
|
|
257
259
|
if (this._connectedTabInfo?.sessionId === sessionId)
|
|
258
260
|
sessionId = undefined;
|
|
259
|
-
return await this._extensionConnection.send(
|
|
261
|
+
return await this._extensionConnection.send("forwardCDPCommand", {
|
|
262
|
+
sessionId,
|
|
263
|
+
method,
|
|
264
|
+
params,
|
|
265
|
+
});
|
|
260
266
|
}
|
|
261
267
|
_sendToPlaywright(message) {
|
|
262
|
-
debugLogger(
|
|
268
|
+
debugLogger("→ Playwright:", `${message.method ?? `response(id=${message.id})`}`);
|
|
263
269
|
this._playwrightConnection?.send(JSON.stringify(message));
|
|
264
270
|
}
|
|
265
271
|
}
|
|
@@ -271,9 +277,9 @@ class ExtensionConnection {
|
|
|
271
277
|
onclose;
|
|
272
278
|
constructor(ws) {
|
|
273
279
|
this._ws = ws;
|
|
274
|
-
this._ws.on(
|
|
275
|
-
this._ws.on(
|
|
276
|
-
this._ws.on(
|
|
280
|
+
this._ws.on("message", this._onMessage.bind(this));
|
|
281
|
+
this._ws.on("close", this._onClose.bind(this));
|
|
282
|
+
this._ws.on("error", this._onError.bind(this));
|
|
277
283
|
}
|
|
278
284
|
async send(method, params, sessionId) {
|
|
279
285
|
if (this._ws.readyState !== WebSocket.OPEN)
|
|
@@ -286,7 +292,7 @@ class ExtensionConnection {
|
|
|
286
292
|
});
|
|
287
293
|
}
|
|
288
294
|
close(message) {
|
|
289
|
-
debugLogger(
|
|
295
|
+
debugLogger("closing extension connection:", message);
|
|
290
296
|
if (this._ws.readyState === WebSocket.OPEN)
|
|
291
297
|
this._ws.close(1000, message);
|
|
292
298
|
}
|
|
@@ -323,7 +329,7 @@ class ExtensionConnection {
|
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
331
|
else if (object.id) {
|
|
326
|
-
debugLogger(
|
|
332
|
+
debugLogger("← Extension: unexpected response", object);
|
|
327
333
|
}
|
|
328
334
|
else {
|
|
329
335
|
this.onmessage?.(object.method, object.params);
|
|
@@ -340,7 +346,7 @@ class ExtensionConnection {
|
|
|
340
346
|
}
|
|
341
347
|
_dispose() {
|
|
342
348
|
for (const callback of this._callbacks.values())
|
|
343
|
-
callback.reject(new Error(
|
|
349
|
+
callback.reject(new Error("WebSocket closed"));
|
|
344
350
|
this._callbacks.clear();
|
|
345
351
|
}
|
|
346
352
|
}
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import debug from
|
|
17
|
-
import * as playwright from
|
|
18
|
-
import { startHttpServer } from
|
|
19
|
-
import { CDPRelayServer } from
|
|
20
|
-
const debugLogger = debug(
|
|
16
|
+
import debug from "debug";
|
|
17
|
+
import * as playwright from "playwright-core";
|
|
18
|
+
import { startHttpServer } from "../utils/httpServer.js";
|
|
19
|
+
import { CDPRelayServer } from "./cdpRelay.js";
|
|
20
|
+
const debugLogger = debug("pw:mcp:relay");
|
|
21
21
|
export class ExtensionContextFactory {
|
|
22
|
-
name =
|
|
23
|
-
description =
|
|
22
|
+
name = "extension";
|
|
23
|
+
description = "Connect to a browser using the Playwright MCP extension";
|
|
24
24
|
_browserChannel;
|
|
25
25
|
_userDataDir;
|
|
26
26
|
constructor(browserChannel, userDataDir) {
|
|
@@ -32,9 +32,9 @@ export class ExtensionContextFactory {
|
|
|
32
32
|
return {
|
|
33
33
|
browserContext: browser.contexts()[0],
|
|
34
34
|
close: async () => {
|
|
35
|
-
debugLogger(
|
|
35
|
+
debugLogger("close() called for browser context");
|
|
36
36
|
await browser.close();
|
|
37
|
-
}
|
|
37
|
+
},
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
async _obtainBrowser(clientInfo, abortSignal) {
|
|
@@ -49,7 +49,7 @@ export class ExtensionContextFactory {
|
|
|
49
49
|
throw new Error(abortSignal.reason);
|
|
50
50
|
}
|
|
51
51
|
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir);
|
|
52
|
-
abortSignal.addEventListener(
|
|
52
|
+
abortSignal.addEventListener("abort", () => cdpRelayServer.stop());
|
|
53
53
|
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
|
|
54
54
|
return cdpRelayServer;
|
|
55
55
|
}
|
package/lib/frameworkPatterns.js
CHANGED
|
@@ -3,33 +3,33 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const FRAMEWORK_STATE_PATTERNS = [
|
|
5
5
|
// React/Next.js
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
"__NEXT_DATA__",
|
|
7
|
+
"__reactServerState",
|
|
8
8
|
// Remix
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
"__remixContext",
|
|
10
|
+
"__remixManifest",
|
|
11
|
+
"__remixRouteModules",
|
|
12
12
|
// Apollo GraphQL
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
"__APOLLO_STATE__",
|
|
14
|
+
"__APOLLO_CLIENT__",
|
|
15
15
|
// Redux/State Management
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
"__PRELOADED_STATE__",
|
|
17
|
+
"__INITIAL_STATE__",
|
|
18
|
+
"__REDUX_STATE__",
|
|
19
19
|
// Vue/Nuxt
|
|
20
|
-
|
|
20
|
+
"__NUXT__",
|
|
21
21
|
// Gatsby
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
"___gatsby",
|
|
23
|
+
"___loader",
|
|
24
24
|
// Generic SSR
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
"__SSR_DATA__",
|
|
26
|
+
"__APP_STATE__",
|
|
27
|
+
"__SERVER_STATE__",
|
|
28
28
|
// Others
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
"__QWIK_STATE__",
|
|
30
|
+
"__SVELTE__",
|
|
31
|
+
"__ANGULAR__",
|
|
32
|
+
"__SOLID__",
|
|
33
|
+
"__ASTRO_DATA__",
|
|
34
34
|
];
|
|
35
35
|
export const MAX_DISPLAY_ITEMS = 5;
|
|
@@ -1,32 +1,35 @@
|
|
|
1
|
-
import ms from
|
|
2
|
-
import { Ok } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
const RESOLUTION_WAIT_MS = ms(
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
import { Ok } from "../utils/result.js";
|
|
3
|
+
import { getEventsAfter, isEventType, trackEvent } from "./events.js";
|
|
4
|
+
import { hookNameSchema } from "./schema.js";
|
|
5
|
+
const RESOLUTION_WAIT_MS = ms("10s");
|
|
6
6
|
const lastProcessedEventIdByContext = new WeakMap();
|
|
7
7
|
const detectedProvidersByContext = new WeakMap();
|
|
8
8
|
const isLikelyResolved = async (ctx) => {
|
|
9
9
|
if (!ctx.tab?.page)
|
|
10
10
|
return false;
|
|
11
11
|
const host = ctx.tab.page.url();
|
|
12
|
-
if (host.includes(
|
|
12
|
+
if (host.includes("challenges.cloudflare.com"))
|
|
13
13
|
return false;
|
|
14
14
|
return ctx.tab.page.evaluate(() => {
|
|
15
15
|
const challengeSelectors = [
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
"#challenge-stage",
|
|
17
|
+
"#cf-challenge-running",
|
|
18
18
|
'iframe[src*="turnstile"]',
|
|
19
19
|
'form[action*="/cdn-cgi/challenge-platform"]',
|
|
20
|
-
|
|
20
|
+
"[data-cf-challenge]",
|
|
21
21
|
];
|
|
22
|
-
const hasChallengeDom = challengeSelectors.some(selector => document.querySelector(selector));
|
|
22
|
+
const hasChallengeDom = challengeSelectors.some((selector) => document.querySelector(selector));
|
|
23
23
|
if (hasChallengeDom)
|
|
24
24
|
return false;
|
|
25
25
|
const title = document.title.toLowerCase();
|
|
26
|
-
const bodyText = (document.body?.innerText ||
|
|
27
|
-
|
|
26
|
+
const bodyText = (document.body?.innerText || "")
|
|
27
|
+
.slice(0, 2000)
|
|
28
|
+
.toLowerCase();
|
|
29
|
+
if (title.includes("just a moment") || bodyText.includes("just a moment"))
|
|
28
30
|
return false;
|
|
29
|
-
if (title.includes(
|
|
31
|
+
if (title.includes("checking your browser") ||
|
|
32
|
+
bodyText.includes("checking your browser"))
|
|
30
33
|
return false;
|
|
31
34
|
return true;
|
|
32
35
|
});
|
|
@@ -48,17 +51,17 @@ const updateLastProcessedEventId = (context, events) => {
|
|
|
48
51
|
const isStatusOk = (status) => status >= 200 && status < 400;
|
|
49
52
|
const providerConfigs = [
|
|
50
53
|
{
|
|
51
|
-
provider:
|
|
52
|
-
match: event => isStatusOk(event.data.status) &&
|
|
53
|
-
event.data.url.includes(
|
|
54
|
-
event.data.url.includes(
|
|
54
|
+
provider: "cloudflare-turnstile",
|
|
55
|
+
match: (event) => isStatusOk(event.data.status) &&
|
|
56
|
+
event.data.url.includes("challenges.cloudflare.com") &&
|
|
57
|
+
event.data.url.includes("/turnstile/"),
|
|
55
58
|
},
|
|
56
59
|
{
|
|
57
|
-
provider:
|
|
58
|
-
match: event => isStatusOk(event.data.status) &&
|
|
59
|
-
event.data.method.toUpperCase() ===
|
|
60
|
-
event.data.url.includes(
|
|
61
|
-
event.data.url.includes(
|
|
60
|
+
provider: "aws-waf",
|
|
61
|
+
match: (event) => isStatusOk(event.data.status) &&
|
|
62
|
+
event.data.method.toUpperCase() === "POST" &&
|
|
63
|
+
event.data.url.includes(".awswaf.com") &&
|
|
64
|
+
event.data.url.includes("/telemetry"),
|
|
62
65
|
},
|
|
63
66
|
];
|
|
64
67
|
export const getAntiBotProviderConfigs = () => providerConfigs;
|
|
@@ -72,7 +75,7 @@ const waitForResolution = async (ctx) => {
|
|
|
72
75
|
};
|
|
73
76
|
const detectAntiBot = (ctx, events) => {
|
|
74
77
|
const detectedProviders = getDetectedProviders(ctx.context);
|
|
75
|
-
const networkEvents = events.filter(isEventType(
|
|
78
|
+
const networkEvents = events.filter(isEventType("network-request"));
|
|
76
79
|
return providerConfigs.reduce((acc, config) => {
|
|
77
80
|
if (detectedProviders.has(config.provider))
|
|
78
81
|
return acc;
|
|
@@ -87,52 +90,52 @@ const detectAntiBot = (ctx, events) => {
|
|
|
87
90
|
provider: config.provider,
|
|
88
91
|
url: match.data.url,
|
|
89
92
|
status: match.data.status,
|
|
90
|
-
}
|
|
93
|
+
},
|
|
91
94
|
],
|
|
92
95
|
};
|
|
93
96
|
}, { hits: [] });
|
|
94
97
|
};
|
|
95
98
|
export const antiBotDetectionPreHook = {
|
|
96
|
-
name: hookNameSchema.enum[
|
|
99
|
+
name: hookNameSchema.enum["anti-bot-detection-pre"],
|
|
97
100
|
handler: async (ctx) => {
|
|
98
101
|
if (lastProcessedEventIdByContext.has(ctx.context))
|
|
99
102
|
return Ok(undefined);
|
|
100
|
-
if (typeof ctx.eventStore.lastSeenEventId ===
|
|
103
|
+
if (typeof ctx.eventStore.lastSeenEventId === "number")
|
|
101
104
|
lastProcessedEventIdByContext.set(ctx.context, ctx.eventStore.lastSeenEventId);
|
|
102
105
|
return Ok(undefined);
|
|
103
106
|
},
|
|
104
107
|
};
|
|
105
108
|
export const antiBotDetectionPostHook = {
|
|
106
|
-
name: hookNameSchema.enum[
|
|
109
|
+
name: hookNameSchema.enum["anti-bot-detection-post"],
|
|
107
110
|
handler: async (ctx) => {
|
|
108
111
|
const newEvents = getEventsAfter(ctx.eventStore, lastProcessedEventIdByContext.get(ctx.context));
|
|
109
112
|
if (newEvents.length === 0)
|
|
110
113
|
return Ok(undefined);
|
|
111
114
|
const detection = detectAntiBot(ctx, newEvents);
|
|
112
115
|
if (detection.hits.length > 0) {
|
|
113
|
-
detection.hits.forEach(hit => {
|
|
116
|
+
detection.hits.forEach((hit) => {
|
|
114
117
|
trackEvent(ctx.context, {
|
|
115
|
-
type:
|
|
118
|
+
type: "anti-bot",
|
|
116
119
|
data: {
|
|
117
120
|
provider: hit.provider,
|
|
118
|
-
detectionMethod:
|
|
121
|
+
detectionMethod: "network-request",
|
|
119
122
|
url: hit.url,
|
|
120
123
|
status: hit.status,
|
|
121
|
-
action:
|
|
124
|
+
action: "detected",
|
|
122
125
|
waitMs: RESOLUTION_WAIT_MS,
|
|
123
126
|
},
|
|
124
127
|
});
|
|
125
128
|
});
|
|
126
129
|
const waitResult = await waitForResolution(ctx);
|
|
127
|
-
detection.hits.forEach(hit => {
|
|
130
|
+
detection.hits.forEach((hit) => {
|
|
128
131
|
trackEvent(ctx.context, {
|
|
129
|
-
type:
|
|
132
|
+
type: "anti-bot",
|
|
130
133
|
data: {
|
|
131
134
|
provider: hit.provider,
|
|
132
|
-
detectionMethod:
|
|
135
|
+
detectionMethod: "network-request",
|
|
133
136
|
url: hit.url,
|
|
134
137
|
status: hit.status,
|
|
135
|
-
action: waitResult.resolved ?
|
|
138
|
+
action: waitResult.resolved ? "resolved" : "still-blocked",
|
|
136
139
|
waitMs: waitResult.waitedMs,
|
|
137
140
|
},
|
|
138
141
|
});
|
|
@@ -147,25 +150,29 @@ export const antiBotDetectionHooks = {
|
|
|
147
150
|
post: antiBotDetectionPostHook,
|
|
148
151
|
};
|
|
149
152
|
export const formatAntiBotEvent = (event) => {
|
|
150
|
-
if (event.data.action ===
|
|
151
|
-
const waitSeconds = event.data.waitMs
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (event.data.provider ===
|
|
155
|
-
return `Anti-bot still active:
|
|
156
|
-
|
|
153
|
+
if (event.data.action === "still-blocked") {
|
|
154
|
+
const waitSeconds = event.data.waitMs
|
|
155
|
+
? Math.round(event.data.waitMs / 1000)
|
|
156
|
+
: 0;
|
|
157
|
+
if (event.data.provider === "cloudflare-turnstile")
|
|
158
|
+
return `Anti-bot still active: Cloudflare Turnstile after ${waitSeconds || "<1"}s wait`;
|
|
159
|
+
if (event.data.provider === "aws-waf")
|
|
160
|
+
return `Anti-bot still active: AWS WAF after ${waitSeconds || "<1"}s wait`;
|
|
161
|
+
return "Anti-bot mechanism still active";
|
|
157
162
|
}
|
|
158
|
-
if (event.data.action ===
|
|
159
|
-
const waitSeconds = event.data.waitMs
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (event.data.provider ===
|
|
163
|
-
return `Anti-bot resolved:
|
|
164
|
-
|
|
163
|
+
if (event.data.action === "resolved") {
|
|
164
|
+
const waitSeconds = event.data.waitMs
|
|
165
|
+
? Math.round(event.data.waitMs / 1000)
|
|
166
|
+
: 0;
|
|
167
|
+
if (event.data.provider === "cloudflare-turnstile")
|
|
168
|
+
return `Anti-bot resolved: Cloudflare Turnstile after ${waitSeconds || "<1"}s wait`;
|
|
169
|
+
if (event.data.provider === "aws-waf")
|
|
170
|
+
return `Anti-bot resolved: AWS WAF after ${waitSeconds || "<1"}s wait`;
|
|
171
|
+
return "Anti-bot mechanism resolved";
|
|
165
172
|
}
|
|
166
|
-
if (event.data.provider ===
|
|
173
|
+
if (event.data.provider === "cloudflare-turnstile")
|
|
167
174
|
return `Anti-bot detected: Cloudflare Turnstile (${event.data.status})`;
|
|
168
|
-
if (event.data.provider ===
|
|
175
|
+
if (event.data.provider === "aws-waf")
|
|
169
176
|
return `Anti-bot detected: AWS WAF telemetry request (${event.data.status})`;
|
|
170
|
-
return
|
|
177
|
+
return "Anti-bot mechanism detected";
|
|
171
178
|
};
|