cdp-tunnel 2.5.21 → 2.6.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/cli/index.js +258 -92
- package/extension-new/background.js +19 -1
- package/extension-new/cdp/handler/special.js +28 -28
- package/extension-new/core/debugger.js +35 -36
- package/extension-new/core/websocket.js +43 -37
- package/extension-new/manifest.json +3 -2
- package/extension-new/popup.html +79 -0
- package/extension-new/popup.js +114 -0
- package/extension-new/utils/config.js +13 -1
- package/package.json +6 -1
- package/server/proxy-server.js +321 -163
- package/server/saas/auth.js +128 -0
- package/server/saas/cdp-proxy.js +36 -0
- package/server/saas/db.js +48 -0
- package/server/saas/index.js +147 -0
- package/server/saas/routes.js +184 -0
- package/server/saas/web/index.html +803 -0
package/server/proxy-server.js
CHANGED
|
@@ -18,10 +18,23 @@ const { execSync, spawn: spawnProcess } = require('child_process');
|
|
|
18
18
|
const { CONFIG, BROWSER_ID, shouldLog } = require('./modules/config');
|
|
19
19
|
const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs, logDisconnect } = require('./modules/logger');
|
|
20
20
|
|
|
21
|
+
try {
|
|
22
|
+
const { validateApiKey } = require('./saas/auth');
|
|
23
|
+
var HAS_SAAS = true;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
var HAS_SAAS = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
const PORT = CONFIG.PORT;
|
|
22
29
|
const CONFIG_DIR = path.join(os.homedir(), '.cdp-tunnel');
|
|
23
|
-
const
|
|
24
|
-
|
|
30
|
+
const INSTANCE_DIR = path.join(CONFIG_DIR, 'instances', PORT.toString());
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(INSTANCE_DIR)) {
|
|
33
|
+
fs.mkdirSync(INSTANCE_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const EXTENSION_STATE_FILE = path.join(INSTANCE_DIR, 'extension-state.json');
|
|
37
|
+
const PLUGIN_EVER_CONNECTED_FILE = path.join(INSTANCE_DIR, 'plugin-ever-connected');
|
|
25
38
|
const SERVER_START_TIME = Date.now();
|
|
26
39
|
|
|
27
40
|
let lastChromeRestartAttempt = 0;
|
|
@@ -142,25 +155,39 @@ const server = http.createServer((req, res) => handleHttpRequest(req, res));
|
|
|
142
155
|
const pluginConnections = new Set();
|
|
143
156
|
const clientConnections = new Set();
|
|
144
157
|
|
|
158
|
+
class PluginNamespace {
|
|
159
|
+
constructor() {
|
|
160
|
+
this.sessionToClientId = new Map();
|
|
161
|
+
this.pendingAttachRequests = new Map();
|
|
162
|
+
this.pendingAttachedEvents = new Map();
|
|
163
|
+
this.pendingTargetCreatedEvents = new Map();
|
|
164
|
+
this.targetIdToClientId = new Map();
|
|
165
|
+
this.browserContextToClientId = new Map();
|
|
166
|
+
this.clientIdToBrowserContext = new Map();
|
|
167
|
+
this.cachedTargets = [];
|
|
168
|
+
this.lastTargetsUpdate = 0;
|
|
169
|
+
this.cachedBrowserVersion = null;
|
|
170
|
+
this.discoveringClientIds = new Map();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pluginNamespaces = new Map();
|
|
175
|
+
|
|
176
|
+
function getNamespace(pluginWs) {
|
|
177
|
+
if (!pluginNamespaces.has(pluginWs)) {
|
|
178
|
+
pluginNamespaces.set(pluginWs, new PluginNamespace());
|
|
179
|
+
}
|
|
180
|
+
return pluginNamespaces.get(pluginWs);
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
const connectionPairs = new Map();
|
|
146
184
|
const clientById = new Map();
|
|
147
|
-
const sessionToClientId = new Map();
|
|
148
|
-
const pendingAttachRequests = new Map();
|
|
149
185
|
const clientIdToPlugin = new Map();
|
|
150
186
|
const globalRequestIdMap = new Map();
|
|
151
|
-
const targetIdToClientId = new Map();
|
|
152
|
-
const pendingAttachedEvents = new Map();
|
|
153
|
-
const pendingTargetCreatedEvents = new Map();
|
|
154
|
-
const browserContextToClientId = new Map();
|
|
155
|
-
const clientIdToBrowserContext = new Map();
|
|
156
187
|
let globalRequestIdCounter = 0;
|
|
157
188
|
|
|
158
189
|
const { version: PKG_VERSION } = require('../package.json');
|
|
159
190
|
|
|
160
|
-
let cachedTargets = [];
|
|
161
|
-
let lastTargetsUpdate = 0;
|
|
162
|
-
let cachedBrowserVersion = null;
|
|
163
|
-
|
|
164
191
|
console.log('='.repeat(60));
|
|
165
192
|
console.log(` WebSocket CDP Proxy Server v${PKG_VERSION}`);
|
|
166
193
|
console.log('='.repeat(60));
|
|
@@ -177,26 +204,24 @@ function getHost(req) {
|
|
|
177
204
|
return req.headers.host || `localhost:${PORT}`;
|
|
178
205
|
}
|
|
179
206
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function buildTargetWebSocketUrl(req, targetId) {
|
|
188
|
-
return `ws://${getHost(req)}/devtools/page/${targetId}`;
|
|
207
|
+
function invalidateTargetsCache(pluginWs) {
|
|
208
|
+
if (pluginWs) {
|
|
209
|
+
getNamespace(pluginWs).lastTargetsUpdate = 0;
|
|
210
|
+
} else {
|
|
211
|
+
pluginNamespaces.forEach(ns => { ns.lastTargetsUpdate = 0; });
|
|
212
|
+
}
|
|
189
213
|
}
|
|
190
214
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
async function requestVersionFromPlugin(pluginWs) {
|
|
216
|
+
if (!pluginWs) {
|
|
217
|
+
pluginWs = pluginConnections.values().next().value;
|
|
218
|
+
}
|
|
219
|
+
if (!pluginWs) return null;
|
|
194
220
|
|
|
195
|
-
|
|
196
|
-
if (cachedBrowserVersion) return cachedBrowserVersion;
|
|
221
|
+
const ns = getNamespace(pluginWs);
|
|
222
|
+
if (ns.cachedBrowserVersion) return ns.cachedBrowserVersion;
|
|
197
223
|
|
|
198
|
-
|
|
199
|
-
if (!plugin || plugin.readyState !== WebSocket.OPEN) {
|
|
224
|
+
if (pluginWs.readyState !== WebSocket.OPEN) {
|
|
200
225
|
return null;
|
|
201
226
|
}
|
|
202
227
|
|
|
@@ -209,35 +234,40 @@ async function requestVersionFromPlugin() {
|
|
|
209
234
|
const msg = JSON.parse(data.toString());
|
|
210
235
|
if (msg.id === requestId && msg.result) {
|
|
211
236
|
clearTimeout(timeout);
|
|
212
|
-
|
|
237
|
+
pluginWs.off('message', handler);
|
|
213
238
|
if (msg.result.product || msg.result.userAgent) {
|
|
214
|
-
cachedBrowserVersion = msg.result;
|
|
239
|
+
ns.cachedBrowserVersion = msg.result;
|
|
215
240
|
}
|
|
216
|
-
resolve(cachedBrowserVersion || msg.result);
|
|
241
|
+
resolve(ns.cachedBrowserVersion || msg.result);
|
|
217
242
|
}
|
|
218
243
|
} catch (e) {}
|
|
219
244
|
};
|
|
220
245
|
|
|
221
|
-
|
|
222
|
-
|
|
246
|
+
pluginWs.on('message', handler);
|
|
247
|
+
pluginWs.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
|
|
223
248
|
});
|
|
224
249
|
}
|
|
225
250
|
|
|
226
|
-
async function requestTargetsFromPlugin() {
|
|
251
|
+
async function requestTargetsFromPlugin(pluginWs) {
|
|
252
|
+
if (!pluginWs) {
|
|
253
|
+
pluginWs = pluginConnections.values().next().value;
|
|
254
|
+
}
|
|
255
|
+
if (!pluginWs) return [];
|
|
256
|
+
|
|
257
|
+
const ns = getNamespace(pluginWs);
|
|
227
258
|
const now = Date.now();
|
|
228
|
-
if (now - lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && cachedTargets.length > 0) {
|
|
229
|
-
return cachedTargets;
|
|
259
|
+
if (now - ns.lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && ns.cachedTargets.length > 0) {
|
|
260
|
+
return ns.cachedTargets;
|
|
230
261
|
}
|
|
231
262
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return cachedTargets;
|
|
263
|
+
if (pluginWs.readyState !== WebSocket.OPEN) {
|
|
264
|
+
return ns.cachedTargets;
|
|
235
265
|
}
|
|
236
266
|
|
|
237
267
|
return new Promise((resolve) => {
|
|
238
268
|
const requestId = `targets_${Date.now()}`;
|
|
239
269
|
const timeout = setTimeout(() => {
|
|
240
|
-
resolve(cachedTargets);
|
|
270
|
+
resolve(ns.cachedTargets);
|
|
241
271
|
}, CONFIG.TARGETS_REQUEST_TIMEOUT);
|
|
242
272
|
|
|
243
273
|
const handler = (data) => {
|
|
@@ -245,36 +275,79 @@ async function requestTargetsFromPlugin() {
|
|
|
245
275
|
const msg = JSON.parse(data.toString());
|
|
246
276
|
if (msg.id === requestId && msg.result?.targetInfos) {
|
|
247
277
|
clearTimeout(timeout);
|
|
248
|
-
|
|
249
|
-
cachedTargets = msg.result.targetInfos;
|
|
250
|
-
lastTargetsUpdate = now;
|
|
251
|
-
resolve(cachedTargets);
|
|
278
|
+
pluginWs.off('message', handler);
|
|
279
|
+
ns.cachedTargets = msg.result.targetInfos;
|
|
280
|
+
ns.lastTargetsUpdate = now;
|
|
281
|
+
resolve(ns.cachedTargets);
|
|
252
282
|
}
|
|
253
283
|
} catch (e) {}
|
|
254
284
|
};
|
|
255
285
|
|
|
256
|
-
|
|
257
|
-
|
|
286
|
+
pluginWs.on('message', handler);
|
|
287
|
+
pluginWs.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
|
|
258
288
|
});
|
|
259
289
|
}
|
|
260
290
|
|
|
291
|
+
/**
|
|
292
|
+
* 生成指定 plugin 的 browser WS URL
|
|
293
|
+
*/
|
|
294
|
+
function buildBrowserWsUrl(pluginId) {
|
|
295
|
+
const host = CONFIG.EXTERNAL_HOST || `localhost:${PORT}`;
|
|
296
|
+
return `ws://${host}/devtools/browser/${pluginId}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
261
299
|
/**
|
|
262
300
|
* 处理 HTTP 请求
|
|
263
301
|
*/
|
|
302
|
+
function resolvePluginFromUrl(url) {
|
|
303
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
304
|
+
if (parts.length >= 3) {
|
|
305
|
+
const pluginId = parts[2];
|
|
306
|
+
for (const pluginWs of pluginConnections) {
|
|
307
|
+
if (pluginWs.pluginId === pluginId) return pluginWs;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return pluginConnections.values().next().value || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
264
313
|
async function handleHttpRequest(req, res) {
|
|
265
314
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
266
315
|
|
|
267
|
-
if (url.pathname === '/json/
|
|
268
|
-
const
|
|
316
|
+
if (url.pathname === '/json/browsers' || url.pathname === '/json/browsers/') {
|
|
317
|
+
const browsers = [];
|
|
318
|
+
for (const pluginWs of pluginConnections) {
|
|
319
|
+
if (pluginWs.readyState !== WebSocket.OPEN) continue;
|
|
320
|
+
const ns = getNamespace(pluginWs);
|
|
321
|
+
browsers.push({
|
|
322
|
+
pluginId: pluginWs.pluginId,
|
|
323
|
+
pluginName: pluginWs.pluginName || 'My Browser',
|
|
324
|
+
userId: pluginWs.userId || null,
|
|
325
|
+
browserName: ns.cachedBrowserVersion?.Browser || 'Unknown',
|
|
326
|
+
targets: ns.cachedTargets.length,
|
|
327
|
+
connected: true,
|
|
328
|
+
connectedAt: pluginWs.connectedAt,
|
|
329
|
+
webSocketDebuggerUrl: buildBrowserWsUrl(pluginWs.pluginId)
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
333
|
+
res.end(JSON.stringify(browsers));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (url.pathname === '/json/version' || url.pathname === '/json/version/' ||
|
|
338
|
+
url.pathname.match(/^\/json\/version\/[^/]+$/)) {
|
|
339
|
+
const pluginWs = resolvePluginFromUrl(url);
|
|
340
|
+
const ver = await requestVersionFromPlugin(pluginWs);
|
|
269
341
|
const userAgent = ver?.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36';
|
|
270
342
|
const product = ver?.product || 'Chrome/131.0.6778.86';
|
|
343
|
+
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
271
344
|
const payload = {
|
|
272
345
|
Browser: `${product} (cdp-tunnel/${PKG_VERSION})`,
|
|
273
346
|
'Protocol-Version': ver?.protocolVersion || '1.3',
|
|
274
347
|
'User-Agent': userAgent,
|
|
275
348
|
'V8-Version': ver?.jsVersion || '',
|
|
276
349
|
'WebKit-Version': '537.36',
|
|
277
|
-
webSocketDebuggerUrl:
|
|
350
|
+
webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/browser/${browserId}`
|
|
278
351
|
};
|
|
279
352
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
280
353
|
res.end(JSON.stringify(payload));
|
|
@@ -282,16 +355,19 @@ async function handleHttpRequest(req, res) {
|
|
|
282
355
|
}
|
|
283
356
|
|
|
284
357
|
if (url.pathname === '/json' || url.pathname === '/json/' ||
|
|
285
|
-
url.pathname === '/json/list' || url.pathname === '/json/list/'
|
|
286
|
-
|
|
358
|
+
url.pathname === '/json/list' || url.pathname === '/json/list/' ||
|
|
359
|
+
url.pathname.match(/^\/json\/list\/[^/]+$/)) {
|
|
360
|
+
const pluginWs = resolvePluginFromUrl(url);
|
|
361
|
+
const targets = await requestTargetsFromPlugin(pluginWs);
|
|
362
|
+
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
287
363
|
const targetList = targets
|
|
288
364
|
.filter(t => {
|
|
289
365
|
if (t.type !== 'page') return false;
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
366
|
+
const tUrl = t.url || '';
|
|
367
|
+
if (tUrl.startsWith('chrome://') ||
|
|
368
|
+
tUrl.startsWith('chrome-extension://') ||
|
|
369
|
+
tUrl.startsWith('devtools://') ||
|
|
370
|
+
tUrl.startsWith('edge://')) {
|
|
295
371
|
return false;
|
|
296
372
|
}
|
|
297
373
|
return true;
|
|
@@ -305,7 +381,7 @@ async function handleHttpRequest(req, res) {
|
|
|
305
381
|
title: t.title || '',
|
|
306
382
|
type: t.type,
|
|
307
383
|
url: t.url || '',
|
|
308
|
-
webSocketDebuggerUrl:
|
|
384
|
+
webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/page/${t.targetId}`
|
|
309
385
|
}));
|
|
310
386
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
311
387
|
res.end(JSON.stringify(targetList));
|
|
@@ -322,8 +398,10 @@ async function handleHttpRequest(req, res) {
|
|
|
322
398
|
server.on('upgrade', (req, socket, head) => {
|
|
323
399
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
324
400
|
const path = url.pathname;
|
|
401
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
325
402
|
const isPlugin = path === '/plugin';
|
|
326
403
|
const isClient = path === '/client' ||
|
|
404
|
+
path.startsWith('/client/') ||
|
|
327
405
|
path.startsWith('/client-') ||
|
|
328
406
|
path.startsWith('/devtools/browser/') ||
|
|
329
407
|
path.startsWith('/devtools/page/');
|
|
@@ -341,6 +419,7 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
341
419
|
wss.on('connection', (ws, req) => {
|
|
342
420
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
343
421
|
const path = url.pathname;
|
|
422
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
344
423
|
|
|
345
424
|
const clientInfo = {
|
|
346
425
|
ip: req.socket.remoteAddress,
|
|
@@ -348,10 +427,16 @@ wss.on('connection', (ws, req) => {
|
|
|
348
427
|
};
|
|
349
428
|
|
|
350
429
|
if (path === '/plugin') {
|
|
351
|
-
handlePluginConnection(ws, clientInfo);
|
|
352
|
-
} else if (path === '/client' || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
|
|
430
|
+
handlePluginConnection(ws, clientInfo, req);
|
|
431
|
+
} else if (path === '/client' || path.startsWith('/client/') || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
|
|
353
432
|
const customClientId = path.startsWith('/client-') ? path.replace('/client-', '') : null;
|
|
354
|
-
|
|
433
|
+
let targetPluginId = null;
|
|
434
|
+
if (pathParts[0] === 'client' && pathParts[1]) {
|
|
435
|
+
targetPluginId = pathParts[1];
|
|
436
|
+
} else if (pathParts[0] === 'devtools' && pathParts[1] === 'browser' && pathParts[2]) {
|
|
437
|
+
targetPluginId = pathParts[2];
|
|
438
|
+
}
|
|
439
|
+
handleClientConnection(ws, clientInfo, customClientId, targetPluginId);
|
|
355
440
|
} else if (path.startsWith('/devtools/page/')) {
|
|
356
441
|
const targetId = path.replace('/devtools/page/', '');
|
|
357
442
|
handlePageConnection(ws, clientInfo, targetId);
|
|
@@ -362,21 +447,26 @@ wss.on('connection', (ws, req) => {
|
|
|
362
447
|
});
|
|
363
448
|
|
|
364
449
|
function cleanupClient(ws, id, reason) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
450
|
+
const pluginWs = ws.pairedPlugin || clientIdToPlugin.get(id);
|
|
451
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
452
|
+
|
|
453
|
+
if (ns) {
|
|
454
|
+
const sessionsToClean = [];
|
|
455
|
+
for (const [sessionId, clientId] of ns.sessionToClientId.entries()) {
|
|
456
|
+
if (clientId === id) {
|
|
457
|
+
sessionsToClean.push(sessionId);
|
|
458
|
+
ns.sessionToClientId.delete(sessionId);
|
|
459
|
+
}
|
|
370
460
|
}
|
|
371
461
|
}
|
|
372
462
|
|
|
373
463
|
clientConnections.delete(ws);
|
|
374
464
|
clientById.delete(id);
|
|
465
|
+
clientIdToPlugin.delete(id);
|
|
375
466
|
|
|
376
467
|
logConnectionEvent('CLIENT_DISCONNECTED', {
|
|
377
468
|
id,
|
|
378
469
|
reason,
|
|
379
|
-
sessionsCleaned: sessionsToClean.length,
|
|
380
470
|
totalPlugins: pluginConnections.size,
|
|
381
471
|
totalClients: clientConnections.size
|
|
382
472
|
});
|
|
@@ -384,7 +474,6 @@ function cleanupClient(ws, id, reason) {
|
|
|
384
474
|
logDisconnect('CLIENT_CLEANUP', {
|
|
385
475
|
clientId: id,
|
|
386
476
|
reason,
|
|
387
|
-
sessionsLost: sessionsToClean.length,
|
|
388
477
|
cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
|
|
389
478
|
uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
|
|
390
479
|
remainingClients: clientConnections.size,
|
|
@@ -401,20 +490,23 @@ function cleanupClient(ws, id, reason) {
|
|
|
401
490
|
safeSend(ws.pairedPlugin, JSON.stringify({
|
|
402
491
|
type: 'client-disconnected',
|
|
403
492
|
clientId: id,
|
|
404
|
-
sessions:
|
|
493
|
+
sessions: []
|
|
405
494
|
}), 'plugin');
|
|
406
495
|
}
|
|
407
496
|
|
|
408
497
|
broadcastClientList();
|
|
409
498
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
clientIdToBrowserContext.
|
|
499
|
+
if (ns) {
|
|
500
|
+
for (const [tId, cId] of ns.targetIdToClientId.entries()) {
|
|
501
|
+
if (cId === id) ns.targetIdToClientId.delete(tId);
|
|
502
|
+
}
|
|
503
|
+
for (const [bcId, cId] of ns.browserContextToClientId.entries()) {
|
|
504
|
+
if (cId === id) ns.browserContextToClientId.delete(bcId);
|
|
505
|
+
}
|
|
506
|
+
if (ns.clientIdToBrowserContext.has(id)) {
|
|
507
|
+
ns.clientIdToBrowserContext.delete(id);
|
|
508
|
+
}
|
|
509
|
+
ns.discoveringClientIds.delete(id);
|
|
418
510
|
}
|
|
419
511
|
for (const [gId, mapping] of globalRequestIdMap.entries()) {
|
|
420
512
|
if (mapping.clientId === id) globalRequestIdMap.delete(gId);
|
|
@@ -446,7 +538,9 @@ function sendPendingRequestErrors(pluginWs) {
|
|
|
446
538
|
}
|
|
447
539
|
|
|
448
540
|
function cleanupPlugin(ws, id, reason) {
|
|
541
|
+
const ns = getNamespace(ws);
|
|
449
542
|
pluginConnections.delete(ws);
|
|
543
|
+
pluginNamespaces.delete(ws);
|
|
450
544
|
|
|
451
545
|
if (pluginConnections.size === 0) {
|
|
452
546
|
updateExtensionState(false);
|
|
@@ -478,8 +572,8 @@ function cleanupPlugin(ws, id, reason) {
|
|
|
478
572
|
remainingPlugins: pluginConnections.size,
|
|
479
573
|
affectedClients,
|
|
480
574
|
uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
|
|
481
|
-
activeSessions: sessionToClientId.size,
|
|
482
|
-
pendingRequests: pendingAttachRequests.size
|
|
575
|
+
activeSessions: ns.sessionToClientId.size,
|
|
576
|
+
pendingRequests: ns.pendingAttachRequests.size
|
|
483
577
|
});
|
|
484
578
|
|
|
485
579
|
if (ws.pairedClientId) {
|
|
@@ -490,35 +584,46 @@ function cleanupPlugin(ws, id, reason) {
|
|
|
490
584
|
/**
|
|
491
585
|
* 处理 Chrome 扩展连接
|
|
492
586
|
*/
|
|
493
|
-
function handlePluginConnection(ws, clientInfo) {
|
|
587
|
+
function handlePluginConnection(ws, clientInfo, request) {
|
|
588
|
+
const req = request;
|
|
494
589
|
const id = generateId('plugin');
|
|
495
590
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
591
|
+
ws.pluginId = 'browser_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const url = new URL(req.url, `http://localhost`);
|
|
595
|
+
const apiKey = url.searchParams.get('key');
|
|
596
|
+
const desiredPluginId = url.searchParams.get('pluginId');
|
|
597
|
+
|
|
598
|
+
if (desiredPluginId && /^browser_[a-zA-Z0-9_]+$/.test(desiredPluginId)) {
|
|
599
|
+
let conflict = false;
|
|
600
|
+
for (const existing of pluginConnections) {
|
|
601
|
+
if (existing.pluginId === desiredPluginId && existing !== ws) {
|
|
602
|
+
conflict = true;
|
|
603
|
+
break;
|
|
503
604
|
}
|
|
504
|
-
toRemove.push(oldWs);
|
|
505
605
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
pluginConnections.delete(oldWs);
|
|
509
|
-
if (shouldLog('info')) {
|
|
510
|
-
console.log(`[PLUGIN] Removed old connection: ${oldWs.id}`);
|
|
606
|
+
if (!conflict) {
|
|
607
|
+
ws.pluginId = desiredPluginId;
|
|
511
608
|
}
|
|
512
|
-
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (HAS_SAAS && apiKey) {
|
|
612
|
+
const keyInfo = validateApiKey(apiKey);
|
|
613
|
+
if (keyInfo) {
|
|
614
|
+
ws.userId = keyInfo.userId;
|
|
615
|
+
ws.apiKeyId = keyInfo.keyId;
|
|
616
|
+
logConnectionEvent('PLUGIN_AUTHED', `userId=${keyInfo.userId} keyName=${keyInfo.keyName}`);
|
|
617
|
+
} else {
|
|
618
|
+
logConnectionEvent('PLUGIN_AUTH_FAIL', 'Invalid API key');
|
|
619
|
+
ws.close(4001, 'Invalid API key');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} catch (e) {
|
|
624
|
+
logConnectionEvent('PLUGIN_AUTH_ERR', e.message);
|
|
513
625
|
}
|
|
514
626
|
|
|
515
|
-
sessionToClientId.clear();
|
|
516
|
-
pendingAttachRequests.clear();
|
|
517
|
-
connectionPairs.clear();
|
|
518
|
-
clientConnections.forEach(clientWs => {
|
|
519
|
-
clientWs.pairedPlugin = null;
|
|
520
|
-
});
|
|
521
|
-
|
|
522
627
|
pluginConnections.add(ws);
|
|
523
628
|
|
|
524
629
|
const pluginType = 'plugin';
|
|
@@ -626,8 +731,8 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
626
731
|
if (!match) {
|
|
627
732
|
console.log(` ↳ Run "cdp-tunnel update" or reload the extension to sync versions`);
|
|
628
733
|
}
|
|
629
|
-
cachedBrowserVersion = null;
|
|
630
|
-
requestVersionFromPlugin();
|
|
734
|
+
getNamespace(ws).cachedBrowserVersion = null;
|
|
735
|
+
requestVersionFromPlugin(ws);
|
|
631
736
|
return;
|
|
632
737
|
}
|
|
633
738
|
|
|
@@ -645,9 +750,9 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
645
750
|
if (parsed.method.startsWith('CDPTunnel.')) {
|
|
646
751
|
console.log(`[EXT DEBUG] ${parsed.method}: ${JSON.stringify(parsed.params)}`);
|
|
647
752
|
}
|
|
648
|
-
// 对于 Target 事件,始终广播给所有客户端
|
|
649
753
|
const targetEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
|
|
650
754
|
if (targetEvents.includes(parsed.method)) {
|
|
755
|
+
const ns = getNamespace(ws);
|
|
651
756
|
const cdpMsg = {
|
|
652
757
|
method: parsed.method,
|
|
653
758
|
params: parsed.params,
|
|
@@ -662,19 +767,19 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
662
767
|
const targetId = parsed.params?.targetInfo?.targetId;
|
|
663
768
|
const openerId = parsed.params?.targetInfo?.openerId;
|
|
664
769
|
if (openerId && targetId) {
|
|
665
|
-
const openerClientId = targetIdToClientId.get(openerId);
|
|
770
|
+
const openerClientId = ns.targetIdToClientId.get(openerId);
|
|
666
771
|
if (openerClientId) {
|
|
667
|
-
targetIdToClientId.set(targetId, openerClientId);
|
|
772
|
+
ns.targetIdToClientId.set(targetId, openerClientId);
|
|
668
773
|
console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8) || 'none'} openerId=${openerId?.substring(0,8) || 'none'} -> clientId=${openerClientId}`);
|
|
669
774
|
}
|
|
670
775
|
}
|
|
671
776
|
}
|
|
672
777
|
|
|
673
|
-
rewriteBrowserContextId(cdpMsg);
|
|
778
|
+
rewriteBrowserContextId(cdpMsg, ws);
|
|
674
779
|
const cdpData = JSON.stringify(cdpMsg);
|
|
675
780
|
|
|
676
781
|
const targetId = parsed.params?.targetInfo?.targetId;
|
|
677
|
-
const eventClientId = targetId ? targetIdToClientId.get(targetId) : null;
|
|
782
|
+
const eventClientId = targetId ? ns.targetIdToClientId.get(targetId) : null;
|
|
678
783
|
|
|
679
784
|
if (eventClientId) {
|
|
680
785
|
const clientWs = clientById.get(eventClientId);
|
|
@@ -683,32 +788,50 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
683
788
|
console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
|
|
684
789
|
}
|
|
685
790
|
} else if (targetId && (parsed.method === 'Target.targetCreated' || parsed.method === 'Target.attachedToTarget')) {
|
|
686
|
-
const pendingMap = parsed.method === 'Target.targetCreated' ? pendingTargetCreatedEvents : pendingAttachedEvents;
|
|
687
|
-
|
|
688
|
-
|
|
791
|
+
const pendingMap = parsed.method === 'Target.targetCreated' ? ns.pendingTargetCreatedEvents : ns.pendingAttachedEvents;
|
|
792
|
+
|
|
793
|
+
let routedToDiscoverer = false;
|
|
794
|
+
if (ns.discoveringClientIds.size > 0) {
|
|
795
|
+
for (const [discClientId, timestamp] of ns.discoveringClientIds) {
|
|
796
|
+
if (Date.now() - timestamp < 30000) {
|
|
797
|
+
const discWs = clientById.get(discClientId);
|
|
798
|
+
if (discWs && discWs.readyState === WebSocket.OPEN) {
|
|
799
|
+
discWs.send(cdpData);
|
|
800
|
+
console.log(`[TARGET EVENT DISCOVERED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> discovering client=${discClientId}`);
|
|
801
|
+
routedToDiscoverer = true;
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
ns.discoveringClientIds.delete(discClientId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!routedToDiscoverer) {
|
|
810
|
+
pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
|
|
811
|
+
console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8)} (cached, waiting for createTarget response)`);
|
|
812
|
+
}
|
|
689
813
|
} else {
|
|
690
814
|
console.log(`[TARGET EVENT DROPPED] ${parsed.method} targetId=${targetId?.substring(0,8) || 'none'} (no owner, dropped for isolation)`);
|
|
691
815
|
}
|
|
692
816
|
}
|
|
693
817
|
|
|
694
|
-
// 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
|
|
695
818
|
if (parsed.method === 'Target.attachedToTarget') {
|
|
819
|
+
const ns = getNamespace(ws);
|
|
696
820
|
const targetId = parsed.params?.targetInfo?.targetId;
|
|
697
821
|
const sessionId = parsed.params?.sessionId;
|
|
698
822
|
|
|
699
823
|
if (targetId && sessionId) {
|
|
700
824
|
const clientId = ws.pairedClientId;
|
|
701
825
|
if (clientId) {
|
|
702
|
-
sessionToClientId.set(sessionId, clientId);
|
|
826
|
+
ns.sessionToClientId.set(sessionId, clientId);
|
|
703
827
|
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> clientId=${clientId?.substring(0,8) || 'none'}`);
|
|
704
828
|
} else {
|
|
705
|
-
sessionToClientId.set(sessionId, targetId);
|
|
829
|
+
ns.sessionToClientId.set(sessionId, targetId);
|
|
706
830
|
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> targetId=${targetId?.substring(0,8) || 'none'} (no pairedClientId)`);
|
|
707
831
|
}
|
|
708
832
|
}
|
|
709
833
|
}
|
|
710
834
|
|
|
711
|
-
// 如果不是 Target 事件,按照原来的逻辑发送
|
|
712
835
|
if (!targetEvents.includes(parsed.method)) {
|
|
713
836
|
const cdpMsg = {
|
|
714
837
|
method: parsed.method,
|
|
@@ -744,45 +867,42 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
744
867
|
if (parsed && parsed.id !== undefined) {
|
|
745
868
|
const globalId = parsed.id;
|
|
746
869
|
const mapping = globalRequestIdMap.get(globalId);
|
|
870
|
+
const ns = getNamespace(ws);
|
|
747
871
|
console.log(`[RESPONSE DEBUG] globalId=${globalId} hasMapping=${!!mapping} sessionId=${parsed.sessionId?.substring(0,8) || 'none'} method=${parsed.method || 'response'}`);
|
|
748
872
|
if (mapping) {
|
|
749
873
|
const clientWs = clientById.get(mapping.clientId);
|
|
750
874
|
if (clientWs && clientWs.readyState === WebSocket.OPEN) {
|
|
751
|
-
// 如果是 Target.createBrowserContext 响应,记录 browserContextId -> clientId 映射
|
|
752
875
|
if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
|
|
753
876
|
const browserContextId = parsed.result.browserContextId;
|
|
754
|
-
browserContextToClientId.set(browserContextId, mapping.clientId);
|
|
755
|
-
clientIdToBrowserContext.set(mapping.clientId, browserContextId);
|
|
877
|
+
ns.browserContextToClientId.set(browserContextId, mapping.clientId);
|
|
878
|
+
ns.clientIdToBrowserContext.set(mapping.clientId, browserContextId);
|
|
756
879
|
console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
|
|
757
880
|
}
|
|
758
881
|
|
|
759
|
-
// 如果是 Target.attachToTarget 响应,建立 sessionId -> clientId 映射
|
|
760
882
|
if (parsed.result?.sessionId && mapping.method === 'Target.attachToTarget') {
|
|
761
|
-
sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
|
|
883
|
+
ns.sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
|
|
762
884
|
console.log(`[SESSION MAPPED from attach response] sessionId=${parsed.result.sessionId?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)}`);
|
|
763
885
|
}
|
|
764
886
|
|
|
765
|
-
// 如果是 Target.createTarget 响应,先发送缓存的 Target.attachedToTarget 事件
|
|
766
|
-
// 然后再发送响应
|
|
767
887
|
if (mapping.isCreateTarget && parsed.result?.targetId) {
|
|
768
888
|
const targetId = parsed.result.targetId;
|
|
769
|
-
targetIdToClientId.set(targetId, mapping.clientId);
|
|
770
|
-
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${targetIdToClientId.size}`);
|
|
889
|
+
ns.targetIdToClientId.set(targetId, mapping.clientId);
|
|
890
|
+
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${ns.targetIdToClientId.size}`);
|
|
771
891
|
|
|
772
|
-
const cachedCreated = pendingTargetCreatedEvents.get(targetId);
|
|
892
|
+
const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
|
|
773
893
|
if (cachedCreated) {
|
|
774
894
|
clientWs.send(cachedCreated.cdpData);
|
|
775
895
|
console.log(`[TARGET CREATED EVENT] Sent cached Target.targetCreated to client: ${mapping.clientId}`);
|
|
776
|
-
pendingTargetCreatedEvents.delete(targetId);
|
|
896
|
+
ns.pendingTargetCreatedEvents.delete(targetId);
|
|
777
897
|
}
|
|
778
898
|
|
|
779
|
-
const cachedEvent = pendingAttachedEvents.get(targetId);
|
|
899
|
+
const cachedEvent = ns.pendingAttachedEvents.get(targetId);
|
|
780
900
|
if (cachedEvent) {
|
|
781
901
|
if (cachedEvent.parsed.sessionId) {
|
|
782
|
-
sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
|
|
902
|
+
ns.sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
|
|
783
903
|
}
|
|
784
904
|
console.log(`[SESSION MAPPED from cached] sessionId=${cachedEvent.parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${mapping.clientId} (targetId=${targetId})`);
|
|
785
|
-
pendingAttachedEvents.delete(targetId);
|
|
905
|
+
ns.pendingAttachedEvents.delete(targetId);
|
|
786
906
|
|
|
787
907
|
const cdpMsg = {
|
|
788
908
|
method: cachedEvent.parsed.method,
|
|
@@ -796,37 +916,35 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
796
916
|
const newTargetInfo = cachedCreated?.parsed?.params?.targetInfo
|
|
797
917
|
|| cachedEvent?.parsed?.params?.targetInfo;
|
|
798
918
|
if (newTargetInfo) {
|
|
799
|
-
const exists = cachedTargets.some(t => t.targetId === targetId);
|
|
919
|
+
const exists = ns.cachedTargets.some(t => t.targetId === targetId);
|
|
800
920
|
if (!exists) {
|
|
801
|
-
cachedTargets.push(newTargetInfo);
|
|
921
|
+
ns.cachedTargets.push(newTargetInfo);
|
|
802
922
|
}
|
|
803
923
|
} else {
|
|
804
|
-
invalidateTargetsCache();
|
|
924
|
+
invalidateTargetsCache(ws);
|
|
805
925
|
}
|
|
806
926
|
}
|
|
807
|
-
// 过滤 Target.getTargets 响应,只返回该客户端拥有的 target
|
|
808
927
|
if (mapping.isGetTargets && parsed.result && parsed.result.targetInfos) {
|
|
809
928
|
const clientId = mapping.clientId;
|
|
929
|
+
const pluginWsForGetTargets = ws;
|
|
810
930
|
parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
|
|
811
931
|
if (t.type !== 'page') return true;
|
|
812
|
-
const ownerClient = targetIdToClientId.get(t.targetId);
|
|
932
|
+
const ownerClient = ns.targetIdToClientId.get(t.targetId);
|
|
933
|
+
if (!ownerClient) return true;
|
|
813
934
|
return ownerClient === clientId;
|
|
814
935
|
});
|
|
815
936
|
console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
|
|
816
937
|
}
|
|
817
|
-
// 清理 Target.closeTarget 成功后的映射
|
|
818
938
|
if (parsed.result && parsed.result.success !== undefined && mapping.method === 'Target.closeTarget') {
|
|
819
939
|
if (mapping.closeTargetId) {
|
|
820
|
-
targetIdToClientId.delete(mapping.closeTargetId);
|
|
940
|
+
ns.targetIdToClientId.delete(mapping.closeTargetId);
|
|
821
941
|
console.log(`[CLOSE TARGET CLEANUP] removed targetId=${mapping.closeTargetId?.substring(0,8)} from mapping`);
|
|
822
942
|
}
|
|
823
|
-
invalidateTargetsCache();
|
|
943
|
+
invalidateTargetsCache(ws);
|
|
824
944
|
}
|
|
825
945
|
|
|
826
|
-
// 然后发送响应给客户端
|
|
827
946
|
const originalId = mapping.originalId;
|
|
828
947
|
parsed.id = originalId;
|
|
829
|
-
// 如果请求有 sessionId,但响应没有,添加 sessionId
|
|
830
948
|
if (mapping.sessionId && !parsed.sessionId) {
|
|
831
949
|
parsed.sessionId = mapping.sessionId;
|
|
832
950
|
}
|
|
@@ -844,7 +962,8 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
844
962
|
|
|
845
963
|
// 2. sessionId 路由:消息属于特定 session(事件,没有 id)
|
|
846
964
|
if (parsed && parsed.sessionId) {
|
|
847
|
-
const
|
|
965
|
+
const ns = getNamespace(ws);
|
|
966
|
+
const targetClientId = ns.sessionToClientId.get(parsed.sessionId);
|
|
848
967
|
console.log(`[SESSION ROUTE] sessionId=${parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${targetClientId || 'not found'}`);
|
|
849
968
|
if (targetClientId) {
|
|
850
969
|
const clientWs = clientById.get(targetClientId);
|
|
@@ -888,7 +1007,6 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
888
1007
|
cleanupPlugin(ws, id, `close:${code}`);
|
|
889
1008
|
});
|
|
890
1009
|
|
|
891
|
-
// 错误处理
|
|
892
1010
|
ws.on('error', (error) => {
|
|
893
1011
|
console.error(`[PLUGIN ERROR] ${id}:`, error.message);
|
|
894
1012
|
|
|
@@ -900,6 +1018,7 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
900
1018
|
});
|
|
901
1019
|
|
|
902
1020
|
pluginConnections.delete(ws);
|
|
1021
|
+
pluginNamespaces.delete(ws);
|
|
903
1022
|
|
|
904
1023
|
clientConnections.forEach(clientWs => {
|
|
905
1024
|
if (clientWs.pairedPlugin === ws) {
|
|
@@ -922,6 +1041,7 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
922
1041
|
type: 'connected',
|
|
923
1042
|
role: 'plugin',
|
|
924
1043
|
id: id,
|
|
1044
|
+
pluginId: ws.pluginId,
|
|
925
1045
|
fresh: (Date.now() - SERVER_START_TIME) < 5000,
|
|
926
1046
|
timestamp: Date.now()
|
|
927
1047
|
}));
|
|
@@ -930,11 +1050,11 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
930
1050
|
/**
|
|
931
1051
|
* 处理 CDP 客户端连接 (Playwright/Puppeteer)
|
|
932
1052
|
*/
|
|
933
|
-
function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
1053
|
+
function handleClientConnection(ws, clientInfo, customClientId = null, targetPluginId = null) {
|
|
934
1054
|
clientConnections.add(ws);
|
|
935
1055
|
const id = customClientId || generateId('client');
|
|
936
1056
|
if (shouldLog('info')) {
|
|
937
|
-
console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}`);
|
|
1057
|
+
console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}${targetPluginId ? ` targetPlugin=${targetPluginId}` : ''}`);
|
|
938
1058
|
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
939
1059
|
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
940
1060
|
}
|
|
@@ -947,7 +1067,6 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
947
1067
|
totalClients: clientConnections.size
|
|
948
1068
|
});
|
|
949
1069
|
|
|
950
|
-
// 检查是否有可用的 plugin 连接
|
|
951
1070
|
if (pluginConnections.size === 0) {
|
|
952
1071
|
if (shouldLog('warn')) {
|
|
953
1072
|
console.log(` - WARNING: No plugin connections available!`);
|
|
@@ -965,27 +1084,36 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
965
1084
|
}
|
|
966
1085
|
}
|
|
967
1086
|
} else {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1087
|
+
let pluginWs;
|
|
1088
|
+
if (targetPluginId) {
|
|
1089
|
+
pluginWs = [...pluginConnections].find(p => p.pluginId === targetPluginId);
|
|
1090
|
+
if (!pluginWs) {
|
|
1091
|
+
if (shouldLog('warn')) {
|
|
1092
|
+
console.log(` - WARNING: Plugin ${targetPluginId} not found!`);
|
|
1093
|
+
}
|
|
1094
|
+
ws.close(4004, `Plugin ${targetPluginId} not found`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
} else {
|
|
1098
|
+
pluginWs = pluginConnections.values().next().value;
|
|
1099
|
+
}
|
|
971
1100
|
if (pluginWs) {
|
|
972
1101
|
connectionPairs.set(id, pluginWs);
|
|
973
1102
|
ws.pairedPlugin = pluginWs;
|
|
1103
|
+
ws.targetPluginId = pluginWs.pluginId;
|
|
974
1104
|
clientIdToPlugin.set(id, pluginWs);
|
|
975
1105
|
|
|
976
1106
|
if (shouldLog('info')) {
|
|
977
|
-
console.log(` - Paired with plugin: ${pluginWs.id} (
|
|
1107
|
+
console.log(` - Paired with plugin: ${pluginWs.id} (pluginId=${pluginWs.pluginId})`);
|
|
978
1108
|
}
|
|
979
1109
|
|
|
980
1110
|
logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
|
|
981
1111
|
|
|
982
|
-
// 通知 Plugin 新客户端已连接
|
|
983
1112
|
pluginWs.send(JSON.stringify({
|
|
984
1113
|
type: 'client-connected',
|
|
985
1114
|
clientId: id
|
|
986
1115
|
}));
|
|
987
1116
|
|
|
988
|
-
// 发送当前所有客户端列表
|
|
989
1117
|
broadcastClientList();
|
|
990
1118
|
}
|
|
991
1119
|
}
|
|
@@ -1063,8 +1191,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
1063
1191
|
}
|
|
1064
1192
|
if (parsed && parsed.id !== undefined) {
|
|
1065
1193
|
if (parsed.method === 'Target.closeTarget') {
|
|
1194
|
+
const pluginWs = ws.pairedPlugin;
|
|
1195
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
1066
1196
|
const targetId = parsed.params?.targetId;
|
|
1067
|
-
const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
|
|
1197
|
+
const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
|
|
1068
1198
|
if (ownerClient && ownerClient !== id) {
|
|
1069
1199
|
console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
|
|
1070
1200
|
const errMsg = JSON.stringify({
|
|
@@ -1080,8 +1210,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
1080
1210
|
currentMapping.closeTargetId = targetId;
|
|
1081
1211
|
}
|
|
1082
1212
|
} else if (parsed.method === 'Target.attachToTarget') {
|
|
1213
|
+
const pluginWs = ws.pairedPlugin;
|
|
1214
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
1083
1215
|
const targetId = parsed.params?.targetId;
|
|
1084
|
-
const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
|
|
1216
|
+
const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
|
|
1085
1217
|
if (ownerClient && ownerClient !== id) {
|
|
1086
1218
|
console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
|
|
1087
1219
|
const errMsg = JSON.stringify({
|
|
@@ -1109,6 +1241,14 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
1109
1241
|
}
|
|
1110
1242
|
}
|
|
1111
1243
|
|
|
1244
|
+
if (parsed && (parsed.method === 'Target.setDiscoverTargets' || parsed.method === 'Target.setAutoAttach')) {
|
|
1245
|
+
const ns = ws.pairedPlugin ? getNamespace(ws.pairedPlugin) : null;
|
|
1246
|
+
if (ns) {
|
|
1247
|
+
ns.discoveringClientIds.set(id, Date.now());
|
|
1248
|
+
console.log(`[DISCOVERING] client=${id} method=${parsed.method}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1112
1252
|
if (parsed && parsed.method === 'Browser.close') {
|
|
1113
1253
|
if (shouldLog('info')) {
|
|
1114
1254
|
console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close, forwarding to plugin`);
|
|
@@ -1173,7 +1313,17 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1173
1313
|
ws.lastActivityTime = Date.now();
|
|
1174
1314
|
clientById.set(id, ws);
|
|
1175
1315
|
|
|
1176
|
-
|
|
1316
|
+
let plugin = null;
|
|
1317
|
+
for (const p of pluginConnections) {
|
|
1318
|
+
const ns = getNamespace(p);
|
|
1319
|
+
if (ns.targetIdToClientId.has(targetId)) {
|
|
1320
|
+
plugin = p;
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (!plugin) {
|
|
1325
|
+
plugin = pluginConnections.values().next().value;
|
|
1326
|
+
}
|
|
1177
1327
|
if (plugin && plugin.readyState === WebSocket.OPEN) {
|
|
1178
1328
|
ws.pairedPlugin = plugin;
|
|
1179
1329
|
if (shouldLog('info')) {
|
|
@@ -1209,7 +1359,7 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1209
1359
|
// 对于全局 Target 事件,需要广播给所有客户端
|
|
1210
1360
|
const broadcastEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
|
|
1211
1361
|
if (broadcastEvents.includes(msg.method)) {
|
|
1212
|
-
rewriteBrowserContextId(cdpMsg);
|
|
1362
|
+
rewriteBrowserContextId(cdpMsg, ws.pairedPlugin);
|
|
1213
1363
|
console.log(`[PLUGIN -> ALL CLIENTS] Broadcasting ${msg.method}`);
|
|
1214
1364
|
broadcastToClients(JSON.stringify(cdpMsg), null);
|
|
1215
1365
|
} else {
|
|
@@ -1313,23 +1463,24 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1313
1463
|
* 插件总是报告 'default',但 Playwright 期望自己创建的 context ID
|
|
1314
1464
|
* 通过 openerId 找到对应的 clientId,再找到该 client 的 browserContextId
|
|
1315
1465
|
*/
|
|
1316
|
-
function rewriteBrowserContextId(cdpMsg) {
|
|
1466
|
+
function rewriteBrowserContextId(cdpMsg, pluginWs) {
|
|
1317
1467
|
const targetInfo = cdpMsg.params?.targetInfo;
|
|
1318
1468
|
if (!targetInfo || targetInfo.browserContextId !== 'default') {
|
|
1319
1469
|
return cdpMsg;
|
|
1320
1470
|
}
|
|
1321
1471
|
|
|
1472
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
1322
1473
|
let clientId = null;
|
|
1323
1474
|
|
|
1324
|
-
if (targetInfo.openerId) {
|
|
1325
|
-
clientId = targetIdToClientId.get(targetInfo.openerId);
|
|
1475
|
+
if (targetInfo.openerId && ns) {
|
|
1476
|
+
clientId = ns.targetIdToClientId.get(targetInfo.openerId);
|
|
1326
1477
|
}
|
|
1327
|
-
if (!clientId && targetInfo.targetId) {
|
|
1328
|
-
clientId = targetIdToClientId.get(targetInfo.targetId);
|
|
1478
|
+
if (!clientId && targetInfo.targetId && ns) {
|
|
1479
|
+
clientId = ns.targetIdToClientId.get(targetInfo.targetId);
|
|
1329
1480
|
}
|
|
1330
1481
|
|
|
1331
|
-
if (clientId) {
|
|
1332
|
-
const contextId = clientIdToBrowserContext.get(clientId);
|
|
1482
|
+
if (clientId && ns) {
|
|
1483
|
+
const contextId = ns.clientIdToBrowserContext.get(clientId);
|
|
1333
1484
|
if (contextId) {
|
|
1334
1485
|
console.log(`[CONTEXT REWRITE] targetId=${targetInfo.targetId?.substring(0,8) || 'none'} browserContextId: 'default' -> '${contextId}' (via openerId=${targetInfo.openerId?.substring(0,8) || 'none'}, clientId=${clientId})`);
|
|
1335
1486
|
targetInfo.browserContextId = contextId;
|
|
@@ -1605,6 +1756,13 @@ setInterval(() => {
|
|
|
1605
1756
|
};
|
|
1606
1757
|
});
|
|
1607
1758
|
|
|
1759
|
+
let totalSessions = 0;
|
|
1760
|
+
let totalPendingAttach = 0;
|
|
1761
|
+
pluginNamespaces.forEach(ns => {
|
|
1762
|
+
totalSessions += ns.sessionToClientId.size;
|
|
1763
|
+
totalPendingAttach += ns.pendingAttachRequests.size;
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1608
1766
|
logStatus({
|
|
1609
1767
|
timestamp: now,
|
|
1610
1768
|
plugins: pluginConnections.size,
|
|
@@ -1614,8 +1772,8 @@ setInterval(() => {
|
|
|
1614
1772
|
pairs: connectionPairs.size,
|
|
1615
1773
|
pluginDetails: pluginList,
|
|
1616
1774
|
clientDetails: clientList,
|
|
1617
|
-
sessions:
|
|
1618
|
-
pendingAttach:
|
|
1775
|
+
sessions: totalSessions,
|
|
1776
|
+
pendingAttach: totalPendingAttach
|
|
1619
1777
|
});
|
|
1620
1778
|
}, CONFIG.STATUS_PRINT_INTERVAL);
|
|
1621
1779
|
|