cdp-tunnel 2.10.9 → 2.10.12
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 +22 -0
- package/extension-new/background.js +5 -3
- package/extension-new/cdp/handler/local.js +2 -1
- package/extension-new/cdp/handler/special.js +4 -1
- package/extension-new/core/debugger.js +23 -0
- package/extension-new/manifest.json +1 -1
- package/extension-new/utils/config.js +1 -1
- package/package.json +1 -1
- package/server/proxy-server.js +25 -4
package/cli/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const PID_FILE = path.join(CONFIG_DIR, 'server.pid');
|
|
|
12
12
|
const LOG_FILE = path.join(CONFIG_DIR, 'server.log');
|
|
13
13
|
const EXTENSION_STATE_FILE = path.join(CONFIG_DIR, 'extension-state.json');
|
|
14
14
|
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
|
|
15
|
+
const INSTANCE_LIFETIME = 2 * 60 * 60 * 1000; // 2小时
|
|
15
16
|
|
|
16
17
|
const INSTANCES_DIR = path.join(CONFIG_DIR, 'instances');
|
|
17
18
|
|
|
@@ -53,6 +54,26 @@ function syncExtensionVersion() {
|
|
|
53
54
|
|
|
54
55
|
syncExtensionVersion();
|
|
55
56
|
|
|
57
|
+
function cleanupStaleInstances() {
|
|
58
|
+
if (!fs.existsSync(INSTANCES_DIR)) return;
|
|
59
|
+
try {
|
|
60
|
+
const ports = fs.readdirSync(INSTANCES_DIR);
|
|
61
|
+
let cleaned = 0;
|
|
62
|
+
ports.forEach(port => {
|
|
63
|
+
const p = parseInt(port);
|
|
64
|
+
if (isNaN(p)) return;
|
|
65
|
+
if (!isServerRunning(p)) {
|
|
66
|
+
fs.rmSync(path.join(INSTANCES_DIR, port), { recursive: true, force: true });
|
|
67
|
+
cleaned++;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (cleaned > 0) {
|
|
71
|
+
console.log('');
|
|
72
|
+
log('gray', `已清理 ${cleaned} 个已停止的实例`);
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
function log(color, ...args) {
|
|
57
78
|
const colors = {
|
|
58
79
|
green: '\x1b[32m',
|
|
@@ -571,6 +592,7 @@ program
|
|
|
571
592
|
.description('查看服务器状态')
|
|
572
593
|
.option('-p, --port <port>', '指定端口', parseInt)
|
|
573
594
|
.action((options) => {
|
|
595
|
+
cleanupStaleInstances();
|
|
574
596
|
console.log('');
|
|
575
597
|
console.log('CDP Tunnel 状态');
|
|
576
598
|
console.log('─'.repeat(30));
|
|
@@ -220,10 +220,12 @@ importScripts('features/automation-badge.js');
|
|
|
220
220
|
SpecialHandler.addTabToAutomationGroup(tabId, clientId, null, ctx);
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
|
-
} else if (
|
|
224
|
-
Logger.info('[Tabs] Tab', tabId, 'left group, no cache and no pending creation — skipping (onRemoved handles re-group)');
|
|
225
|
-
} else {
|
|
223
|
+
} else if (groupPromise) {
|
|
226
224
|
Logger.info('[Tabs] Tab', tabId, 'left group, group creation pending — skipping');
|
|
225
|
+
} else {
|
|
226
|
+
Logger.info('[Tabs] Tab', tabId, 'left group, no cached groupId — force re-group');
|
|
227
|
+
var ctx = { _state: state, _wsManager: wsManager, clientId: clientId, mode: state.mode };
|
|
228
|
+
SpecialHandler.addTabToAutomationGroup(tabId, clientId, null, ctx);
|
|
227
229
|
}
|
|
228
230
|
}
|
|
229
231
|
}
|
|
@@ -501,7 +501,8 @@ var LocalHandler = (function() {
|
|
|
501
501
|
targetId: target.id || String(target.tabId),
|
|
502
502
|
type: target.type || 'page',
|
|
503
503
|
title: target.title || '',
|
|
504
|
-
|
|
504
|
+
// 原生 Chrome CDP 中 page 类型 target 的 url 一定存在(至少 "about:blank")
|
|
505
|
+
url: target.url || 'about:blank',
|
|
505
506
|
attached: !!target.attached,
|
|
506
507
|
canAccessOpener: false,
|
|
507
508
|
browserContextId: 'default'
|
|
@@ -416,11 +416,14 @@ var SpecialHandler = (function() {
|
|
|
416
416
|
var baseName = CDPUtils.getGroupBaseName(clientId, connectionTag, mode);
|
|
417
417
|
var newName = baseName + ' (' + tabs.length + ')';
|
|
418
418
|
|
|
419
|
+
if (chrome.runtime.lastError || tabs.length === 0) return;
|
|
419
420
|
chrome.tabGroups.update(groupId, {
|
|
420
421
|
title: newName
|
|
421
422
|
}, function() {
|
|
422
423
|
if (chrome.runtime.lastError) {
|
|
423
|
-
|
|
424
|
+
if (!chrome.runtime.lastError.message.includes('No group with id')) {
|
|
425
|
+
Logger.error('[TabGroup] Failed to update group name:', chrome.runtime.lastError.message);
|
|
426
|
+
}
|
|
424
427
|
} else {
|
|
425
428
|
Logger.info('[TabGroup] Updated group name:', newName);
|
|
426
429
|
}
|
|
@@ -72,6 +72,10 @@ var DebuggerManager = (function() {
|
|
|
72
72
|
})();
|
|
73
73
|
`;
|
|
74
74
|
|
|
75
|
+
// 跟踪每个 tab 最后一次非 about:blank 的导航 url
|
|
76
|
+
// 用于过滤 attach 时 Chrome 重放的 about:blank frameNavigated 事件
|
|
77
|
+
var tabLastRealUrl = {};
|
|
78
|
+
|
|
75
79
|
function attach(tabId, connState) {
|
|
76
80
|
var state = connState || _getAnyStateForTab(tabId);
|
|
77
81
|
if (tabId == null) {
|
|
@@ -289,6 +293,23 @@ var DebuggerManager = (function() {
|
|
|
289
293
|
}
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
// 过滤 attach 时 Chrome 重放的 about:blank frameNavigated 事件
|
|
297
|
+
// 原生 Chrome CDP 中,navigate 成功后不会回退到 about:blank
|
|
298
|
+
if (method === 'Page.frameNavigated' && params && params.frame) {
|
|
299
|
+
var navUrl = params.frame.url || '';
|
|
300
|
+
var tabIdKey = String(source.tabId);
|
|
301
|
+
|
|
302
|
+
if (navUrl && navUrl !== 'about:blank') {
|
|
303
|
+
// 记录真实导航 url
|
|
304
|
+
tabLastRealUrl[tabIdKey] = navUrl;
|
|
305
|
+
} else if (navUrl === 'about:blank' && tabLastRealUrl[tabIdKey]) {
|
|
306
|
+
// tab 已经导航到真实 url,但收到 about:blank 的 frameNavigated
|
|
307
|
+
// 这是 attach 时 Chrome 重放的旧事件,过滤掉
|
|
308
|
+
Logger.info('[Event] Filtering stale about:blank frameNavigated for tab', source.tabId, '(real url:', tabLastRealUrl[tabIdKey] + ')');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
292
313
|
for (var i = 0; i < sessionIds.length; i++) {
|
|
293
314
|
EventBuilder.send(method, params, sessionIds[i], wsManager);
|
|
294
315
|
}
|
|
@@ -297,6 +318,8 @@ var DebuggerManager = (function() {
|
|
|
297
318
|
function handleDetach(source, reason) {
|
|
298
319
|
Logger.info('[Debugger] Detached from tab', source.tabId, ', reason:', reason);
|
|
299
320
|
|
|
321
|
+
delete tabLastRealUrl[String(source.tabId)];
|
|
322
|
+
|
|
300
323
|
var entry = ConnectionManager.getConnectionByTabId(source.tabId);
|
|
301
324
|
var state = entry ? entry.state : null;
|
|
302
325
|
var wsManager = entry ? entry.wsManager : null;
|
package/package.json
CHANGED
package/server/proxy-server.js
CHANGED
|
@@ -361,6 +361,9 @@ async function handleHttpRequest(req, res) {
|
|
|
361
361
|
if (url.pathname === '/json' || url.pathname === '/json/' ||
|
|
362
362
|
url.pathname === '/json/list' || url.pathname === '/json/list/' ||
|
|
363
363
|
url.pathname.match(/^\/json\/list\/[^/]+$/)) {
|
|
364
|
+
// 注意:Playwright connectOverCDP 和 Puppeteer connect 都依赖 /json/list 发现 targets
|
|
365
|
+
// HTTP 端无 clientId 上下文,无法做归属过滤,但 attach 路径(handlePageConnection)有归属校验
|
|
366
|
+
// 所以即使列表可见,无归属 target 也无法被 attach(close 1008)
|
|
364
367
|
const pluginWs = resolvePluginFromUrl(url);
|
|
365
368
|
const targets = await requestTargetsFromPlugin(pluginWs);
|
|
366
369
|
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
@@ -444,7 +447,8 @@ wss.on('connection', (ws, req) => {
|
|
|
444
447
|
handleClientConnection(ws, clientInfo, customClientId, targetPluginId, mode);
|
|
445
448
|
} else if (path.startsWith('/devtools/page/')) {
|
|
446
449
|
const targetId = path.replace('/devtools/page/', '');
|
|
447
|
-
|
|
450
|
+
const mode = req._takeoverMode ? 'takeover' : 'create';
|
|
451
|
+
handlePageConnection(ws, clientInfo, targetId, mode);
|
|
448
452
|
} else {
|
|
449
453
|
console.log(`[REJECTED] Unknown path: ${path} from ${clientInfo.ip}:${clientInfo.port}`);
|
|
450
454
|
ws.close(1008, 'Invalid path. Use /plugin or /client');
|
|
@@ -1457,11 +1461,11 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
|
|
|
1457
1461
|
});
|
|
1458
1462
|
}
|
|
1459
1463
|
|
|
1460
|
-
function handlePageConnection(ws, clientInfo, targetId) {
|
|
1464
|
+
function handlePageConnection(ws, clientInfo, targetId, mode = 'create') {
|
|
1461
1465
|
clientConnections.add(ws);
|
|
1462
1466
|
const id = generateId('page');
|
|
1463
1467
|
if (shouldLog('info')) {
|
|
1464
|
-
console.log(`\n[PAGE CONNECTED] ID: ${id}, targetId: ${targetId}`);
|
|
1468
|
+
console.log(`\n[PAGE CONNECTED] ID: ${id}, targetId: ${targetId}, mode: ${mode}`);
|
|
1465
1469
|
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
1466
1470
|
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
1467
1471
|
}
|
|
@@ -1470,17 +1474,31 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1470
1474
|
ws.isAlive = true;
|
|
1471
1475
|
ws.cdpTrace = [];
|
|
1472
1476
|
ws.targetId = targetId;
|
|
1477
|
+
ws.mode = mode;
|
|
1473
1478
|
ws.lastActivityTime = Date.now();
|
|
1474
1479
|
clientById.set(id, ws);
|
|
1475
1480
|
|
|
1481
|
+
// 查找 target 归属的 plugin
|
|
1476
1482
|
let plugin = null;
|
|
1483
|
+
let ownerClientId = null;
|
|
1477
1484
|
for (const p of pluginConnections) {
|
|
1478
1485
|
const ns = getNamespace(p);
|
|
1479
1486
|
if (ns.targetIdToClientId.has(targetId)) {
|
|
1480
1487
|
plugin = p;
|
|
1488
|
+
ownerClientId = ns.targetIdToClientId.get(targetId);
|
|
1481
1489
|
break;
|
|
1482
1490
|
}
|
|
1483
1491
|
}
|
|
1492
|
+
|
|
1493
|
+
// create 模式:target 必须有明确的 clientId 归属,否则拒绝 attach(防止跨 client 越权)
|
|
1494
|
+
if (mode !== 'takeover' && !ownerClientId) {
|
|
1495
|
+
console.log(`[PAGE REJECTED] targetId=${targetId?.substring(0, 8)} has no owner in create mode — possible cross-client attach attempt from ${clientInfo.ip}`);
|
|
1496
|
+
ws.close(1008, 'Target does not belong to any client');
|
|
1497
|
+
clientConnections.delete(ws);
|
|
1498
|
+
clientById.delete(id);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
// takeover 模式:允许无归属 target(操作的是用户自己的 tab)
|
|
1484
1502
|
if (!plugin) {
|
|
1485
1503
|
plugin = pluginConnections.values().next().value;
|
|
1486
1504
|
}
|
|
@@ -1996,7 +2014,10 @@ server.on('error', (err) => {
|
|
|
1996
2014
|
|
|
1997
2015
|
server.listen(PORT, '0.0.0.0');
|
|
1998
2016
|
|
|
1999
|
-
const takeoverServer = http.createServer((req, res) =>
|
|
2017
|
+
const takeoverServer = http.createServer((req, res) => {
|
|
2018
|
+
req._takeoverMode = true;
|
|
2019
|
+
handleHttpRequest(req, res);
|
|
2020
|
+
});
|
|
2000
2021
|
takeoverServer.on('upgrade', (req, socket, head) => {
|
|
2001
2022
|
req._takeoverMode = true;
|
|
2002
2023
|
const url = new URL(req.url, `http://localhost:${TAKEOVER_PORT}`);
|