cdp-tunnel 2.10.14 → 3.0.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/package.json
CHANGED
package/server/modules/config.js
CHANGED
|
@@ -11,7 +11,10 @@ const CONFIG = {
|
|
|
11
11
|
AUTO_RESTART: process.env.AUTO_RESTART === 'true',
|
|
12
12
|
CHROME_RESTART_COOLDOWN: 30000,
|
|
13
13
|
PLUGIN_MAX_MISSED_PINGS: 3,
|
|
14
|
-
TAKEOVER_PORT: process.env.TAKEOVER_PORT ? parseInt(process.env.TAKEOVER_PORT) : (parseInt(process.env.PORT || '9221') + 1)
|
|
14
|
+
TAKEOVER_PORT: process.env.TAKEOVER_PORT ? parseInt(process.env.TAKEOVER_PORT) : (parseInt(process.env.PORT || '9221') + 1),
|
|
15
|
+
POOL_TAKEOVER_PORT: process.env.POOL_TAKEOVER_PORT ? parseInt(process.env.POOL_TAKEOVER_PORT) : 9220,
|
|
16
|
+
POOL_START: process.env.POOL_START ? parseInt(process.env.POOL_START) : 9231,
|
|
17
|
+
POOL_SIZE: process.env.POOL_SIZE ? parseInt(process.env.POOL_SIZE) : 9
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
const LOG_LEVELS = {
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 端口池管理器(v3.0)
|
|
5
|
+
*
|
|
6
|
+
* 在现有 proxy 之外,额外启动一组 create 端口(9222-9230)+ 一个 takeover 端口(9220)。
|
|
7
|
+
* 每个 create 端口 = 一个独立的隔离环境。
|
|
8
|
+
*
|
|
9
|
+
* 核心设计:
|
|
10
|
+
* - 复用现有的 plugin 连接(扩展只连一次 9221/plugin)
|
|
11
|
+
* - 每个 create 端口有独立的 PortSession(targetId 集合)
|
|
12
|
+
* - 命令转发给 plugin 时带 __portIndex 标记
|
|
13
|
+
* - plugin 返回的事件按 __portIndex 路由回对应端口
|
|
14
|
+
* - 对齐原生 Chrome:多客户端共享、断开不清理 tab
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const WebSocket = require('ws');
|
|
19
|
+
const { CONFIG } = require('./config');
|
|
20
|
+
|
|
21
|
+
class PortPoolManager {
|
|
22
|
+
constructor(mainProxy) {
|
|
23
|
+
this.mainProxy = mainProxy; // 现有 proxy 的引用(拿 plugin 连接)
|
|
24
|
+
this.createServers = []; // [http.Server] 每个 create 端口一个
|
|
25
|
+
this.createWss = []; // [WebSocket.Server] 每个 create 端口一个
|
|
26
|
+
this.portSessions = []; // [PortSession] 每个 create 端口一个
|
|
27
|
+
this.takeoverServer = null;
|
|
28
|
+
this.takeoverWss = null;
|
|
29
|
+
this.targetToPort = new Map(); // targetId → portIndex(事件路由用)
|
|
30
|
+
this.sessionToPort = new Map(); // CDP sessionId → portIndex
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PortSession:一个 create 端口的隔离状态
|
|
35
|
+
*/
|
|
36
|
+
static PortSession = class {
|
|
37
|
+
constructor(portIndex, port) {
|
|
38
|
+
this.portIndex = portIndex;
|
|
39
|
+
this.port = port;
|
|
40
|
+
this.targetIds = new Set(); // 这个端口创建的所有 targetId
|
|
41
|
+
this.tabIds = new Set(); // Chrome tabId(扩展端返回的)
|
|
42
|
+
this.clients = new Set(); // 连接到这个端口的 client WebSocket
|
|
43
|
+
this.cdpIdCounter = 1; // CDP 命令 id 计数器
|
|
44
|
+
this.pendingRequests = new Map(); // id → {clientWs, resolve, reject}
|
|
45
|
+
this.targetToClient = new Map(); // targetId → clientWs(attachToTarget 时绑定)
|
|
46
|
+
this.browserWsUrl = null; // /json/version 返回的 webSocketDebuggerUrl
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
start() {
|
|
51
|
+
// 启动 create 端口(9222-9230)
|
|
52
|
+
for (let i = 0; i < CONFIG.POOL_SIZE; i++) {
|
|
53
|
+
const port = CONFIG.POOL_START + i;
|
|
54
|
+
this._startCreatePort(i, port);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 启动 takeover 端口(9220)
|
|
58
|
+
this._startTakeoverPort();
|
|
59
|
+
|
|
60
|
+
console.log(`\n[PORT POOL] Started: takeover=${CONFIG.POOL_TAKEOVER_PORT}, create=${CONFIG.POOL_START}-${CONFIG.POOL_START + CONFIG.POOL_SIZE - 1}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_startCreatePort(portIndex, port) {
|
|
64
|
+
const session = new PortPoolManager.PortSession(portIndex, port);
|
|
65
|
+
this.portSessions[portIndex] = session;
|
|
66
|
+
|
|
67
|
+
const server = http.createServer((req, res) => {
|
|
68
|
+
this._handleHttp(req, res, session);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const wss = new WebSocket.Server({ noServer: true });
|
|
72
|
+
|
|
73
|
+
server.on('upgrade', (req, socket, head) => {
|
|
74
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
75
|
+
const path = url.pathname;
|
|
76
|
+
|
|
77
|
+
// 只允许 client 连接(plugin 连的是 9221)
|
|
78
|
+
if (path !== '/client' && !path.startsWith('/client/') &&
|
|
79
|
+
!path.startsWith('/devtools/browser/') && !path.startsWith('/devtools/page/')) {
|
|
80
|
+
socket.destroy();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
85
|
+
this._handleClientConnect(ws, req, session);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.on('error', (err) => {
|
|
90
|
+
if (err.code === 'EADDRINUSE') {
|
|
91
|
+
console.error(`[PORT POOL] Port ${port} in use, skipping create port ${portIndex}`);
|
|
92
|
+
} else {
|
|
93
|
+
console.error(`[PORT POOL] Create port ${port} error:`, err.message);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
server.listen(port, '0.0.0.0', () => {
|
|
98
|
+
console.log(`[CREATE PORT ${portIndex}] Listening on ${port}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.createServers[portIndex] = server;
|
|
102
|
+
this.createWss[portIndex] = wss;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_startTakeoverPort() {
|
|
106
|
+
const port = CONFIG.POOL_TAKEOVER_PORT;
|
|
107
|
+
const server = http.createServer((req, res) => {
|
|
108
|
+
// takeover 的 HTTP 请求转发给主 proxy 的 handleHttpRequest
|
|
109
|
+
req._takeoverMode = true;
|
|
110
|
+
this.mainProxy.handleHttpRequest(req, res);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
server.on('upgrade', (req, socket, head) => {
|
|
114
|
+
req._takeoverMode = true;
|
|
115
|
+
// takeover 的 WS 连接转发给主 proxy 的 wss
|
|
116
|
+
this.mainProxy.handleTakeoverUpgrade(req, socket, head);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.on('error', (err) => {
|
|
120
|
+
if (err.code === 'EADDRINUSE') {
|
|
121
|
+
console.error(`[PORT POOL] Takeover port ${port} in use`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.listen(port, '0.0.0.0', () => {
|
|
126
|
+
console.log(`[TAKEOVER POOL] Listening on ${port}`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this.takeoverServer = server;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* HTTP 请求处理(/json/version, /json/list 等)
|
|
134
|
+
* 每个 create 端口只返回自己的 target
|
|
135
|
+
*/
|
|
136
|
+
async _handleHttp(req, res, session) {
|
|
137
|
+
const url = new URL(req.url, `http://localhost:${session.port}`);
|
|
138
|
+
const path = url.pathname;
|
|
139
|
+
|
|
140
|
+
if (path === '/json/version') {
|
|
141
|
+
// 返回版本信息,webSocketDebuggerUrl 指向当前端口
|
|
142
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({
|
|
144
|
+
Browser: 'Chrome/131.0.6778.86 (cdp-tunnel)',
|
|
145
|
+
'Protocol-Version': '1.3',
|
|
146
|
+
'User-Agent': 'cdp-tunnel/3.0',
|
|
147
|
+
'webSocketDebuggerUrl': `ws://localhost:${session.port}/devtools/browser/pool_${session.portIndex}`
|
|
148
|
+
}));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (path === '/json' || path === '/json/list') {
|
|
153
|
+
// 只返回这个端口的 target(从主 proxy 拿全量后过滤)
|
|
154
|
+
const allTargets = await this.mainProxy.getAllTargets(session.portIndex);
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify(allTargets));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (path === '/json/new') {
|
|
161
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
162
|
+
res.end(JSON.stringify({ error: 'Use Target.createTarget via WebSocket' }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
167
|
+
res.end('Not Found');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Client WebSocket 连接处理
|
|
172
|
+
*/
|
|
173
|
+
_handleClientConnect(ws, req, session) {
|
|
174
|
+
session.clients.add(ws);
|
|
175
|
+
console.log(`[PORT ${session.port}] Client connected (total: ${session.clients.size})`);
|
|
176
|
+
|
|
177
|
+
// 找到 plugin 连接(从主 proxy 获取)
|
|
178
|
+
const pluginWs = this.mainProxy.getPluginConnection();
|
|
179
|
+
if (!pluginWs) {
|
|
180
|
+
ws.close(1011, 'No extension connected');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 消息处理:client → plugin(带 portIndex 标记)
|
|
185
|
+
ws.on('message', (data) => {
|
|
186
|
+
let msg;
|
|
187
|
+
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
188
|
+
|
|
189
|
+
if (msg.id !== undefined) {
|
|
190
|
+
// 命令:分配新 id,记录映射,转发给 plugin
|
|
191
|
+
const newId = `pool${session.portIndex}_${msg.id}`;
|
|
192
|
+
session.pendingRequests.set(newId, {
|
|
193
|
+
originalId: msg.id,
|
|
194
|
+
clientWs: ws
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 特殊处理 createTarget:记录 targetId 归属
|
|
198
|
+
if (msg.method === 'Target.createTarget') {
|
|
199
|
+
msg.params = msg.params || {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const forwarded = { ...msg, id: newId, __portIndex: session.portIndex };
|
|
203
|
+
pluginWs.send(JSON.stringify(forwarded));
|
|
204
|
+
} else {
|
|
205
|
+
// 无 id 的消息(事件),直接转发
|
|
206
|
+
pluginWs.send(JSON.stringify({ ...msg, __portIndex: session.portIndex }));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
ws.on('close', () => {
|
|
211
|
+
session.clients.delete(ws);
|
|
212
|
+
console.log(`[PORT ${session.port}] Client disconnected (remaining: ${session.clients.size})`);
|
|
213
|
+
// 对齐原生 Chrome:断开不清理 tab
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
ws.on('error', () => {
|
|
217
|
+
session.clients.delete(ws);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 处理从 plugin 返回的消息(按 portIndex 路由回对应端口的 client)
|
|
223
|
+
* 返回 true 表示已处理(端口池的消息),false 表示不是端口池的
|
|
224
|
+
*/
|
|
225
|
+
handlePluginMessage(msg, pluginWs) {
|
|
226
|
+
if (!msg) return false;
|
|
227
|
+
|
|
228
|
+
// 1. 响应消息:id 以 pool 开头
|
|
229
|
+
if (msg.id && typeof msg.id === 'string' && msg.id.startsWith('pool')) {
|
|
230
|
+
const match = msg.id.match(/^pool(\d+)_(.+)$/);
|
|
231
|
+
if (!match) return false;
|
|
232
|
+
|
|
233
|
+
const portIndex = parseInt(match[1]);
|
|
234
|
+
const originalId = match[2];
|
|
235
|
+
const session = this.portSessions[portIndex];
|
|
236
|
+
if (!session) return false;
|
|
237
|
+
|
|
238
|
+
const pending = session.pendingRequests.get(msg.id);
|
|
239
|
+
session.pendingRequests.delete(msg.id);
|
|
240
|
+
|
|
241
|
+
// 如果是 createTarget 响应,记录 targetId → portIndex 归属
|
|
242
|
+
if (msg.result && msg.result.targetId) {
|
|
243
|
+
session.targetIds.add(msg.result.targetId);
|
|
244
|
+
this.targetToPort.set(msg.result.targetId, portIndex);
|
|
245
|
+
console.log(`[PORT POOL] targetId=${msg.result.targetId.slice(0,12)} → port ${session.port}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 如果是 attachToTarget 响应,记录 sessionId → portIndex
|
|
249
|
+
if (msg.result && msg.result.sessionId) {
|
|
250
|
+
this.sessionToPort.set(msg.result.sessionId, portIndex);
|
|
251
|
+
console.log(`[PORT POOL] sessionId=${msg.result.sessionId.slice(0,12)} → port ${session.port}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 恢复原始 id,发给发起请求的 client
|
|
255
|
+
const response = { ...msg, id: this._parseOriginalId(originalId) };
|
|
256
|
+
if (pending && pending.clientWs && pending.clientWs.readyState === WebSocket.OPEN) {
|
|
257
|
+
pending.clientWs.send(JSON.stringify(response));
|
|
258
|
+
} else {
|
|
259
|
+
// 广播给这个端口的所有 client
|
|
260
|
+
this._broadcastToPort(portIndex, response);
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 2. 事件消息(无 id):按 targetId 路由
|
|
266
|
+
if (msg.method && msg.params) {
|
|
267
|
+
// 从事件参数里提取 targetId
|
|
268
|
+
let targetId = null;
|
|
269
|
+
if (msg.params.targetId) targetId = msg.params.targetId;
|
|
270
|
+
else if (msg.params.targetInfo && msg.params.targetInfo.targetId) targetId = msg.params.targetInfo.targetId;
|
|
271
|
+
|
|
272
|
+
if (targetId) {
|
|
273
|
+
const portIndex = this.targetToPort.get(targetId);
|
|
274
|
+
if (portIndex !== undefined) {
|
|
275
|
+
const session = this.portSessions[portIndex];
|
|
276
|
+
if (session) {
|
|
277
|
+
this._broadcastToPort(portIndex, msg);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// sessionId 路由(attachedToTarget 后的 session 事件)
|
|
284
|
+
if (msg.sessionId) {
|
|
285
|
+
const portIndex = this.sessionToPort.get(msg.sessionId);
|
|
286
|
+
if (portIndex !== undefined) {
|
|
287
|
+
this._broadcastToPort(portIndex, msg);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false; // 不是端口池的消息
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_parseOriginalId(idStr) {
|
|
297
|
+
const n = parseInt(idStr);
|
|
298
|
+
return isNaN(n) ? idStr : n;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_broadcastToPort(portIndex, msg) {
|
|
302
|
+
const session = this.portSessions[portIndex];
|
|
303
|
+
if (!session) return;
|
|
304
|
+
const data = JSON.stringify(msg);
|
|
305
|
+
session.clients.forEach(clientWs => {
|
|
306
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
307
|
+
clientWs.send(data);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
stop() {
|
|
313
|
+
this.createServers.forEach(s => { try { s.close(); } catch {} });
|
|
314
|
+
if (this.takeoverServer) { try { this.takeoverServer.close(); } catch {} }
|
|
315
|
+
console.log('[PORT POOL] Stopped');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { PortPoolManager };
|
package/server/proxy-server.js
CHANGED
|
@@ -16,7 +16,11 @@ const path = require('path');
|
|
|
16
16
|
const os = require('os');
|
|
17
17
|
const { execSync, spawn: spawnProcess } = require('child_process');
|
|
18
18
|
const { CONFIG, BROWSER_ID, shouldLog } = require('./modules/config');
|
|
19
|
+
const { PortPoolManager } = require('./modules/port-pool');
|
|
19
20
|
const TAKEOVER_PORT = CONFIG.TAKEOVER_PORT;
|
|
21
|
+
|
|
22
|
+
// v3.0 端口池(提前声明,后面初始化)
|
|
23
|
+
let portPool = null;
|
|
20
24
|
const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs, logDisconnect } = require('./modules/logger');
|
|
21
25
|
|
|
22
26
|
try {
|
|
@@ -790,7 +794,7 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
790
794
|
// 消息转发: Plugin -> Client
|
|
791
795
|
ws.on('message', (data) => {
|
|
792
796
|
console.log(`[PLUGIN MESSAGE] size=${data.length}`);
|
|
793
|
-
|
|
797
|
+
|
|
794
798
|
const messageSize = data.length;
|
|
795
799
|
let messagePreview;
|
|
796
800
|
let parsed;
|
|
@@ -805,7 +809,12 @@ function handlePluginConnection(ws, clientInfo, request) {
|
|
|
805
809
|
if (shouldLog('debug')) {
|
|
806
810
|
console.log(`[PLUGIN -> CLIENT] ${id}: ${messagePreview}`);
|
|
807
811
|
}
|
|
808
|
-
|
|
812
|
+
|
|
813
|
+
// v3.0 端口池 hook:先让 PortPoolManager 处理端口池的消息
|
|
814
|
+
if (parsed && portPool && portPool.handlePluginMessage(parsed, ws)) {
|
|
815
|
+
return; // 已被端口池处理
|
|
816
|
+
}
|
|
817
|
+
|
|
809
818
|
// 处理 keepalive 消息
|
|
810
819
|
if (parsed && parsed.type === 'keepalive') {
|
|
811
820
|
ws.isAlive = true;
|
|
@@ -2114,3 +2123,29 @@ takeoverServer.on('error', (err) => {
|
|
|
2114
2123
|
takeoverServer.listen(TAKEOVER_PORT, '0.0.0.0', () => {
|
|
2115
2124
|
console.log(`[TAKEOVER] Listening on port ${TAKEOVER_PORT}`);
|
|
2116
2125
|
});
|
|
2126
|
+
|
|
2127
|
+
// v3.0 端口池启动
|
|
2128
|
+
portPool = new PortPoolManager({
|
|
2129
|
+
getPluginConnection: () => {
|
|
2130
|
+
for (const ws of pluginConnections) return ws;
|
|
2131
|
+
return null;
|
|
2132
|
+
},
|
|
2133
|
+
getAllTargets: async (portIndex) => {
|
|
2134
|
+
const session = portPool.portSessions[portIndex];
|
|
2135
|
+
if (!session) return [];
|
|
2136
|
+
// 从主 proxy 的 namespace 拿所有 target,过滤出这个端口的
|
|
2137
|
+
const targets = [];
|
|
2138
|
+
for (const [, ns] of pluginNamespaces) {
|
|
2139
|
+
if (ns.cachedTargets) {
|
|
2140
|
+
for (const t of ns.cachedTargets) {
|
|
2141
|
+
if (session.targetIds.has(t.targetId)) {
|
|
2142
|
+
targets.push(t);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return targets;
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
portPool.start();
|
|
2151
|
+
|