cdp-tunnel 2.10.8 → 2.10.11
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
CHANGED
|
@@ -55,45 +55,40 @@
|
|
|
55
55
|
|
|
56
56
|
## Quick Start
|
|
57
57
|
|
|
58
|
-
### Option 1: Install
|
|
58
|
+
### Option 1: Install Extension Only (No npm needed)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
# Install globally
|
|
62
|
-
npm install -g cdp-tunnel
|
|
63
|
-
|
|
64
|
-
# Start server
|
|
65
|
-
cdp-tunnel start
|
|
60
|
+
Download and install the Chrome extension directly:
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
1. Download [`cdp-tunnel-extension.zip`](https://github.com/dyyz1993/cdp-tunnel/releases/latest/download/cdp-tunnel-extension.zip) from the latest release
|
|
63
|
+
2. Unzip it
|
|
64
|
+
3. Open `chrome://extensions/` in Chrome
|
|
65
|
+
4. Enable **Developer mode** (top right)
|
|
66
|
+
5. Click **Load unpacked** → select the unzipped folder
|
|
67
|
+
6. Start the proxy server separately (see Option 2 or 3 below)
|
|
73
68
|
|
|
74
|
-
### Option 2:
|
|
69
|
+
### Option 2: Install via npm (Recommended)
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
```bash
|
|
72
|
+
# One command to set up everything
|
|
73
|
+
npx cdp-tunnel setup
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
# Or install globally
|
|
76
|
+
npm install -g cdp-tunnel
|
|
77
|
+
cdp-tunnel setup # Start server + auto-load extension into Chrome
|
|
78
|
+
cdp-tunnel status # Check status
|
|
79
|
+
cdp-tunnel diagnose # Diagnose connection issues
|
|
80
|
+
```
|
|
84
81
|
|
|
85
82
|
### Option 3: Manual Installation
|
|
86
83
|
|
|
87
84
|
#### 1. Start the Proxy Server
|
|
88
85
|
|
|
89
86
|
```bash
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
npx cdp-tunnel start
|
|
88
|
+
# or manually:
|
|
89
|
+
npx cdp-tunnel start -p 9221
|
|
93
90
|
```
|
|
94
91
|
|
|
95
|
-
The server will start on `localhost:9221`.
|
|
96
|
-
|
|
97
92
|
#### 2. Install Chrome Extension
|
|
98
93
|
|
|
99
94
|
1. Open `chrome://extensions/`
|
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
|
}
|
|
@@ -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
|
}
|
package/package.json
CHANGED
package/server/proxy-server.js
CHANGED
|
@@ -361,6 +361,17 @@ 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
|
+
// create 模式(9221):HTTP /json/list 无 clientId 上下文,无法做归属过滤
|
|
365
|
+
// 标准客户端(Playwright/Puppeteer)走 WS Target.setAutoAttach,不依赖此接口
|
|
366
|
+
// takeover 模式(9222):操作的是用户自己的 tab,返回全部是预期行为
|
|
367
|
+
if (!req._takeoverMode) {
|
|
368
|
+
if (shouldLog('info')) {
|
|
369
|
+
console.log(`[JSON LIST] create mode — returning empty list for isolation`);
|
|
370
|
+
}
|
|
371
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
372
|
+
res.end(JSON.stringify([]));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
364
375
|
const pluginWs = resolvePluginFromUrl(url);
|
|
365
376
|
const targets = await requestTargetsFromPlugin(pluginWs);
|
|
366
377
|
const browserId = pluginWs ? pluginWs.pluginId : BROWSER_ID;
|
|
@@ -444,7 +455,8 @@ wss.on('connection', (ws, req) => {
|
|
|
444
455
|
handleClientConnection(ws, clientInfo, customClientId, targetPluginId, mode);
|
|
445
456
|
} else if (path.startsWith('/devtools/page/')) {
|
|
446
457
|
const targetId = path.replace('/devtools/page/', '');
|
|
447
|
-
|
|
458
|
+
const mode = req._takeoverMode ? 'takeover' : 'create';
|
|
459
|
+
handlePageConnection(ws, clientInfo, targetId, mode);
|
|
448
460
|
} else {
|
|
449
461
|
console.log(`[REJECTED] Unknown path: ${path} from ${clientInfo.ip}:${clientInfo.port}`);
|
|
450
462
|
ws.close(1008, 'Invalid path. Use /plugin or /client');
|
|
@@ -1457,11 +1469,11 @@ function handleClientConnection(ws, clientInfo, customClientId = null, targetPlu
|
|
|
1457
1469
|
});
|
|
1458
1470
|
}
|
|
1459
1471
|
|
|
1460
|
-
function handlePageConnection(ws, clientInfo, targetId) {
|
|
1472
|
+
function handlePageConnection(ws, clientInfo, targetId, mode = 'create') {
|
|
1461
1473
|
clientConnections.add(ws);
|
|
1462
1474
|
const id = generateId('page');
|
|
1463
1475
|
if (shouldLog('info')) {
|
|
1464
|
-
console.log(`\n[PAGE CONNECTED] ID: ${id}, targetId: ${targetId}`);
|
|
1476
|
+
console.log(`\n[PAGE CONNECTED] ID: ${id}, targetId: ${targetId}, mode: ${mode}`);
|
|
1465
1477
|
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
1466
1478
|
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
1467
1479
|
}
|
|
@@ -1470,17 +1482,31 @@ function handlePageConnection(ws, clientInfo, targetId) {
|
|
|
1470
1482
|
ws.isAlive = true;
|
|
1471
1483
|
ws.cdpTrace = [];
|
|
1472
1484
|
ws.targetId = targetId;
|
|
1485
|
+
ws.mode = mode;
|
|
1473
1486
|
ws.lastActivityTime = Date.now();
|
|
1474
1487
|
clientById.set(id, ws);
|
|
1475
1488
|
|
|
1489
|
+
// 查找 target 归属的 plugin
|
|
1476
1490
|
let plugin = null;
|
|
1491
|
+
let ownerClientId = null;
|
|
1477
1492
|
for (const p of pluginConnections) {
|
|
1478
1493
|
const ns = getNamespace(p);
|
|
1479
1494
|
if (ns.targetIdToClientId.has(targetId)) {
|
|
1480
1495
|
plugin = p;
|
|
1496
|
+
ownerClientId = ns.targetIdToClientId.get(targetId);
|
|
1481
1497
|
break;
|
|
1482
1498
|
}
|
|
1483
1499
|
}
|
|
1500
|
+
|
|
1501
|
+
// create 模式:target 必须有明确的 clientId 归属,否则拒绝 attach(防止跨 client 越权)
|
|
1502
|
+
if (mode !== 'takeover' && !ownerClientId) {
|
|
1503
|
+
console.log(`[PAGE REJECTED] targetId=${targetId?.substring(0, 8)} has no owner in create mode — possible cross-client attach attempt from ${clientInfo.ip}`);
|
|
1504
|
+
ws.close(1008, 'Target does not belong to any client');
|
|
1505
|
+
clientConnections.delete(ws);
|
|
1506
|
+
clientById.delete(id);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
// takeover 模式:允许无归属 target(操作的是用户自己的 tab)
|
|
1484
1510
|
if (!plugin) {
|
|
1485
1511
|
plugin = pluginConnections.values().next().value;
|
|
1486
1512
|
}
|
|
@@ -1996,7 +2022,10 @@ server.on('error', (err) => {
|
|
|
1996
2022
|
|
|
1997
2023
|
server.listen(PORT, '0.0.0.0');
|
|
1998
2024
|
|
|
1999
|
-
const takeoverServer = http.createServer((req, res) =>
|
|
2025
|
+
const takeoverServer = http.createServer((req, res) => {
|
|
2026
|
+
req._takeoverMode = true;
|
|
2027
|
+
handleHttpRequest(req, res);
|
|
2028
|
+
});
|
|
2000
2029
|
takeoverServer.on('upgrade', (req, socket, head) => {
|
|
2001
2030
|
req._takeoverMode = true;
|
|
2002
2031
|
const url = new URL(req.url, `http://localhost:${TAKEOVER_PORT}`);
|