cdp-tunnel 2.5.21 → 2.5.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/index.js +258 -92
- package/extension-new/background.js +19 -1
- 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 +288 -161
- 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,38 @@ 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
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pluginNamespaces = new Map();
|
|
174
|
+
|
|
175
|
+
function getNamespace(pluginWs) {
|
|
176
|
+
if (!pluginNamespaces.has(pluginWs)) {
|
|
177
|
+
pluginNamespaces.set(pluginWs, new PluginNamespace());
|
|
178
|
+
}
|
|
179
|
+
return pluginNamespaces.get(pluginWs);
|
|
180
|
+
}
|
|
181
|
+
|
|
145
182
|
const connectionPairs = new Map();
|
|
146
183
|
const clientById = new Map();
|
|
147
|
-
const sessionToClientId = new Map();
|
|
148
|
-
const pendingAttachRequests = new Map();
|
|
149
184
|
const clientIdToPlugin = new Map();
|
|
150
185
|
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
186
|
let globalRequestIdCounter = 0;
|
|
157
187
|
|
|
158
188
|
const { version: PKG_VERSION } = require('../package.json');
|
|
159
189
|
|
|
160
|
-
let cachedTargets = [];
|
|
161
|
-
let lastTargetsUpdate = 0;
|
|
162
|
-
let cachedBrowserVersion = null;
|
|
163
|
-
|
|
164
190
|
console.log('='.repeat(60));
|
|
165
191
|
console.log(` WebSocket CDP Proxy Server v${PKG_VERSION}`);
|
|
166
192
|
console.log('='.repeat(60));
|
|
@@ -177,26 +203,24 @@ function getHost(req) {
|
|
|
177
203
|
return req.headers.host || `localhost:${PORT}`;
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function buildTargetWebSocketUrl(req, targetId) {
|
|
188
|
-
return `ws://${getHost(req)}/devtools/page/${targetId}`;
|
|
206
|
+
function invalidateTargetsCache(pluginWs) {
|
|
207
|
+
if (pluginWs) {
|
|
208
|
+
getNamespace(pluginWs).lastTargetsUpdate = 0;
|
|
209
|
+
} else {
|
|
210
|
+
pluginNamespaces.forEach(ns => { ns.lastTargetsUpdate = 0; });
|
|
211
|
+
}
|
|
189
212
|
}
|
|
190
213
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
async function requestVersionFromPlugin(pluginWs) {
|
|
215
|
+
if (!pluginWs) {
|
|
216
|
+
pluginWs = pluginConnections.values().next().value;
|
|
217
|
+
}
|
|
218
|
+
if (!pluginWs) return null;
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
if (cachedBrowserVersion) return cachedBrowserVersion;
|
|
220
|
+
const ns = getNamespace(pluginWs);
|
|
221
|
+
if (ns.cachedBrowserVersion) return ns.cachedBrowserVersion;
|
|
197
222
|
|
|
198
|
-
|
|
199
|
-
if (!plugin || plugin.readyState !== WebSocket.OPEN) {
|
|
223
|
+
if (pluginWs.readyState !== WebSocket.OPEN) {
|
|
200
224
|
return null;
|
|
201
225
|
}
|
|
202
226
|
|
|
@@ -209,35 +233,40 @@ async function requestVersionFromPlugin() {
|
|
|
209
233
|
const msg = JSON.parse(data.toString());
|
|
210
234
|
if (msg.id === requestId && msg.result) {
|
|
211
235
|
clearTimeout(timeout);
|
|
212
|
-
|
|
236
|
+
pluginWs.off('message', handler);
|
|
213
237
|
if (msg.result.product || msg.result.userAgent) {
|
|
214
|
-
cachedBrowserVersion = msg.result;
|
|
238
|
+
ns.cachedBrowserVersion = msg.result;
|
|
215
239
|
}
|
|
216
|
-
resolve(cachedBrowserVersion || msg.result);
|
|
240
|
+
resolve(ns.cachedBrowserVersion || msg.result);
|
|
217
241
|
}
|
|
218
242
|
} catch (e) {}
|
|
219
243
|
};
|
|
220
244
|
|
|
221
|
-
|
|
222
|
-
|
|
245
|
+
pluginWs.on('message', handler);
|
|
246
|
+
pluginWs.send(JSON.stringify({ id: requestId, method: 'Browser.getVersion' }));
|
|
223
247
|
});
|
|
224
248
|
}
|
|
225
249
|
|
|
226
|
-
async function requestTargetsFromPlugin() {
|
|
250
|
+
async function requestTargetsFromPlugin(pluginWs) {
|
|
251
|
+
if (!pluginWs) {
|
|
252
|
+
pluginWs = pluginConnections.values().next().value;
|
|
253
|
+
}
|
|
254
|
+
if (!pluginWs) return [];
|
|
255
|
+
|
|
256
|
+
const ns = getNamespace(pluginWs);
|
|
227
257
|
const now = Date.now();
|
|
228
|
-
if (now - lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && cachedTargets.length > 0) {
|
|
229
|
-
return cachedTargets;
|
|
258
|
+
if (now - ns.lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && ns.cachedTargets.length > 0) {
|
|
259
|
+
return ns.cachedTargets;
|
|
230
260
|
}
|
|
231
261
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return cachedTargets;
|
|
262
|
+
if (pluginWs.readyState !== WebSocket.OPEN) {
|
|
263
|
+
return ns.cachedTargets;
|
|
235
264
|
}
|
|
236
265
|
|
|
237
266
|
return new Promise((resolve) => {
|
|
238
267
|
const requestId = `targets_${Date.now()}`;
|
|
239
268
|
const timeout = setTimeout(() => {
|
|
240
|
-
resolve(cachedTargets);
|
|
269
|
+
resolve(ns.cachedTargets);
|
|
241
270
|
}, CONFIG.TARGETS_REQUEST_TIMEOUT);
|
|
242
271
|
|
|
243
272
|
const handler = (data) => {
|
|
@@ -245,36 +274,79 @@ async function requestTargetsFromPlugin() {
|
|
|
245
274
|
const msg = JSON.parse(data.toString());
|
|
246
275
|
if (msg.id === requestId && msg.result?.targetInfos) {
|
|
247
276
|
clearTimeout(timeout);
|
|
248
|
-
|
|
249
|
-
cachedTargets = msg.result.targetInfos;
|
|
250
|
-
lastTargetsUpdate = now;
|
|
251
|
-
resolve(cachedTargets);
|
|
277
|
+
pluginWs.off('message', handler);
|
|
278
|
+
ns.cachedTargets = msg.result.targetInfos;
|
|
279
|
+
ns.lastTargetsUpdate = now;
|
|
280
|
+
resolve(ns.cachedTargets);
|
|
252
281
|
}
|
|
253
282
|
} catch (e) {}
|
|
254
283
|
};
|
|
255
284
|
|
|
256
|
-
|
|
257
|
-
|
|
285
|
+
pluginWs.on('message', handler);
|
|
286
|
+
pluginWs.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
|
|
258
287
|
});
|
|
259
288
|
}
|
|
260
289
|
|
|
290
|
+
/**
|
|
291
|
+
* 生成指定 plugin 的 browser WS URL
|
|
292
|
+
*/
|
|
293
|
+
function buildBrowserWsUrl(pluginId) {
|
|
294
|
+
const host = CONFIG.EXTERNAL_HOST || `localhost:${PORT}`;
|
|
295
|
+
return `ws://${host}/devtools/browser/${pluginId}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
261
298
|
/**
|
|
262
299
|
* 处理 HTTP 请求
|
|
263
300
|
*/
|
|
301
|
+
function resolvePluginFromUrl(url) {
|
|
302
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
303
|
+
if (parts.length >= 3) {
|
|
304
|
+
const pluginId = parts[2];
|
|
305
|
+
for (const pluginWs of pluginConnections) {
|
|
306
|
+
if (pluginWs.pluginId === pluginId) return pluginWs;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return pluginConnections.values().next().value || null;
|
|
310
|
+
}
|
|
311
|
+
|
|
264
312
|
async function handleHttpRequest(req, res) {
|
|
265
313
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
266
314
|
|
|
267
|
-
if (url.pathname === '/json/
|
|
268
|
-
const
|
|
315
|
+
if (url.pathname === '/json/browsers' || url.pathname === '/json/browsers/') {
|
|
316
|
+
const browsers = [];
|
|
317
|
+
for (const pluginWs of pluginConnections) {
|
|
318
|
+
if (pluginWs.readyState !== WebSocket.OPEN) continue;
|
|
319
|
+
const ns = getNamespace(pluginWs);
|
|
320
|
+
browsers.push({
|
|
321
|
+
pluginId: pluginWs.pluginId,
|
|
322
|
+
pluginName: pluginWs.pluginName || 'My Browser',
|
|
323
|
+
userId: pluginWs.userId || null,
|
|
324
|
+
browserName: ns.cachedBrowserVersion?.Browser || 'Unknown',
|
|
325
|
+
targets: ns.cachedTargets.length,
|
|
326
|
+
connected: true,
|
|
327
|
+
connectedAt: pluginWs.connectedAt,
|
|
328
|
+
webSocketDebuggerUrl: buildBrowserWsUrl(pluginWs.pluginId)
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
332
|
+
res.end(JSON.stringify(browsers));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (url.pathname === '/json/version' || url.pathname === '/json/version/' ||
|
|
337
|
+
url.pathname.match(/^\/json\/version\/[^/]+$/)) {
|
|
338
|
+
const pluginWs = resolvePluginFromUrl(url);
|
|
339
|
+
const ver = await requestVersionFromPlugin(pluginWs);
|
|
269
340
|
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
341
|
const product = ver?.product || 'Chrome/131.0.6778.86';
|
|
342
|
+
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
271
343
|
const payload = {
|
|
272
344
|
Browser: `${product} (cdp-tunnel/${PKG_VERSION})`,
|
|
273
345
|
'Protocol-Version': ver?.protocolVersion || '1.3',
|
|
274
346
|
'User-Agent': userAgent,
|
|
275
347
|
'V8-Version': ver?.jsVersion || '',
|
|
276
348
|
'WebKit-Version': '537.36',
|
|
277
|
-
webSocketDebuggerUrl:
|
|
349
|
+
webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/browser/${browserId}`
|
|
278
350
|
};
|
|
279
351
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
280
352
|
res.end(JSON.stringify(payload));
|
|
@@ -282,16 +354,19 @@ async function handleHttpRequest(req, res) {
|
|
|
282
354
|
}
|
|
283
355
|
|
|
284
356
|
if (url.pathname === '/json' || url.pathname === '/json/' ||
|
|
285
|
-
url.pathname === '/json/list' || url.pathname === '/json/list/'
|
|
286
|
-
|
|
357
|
+
url.pathname === '/json/list' || url.pathname === '/json/list/' ||
|
|
358
|
+
url.pathname.match(/^\/json\/list\/[^/]+$/)) {
|
|
359
|
+
const pluginWs = resolvePluginFromUrl(url);
|
|
360
|
+
const targets = await requestTargetsFromPlugin(pluginWs);
|
|
361
|
+
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
287
362
|
const targetList = targets
|
|
288
363
|
.filter(t => {
|
|
289
364
|
if (t.type !== 'page') return false;
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
365
|
+
const tUrl = t.url || '';
|
|
366
|
+
if (tUrl.startsWith('chrome://') ||
|
|
367
|
+
tUrl.startsWith('chrome-extension://') ||
|
|
368
|
+
tUrl.startsWith('devtools://') ||
|
|
369
|
+
tUrl.startsWith('edge://')) {
|
|
295
370
|
return false;
|
|
296
371
|
}
|
|
297
372
|
return true;
|
|
@@ -305,7 +380,7 @@ async function handleHttpRequest(req, res) {
|
|
|
305
380
|
title: t.title || '',
|
|
306
381
|
type: t.type,
|
|
307
382
|
url: t.url || '',
|
|
308
|
-
webSocketDebuggerUrl:
|
|
383
|
+
webSocketDebuggerUrl: `ws://${getHost(req)}/devtools/page/${t.targetId}`
|
|
309
384
|
}));
|
|
310
385
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
311
386
|
res.end(JSON.stringify(targetList));
|
|
@@ -322,8 +397,10 @@ async function handleHttpRequest(req, res) {
|
|
|
322
397
|
server.on('upgrade', (req, socket, head) => {
|
|
323
398
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
324
399
|
const path = url.pathname;
|
|
400
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
325
401
|
const isPlugin = path === '/plugin';
|
|
326
402
|
const isClient = path === '/client' ||
|
|
403
|
+
path.startsWith('/client/') ||
|
|
327
404
|
path.startsWith('/client-') ||
|
|
328
405
|
path.startsWith('/devtools/browser/') ||
|
|
329
406
|
path.startsWith('/devtools/page/');
|
|
@@ -341,6 +418,7 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
341
418
|
wss.on('connection', (ws, req) => {
|
|
342
419
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
343
420
|
const path = url.pathname;
|
|
421
|
+
const pathParts = path.split('/').filter(Boolean);
|
|
344
422
|
|
|
345
423
|
const clientInfo = {
|
|
346
424
|
ip: req.socket.remoteAddress,
|
|
@@ -348,10 +426,16 @@ wss.on('connection', (ws, req) => {
|
|
|
348
426
|
};
|
|
349
427
|
|
|
350
428
|
if (path === '/plugin') {
|
|
351
|
-
handlePluginConnection(ws, clientInfo);
|
|
352
|
-
} else if (path === '/client' || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
|
|
429
|
+
handlePluginConnection(ws, clientInfo, req);
|
|
430
|
+
} else if (path === '/client' || path.startsWith('/client/') || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
|
|
353
431
|
const customClientId = path.startsWith('/client-') ? path.replace('/client-', '') : null;
|
|
354
|
-
|
|
432
|
+
let targetPluginId = null;
|
|
433
|
+
if (pathParts[0] === 'client' && pathParts[1]) {
|
|
434
|
+
targetPluginId = pathParts[1];
|
|
435
|
+
} else if (pathParts[0] === 'devtools' && pathParts[1] === 'browser' && pathParts[2]) {
|
|
436
|
+
targetPluginId = pathParts[2];
|
|
437
|
+
}
|
|
438
|
+
handleClientConnection(ws, clientInfo, customClientId, targetPluginId);
|
|
355
439
|
} else if (path.startsWith('/devtools/page/')) {
|
|
356
440
|
const targetId = path.replace('/devtools/page/', '');
|
|
357
441
|
handlePageConnection(ws, clientInfo, targetId);
|
|
@@ -362,21 +446,26 @@ wss.on('connection', (ws, req) => {
|
|
|
362
446
|
});
|
|
363
447
|
|
|
364
448
|
function cleanupClient(ws, id, reason) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
449
|
+
const pluginWs = ws.pairedPlugin || clientIdToPlugin.get(id);
|
|
450
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
451
|
+
|
|
452
|
+
if (ns) {
|
|
453
|
+
const sessionsToClean = [];
|
|
454
|
+
for (const [sessionId, clientId] of ns.sessionToClientId.entries()) {
|
|
455
|
+
if (clientId === id) {
|
|
456
|
+
sessionsToClean.push(sessionId);
|
|
457
|
+
ns.sessionToClientId.delete(sessionId);
|
|
458
|
+
}
|
|
370
459
|
}
|
|
371
460
|
}
|
|
372
461
|
|
|
373
462
|
clientConnections.delete(ws);
|
|
374
463
|
clientById.delete(id);
|
|
464
|
+
clientIdToPlugin.delete(id);
|
|
375
465
|
|
|
376
466
|
logConnectionEvent('CLIENT_DISCONNECTED', {
|
|
377
467
|
id,
|
|
378
468
|
reason,
|
|
379
|
-
sessionsCleaned: sessionsToClean.length,
|
|
380
469
|
totalPlugins: pluginConnections.size,
|
|
381
470
|
totalClients: clientConnections.size
|
|
382
471
|
});
|
|
@@ -384,7 +473,6 @@ function cleanupClient(ws, id, reason) {
|
|
|
384
473
|
logDisconnect('CLIENT_CLEANUP', {
|
|
385
474
|
clientId: id,
|
|
386
475
|
reason,
|
|
387
|
-
sessionsLost: sessionsToClean.length,
|
|
388
476
|
cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
|
|
389
477
|
uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
|
|
390
478
|
remainingClients: clientConnections.size,
|
|
@@ -401,20 +489,22 @@ function cleanupClient(ws, id, reason) {
|
|
|
401
489
|
safeSend(ws.pairedPlugin, JSON.stringify({
|
|
402
490
|
type: 'client-disconnected',
|
|
403
491
|
clientId: id,
|
|
404
|
-
sessions:
|
|
492
|
+
sessions: []
|
|
405
493
|
}), 'plugin');
|
|
406
494
|
}
|
|
407
495
|
|
|
408
496
|
broadcastClientList();
|
|
409
497
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
clientIdToBrowserContext.
|
|
498
|
+
if (ns) {
|
|
499
|
+
for (const [tId, cId] of ns.targetIdToClientId.entries()) {
|
|
500
|
+
if (cId === id) ns.targetIdToClientId.delete(tId);
|
|
501
|
+
}
|
|
502
|
+
for (const [bcId, cId] of ns.browserContextToClientId.entries()) {
|
|
503
|
+
if (cId === id) ns.browserContextToClientId.delete(bcId);
|
|
504
|
+
}
|
|
505
|
+
if (ns.clientIdToBrowserContext.has(id)) {
|
|
506
|
+
ns.clientIdToBrowserContext.delete(id);
|
|
507
|
+
}
|
|
418
508
|
}
|
|
419
509
|
for (const [gId, mapping] of globalRequestIdMap.entries()) {
|
|
420
510
|
if (mapping.clientId === id) globalRequestIdMap.delete(gId);
|
|
@@ -446,7 +536,9 @@ function sendPendingRequestErrors(pluginWs) {
|
|
|
446
536
|
}
|
|
447
537
|
|
|
448
538
|
function cleanupPlugin(ws, id, reason) {
|
|
539
|
+
const ns = getNamespace(ws);
|
|
449
540
|
pluginConnections.delete(ws);
|
|
541
|
+
pluginNamespaces.delete(ws);
|
|
450
542
|
|
|
451
543
|
if (pluginConnections.size === 0) {
|
|
452
544
|
updateExtensionState(false);
|
|
@@ -478,8 +570,8 @@ function cleanupPlugin(ws, id, reason) {
|
|
|
478
570
|
remainingPlugins: pluginConnections.size,
|
|
479
571
|
affectedClients,
|
|
480
572
|
uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
|
|
481
|
-
activeSessions: sessionToClientId.size,
|
|
482
|
-
pendingRequests: pendingAttachRequests.size
|
|
573
|
+
activeSessions: ns.sessionToClientId.size,
|
|
574
|
+
pendingRequests: ns.pendingAttachRequests.size
|
|
483
575
|
});
|
|
484
576
|
|
|
485
577
|
if (ws.pairedClientId) {
|
|
@@ -490,35 +582,46 @@ function cleanupPlugin(ws, id, reason) {
|
|
|
490
582
|
/**
|
|
491
583
|
* 处理 Chrome 扩展连接
|
|
492
584
|
*/
|
|
493
|
-
function handlePluginConnection(ws, clientInfo) {
|
|
585
|
+
function handlePluginConnection(ws, clientInfo, request) {
|
|
586
|
+
const req = request;
|
|
494
587
|
const id = generateId('plugin');
|
|
495
588
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
589
|
+
ws.pluginId = 'browser_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
const url = new URL(req.url, `http://localhost`);
|
|
593
|
+
const apiKey = url.searchParams.get('key');
|
|
594
|
+
const desiredPluginId = url.searchParams.get('pluginId');
|
|
595
|
+
|
|
596
|
+
if (desiredPluginId && /^browser_[a-zA-Z0-9_]+$/.test(desiredPluginId)) {
|
|
597
|
+
let conflict = false;
|
|
598
|
+
for (const existing of pluginConnections) {
|
|
599
|
+
if (existing.pluginId === desiredPluginId && existing !== ws) {
|
|
600
|
+
conflict = true;
|
|
601
|
+
break;
|
|
503
602
|
}
|
|
504
|
-
toRemove.push(oldWs);
|
|
505
603
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
pluginConnections.delete(oldWs);
|
|
509
|
-
if (shouldLog('info')) {
|
|
510
|
-
console.log(`[PLUGIN] Removed old connection: ${oldWs.id}`);
|
|
604
|
+
if (!conflict) {
|
|
605
|
+
ws.pluginId = desiredPluginId;
|
|
511
606
|
}
|
|
512
|
-
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (HAS_SAAS && apiKey) {
|
|
610
|
+
const keyInfo = validateApiKey(apiKey);
|
|
611
|
+
if (keyInfo) {
|
|
612
|
+
ws.userId = keyInfo.userId;
|
|
613
|
+
ws.apiKeyId = keyInfo.keyId;
|
|
614
|
+
logConnectionEvent('PLUGIN_AUTHED', `userId=${keyInfo.userId} keyName=${keyInfo.keyName}`);
|
|
615
|
+
} else {
|
|
616
|
+
logConnectionEvent('PLUGIN_AUTH_FAIL', 'Invalid API key');
|
|
617
|
+
ws.close(4001, 'Invalid API key');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
} catch (e) {
|
|
622
|
+
logConnectionEvent('PLUGIN_AUTH_ERR', e.message);
|
|
513
623
|
}
|
|
514
624
|
|
|
515
|
-
sessionToClientId.clear();
|
|
516
|
-
pendingAttachRequests.clear();
|
|
517
|
-
connectionPairs.clear();
|
|
518
|
-
clientConnections.forEach(clientWs => {
|
|
519
|
-
clientWs.pairedPlugin = null;
|
|
520
|
-
});
|
|
521
|
-
|
|
522
625
|
pluginConnections.add(ws);
|
|
523
626
|
|
|
524
627
|
const pluginType = 'plugin';
|
|
@@ -626,8 +729,8 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
626
729
|
if (!match) {
|
|
627
730
|
console.log(` ↳ Run "cdp-tunnel update" or reload the extension to sync versions`);
|
|
628
731
|
}
|
|
629
|
-
cachedBrowserVersion = null;
|
|
630
|
-
requestVersionFromPlugin();
|
|
732
|
+
getNamespace(ws).cachedBrowserVersion = null;
|
|
733
|
+
requestVersionFromPlugin(ws);
|
|
631
734
|
return;
|
|
632
735
|
}
|
|
633
736
|
|
|
@@ -645,9 +748,9 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
645
748
|
if (parsed.method.startsWith('CDPTunnel.')) {
|
|
646
749
|
console.log(`[EXT DEBUG] ${parsed.method}: ${JSON.stringify(parsed.params)}`);
|
|
647
750
|
}
|
|
648
|
-
// 对于 Target 事件,始终广播给所有客户端
|
|
649
751
|
const targetEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
|
|
650
752
|
if (targetEvents.includes(parsed.method)) {
|
|
753
|
+
const ns = getNamespace(ws);
|
|
651
754
|
const cdpMsg = {
|
|
652
755
|
method: parsed.method,
|
|
653
756
|
params: parsed.params,
|
|
@@ -662,19 +765,19 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
662
765
|
const targetId = parsed.params?.targetInfo?.targetId;
|
|
663
766
|
const openerId = parsed.params?.targetInfo?.openerId;
|
|
664
767
|
if (openerId && targetId) {
|
|
665
|
-
const openerClientId = targetIdToClientId.get(openerId);
|
|
768
|
+
const openerClientId = ns.targetIdToClientId.get(openerId);
|
|
666
769
|
if (openerClientId) {
|
|
667
|
-
targetIdToClientId.set(targetId, openerClientId);
|
|
770
|
+
ns.targetIdToClientId.set(targetId, openerClientId);
|
|
668
771
|
console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8) || 'none'} openerId=${openerId?.substring(0,8) || 'none'} -> clientId=${openerClientId}`);
|
|
669
772
|
}
|
|
670
773
|
}
|
|
671
774
|
}
|
|
672
775
|
|
|
673
|
-
rewriteBrowserContextId(cdpMsg);
|
|
776
|
+
rewriteBrowserContextId(cdpMsg, ws);
|
|
674
777
|
const cdpData = JSON.stringify(cdpMsg);
|
|
675
778
|
|
|
676
779
|
const targetId = parsed.params?.targetInfo?.targetId;
|
|
677
|
-
const eventClientId = targetId ? targetIdToClientId.get(targetId) : null;
|
|
780
|
+
const eventClientId = targetId ? ns.targetIdToClientId.get(targetId) : null;
|
|
678
781
|
|
|
679
782
|
if (eventClientId) {
|
|
680
783
|
const clientWs = clientById.get(eventClientId);
|
|
@@ -683,7 +786,7 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
683
786
|
console.log(`[TARGET EVENT ROUTED] ${parsed.method} targetId=${targetId?.substring(0,8)} -> clientId=${eventClientId}`);
|
|
684
787
|
}
|
|
685
788
|
} else if (targetId && (parsed.method === 'Target.targetCreated' || parsed.method === 'Target.attachedToTarget')) {
|
|
686
|
-
const pendingMap = parsed.method === 'Target.targetCreated' ? pendingTargetCreatedEvents : pendingAttachedEvents;
|
|
789
|
+
const pendingMap = parsed.method === 'Target.targetCreated' ? ns.pendingTargetCreatedEvents : ns.pendingAttachedEvents;
|
|
687
790
|
pendingMap.set(targetId, { parsed: JSON.parse(JSON.stringify(parsed)), cdpData });
|
|
688
791
|
console.log(`[TARGET EVENT PENDING] ${parsed.method} targetId=${targetId?.substring(0,8)} (cached, waiting for createTarget response)`);
|
|
689
792
|
} else {
|
|
@@ -691,24 +794,23 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
691
794
|
}
|
|
692
795
|
}
|
|
693
796
|
|
|
694
|
-
// 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
|
|
695
797
|
if (parsed.method === 'Target.attachedToTarget') {
|
|
798
|
+
const ns = getNamespace(ws);
|
|
696
799
|
const targetId = parsed.params?.targetInfo?.targetId;
|
|
697
800
|
const sessionId = parsed.params?.sessionId;
|
|
698
801
|
|
|
699
802
|
if (targetId && sessionId) {
|
|
700
803
|
const clientId = ws.pairedClientId;
|
|
701
804
|
if (clientId) {
|
|
702
|
-
sessionToClientId.set(sessionId, clientId);
|
|
805
|
+
ns.sessionToClientId.set(sessionId, clientId);
|
|
703
806
|
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> clientId=${clientId?.substring(0,8) || 'none'}`);
|
|
704
807
|
} else {
|
|
705
|
-
sessionToClientId.set(sessionId, targetId);
|
|
808
|
+
ns.sessionToClientId.set(sessionId, targetId);
|
|
706
809
|
console.log(`[SESSION MAPPED] sessionId=${sessionId?.substring(0,8) || 'none'} -> targetId=${targetId?.substring(0,8) || 'none'} (no pairedClientId)`);
|
|
707
810
|
}
|
|
708
811
|
}
|
|
709
812
|
}
|
|
710
813
|
|
|
711
|
-
// 如果不是 Target 事件,按照原来的逻辑发送
|
|
712
814
|
if (!targetEvents.includes(parsed.method)) {
|
|
713
815
|
const cdpMsg = {
|
|
714
816
|
method: parsed.method,
|
|
@@ -744,45 +846,42 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
744
846
|
if (parsed && parsed.id !== undefined) {
|
|
745
847
|
const globalId = parsed.id;
|
|
746
848
|
const mapping = globalRequestIdMap.get(globalId);
|
|
849
|
+
const ns = getNamespace(ws);
|
|
747
850
|
console.log(`[RESPONSE DEBUG] globalId=${globalId} hasMapping=${!!mapping} sessionId=${parsed.sessionId?.substring(0,8) || 'none'} method=${parsed.method || 'response'}`);
|
|
748
851
|
if (mapping) {
|
|
749
852
|
const clientWs = clientById.get(mapping.clientId);
|
|
750
853
|
if (clientWs && clientWs.readyState === WebSocket.OPEN) {
|
|
751
|
-
// 如果是 Target.createBrowserContext 响应,记录 browserContextId -> clientId 映射
|
|
752
854
|
if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
|
|
753
855
|
const browserContextId = parsed.result.browserContextId;
|
|
754
|
-
browserContextToClientId.set(browserContextId, mapping.clientId);
|
|
755
|
-
clientIdToBrowserContext.set(mapping.clientId, browserContextId);
|
|
856
|
+
ns.browserContextToClientId.set(browserContextId, mapping.clientId);
|
|
857
|
+
ns.clientIdToBrowserContext.set(mapping.clientId, browserContextId);
|
|
756
858
|
console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
|
|
757
859
|
}
|
|
758
860
|
|
|
759
|
-
// 如果是 Target.attachToTarget 响应,建立 sessionId -> clientId 映射
|
|
760
861
|
if (parsed.result?.sessionId && mapping.method === 'Target.attachToTarget') {
|
|
761
|
-
sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
|
|
862
|
+
ns.sessionToClientId.set(parsed.result.sessionId, mapping.clientId);
|
|
762
863
|
console.log(`[SESSION MAPPED from attach response] sessionId=${parsed.result.sessionId?.substring(0,8)} -> clientId=${mapping.clientId?.substring(0,8)}`);
|
|
763
864
|
}
|
|
764
865
|
|
|
765
|
-
// 如果是 Target.createTarget 响应,先发送缓存的 Target.attachedToTarget 事件
|
|
766
|
-
// 然后再发送响应
|
|
767
866
|
if (mapping.isCreateTarget && parsed.result?.targetId) {
|
|
768
867
|
const targetId = parsed.result.targetId;
|
|
769
|
-
targetIdToClientId.set(targetId, mapping.clientId);
|
|
770
|
-
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${targetIdToClientId.size}`);
|
|
868
|
+
ns.targetIdToClientId.set(targetId, mapping.clientId);
|
|
869
|
+
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId} mapSize=${ns.targetIdToClientId.size}`);
|
|
771
870
|
|
|
772
|
-
const cachedCreated = pendingTargetCreatedEvents.get(targetId);
|
|
871
|
+
const cachedCreated = ns.pendingTargetCreatedEvents.get(targetId);
|
|
773
872
|
if (cachedCreated) {
|
|
774
873
|
clientWs.send(cachedCreated.cdpData);
|
|
775
874
|
console.log(`[TARGET CREATED EVENT] Sent cached Target.targetCreated to client: ${mapping.clientId}`);
|
|
776
|
-
pendingTargetCreatedEvents.delete(targetId);
|
|
875
|
+
ns.pendingTargetCreatedEvents.delete(targetId);
|
|
777
876
|
}
|
|
778
877
|
|
|
779
|
-
const cachedEvent = pendingAttachedEvents.get(targetId);
|
|
878
|
+
const cachedEvent = ns.pendingAttachedEvents.get(targetId);
|
|
780
879
|
if (cachedEvent) {
|
|
781
880
|
if (cachedEvent.parsed.sessionId) {
|
|
782
|
-
sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
|
|
881
|
+
ns.sessionToClientId.set(cachedEvent.parsed.sessionId, mapping.clientId);
|
|
783
882
|
}
|
|
784
883
|
console.log(`[SESSION MAPPED from cached] sessionId=${cachedEvent.parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${mapping.clientId} (targetId=${targetId})`);
|
|
785
|
-
pendingAttachedEvents.delete(targetId);
|
|
884
|
+
ns.pendingAttachedEvents.delete(targetId);
|
|
786
885
|
|
|
787
886
|
const cdpMsg = {
|
|
788
887
|
method: cachedEvent.parsed.method,
|
|
@@ -796,37 +895,33 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
796
895
|
const newTargetInfo = cachedCreated?.parsed?.params?.targetInfo
|
|
797
896
|
|| cachedEvent?.parsed?.params?.targetInfo;
|
|
798
897
|
if (newTargetInfo) {
|
|
799
|
-
const exists = cachedTargets.some(t => t.targetId === targetId);
|
|
898
|
+
const exists = ns.cachedTargets.some(t => t.targetId === targetId);
|
|
800
899
|
if (!exists) {
|
|
801
|
-
cachedTargets.push(newTargetInfo);
|
|
900
|
+
ns.cachedTargets.push(newTargetInfo);
|
|
802
901
|
}
|
|
803
902
|
} else {
|
|
804
|
-
invalidateTargetsCache();
|
|
903
|
+
invalidateTargetsCache(ws);
|
|
805
904
|
}
|
|
806
905
|
}
|
|
807
|
-
// 过滤 Target.getTargets 响应,只返回该客户端拥有的 target
|
|
808
906
|
if (mapping.isGetTargets && parsed.result && parsed.result.targetInfos) {
|
|
809
907
|
const clientId = mapping.clientId;
|
|
810
908
|
parsed.result.targetInfos = parsed.result.targetInfos.filter(t => {
|
|
811
909
|
if (t.type !== 'page') return true;
|
|
812
|
-
const ownerClient = targetIdToClientId.get(t.targetId);
|
|
910
|
+
const ownerClient = ns.targetIdToClientId.get(t.targetId);
|
|
813
911
|
return ownerClient === clientId;
|
|
814
912
|
});
|
|
815
913
|
console.log(`[GET TARGETS FILTERED] client=${clientId} returned ${parsed.result.targetInfos.filter(t => t.type === 'page').length} page targets`);
|
|
816
914
|
}
|
|
817
|
-
// 清理 Target.closeTarget 成功后的映射
|
|
818
915
|
if (parsed.result && parsed.result.success !== undefined && mapping.method === 'Target.closeTarget') {
|
|
819
916
|
if (mapping.closeTargetId) {
|
|
820
|
-
targetIdToClientId.delete(mapping.closeTargetId);
|
|
917
|
+
ns.targetIdToClientId.delete(mapping.closeTargetId);
|
|
821
918
|
console.log(`[CLOSE TARGET CLEANUP] removed targetId=${mapping.closeTargetId?.substring(0,8)} from mapping`);
|
|
822
919
|
}
|
|
823
|
-
invalidateTargetsCache();
|
|
920
|
+
invalidateTargetsCache(ws);
|
|
824
921
|
}
|
|
825
922
|
|
|
826
|
-
// 然后发送响应给客户端
|
|
827
923
|
const originalId = mapping.originalId;
|
|
828
924
|
parsed.id = originalId;
|
|
829
|
-
// 如果请求有 sessionId,但响应没有,添加 sessionId
|
|
830
925
|
if (mapping.sessionId && !parsed.sessionId) {
|
|
831
926
|
parsed.sessionId = mapping.sessionId;
|
|
832
927
|
}
|
|
@@ -844,7 +939,8 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
844
939
|
|
|
845
940
|
// 2. sessionId 路由:消息属于特定 session(事件,没有 id)
|
|
846
941
|
if (parsed && parsed.sessionId) {
|
|
847
|
-
const
|
|
942
|
+
const ns = getNamespace(ws);
|
|
943
|
+
const targetClientId = ns.sessionToClientId.get(parsed.sessionId);
|
|
848
944
|
console.log(`[SESSION ROUTE] sessionId=${parsed.sessionId?.substring(0,8) || 'none'} -> clientId=${targetClientId || 'not found'}`);
|
|
849
945
|
if (targetClientId) {
|
|
850
946
|
const clientWs = clientById.get(targetClientId);
|
|
@@ -888,7 +984,6 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
888
984
|
cleanupPlugin(ws, id, `close:${code}`);
|
|
889
985
|
});
|
|
890
986
|
|
|
891
|
-
// 错误处理
|
|
892
987
|
ws.on('error', (error) => {
|
|
893
988
|
console.error(`[PLUGIN ERROR] ${id}:`, error.message);
|
|
894
989
|
|
|
@@ -900,6 +995,7 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
900
995
|
});
|
|
901
996
|
|
|
902
997
|
pluginConnections.delete(ws);
|
|
998
|
+
pluginNamespaces.delete(ws);
|
|
903
999
|
|
|
904
1000
|
clientConnections.forEach(clientWs => {
|
|
905
1001
|
if (clientWs.pairedPlugin === ws) {
|
|
@@ -922,6 +1018,7 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
922
1018
|
type: 'connected',
|
|
923
1019
|
role: 'plugin',
|
|
924
1020
|
id: id,
|
|
1021
|
+
pluginId: ws.pluginId,
|
|
925
1022
|
fresh: (Date.now() - SERVER_START_TIME) < 5000,
|
|
926
1023
|
timestamp: Date.now()
|
|
927
1024
|
}));
|
|
@@ -930,11 +1027,11 @@ function handlePluginConnection(ws, clientInfo) {
|
|
|
930
1027
|
/**
|
|
931
1028
|
* 处理 CDP 客户端连接 (Playwright/Puppeteer)
|
|
932
1029
|
*/
|
|
933
|
-
function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
1030
|
+
function handleClientConnection(ws, clientInfo, customClientId = null, targetPluginId = null) {
|
|
934
1031
|
clientConnections.add(ws);
|
|
935
1032
|
const id = customClientId || generateId('client');
|
|
936
1033
|
if (shouldLog('info')) {
|
|
937
|
-
console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}`);
|
|
1034
|
+
console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}${targetPluginId ? ` targetPlugin=${targetPluginId}` : ''}`);
|
|
938
1035
|
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
939
1036
|
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
940
1037
|
}
|
|
@@ -947,7 +1044,6 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
947
1044
|
totalClients: clientConnections.size
|
|
948
1045
|
});
|
|
949
1046
|
|
|
950
|
-
// 检查是否有可用的 plugin 连接
|
|
951
1047
|
if (pluginConnections.size === 0) {
|
|
952
1048
|
if (shouldLog('warn')) {
|
|
953
1049
|
console.log(` - WARNING: No plugin connections available!`);
|
|
@@ -965,27 +1061,36 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
965
1061
|
}
|
|
966
1062
|
}
|
|
967
1063
|
} else {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1064
|
+
let pluginWs;
|
|
1065
|
+
if (targetPluginId) {
|
|
1066
|
+
pluginWs = [...pluginConnections].find(p => p.pluginId === targetPluginId);
|
|
1067
|
+
if (!pluginWs) {
|
|
1068
|
+
if (shouldLog('warn')) {
|
|
1069
|
+
console.log(` - WARNING: Plugin ${targetPluginId} not found!`);
|
|
1070
|
+
}
|
|
1071
|
+
ws.close(4004, `Plugin ${targetPluginId} not found`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
} else {
|
|
1075
|
+
pluginWs = pluginConnections.values().next().value;
|
|
1076
|
+
}
|
|
971
1077
|
if (pluginWs) {
|
|
972
1078
|
connectionPairs.set(id, pluginWs);
|
|
973
1079
|
ws.pairedPlugin = pluginWs;
|
|
1080
|
+
ws.targetPluginId = pluginWs.pluginId;
|
|
974
1081
|
clientIdToPlugin.set(id, pluginWs);
|
|
975
1082
|
|
|
976
1083
|
if (shouldLog('info')) {
|
|
977
|
-
console.log(` - Paired with plugin: ${pluginWs.id} (
|
|
1084
|
+
console.log(` - Paired with plugin: ${pluginWs.id} (pluginId=${pluginWs.pluginId})`);
|
|
978
1085
|
}
|
|
979
1086
|
|
|
980
1087
|
logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
|
|
981
1088
|
|
|
982
|
-
// 通知 Plugin 新客户端已连接
|
|
983
1089
|
pluginWs.send(JSON.stringify({
|
|
984
1090
|
type: 'client-connected',
|
|
985
1091
|
clientId: id
|
|
986
1092
|
}));
|
|
987
1093
|
|
|
988
|
-
// 发送当前所有客户端列表
|
|
989
1094
|
broadcastClientList();
|
|
990
1095
|
}
|
|
991
1096
|
}
|
|
@@ -1063,8 +1168,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
1063
1168
|
}
|
|
1064
1169
|
if (parsed && parsed.id !== undefined) {
|
|
1065
1170
|
if (parsed.method === 'Target.closeTarget') {
|
|
1171
|
+
const pluginWs = ws.pairedPlugin;
|
|
1172
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
1066
1173
|
const targetId = parsed.params?.targetId;
|
|
1067
|
-
const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
|
|
1174
|
+
const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
|
|
1068
1175
|
if (ownerClient && ownerClient !== id) {
|
|
1069
1176
|
console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
|
|
1070
1177
|
const errMsg = JSON.stringify({
|
|
@@ -1080,8 +1187,10 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
|
1080
1187
|
currentMapping.closeTargetId = targetId;
|
|
1081
1188
|
}
|
|
1082
1189
|
} else if (parsed.method === 'Target.attachToTarget') {
|
|
1190
|
+
const pluginWs = ws.pairedPlugin;
|
|
1191
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
1083
1192
|
const targetId = parsed.params?.targetId;
|
|
1084
|
-
const ownerClient = targetId ? targetIdToClientId.get(targetId) : null;
|
|
1193
|
+
const ownerClient = (ns && targetId) ? ns.targetIdToClientId.get(targetId) : null;
|
|
1085
1194
|
if (ownerClient && ownerClient !== id) {
|
|
1086
1195
|
console.log(`[BLOCKED] ${parsed.method} targetId=${targetId?.substring(0,8)} owner=${ownerClient?.substring(0,8)} requester=${id?.substring(0,8)} — not owner`);
|
|
1087
1196
|
const errMsg = JSON.stringify({
|
|
@@ -1173,7 +1282,17 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1173
1282
|
ws.lastActivityTime = Date.now();
|
|
1174
1283
|
clientById.set(id, ws);
|
|
1175
1284
|
|
|
1176
|
-
|
|
1285
|
+
let plugin = null;
|
|
1286
|
+
for (const p of pluginConnections) {
|
|
1287
|
+
const ns = getNamespace(p);
|
|
1288
|
+
if (ns.targetIdToClientId.has(targetId)) {
|
|
1289
|
+
plugin = p;
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (!plugin) {
|
|
1294
|
+
plugin = pluginConnections.values().next().value;
|
|
1295
|
+
}
|
|
1177
1296
|
if (plugin && plugin.readyState === WebSocket.OPEN) {
|
|
1178
1297
|
ws.pairedPlugin = plugin;
|
|
1179
1298
|
if (shouldLog('info')) {
|
|
@@ -1209,7 +1328,7 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1209
1328
|
// 对于全局 Target 事件,需要广播给所有客户端
|
|
1210
1329
|
const broadcastEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
|
|
1211
1330
|
if (broadcastEvents.includes(msg.method)) {
|
|
1212
|
-
rewriteBrowserContextId(cdpMsg);
|
|
1331
|
+
rewriteBrowserContextId(cdpMsg, ws.pairedPlugin);
|
|
1213
1332
|
console.log(`[PLUGIN -> ALL CLIENTS] Broadcasting ${msg.method}`);
|
|
1214
1333
|
broadcastToClients(JSON.stringify(cdpMsg), null);
|
|
1215
1334
|
} else {
|
|
@@ -1313,23 +1432,24 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1313
1432
|
* 插件总是报告 'default',但 Playwright 期望自己创建的 context ID
|
|
1314
1433
|
* 通过 openerId 找到对应的 clientId,再找到该 client 的 browserContextId
|
|
1315
1434
|
*/
|
|
1316
|
-
function rewriteBrowserContextId(cdpMsg) {
|
|
1435
|
+
function rewriteBrowserContextId(cdpMsg, pluginWs) {
|
|
1317
1436
|
const targetInfo = cdpMsg.params?.targetInfo;
|
|
1318
1437
|
if (!targetInfo || targetInfo.browserContextId !== 'default') {
|
|
1319
1438
|
return cdpMsg;
|
|
1320
1439
|
}
|
|
1321
1440
|
|
|
1441
|
+
const ns = pluginWs ? getNamespace(pluginWs) : null;
|
|
1322
1442
|
let clientId = null;
|
|
1323
1443
|
|
|
1324
|
-
if (targetInfo.openerId) {
|
|
1325
|
-
clientId = targetIdToClientId.get(targetInfo.openerId);
|
|
1444
|
+
if (targetInfo.openerId && ns) {
|
|
1445
|
+
clientId = ns.targetIdToClientId.get(targetInfo.openerId);
|
|
1326
1446
|
}
|
|
1327
|
-
if (!clientId && targetInfo.targetId) {
|
|
1328
|
-
clientId = targetIdToClientId.get(targetInfo.targetId);
|
|
1447
|
+
if (!clientId && targetInfo.targetId && ns) {
|
|
1448
|
+
clientId = ns.targetIdToClientId.get(targetInfo.targetId);
|
|
1329
1449
|
}
|
|
1330
1450
|
|
|
1331
|
-
if (clientId) {
|
|
1332
|
-
const contextId = clientIdToBrowserContext.get(clientId);
|
|
1451
|
+
if (clientId && ns) {
|
|
1452
|
+
const contextId = ns.clientIdToBrowserContext.get(clientId);
|
|
1333
1453
|
if (contextId) {
|
|
1334
1454
|
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
1455
|
targetInfo.browserContextId = contextId;
|
|
@@ -1605,6 +1725,13 @@ setInterval(() => {
|
|
|
1605
1725
|
};
|
|
1606
1726
|
});
|
|
1607
1727
|
|
|
1728
|
+
let totalSessions = 0;
|
|
1729
|
+
let totalPendingAttach = 0;
|
|
1730
|
+
pluginNamespaces.forEach(ns => {
|
|
1731
|
+
totalSessions += ns.sessionToClientId.size;
|
|
1732
|
+
totalPendingAttach += ns.pendingAttachRequests.size;
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1608
1735
|
logStatus({
|
|
1609
1736
|
timestamp: now,
|
|
1610
1737
|
plugins: pluginConnections.size,
|
|
@@ -1614,8 +1741,8 @@ setInterval(() => {
|
|
|
1614
1741
|
pairs: connectionPairs.size,
|
|
1615
1742
|
pluginDetails: pluginList,
|
|
1616
1743
|
clientDetails: clientList,
|
|
1617
|
-
sessions:
|
|
1618
|
-
pendingAttach:
|
|
1744
|
+
sessions: totalSessions,
|
|
1745
|
+
pendingAttach: totalPendingAttach
|
|
1619
1746
|
});
|
|
1620
1747
|
}, CONFIG.STATUS_PRINT_INTERVAL);
|
|
1621
1748
|
|