cdp-tunnel 3.5.0 → 3.6.1
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/README.md +222 -133
- package/extension-new/background.js +6 -2
- package/extension-new/cdp/handler/forward.js +10 -1
- package/extension-new/cdp/handler/local.js +31 -6
- package/extension-new/cdp/handler/special.js +12 -12
- package/extension-new/cdp/index.js +1 -3
- package/extension-new/core/connection-state.js +9 -13
- package/extension-new/core/debugger.js +0 -5
- package/extension-new/core/state.js +0 -8
- package/extension-new/core/websocket.js +8 -18
- package/extension-new/manifest.json +1 -1
- package/extension-new/utils/config.js +1 -1
- package/extension-new/utils/helpers.js +9 -3
- package/package.json +1 -1
- package/server/admin-console.html +481 -0
- package/server/modules/port-pool.js +48 -6
- package/server/proxy-server.js +269 -99
package/server/proxy-server.js
CHANGED
|
@@ -24,8 +24,10 @@ let portPool = null;
|
|
|
24
24
|
const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs, logDisconnect } = require('./modules/logger');
|
|
25
25
|
|
|
26
26
|
let validateApiKey = null;
|
|
27
|
+
let saasAuth = null; // 完整 SaaS auth 模块(createApiKey/listApiKeys/revokeApiKey)
|
|
27
28
|
try {
|
|
28
|
-
|
|
29
|
+
saasAuth = require('./saas/auth');
|
|
30
|
+
validateApiKey = saasAuth.validateApiKey;
|
|
29
31
|
var HAS_SAAS = true;
|
|
30
32
|
} catch (e) {
|
|
31
33
|
var HAS_SAAS = false;
|
|
@@ -164,7 +166,6 @@ const clientConnections = new Set();
|
|
|
164
166
|
class PluginNamespace {
|
|
165
167
|
constructor() {
|
|
166
168
|
this.sessionToClientId = new Map();
|
|
167
|
-
this.pendingAttachRequests = new Map();
|
|
168
169
|
this.pendingAttachedEvents = new Map();
|
|
169
170
|
this.pendingTargetCreatedEvents = new Map();
|
|
170
171
|
this.targetIdToClientId = new Map();
|
|
@@ -318,6 +319,250 @@ function resolvePluginFromUrl(url) {
|
|
|
318
319
|
return pluginConnections.values().next().value || null;
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
// ===== 管理控制台 API =====
|
|
323
|
+
// 鉴权:ADMIN_TOKEN 环境变量(Bearer token 或 query ?token=)
|
|
324
|
+
// 未设 ADMIN_TOKEN 时只允许 localhost(开发友好)
|
|
325
|
+
function checkAdminAuth(req, url) {
|
|
326
|
+
const token = process.env.ADMIN_TOKEN;
|
|
327
|
+
if (!token) {
|
|
328
|
+
// 未设 token:只允许 localhost
|
|
329
|
+
const ip = req.socket.remoteAddress;
|
|
330
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
331
|
+
}
|
|
332
|
+
// 设了 token:校验 Bearer 或 query
|
|
333
|
+
const authHeader = req.headers.authorization || '';
|
|
334
|
+
const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
335
|
+
const queryToken = url.searchParams.get('token');
|
|
336
|
+
return bearer === token || queryToken === token;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function adminJson(res, code, data) {
|
|
340
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
341
|
+
res.end(JSON.stringify(data));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function readBody(req) {
|
|
345
|
+
return new Promise(resolve => {
|
|
346
|
+
let d = ''; req.on('data', c => d += c);
|
|
347
|
+
req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function handleAdminApi(req, res, url) {
|
|
352
|
+
const path = url.pathname.replace('/admin/api/', '');
|
|
353
|
+
const method = req.method;
|
|
354
|
+
|
|
355
|
+
if (!checkAdminAuth(req, url)) {
|
|
356
|
+
return adminJson(res, 401, { error: 'Unauthorized. Set ADMIN_TOKEN env or access from localhost.' });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
// 浏览器列表
|
|
361
|
+
if (path === 'browsers' && method === 'GET') {
|
|
362
|
+
const browsers = [];
|
|
363
|
+
for (const pluginWs of pluginConnections) {
|
|
364
|
+
if (pluginWs.readyState !== WebSocket.OPEN) continue;
|
|
365
|
+
const ns = getNamespace(pluginWs);
|
|
366
|
+
browsers.push({
|
|
367
|
+
pluginId: pluginWs.pluginId,
|
|
368
|
+
pluginName: pluginWs.pluginName || 'My Browser',
|
|
369
|
+
userId: pluginWs.userId || null,
|
|
370
|
+
apiKeyId: pluginWs.apiKeyId || null,
|
|
371
|
+
browserName: ns.cachedBrowserVersion?.Browser || 'Unknown',
|
|
372
|
+
userAgent: ns.cachedBrowserVersion?.['User-Agent'] || '',
|
|
373
|
+
targets: ns.cachedTargets?.length || 0,
|
|
374
|
+
connected: true,
|
|
375
|
+
connectedAt: pluginWs.connectedAt,
|
|
376
|
+
extVersion: pluginWs.extVersion || 'unknown'
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return adminJson(res, 200, browsers);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// key 管理(需要 SaaS)
|
|
383
|
+
if (path === 'keys' && method === 'GET') {
|
|
384
|
+
if (!saasAuth) return adminJson(res, 503, { error: 'SaaS not available' });
|
|
385
|
+
return adminJson(res, 200, saasAuth.listApiKeys('builtin-admin'));
|
|
386
|
+
}
|
|
387
|
+
if (path === 'keys' && method === 'POST') {
|
|
388
|
+
if (!saasAuth) return adminJson(res, 503, { error: 'SaaS not available' });
|
|
389
|
+
const body = await readBody(req);
|
|
390
|
+
const name = body.name || ('browser-' + Date.now().toString(36));
|
|
391
|
+
const result = saasAuth.createApiKey('builtin-admin', name);
|
|
392
|
+
const port = PORT;
|
|
393
|
+
return adminJson(res, 200, {
|
|
394
|
+
...result,
|
|
395
|
+
pluginUrl: `ws://localhost:${port}/plugin?key=${result.key}`,
|
|
396
|
+
clientUrl: `ws://localhost:${port}/client?key=${result.key}`
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (path.startsWith('keys/') && method === 'DELETE') {
|
|
400
|
+
if (!saasAuth) return adminJson(res, 503, { error: 'SaaS not available' });
|
|
401
|
+
const keyId = path.split('/')[1];
|
|
402
|
+
const result = saasAuth.revokeApiKey(keyId, 'builtin-admin');
|
|
403
|
+
return adminJson(res, result.changes > 0 ? 200 : 404, { ok: result.changes > 0, changes: result.changes });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// CDP 快捷操作(通过 pluginId 直接连已在线浏览器)
|
|
407
|
+
if (path.startsWith('cdp/') && method === 'POST') {
|
|
408
|
+
const body = await readBody(req);
|
|
409
|
+
if (!body.pluginId) return adminJson(res, 400, { error: 'Missing pluginId' });
|
|
410
|
+
const result = await execCdpViaPlugin(body.pluginId, body);
|
|
411
|
+
return adminJson(res, result.error ? 500 : 200, result);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return adminJson(res, 404, { error: 'Not found: ' + path });
|
|
415
|
+
} catch (e) {
|
|
416
|
+
return adminJson(res, 500, { error: e.message });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 通过 pluginId 走端口池 /client 路径执行 CDP 命令(复用完整分组/session 路由逻辑)
|
|
421
|
+
async function execCdpViaPlugin(pluginId, params) {
|
|
422
|
+
const action = params.action || 'evaluate';
|
|
423
|
+
// 找到 pluginId 对应的 plugin 连接
|
|
424
|
+
let pluginWs = null;
|
|
425
|
+
for (const ws of pluginConnections) {
|
|
426
|
+
if (ws.readyState === WebSocket.OPEN && ws.pluginId === pluginId) { pluginWs = ws; break; }
|
|
427
|
+
}
|
|
428
|
+
if (!pluginWs) return { error: 'Browser not found or offline' };
|
|
429
|
+
|
|
430
|
+
// closebrowser 特殊处理:不走 /client(会被 Browser.close 断开),
|
|
431
|
+
// 直接用 pluginWs 发 client-disconnected + closeTarget
|
|
432
|
+
if (action === 'closebrowser') {
|
|
433
|
+
// 从 apiKey 找到对应的 key session 的端口,拿到正确的 clientId
|
|
434
|
+
let clientId = `pool_${PORT}`; // fallback
|
|
435
|
+
if (pluginWs.apiKey && portPool && portPool._keyToPortIndex) {
|
|
436
|
+
const portIdx = portPool._keyToPortIndex.get(pluginWs.apiKey);
|
|
437
|
+
if (portIdx !== undefined && portPool.portSessions[portIdx]) {
|
|
438
|
+
clientId = `pool_${portPool.portSessions[portIdx].port}`;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// 发 client-disconnected 让扩展关分组 + 关 tab
|
|
442
|
+
try {
|
|
443
|
+
pluginWs.send(JSON.stringify({ type: 'client-disconnected', clientId }));
|
|
444
|
+
} catch (e) {}
|
|
445
|
+
// 清理 key session(让下次连接重新分配)
|
|
446
|
+
if (pluginWs.apiKey && portPool) {
|
|
447
|
+
portPool.keySessions.delete(pluginWs.apiKey);
|
|
448
|
+
if (portPool._keyToPortIndex) portPool._keyToPortIndex.delete(pluginWs.apiKey);
|
|
449
|
+
}
|
|
450
|
+
return { closed: true };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const apiKey = pluginWs.apiKey;
|
|
454
|
+
if (!apiKey) return { error: 'Browser has no API key (not connected via /plugin?key=)' };
|
|
455
|
+
|
|
456
|
+
// 内部连 /client?key=xxx,走端口池完整路径(建组 + session 路由 + 分组)
|
|
457
|
+
const port = PORT;
|
|
458
|
+
return new Promise((resolve) => {
|
|
459
|
+
const ws = new WebSocket(`ws://127.0.0.1:${port}/client?key=${apiKey}`);
|
|
460
|
+
const pending = new Map(); let id = 1; let sessionId = null;
|
|
461
|
+
const timeout = setTimeout(() => { try { ws.close(); } catch {}; resolve({ error: 'Timeout' }); }, 15000);
|
|
462
|
+
|
|
463
|
+
function cdp(method, p = {}, sid) {
|
|
464
|
+
return new Promise((res, rej) => {
|
|
465
|
+
const i = id++; pending.set(i, { resolve: res, reject: rej });
|
|
466
|
+
const o = { id: i, method, params: p };
|
|
467
|
+
if (sid) o.sessionId = sid; else if (sessionId) o.sessionId = sessionId;
|
|
468
|
+
ws.send(JSON.stringify(o));
|
|
469
|
+
setTimeout(() => { if (pending.has(i)) { pending.delete(i); rej(new Error('CDP timeout: ' + method)); } }, 12000);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function finish(result) { clearTimeout(timeout); try { ws.close(); } catch {}; resolve(result); }
|
|
474
|
+
|
|
475
|
+
ws.on('message', async (data) => {
|
|
476
|
+
const m = JSON.parse(data.toString());
|
|
477
|
+
if (m.id && pending.has(m.id)) {
|
|
478
|
+
const { resolve: r, reject: rj } = pending.get(m.id); pending.delete(m.id);
|
|
479
|
+
m.error ? rj(new Error(JSON.stringify(m.error))) : r(m.result);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
// attachedToTarget 事件 → 记 sessionId
|
|
483
|
+
if (m.method === 'Target.attachedToTarget' && m.params?.sessionId && !sessionId) {
|
|
484
|
+
sessionId = m.params.sessionId;
|
|
485
|
+
try {
|
|
486
|
+
if (action === 'evaluate') {
|
|
487
|
+
await cdp('Runtime.enable', {}, sessionId);
|
|
488
|
+
const ev = await cdp('Runtime.evaluate', { expression: params.expression || 'document.title', returnByValue: true }, sessionId);
|
|
489
|
+
finish({ result: ev.result.value });
|
|
490
|
+
} else if (action === 'screenshot') {
|
|
491
|
+
await cdp('Page.enable', {}, sessionId);
|
|
492
|
+
const shot = await cdp('Page.captureScreenshot', { format: 'png' }, sessionId);
|
|
493
|
+
finish({ screenshot: shot.data });
|
|
494
|
+
}
|
|
495
|
+
} catch (e) { finish({ error: e.message }); }
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
ws.on('open', async () => {
|
|
500
|
+
try {
|
|
501
|
+
// listtabs:走 /client 路径,getTargets 被端口池按 key 自动过滤
|
|
502
|
+
if (action === 'listtabs') {
|
|
503
|
+
await cdp('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
504
|
+
const tg = await cdp('Target.getTargets');
|
|
505
|
+
const tabs = (tg?.targetInfos || [])
|
|
506
|
+
.filter(t => t.type === 'page')
|
|
507
|
+
.map(t => ({ targetId: t.targetId, url: t.url, title: t.title || '', attached: t.attached }));
|
|
508
|
+
finish({ tabs });
|
|
509
|
+
}
|
|
510
|
+
// closetab:关闭指定 targetId
|
|
511
|
+
else if (action === 'closetab') {
|
|
512
|
+
await cdp('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
513
|
+
await cdp('Target.closeTarget', { targetId: params.targetId });
|
|
514
|
+
finish({ closed: true, targetId: params.targetId });
|
|
515
|
+
}
|
|
516
|
+
// switchtab:切换到指定 targetId(bringToFront)
|
|
517
|
+
else if (action === 'switchtab') {
|
|
518
|
+
await cdp('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
519
|
+
const at = await cdp('Target.attachToTarget', { targetId: params.targetId, flatten: true });
|
|
520
|
+
if (at.sessionId) {
|
|
521
|
+
await cdp('Page.enable', {}, at.sessionId);
|
|
522
|
+
await cdp('Page.bringToFront', {}, at.sessionId);
|
|
523
|
+
}
|
|
524
|
+
finish({ switched: true, targetId: params.targetId });
|
|
525
|
+
}
|
|
526
|
+
else if (action === 'newtab') {
|
|
527
|
+
await cdp('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
528
|
+
const created = await cdp('Target.createTarget', { url: params.url || 'about:blank' });
|
|
529
|
+
finish({ targetId: created.targetId });
|
|
530
|
+
} else {
|
|
531
|
+
// evaluate/screenshot:直接 getTargets + attach(不等 setAutoAttach 事件,避免超时)
|
|
532
|
+
await cdp('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
533
|
+
const tg = await cdp('Target.getTargets');
|
|
534
|
+
let page = (tg.targetInfos || []).find(t => t.type === 'page');
|
|
535
|
+
if (!page) {
|
|
536
|
+
const created = await cdp('Target.createTarget', { url: 'about:blank' });
|
|
537
|
+
page = { targetId: created.targetId };
|
|
538
|
+
}
|
|
539
|
+
// attach 后等 attachedToTarget 事件触发 evaluate/screenshot
|
|
540
|
+
// 如果事件没来,1 秒后用手动方式(attach 响应里的 sessionId)
|
|
541
|
+
const at = await cdp('Target.attachToTarget', { targetId: page.targetId, flatten: true });
|
|
542
|
+
// 如果事件路径没处理,手动执行
|
|
543
|
+
setTimeout(async () => {
|
|
544
|
+
if (sessionId || !at.sessionId) return;
|
|
545
|
+
sessionId = at.sessionId;
|
|
546
|
+
try {
|
|
547
|
+
if (action === 'evaluate') {
|
|
548
|
+
await cdp('Runtime.enable', {}, sessionId);
|
|
549
|
+
const ev = await cdp('Runtime.evaluate', { expression: params.expression || 'document.title', returnByValue: true }, sessionId);
|
|
550
|
+
finish({ result: ev.result.value });
|
|
551
|
+
} else if (action === 'screenshot') {
|
|
552
|
+
await cdp('Page.enable', {}, sessionId);
|
|
553
|
+
const shot = await cdp('Page.captureScreenshot', { format: 'png' }, sessionId);
|
|
554
|
+
finish({ screenshot: shot.data });
|
|
555
|
+
}
|
|
556
|
+
} catch (e) { finish({ error: e.message }); }
|
|
557
|
+
}, 1000);
|
|
558
|
+
}
|
|
559
|
+
} catch (e) { finish({ error: e.message }); }
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
ws.on('error', e => { clearTimeout(timeout); resolve({ error: e.message }); });
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
321
566
|
async function handleHttpRequest(req, res) {
|
|
322
567
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
323
568
|
|
|
@@ -333,6 +578,21 @@ async function handleHttpRequest(req, res) {
|
|
|
333
578
|
return;
|
|
334
579
|
}
|
|
335
580
|
|
|
581
|
+
// 管理控制台:/admin 返回页面,/admin/api/* 返回 JSON
|
|
582
|
+
if (url.pathname === '/admin' || url.pathname === '/admin/') {
|
|
583
|
+
try {
|
|
584
|
+
const html = fs.readFileSync(path.join(__dirname, 'admin-console.html'), 'utf8');
|
|
585
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
586
|
+
res.end(html);
|
|
587
|
+
} catch (e) {
|
|
588
|
+
res.writeHead(500); res.end('Admin console not found: ' + e.message);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (url.pathname.startsWith('/admin/api/')) {
|
|
593
|
+
return handleAdminApi(req, res, url);
|
|
594
|
+
}
|
|
595
|
+
|
|
336
596
|
if (url.pathname === '/json/browsers' || url.pathname === '/json/browsers/') {
|
|
337
597
|
const browsers = [];
|
|
338
598
|
for (const pluginWs of pluginConnections) {
|
|
@@ -707,8 +967,7 @@ function cleanupPlugin(ws, id, reason) {
|
|
|
707
967
|
remainingPlugins: pluginConnections.size,
|
|
708
968
|
affectedClients,
|
|
709
969
|
uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
|
|
710
|
-
activeSessions: ns.sessionToClientId.size
|
|
711
|
-
pendingRequests: ns.pendingAttachRequests.size
|
|
970
|
+
activeSessions: ns.sessionToClientId.size
|
|
712
971
|
});
|
|
713
972
|
|
|
714
973
|
if (ws.pairedClientId) {
|
|
@@ -756,6 +1015,7 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
756
1015
|
ws.userId = keyInfo.userId;
|
|
757
1016
|
ws.apiKeyId = keyInfo.keyId;
|
|
758
1017
|
ws.apiKey = apiKey; // 记录 key 字符串,供 client 连接时反查 plugin
|
|
1018
|
+
ws.apiKeyName = keyInfo.keyName; // key 名称,用作 Chrome 分组名
|
|
759
1019
|
logConnectionEvent('PLUGIN_AUTHED', `userId=${keyInfo.userId} keyName=${keyInfo.keyName}`);
|
|
760
1020
|
} else {
|
|
761
1021
|
logConnectionEvent('PLUGIN_AUTH_FAIL', 'Invalid API key');
|
|
@@ -1123,23 +1383,7 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
1123
1383
|
invalidateTargetsCache(ws);
|
|
1124
1384
|
}
|
|
1125
1385
|
|
|
1126
|
-
|
|
1127
|
-
console.log(`[AUTO DEFAULT PAGE] createTarget response received for client=${mapping.clientId}, targetId=${parsed.result?.targetId?.substring(0,8) || 'none'} — skipping response send to client`);
|
|
1128
|
-
|
|
1129
|
-
if (mapping.pendingSetAutoAttach) {
|
|
1130
|
-
const pending = mapping.pendingSetAutoAttach;
|
|
1131
|
-
const pendingParsed = pending.parsed;
|
|
1132
|
-
const pendingClientId = pending.clientId;
|
|
1133
|
-
|
|
1134
|
-
console.log(`[AUTO DEFAULT PAGE] Now forwarding pending setAutoAttach for client=${pendingClientId}`);
|
|
1135
|
-
|
|
1136
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1137
|
-
const forwardMsg = { ...pendingParsed, __clientId: pendingClientId };
|
|
1138
|
-
ws.send(JSON.stringify(forwardMsg));
|
|
1139
|
-
console.log(`[SEND TO PLUGIN] Forwarding setAutoAttach for client=${pendingClientId}`);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
} else {
|
|
1386
|
+
{
|
|
1143
1387
|
const originalId = mapping.originalId;
|
|
1144
1388
|
parsed.id = originalId;
|
|
1145
1389
|
if (mapping.sessionId && !parsed.sessionId) {
|
|
@@ -1245,52 +1489,6 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
1245
1489
|
}));
|
|
1246
1490
|
}
|
|
1247
1491
|
|
|
1248
|
-
function autoCreateDefaultPageAndForward(clientWs, setAutoAttachParsed, originalData, clientId, originalRequestId) {
|
|
1249
|
-
const pluginWs = clientWs.pairedPlugin;
|
|
1250
|
-
if (!pluginWs || pluginWs.readyState !== WebSocket.OPEN) {
|
|
1251
|
-
forwardToPlugin(clientWs, originalData, clientId);
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
globalRequestIdCounter++;
|
|
1256
|
-
const createGlobalId = globalRequestIdCounter;
|
|
1257
|
-
|
|
1258
|
-
globalRequestIdMap.set(createGlobalId, {
|
|
1259
|
-
clientId: clientId,
|
|
1260
|
-
originalId: -1,
|
|
1261
|
-
sessionId: null,
|
|
1262
|
-
method: 'Target.createTarget',
|
|
1263
|
-
isCreateTarget: true,
|
|
1264
|
-
isAutoDefaultPage: true,
|
|
1265
|
-
pendingSetAutoAttach: {
|
|
1266
|
-
parsed: setAutoAttachParsed,
|
|
1267
|
-
data: originalData,
|
|
1268
|
-
clientId: clientId,
|
|
1269
|
-
originalRequestId: originalRequestId
|
|
1270
|
-
}
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
const request = {
|
|
1274
|
-
id: createGlobalId,
|
|
1275
|
-
method: 'Target.createTarget',
|
|
1276
|
-
params: { url: 'about:blank' },
|
|
1277
|
-
__clientId: clientId
|
|
1278
|
-
};
|
|
1279
|
-
|
|
1280
|
-
console.log(`[AUTO DEFAULT PAGE] Sending Target.createTarget for client=${clientId} globalId=${createGlobalId}, will forward setAutoAttach after`);
|
|
1281
|
-
pluginWs.send(JSON.stringify(request));
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function forwardToPlugin(clientWs, data, clientId) {
|
|
1285
|
-
const pluginWs = clientWs.pairedPlugin;
|
|
1286
|
-
if (pluginWs && pluginWs.readyState === WebSocket.OPEN) {
|
|
1287
|
-
console.log(`[SEND TO PLUGIN] method=Target.setAutoAttach clientId=${clientId}`);
|
|
1288
|
-
pluginWs.send(data);
|
|
1289
|
-
} else {
|
|
1290
|
-
broadcastToPlugins(data, clientWs);
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
1492
|
/**
|
|
1295
1493
|
* 处理 CDP 客户端连接 (Playwright/Puppeteer)
|
|
1296
1494
|
*/
|
|
@@ -1513,13 +1711,9 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
|
|
|
1513
1711
|
|
|
1514
1712
|
if (parsed && parsed.method === 'Target.setAutoAttach' && parsed.params?.autoAttach && !ws._autoDefaultPageSent) {
|
|
1515
1713
|
ws._autoDefaultPageSent = true;
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
ws.pairedPlugin.send(JSON.stringify(takeoverMsg));
|
|
1520
|
-
}
|
|
1521
|
-
} else {
|
|
1522
|
-
autoCreateDefaultPageAndForward(ws, parsed, modifiedData, id, originalId);
|
|
1714
|
+
const forwardMsg = { ...parsed, __clientId: id, __mode: ws.mode };
|
|
1715
|
+
if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
|
|
1716
|
+
ws.pairedPlugin.send(JSON.stringify(forwardMsg));
|
|
1523
1717
|
}
|
|
1524
1718
|
return;
|
|
1525
1719
|
}
|
|
@@ -2069,10 +2263,8 @@ setInterval(() => {
|
|
|
2069
2263
|
});
|
|
2070
2264
|
|
|
2071
2265
|
let totalSessions = 0;
|
|
2072
|
-
let totalPendingAttach = 0;
|
|
2073
2266
|
pluginNamespaces.forEach(ns => {
|
|
2074
2267
|
totalSessions += ns.sessionToClientId.size;
|
|
2075
|
-
totalPendingAttach += ns.pendingAttachRequests.size;
|
|
2076
2268
|
});
|
|
2077
2269
|
|
|
2078
2270
|
logStatus({
|
|
@@ -2084,8 +2276,7 @@ setInterval(() => {
|
|
|
2084
2276
|
pairs: connectionPairs.size,
|
|
2085
2277
|
pluginDetails: pluginList,
|
|
2086
2278
|
clientDetails: clientList,
|
|
2087
|
-
sessions: totalSessions
|
|
2088
|
-
pendingAttach: totalPendingAttach
|
|
2279
|
+
sessions: totalSessions
|
|
2089
2280
|
});
|
|
2090
2281
|
}, CONFIG.STATUS_PRINT_INTERVAL);
|
|
2091
2282
|
|
|
@@ -2162,11 +2353,6 @@ portPool = new PortPoolManager({
|
|
|
2162
2353
|
if (!apiKey) return false;
|
|
2163
2354
|
return !!validateApiKey(apiKey);
|
|
2164
2355
|
},
|
|
2165
|
-
handlePluginUpgrade: (req, socket, head) => {
|
|
2166
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2167
|
-
wss.emit('connection', ws, req);
|
|
2168
|
-
});
|
|
2169
|
-
},
|
|
2170
2356
|
handleTakeoverUpgrade: (req, socket, head) => {
|
|
2171
2357
|
req._takeoverMode = true;
|
|
2172
2358
|
const url = new URL(req.url, `http://localhost:${CONFIG.TAKEOVER_PORT}`);
|
|
@@ -2186,22 +2372,6 @@ portPool = new PortPoolManager({
|
|
|
2186
2372
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2187
2373
|
wss.emit('connection', ws, req);
|
|
2188
2374
|
});
|
|
2189
|
-
},
|
|
2190
|
-
getAllTargets: async (portIndex) => {
|
|
2191
|
-
const session = portPool.portSessions[portIndex];
|
|
2192
|
-
if (!session) return [];
|
|
2193
|
-
// 从主 proxy 的 namespace 拿所有 target,过滤出这个端口的
|
|
2194
|
-
const targets = [];
|
|
2195
|
-
for (const [, ns] of pluginNamespaces) {
|
|
2196
|
-
if (ns.cachedTargets) {
|
|
2197
|
-
for (const t of ns.cachedTargets) {
|
|
2198
|
-
if (session.targetIds.has(t.targetId)) {
|
|
2199
|
-
targets.push(t);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
return targets;
|
|
2205
2375
|
}
|
|
2206
2376
|
});
|
|
2207
2377
|
portPool.start();
|