chrome-cdp-cli 1.6.0 → 1.7.0
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/dist/cli/CLIApplication.js +7 -0
- package/dist/cli/CommandRouter.js +5 -0
- package/dist/client/ProxyClient.js +25 -1
- package/dist/handlers/EvaluateScriptHandler.js +82 -65
- package/dist/proxy/server/CDPProxyServer.js +1 -0
- package/dist/proxy/server/CommandExecutionService.js +297 -0
- package/dist/proxy/server/ConnectionPool.js +9 -2
- package/dist/proxy/server/ProxyAPIServer.js +211 -0
- package/dist/proxy/server/WSProxy.js +12 -0
- package/package.json +1 -1
|
@@ -37,19 +37,26 @@ class CLIApplication {
|
|
|
37
37
|
}
|
|
38
38
|
async run(argv) {
|
|
39
39
|
try {
|
|
40
|
+
console.log('[DEBUG] CLIApplication.run called with argv:', argv);
|
|
40
41
|
const command = this.cli.parseArgs(argv);
|
|
42
|
+
console.log('[DEBUG] Parsed command:', command);
|
|
41
43
|
if (command.config.verbose) {
|
|
42
44
|
this.proxyManager.setLogging(true);
|
|
43
45
|
}
|
|
46
|
+
console.log('[DEBUG] Ensuring proxy is ready...');
|
|
44
47
|
await this.ensureProxyReady();
|
|
45
48
|
if (this.needsConnection(command.name)) {
|
|
49
|
+
console.log('[DEBUG] Command needs connection, ensuring connection...');
|
|
46
50
|
await this.ensureConnection(command);
|
|
47
51
|
}
|
|
52
|
+
console.log('[DEBUG] Executing command via CLI interface...');
|
|
48
53
|
const result = await this.cli.execute(command);
|
|
54
|
+
console.log('[DEBUG] Command execution result:', result);
|
|
49
55
|
this.outputResult(result, command);
|
|
50
56
|
return result.exitCode || (result.success ? CommandRouter_1.ExitCode.SUCCESS : CommandRouter_1.ExitCode.GENERAL_ERROR);
|
|
51
57
|
}
|
|
52
58
|
catch (error) {
|
|
59
|
+
console.log('[DEBUG] Error in CLIApplication.run:', error);
|
|
53
60
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
54
61
|
console.error(`Error: ${errorMessage}`);
|
|
55
62
|
return CommandRouter_1.ExitCode.GENERAL_ERROR;
|
|
@@ -169,14 +169,18 @@ class CommandRouter {
|
|
|
169
169
|
}
|
|
170
170
|
async executeWithTimeout(handler, command) {
|
|
171
171
|
const timeout = command.config.timeout;
|
|
172
|
+
console.log(`[DEBUG] CommandRouter.executeWithTimeout called for command: ${command.name}, timeout: ${timeout}ms`);
|
|
172
173
|
const timeoutPromise = new Promise((_, reject) => {
|
|
173
174
|
setTimeout(() => {
|
|
175
|
+
console.log(`[DEBUG] Command timeout reached for: ${command.name} after ${timeout}ms`);
|
|
174
176
|
reject(new Error(`Command timeout after ${timeout}ms`));
|
|
175
177
|
}, timeout);
|
|
176
178
|
});
|
|
179
|
+
console.log(`[DEBUG] Starting handler execution for: ${command.name}`);
|
|
177
180
|
const executionPromise = handler.execute(this.client, command.args);
|
|
178
181
|
try {
|
|
179
182
|
const result = await Promise.race([executionPromise, timeoutPromise]);
|
|
183
|
+
console.log(`[DEBUG] Command completed successfully for: ${command.name}`);
|
|
180
184
|
if (!result || typeof result !== 'object') {
|
|
181
185
|
return {
|
|
182
186
|
success: false,
|
|
@@ -190,6 +194,7 @@ class CommandRouter {
|
|
|
190
194
|
return result;
|
|
191
195
|
}
|
|
192
196
|
catch (error) {
|
|
197
|
+
console.log(`[DEBUG] Command execution error for: ${command.name}:`, error);
|
|
193
198
|
if (error instanceof Error && error.message.includes('timeout')) {
|
|
194
199
|
return {
|
|
195
200
|
success: false,
|
|
@@ -172,25 +172,32 @@ class ProxyClient {
|
|
|
172
172
|
if (!this.connectionId) {
|
|
173
173
|
throw new Error('No active connection. Call connect() first.');
|
|
174
174
|
}
|
|
175
|
+
console.log(`[DEBUG] Creating WebSocket proxy for connection: ${this.connectionId}`);
|
|
175
176
|
try {
|
|
176
177
|
const wsUrl = this.config.proxyUrl.replace('http://', 'ws://').replace('https://', 'wss://');
|
|
177
|
-
const
|
|
178
|
+
const fullWsUrl = `${wsUrl}/ws/${this.connectionId}`;
|
|
179
|
+
console.log(`[DEBUG] WebSocket URL: ${fullWsUrl}`);
|
|
180
|
+
const ws = new ws_1.WebSocket(fullWsUrl);
|
|
178
181
|
return new Promise((resolve, reject) => {
|
|
179
182
|
const timeout = setTimeout(() => {
|
|
183
|
+
console.log(`[DEBUG] WebSocket connection timeout for ${this.connectionId}`);
|
|
180
184
|
reject(new Error('WebSocket connection timeout'));
|
|
181
185
|
}, 10000);
|
|
182
186
|
ws.on('open', () => {
|
|
187
|
+
console.log(`[DEBUG] WebSocket connection opened for ${this.connectionId}`);
|
|
183
188
|
clearTimeout(timeout);
|
|
184
189
|
this.wsConnection = ws;
|
|
185
190
|
resolve(ws);
|
|
186
191
|
});
|
|
187
192
|
ws.on('error', (error) => {
|
|
193
|
+
console.log(`[DEBUG] WebSocket connection error for ${this.connectionId}:`, error);
|
|
188
194
|
clearTimeout(timeout);
|
|
189
195
|
reject(error);
|
|
190
196
|
});
|
|
191
197
|
});
|
|
192
198
|
}
|
|
193
199
|
catch (error) {
|
|
200
|
+
console.log(`[DEBUG] Failed to create WebSocket proxy for ${this.connectionId}:`, error);
|
|
194
201
|
throw new Error(`Failed to create WebSocket proxy: ${error instanceof Error ? error.message : error}`);
|
|
195
202
|
}
|
|
196
203
|
}
|
|
@@ -222,6 +229,23 @@ class ProxyClient {
|
|
|
222
229
|
}
|
|
223
230
|
async disconnect() {
|
|
224
231
|
try {
|
|
232
|
+
if (this.connectionId) {
|
|
233
|
+
try {
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
236
|
+
await (0, node_fetch_1.default)(`${this.config.proxyUrl}/api/client/release`, {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: {
|
|
239
|
+
'Content-Type': 'application/json',
|
|
240
|
+
'x-client-id': `proxy_client_${Date.now()}`
|
|
241
|
+
},
|
|
242
|
+
signal: controller.signal
|
|
243
|
+
});
|
|
244
|
+
clearTimeout(timeout);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
225
249
|
if (this.wsConnection) {
|
|
226
250
|
this.wsConnection.close();
|
|
227
251
|
this.wsConnection = undefined;
|
|
@@ -8,12 +8,13 @@ const ProxyClient_1 = require("../client/ProxyClient");
|
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
10
10
|
class EvaluateScriptHandler {
|
|
11
|
-
constructor(useProxy =
|
|
11
|
+
constructor(useProxy = false) {
|
|
12
12
|
this.name = 'eval';
|
|
13
13
|
this.proxyClient = new ProxyClient_1.ProxyClient();
|
|
14
14
|
this.useProxy = useProxy;
|
|
15
15
|
}
|
|
16
16
|
async execute(client, args) {
|
|
17
|
+
console.log('[DEBUG] EvaluateScriptHandler.execute called with args:', args);
|
|
17
18
|
const scriptArgs = args;
|
|
18
19
|
if (!scriptArgs.expression && !scriptArgs.file) {
|
|
19
20
|
return {
|
|
@@ -27,12 +28,18 @@ class EvaluateScriptHandler {
|
|
|
27
28
|
error: 'Cannot specify both "expression" and "file" arguments'
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
console.log('[DEBUG] Arguments validated, useProxy:', this.useProxy);
|
|
30
32
|
try {
|
|
31
33
|
if (this.useProxy) {
|
|
34
|
+
console.log('[DEBUG] Checking proxy availability...');
|
|
32
35
|
const proxyAvailable = await this.proxyClient.isProxyAvailable();
|
|
36
|
+
console.log('[DEBUG] Proxy available:', proxyAvailable);
|
|
33
37
|
if (proxyAvailable) {
|
|
34
38
|
console.log('[INFO] Using proxy connection for script evaluation');
|
|
35
|
-
|
|
39
|
+
console.log('[DEBUG] About to call executeWithProxy...');
|
|
40
|
+
const result = await this.executeWithProxy(scriptArgs);
|
|
41
|
+
console.log('[DEBUG] executeWithProxy returned:', result);
|
|
42
|
+
return result;
|
|
36
43
|
}
|
|
37
44
|
else {
|
|
38
45
|
console.warn('[WARN] Proxy not available, falling back to direct CDP connection');
|
|
@@ -42,10 +49,12 @@ class EvaluateScriptHandler {
|
|
|
42
49
|
catch (error) {
|
|
43
50
|
console.warn('[WARN] Proxy execution failed, falling back to direct CDP:', error instanceof Error ? error.message : error);
|
|
44
51
|
}
|
|
52
|
+
console.log('[DEBUG] Falling back to direct CDP');
|
|
45
53
|
return await this.executeWithDirectCDP(client, scriptArgs);
|
|
46
54
|
}
|
|
47
55
|
async executeWithProxy(scriptArgs) {
|
|
48
56
|
try {
|
|
57
|
+
console.log('[DEBUG] Starting executeWithProxy');
|
|
49
58
|
let expression;
|
|
50
59
|
if (scriptArgs.file) {
|
|
51
60
|
expression = await this.readScriptFile(scriptArgs.file);
|
|
@@ -53,83 +62,38 @@ class EvaluateScriptHandler {
|
|
|
53
62
|
else {
|
|
54
63
|
expression = scriptArgs.expression;
|
|
55
64
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
const result = await response.json();
|
|
61
|
-
if (!result.success || !result.data.connections || result.data.connections.length === 0) {
|
|
62
|
-
throw new Error('No active proxy connections found');
|
|
63
|
-
}
|
|
64
|
-
const connection = result.data.connections.find((conn) => conn.isHealthy);
|
|
65
|
-
if (!connection) {
|
|
66
|
-
throw new Error('No healthy proxy connections found');
|
|
67
|
-
}
|
|
68
|
-
console.log(`[INFO] Using existing proxy connection: ${connection.id}`);
|
|
69
|
-
this.proxyClient.connectionId = connection.id;
|
|
70
|
-
const ws = await this.proxyClient.createWebSocketProxy();
|
|
65
|
+
console.log('[DEBUG] Expression to execute:', expression.substring(0, 100));
|
|
66
|
+
console.log('[DEBUG] Creating new proxy connection...');
|
|
67
|
+
const connectionId = await this.proxyClient.connect('localhost', 9222);
|
|
68
|
+
console.log(`[DEBUG] Created new proxy connection: ${connectionId}`);
|
|
71
69
|
try {
|
|
72
|
-
const result = await this.
|
|
70
|
+
const result = await this.executeScriptThroughHTTP(connectionId, expression, scriptArgs);
|
|
73
71
|
return {
|
|
74
72
|
success: true,
|
|
75
73
|
data: result
|
|
76
74
|
};
|
|
77
75
|
}
|
|
78
76
|
finally {
|
|
79
|
-
|
|
77
|
+
await this.proxyClient.disconnect();
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
catch (error) {
|
|
81
|
+
console.log('[DEBUG] Error in executeWithProxy:', error);
|
|
83
82
|
return {
|
|
84
83
|
success: false,
|
|
85
84
|
error: error instanceof Error ? error.message : String(error)
|
|
86
85
|
};
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
|
-
async
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
ws.on('message', (data) => {
|
|
99
|
-
try {
|
|
100
|
-
const response = JSON.parse(data.toString());
|
|
101
|
-
if (response.id === commandId) {
|
|
102
|
-
clearTimeout(timeoutHandle);
|
|
103
|
-
if (response.error) {
|
|
104
|
-
reject(new Error(`CDP Error: ${response.error.message}`));
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const result = response.result;
|
|
108
|
-
if (result.exceptionDetails) {
|
|
109
|
-
const error = new Error(result.result?.description || 'Script execution failed');
|
|
110
|
-
error.exceptionDetails = result.exceptionDetails;
|
|
111
|
-
reject(error);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
let value = result.result?.value;
|
|
115
|
-
if (result.result?.type === 'undefined') {
|
|
116
|
-
value = undefined;
|
|
117
|
-
}
|
|
118
|
-
else if (result.result?.unserializableValue) {
|
|
119
|
-
value = result.result.unserializableValue;
|
|
120
|
-
}
|
|
121
|
-
resolve(value);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
clearTimeout(timeoutHandle);
|
|
126
|
-
reject(new Error(`Failed to parse CDP response: ${error instanceof Error ? error.message : error}`));
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
ws.on('error', (error) => {
|
|
130
|
-
clearTimeout(timeoutHandle);
|
|
131
|
-
reject(new Error(`WebSocket error: ${error.message}`));
|
|
132
|
-
});
|
|
88
|
+
async executeScriptThroughHTTP(connectionId, expression, args) {
|
|
89
|
+
const timeout = args.timeout || 30000;
|
|
90
|
+
const awaitPromise = args.awaitPromise ?? true;
|
|
91
|
+
const returnByValue = args.returnByValue ?? true;
|
|
92
|
+
console.log(`[DEBUG] Starting HTTP script execution, timeout: ${timeout}ms`);
|
|
93
|
+
console.log(`[DEBUG] Expression: ${expression.substring(0, 100)}${expression.length > 100 ? '...' : ''}`);
|
|
94
|
+
try {
|
|
95
|
+
const proxyUrl = this.proxyClient.getConfig().proxyUrl;
|
|
96
|
+
const commandId = Date.now() + Math.floor(Math.random() * 10000);
|
|
133
97
|
const command = {
|
|
134
98
|
id: commandId,
|
|
135
99
|
method: 'Runtime.evaluate',
|
|
@@ -141,8 +105,61 @@ class EvaluateScriptHandler {
|
|
|
141
105
|
generatePreview: false
|
|
142
106
|
}
|
|
143
107
|
};
|
|
144
|
-
|
|
145
|
-
|
|
108
|
+
console.log(`[DEBUG] Sending HTTP command to ${proxyUrl}/api/execute/${connectionId}`);
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeout);
|
|
111
|
+
try {
|
|
112
|
+
const response = await (0, node_fetch_1.default)(`${proxyUrl}/api/execute/${connectionId}`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
'x-client-id': `eval_handler_${Date.now()}`
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
command,
|
|
120
|
+
timeout
|
|
121
|
+
}),
|
|
122
|
+
signal: controller.signal
|
|
123
|
+
});
|
|
124
|
+
clearTimeout(timeoutHandle);
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const errorData = await response.json().catch(() => ({}));
|
|
127
|
+
throw new Error(`HTTP ${response.status}: ${errorData.error || response.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
const result = await response.json();
|
|
130
|
+
console.log(`[DEBUG] HTTP command response:`, result);
|
|
131
|
+
if (!result.success) {
|
|
132
|
+
throw new Error(`Command execution failed: ${result.error || 'Unknown error'}`);
|
|
133
|
+
}
|
|
134
|
+
const commandResult = result.data.result;
|
|
135
|
+
if (result.data.error) {
|
|
136
|
+
throw new Error(`CDP Error: ${result.data.error.message}`);
|
|
137
|
+
}
|
|
138
|
+
if (commandResult.exceptionDetails) {
|
|
139
|
+
console.log(`[DEBUG] Exception details:`, commandResult.exceptionDetails);
|
|
140
|
+
const error = new Error(commandResult.result?.description || 'Script execution failed');
|
|
141
|
+
error.exceptionDetails = commandResult.exceptionDetails;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
let value = commandResult.result?.value;
|
|
145
|
+
if (commandResult.result?.type === 'undefined') {
|
|
146
|
+
value = undefined;
|
|
147
|
+
}
|
|
148
|
+
else if (commandResult.result?.unserializableValue) {
|
|
149
|
+
value = commandResult.result.unserializableValue;
|
|
150
|
+
}
|
|
151
|
+
console.log(`[DEBUG] Successful HTTP result:`, value);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
clearTimeout(timeoutHandle);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.log(`[DEBUG] Error in HTTP script execution:`, error);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
146
163
|
}
|
|
147
164
|
async executeWithDirectCDP(client, scriptArgs) {
|
|
148
165
|
try {
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommandExecutionService = void 0;
|
|
4
|
+
const logger_1 = require("../../utils/logger");
|
|
5
|
+
class CommandExecutionService {
|
|
6
|
+
constructor(connectionPool) {
|
|
7
|
+
this.pendingCommands = new Map();
|
|
8
|
+
this.commandMetrics = {
|
|
9
|
+
totalCommands: 0,
|
|
10
|
+
successfulCommands: 0,
|
|
11
|
+
failedCommands: 0,
|
|
12
|
+
averageExecutionTime: 0,
|
|
13
|
+
timeoutCount: 0
|
|
14
|
+
};
|
|
15
|
+
this.messageHandlers = new Map();
|
|
16
|
+
this.activeCLIClient = null;
|
|
17
|
+
this.connectionPool = connectionPool;
|
|
18
|
+
this.logger = (0, logger_1.createLogger)({ component: 'CommandExecutionService' });
|
|
19
|
+
}
|
|
20
|
+
async executeCommand(request, clientId) {
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
const commandId = this.generateCommandId();
|
|
23
|
+
const timeout = request.timeout || 30000;
|
|
24
|
+
try {
|
|
25
|
+
if (this.activeCLIClient && clientId && this.activeCLIClient !== clientId) {
|
|
26
|
+
throw new Error('Another CLI client is already connected. Only one CLI client can use the proxy at a time.');
|
|
27
|
+
}
|
|
28
|
+
if (clientId && !this.activeCLIClient) {
|
|
29
|
+
this.activeCLIClient = clientId;
|
|
30
|
+
this.logger.info(`CLI client ${clientId} is now the active client`);
|
|
31
|
+
}
|
|
32
|
+
const connection = this.connectionPool.getConnectionInfo(request.connectionId);
|
|
33
|
+
if (!connection) {
|
|
34
|
+
throw new Error(`Connection ${request.connectionId} not found`);
|
|
35
|
+
}
|
|
36
|
+
if (!connection.isHealthy) {
|
|
37
|
+
throw new Error(`Connection ${request.connectionId} is not healthy`);
|
|
38
|
+
}
|
|
39
|
+
this.logger.debug(`Executing CDP command: ${request.command.method}`, {
|
|
40
|
+
connectionId: request.connectionId,
|
|
41
|
+
commandId,
|
|
42
|
+
method: request.command.method,
|
|
43
|
+
timeout,
|
|
44
|
+
clientId: clientId || 'unknown'
|
|
45
|
+
});
|
|
46
|
+
const cdpMessage = {
|
|
47
|
+
id: typeof request.command.id === 'number' ? request.command.id : this.generateCDPMessageId(),
|
|
48
|
+
method: request.command.method,
|
|
49
|
+
params: request.command.params
|
|
50
|
+
};
|
|
51
|
+
const result = await this.sendCDPCommand(request.connectionId, cdpMessage, timeout);
|
|
52
|
+
const executionTime = Date.now() - startTime;
|
|
53
|
+
this.updateMetrics(true, executionTime);
|
|
54
|
+
this.logger.debug(`CDP command executed successfully: ${request.command.method}`, {
|
|
55
|
+
connectionId: request.connectionId,
|
|
56
|
+
commandId,
|
|
57
|
+
executionTime,
|
|
58
|
+
clientId: clientId || 'unknown'
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
result,
|
|
63
|
+
executionTime
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const executionTime = Date.now() - startTime;
|
|
68
|
+
this.updateMetrics(false, executionTime);
|
|
69
|
+
this.logger.error(`CDP command execution failed: ${request.command.method}`, {
|
|
70
|
+
connectionId: request.connectionId,
|
|
71
|
+
commandId,
|
|
72
|
+
executionTime,
|
|
73
|
+
clientId: clientId || 'unknown',
|
|
74
|
+
error: error instanceof Error ? error.message : String(error)
|
|
75
|
+
});
|
|
76
|
+
let errorCode = 500;
|
|
77
|
+
let errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
78
|
+
if (errorMessage.includes('Another CLI client')) {
|
|
79
|
+
errorCode = 409;
|
|
80
|
+
}
|
|
81
|
+
else if (errorMessage.includes('timeout')) {
|
|
82
|
+
errorCode = 408;
|
|
83
|
+
this.commandMetrics.timeoutCount++;
|
|
84
|
+
}
|
|
85
|
+
else if (errorMessage.includes('not found')) {
|
|
86
|
+
errorCode = 404;
|
|
87
|
+
}
|
|
88
|
+
else if (errorMessage.includes('not healthy')) {
|
|
89
|
+
errorCode = 503;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: {
|
|
94
|
+
code: errorCode,
|
|
95
|
+
message: errorMessage
|
|
96
|
+
},
|
|
97
|
+
executionTime
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async sendCDPCommand(connectionId, command, timeout) {
|
|
102
|
+
const connection = this.connectionPool.getConnectionInfo(connectionId);
|
|
103
|
+
if (!connection) {
|
|
104
|
+
throw new Error(`Connection ${connectionId} not found`);
|
|
105
|
+
}
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const commandKey = `${connectionId}:${command.id}`;
|
|
108
|
+
const pendingCommand = {
|
|
109
|
+
id: commandKey,
|
|
110
|
+
connectionId,
|
|
111
|
+
command,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
timeout,
|
|
114
|
+
resolve,
|
|
115
|
+
reject
|
|
116
|
+
};
|
|
117
|
+
this.pendingCommands.set(commandKey, pendingCommand);
|
|
118
|
+
this.setupMessageHandler(connectionId);
|
|
119
|
+
const timeoutHandle = setTimeout(() => {
|
|
120
|
+
if (this.pendingCommands.has(commandKey)) {
|
|
121
|
+
this.pendingCommands.delete(commandKey);
|
|
122
|
+
reject(new Error(`Command timeout after ${timeout}ms: ${command.method}`));
|
|
123
|
+
}
|
|
124
|
+
}, timeout);
|
|
125
|
+
pendingCommand.resolve = (value) => {
|
|
126
|
+
clearTimeout(timeoutHandle);
|
|
127
|
+
resolve(value);
|
|
128
|
+
};
|
|
129
|
+
pendingCommand.reject = (error) => {
|
|
130
|
+
clearTimeout(timeoutHandle);
|
|
131
|
+
reject(error);
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
const messageStr = JSON.stringify(command);
|
|
135
|
+
connection.connection.send(messageStr);
|
|
136
|
+
this.logger.debug(`Sent CDP command to Chrome: ${command.method}`, {
|
|
137
|
+
connectionId,
|
|
138
|
+
commandId: command.id,
|
|
139
|
+
messageLength: messageStr.length
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
clearTimeout(timeoutHandle);
|
|
144
|
+
this.pendingCommands.delete(commandKey);
|
|
145
|
+
reject(new Error(`Failed to send command: ${error instanceof Error ? error.message : String(error)}`));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
setupMessageHandler(connectionId) {
|
|
150
|
+
if (this.messageHandlers.has(connectionId)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const connection = this.connectionPool.getConnectionInfo(connectionId);
|
|
154
|
+
if (!connection) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const messageHandler = (data) => {
|
|
158
|
+
try {
|
|
159
|
+
const message = data.toString();
|
|
160
|
+
let cdpResponse;
|
|
161
|
+
try {
|
|
162
|
+
cdpResponse = JSON.parse(message);
|
|
163
|
+
}
|
|
164
|
+
catch (parseError) {
|
|
165
|
+
this.logger.warn(`Invalid JSON from CDP connection ${connectionId}: ${message.substring(0, 100)}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (typeof cdpResponse.id === 'undefined') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const commandKey = `${connectionId}:${cdpResponse.id}`;
|
|
172
|
+
const pendingCommand = this.pendingCommands.get(commandKey);
|
|
173
|
+
if (pendingCommand) {
|
|
174
|
+
this.pendingCommands.delete(commandKey);
|
|
175
|
+
this.logger.debug(`Received CDP response for command: ${pendingCommand.command.method}`, {
|
|
176
|
+
connectionId,
|
|
177
|
+
commandId: cdpResponse.id,
|
|
178
|
+
hasResult: !!cdpResponse.result,
|
|
179
|
+
hasError: !!cdpResponse.error
|
|
180
|
+
});
|
|
181
|
+
if (cdpResponse.error) {
|
|
182
|
+
pendingCommand.reject(new Error(`CDP Error: ${cdpResponse.error.message} (Code: ${cdpResponse.error.code})`));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
pendingCommand.resolve(cdpResponse.result);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.logger.debug(`Received CDP response for unknown command ID: ${cdpResponse.id}`, {
|
|
190
|
+
connectionId
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
this.logger.error(`Error handling CDP message for connection ${connectionId}:`, error);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
connection.connection.on('message', messageHandler);
|
|
199
|
+
this.messageHandlers.set(connectionId, messageHandler);
|
|
200
|
+
this.logger.debug(`Set up message handler for connection: ${connectionId}`);
|
|
201
|
+
connection.connection.on('close', () => {
|
|
202
|
+
this.cleanupMessageHandler(connectionId);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
cleanupMessageHandler(connectionId) {
|
|
206
|
+
const handler = this.messageHandlers.get(connectionId);
|
|
207
|
+
if (handler) {
|
|
208
|
+
const connection = this.connectionPool.getConnectionInfo(connectionId);
|
|
209
|
+
if (connection) {
|
|
210
|
+
connection.connection.off('message', handler);
|
|
211
|
+
}
|
|
212
|
+
this.messageHandlers.delete(connectionId);
|
|
213
|
+
this.logger.debug(`Cleaned up message handler for connection: ${connectionId}`);
|
|
214
|
+
}
|
|
215
|
+
const commandsToCleanup = [];
|
|
216
|
+
for (const [, pendingCommand] of this.pendingCommands.entries()) {
|
|
217
|
+
if (pendingCommand.connectionId === connectionId) {
|
|
218
|
+
commandsToCleanup.push(pendingCommand.id);
|
|
219
|
+
pendingCommand.reject(new Error('Connection closed'));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const commandKey of commandsToCleanup) {
|
|
223
|
+
this.pendingCommands.delete(commandKey);
|
|
224
|
+
}
|
|
225
|
+
if (commandsToCleanup.length > 0) {
|
|
226
|
+
this.logger.debug(`Cleaned up ${commandsToCleanup.length} pending commands for connection: ${connectionId}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
getMetrics() {
|
|
230
|
+
return { ...this.commandMetrics };
|
|
231
|
+
}
|
|
232
|
+
getPendingCommandsCount() {
|
|
233
|
+
return this.pendingCommands.size;
|
|
234
|
+
}
|
|
235
|
+
getPendingCommandsForConnection(connectionId) {
|
|
236
|
+
let count = 0;
|
|
237
|
+
for (const pendingCommand of this.pendingCommands.values()) {
|
|
238
|
+
if (pendingCommand.connectionId === connectionId) {
|
|
239
|
+
count++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return count;
|
|
243
|
+
}
|
|
244
|
+
setActiveCLIClient(clientId) {
|
|
245
|
+
if (this.activeCLIClient && this.activeCLIClient !== clientId) {
|
|
246
|
+
this.logger.warn(`Replacing active CLI client ${this.activeCLIClient} with ${clientId}`);
|
|
247
|
+
}
|
|
248
|
+
this.activeCLIClient = clientId;
|
|
249
|
+
this.logger.info(`CLI client ${clientId} is now the active client`);
|
|
250
|
+
}
|
|
251
|
+
releaseActiveCLIClient(clientId) {
|
|
252
|
+
if (clientId && this.activeCLIClient !== clientId) {
|
|
253
|
+
this.logger.warn(`Attempted to release CLI client ${clientId}, but active client is ${this.activeCLIClient}`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const previousClient = this.activeCLIClient;
|
|
257
|
+
this.activeCLIClient = null;
|
|
258
|
+
if (previousClient) {
|
|
259
|
+
this.logger.info(`Released active CLI client: ${previousClient}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
getActiveCLIClient() {
|
|
263
|
+
return this.activeCLIClient;
|
|
264
|
+
}
|
|
265
|
+
hasActiveCLIClient() {
|
|
266
|
+
return this.activeCLIClient !== null;
|
|
267
|
+
}
|
|
268
|
+
cleanup() {
|
|
269
|
+
for (const [, pendingCommand] of this.pendingCommands.entries()) {
|
|
270
|
+
pendingCommand.reject(new Error('Service shutting down'));
|
|
271
|
+
}
|
|
272
|
+
this.pendingCommands.clear();
|
|
273
|
+
for (const connectionId of this.messageHandlers.keys()) {
|
|
274
|
+
this.cleanupMessageHandler(connectionId);
|
|
275
|
+
}
|
|
276
|
+
this.activeCLIClient = null;
|
|
277
|
+
this.logger.info('CommandExecutionService cleanup completed');
|
|
278
|
+
}
|
|
279
|
+
generateCommandId() {
|
|
280
|
+
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
281
|
+
}
|
|
282
|
+
generateCDPMessageId() {
|
|
283
|
+
return Math.floor(Date.now() % 1000000) + Math.floor(Math.random() * 10000);
|
|
284
|
+
}
|
|
285
|
+
updateMetrics(success, executionTime) {
|
|
286
|
+
this.commandMetrics.totalCommands++;
|
|
287
|
+
if (success) {
|
|
288
|
+
this.commandMetrics.successfulCommands++;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
this.commandMetrics.failedCommands++;
|
|
292
|
+
}
|
|
293
|
+
const totalTime = this.commandMetrics.averageExecutionTime * (this.commandMetrics.totalCommands - 1) + executionTime;
|
|
294
|
+
this.commandMetrics.averageExecutionTime = totalTime / this.commandMetrics.totalCommands;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
exports.CommandExecutionService = CommandExecutionService;
|
|
@@ -172,29 +172,36 @@ class ConnectionPool {
|
|
|
172
172
|
connection.isHealthy = false;
|
|
173
173
|
return false;
|
|
174
174
|
}
|
|
175
|
-
const messageId = Math.floor(Date.now() % 1000000) + Math.floor(Math.random() *
|
|
175
|
+
const messageId = Math.floor(Date.now() % 1000000) + Math.floor(Math.random() * 10000);
|
|
176
176
|
const testMessage = {
|
|
177
177
|
id: messageId,
|
|
178
178
|
method: 'Runtime.evaluate',
|
|
179
179
|
params: { expression: '1+1' }
|
|
180
180
|
};
|
|
181
|
+
console.log(`[DEBUG] Health check starting for connection ${connectionId} with message ID: ${messageId}`);
|
|
181
182
|
const healthCheckPromise = new Promise((resolve) => {
|
|
182
183
|
const timeout = setTimeout(() => {
|
|
184
|
+
console.log(`[DEBUG] Health check timeout for connection ${connectionId}, message ID: ${messageId}`);
|
|
183
185
|
resolve(false);
|
|
184
186
|
}, 5000);
|
|
185
187
|
const messageHandler = (data) => {
|
|
186
188
|
try {
|
|
187
189
|
const response = JSON.parse(data.toString());
|
|
190
|
+
console.log(`[DEBUG] Health check received message for connection ${connectionId}, response ID: ${response.id}, expected: ${messageId}`);
|
|
188
191
|
if (response.id === testMessage.id) {
|
|
189
192
|
clearTimeout(timeout);
|
|
190
193
|
connection.connection.off('message', messageHandler);
|
|
191
|
-
|
|
194
|
+
const isHealthy = response.result && !response.error;
|
|
195
|
+
console.log(`[DEBUG] Health check result for connection ${connectionId}: ${isHealthy}`);
|
|
196
|
+
resolve(isHealthy);
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
catch (error) {
|
|
200
|
+
console.log(`[DEBUG] Health check message parsing error for connection ${connectionId}:`, error);
|
|
195
201
|
}
|
|
196
202
|
};
|
|
197
203
|
connection.connection.on('message', messageHandler);
|
|
204
|
+
console.log(`[DEBUG] Sending health check command for connection ${connectionId}:`, testMessage);
|
|
198
205
|
connection.connection.send(JSON.stringify(testMessage));
|
|
199
206
|
});
|
|
200
207
|
const isHealthy = await healthCheckPromise;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ProxyAPIServer = void 0;
|
|
4
4
|
const SecurityManager_1 = require("./SecurityManager");
|
|
5
|
+
const CommandExecutionService_1 = require("./CommandExecutionService");
|
|
5
6
|
const logger_1 = require("../../utils/logger");
|
|
6
7
|
class ProxyAPIServer {
|
|
7
8
|
constructor(connectionPool, messageStore, healthMonitor, performanceMonitor, securityManager) {
|
|
@@ -10,6 +11,7 @@ class ProxyAPIServer {
|
|
|
10
11
|
this.healthMonitor = healthMonitor;
|
|
11
12
|
this.performanceMonitor = performanceMonitor;
|
|
12
13
|
this.securityManager = securityManager || new SecurityManager_1.SecurityManager();
|
|
14
|
+
this.commandExecutionService = new CommandExecutionService_1.CommandExecutionService(connectionPool);
|
|
13
15
|
this.logger = new logger_1.Logger();
|
|
14
16
|
}
|
|
15
17
|
setupRoutes(app) {
|
|
@@ -20,6 +22,9 @@ class ProxyAPIServer {
|
|
|
20
22
|
app.get('/api/console/:connectionId', this.validateConnectionId.bind(this), this.handleGetConsoleMessages.bind(this));
|
|
21
23
|
app.get('/api/network/:connectionId', this.validateConnectionId.bind(this), this.handleGetNetworkRequests.bind(this));
|
|
22
24
|
app.get('/api/health/:connectionId', this.validateConnectionId.bind(this), this.handleHealthCheck.bind(this));
|
|
25
|
+
app.post('/api/execute/:connectionId', this.securityManager.getStrictRateLimiter(), this.validateConnectionId.bind(this), this.validateCommandExecutionRequest.bind(this), this.handleCommandExecution.bind(this));
|
|
26
|
+
app.post('/api/client/release', this.handleReleaseCLIClient.bind(this));
|
|
27
|
+
app.get('/api/client/status', this.handleGetCLIClientStatus.bind(this));
|
|
23
28
|
app.get('/api/health', this.handleServerHealth.bind(this));
|
|
24
29
|
app.get('/api/health/detailed', this.handleDetailedHealthCheck.bind(this));
|
|
25
30
|
app.get('/api/health/statistics', this.handleHealthStatistics.bind(this));
|
|
@@ -33,6 +38,13 @@ class ProxyAPIServer {
|
|
|
33
38
|
app.get('/api/status', this.handleServerStatus.bind(this));
|
|
34
39
|
this.logger.info('API routes configured with enhanced security measures');
|
|
35
40
|
}
|
|
41
|
+
cleanup() {
|
|
42
|
+
this.commandExecutionService.cleanup();
|
|
43
|
+
this.logger.info('ProxyAPIServer cleanup completed');
|
|
44
|
+
}
|
|
45
|
+
getCommandExecutionService() {
|
|
46
|
+
return this.commandExecutionService;
|
|
47
|
+
}
|
|
36
48
|
async handleConnect(req, res) {
|
|
37
49
|
try {
|
|
38
50
|
const { host, port, targetId } = req.body;
|
|
@@ -245,6 +257,78 @@ class ProxyAPIServer {
|
|
|
245
257
|
});
|
|
246
258
|
}
|
|
247
259
|
}
|
|
260
|
+
async handleCommandExecution(req, res) {
|
|
261
|
+
try {
|
|
262
|
+
const { connectionId } = req.params;
|
|
263
|
+
const { command, timeout } = req.body;
|
|
264
|
+
const clientId = req.headers['x-client-id'] || `client_${req.ip}_${Date.now()}`;
|
|
265
|
+
const connection = this.connectionPool.getConnectionInfo(connectionId);
|
|
266
|
+
if (!connection) {
|
|
267
|
+
res.status(404).json({
|
|
268
|
+
success: false,
|
|
269
|
+
error: 'Connection not found',
|
|
270
|
+
timestamp: Date.now()
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!connection.isHealthy) {
|
|
275
|
+
res.status(503).json({
|
|
276
|
+
success: false,
|
|
277
|
+
error: 'Connection is not healthy',
|
|
278
|
+
timestamp: Date.now()
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.logger.debug(`Executing CDP command: ${command.method}`, {
|
|
283
|
+
connectionId,
|
|
284
|
+
method: command.method,
|
|
285
|
+
hasParams: !!command.params,
|
|
286
|
+
timeout: timeout || 30000,
|
|
287
|
+
clientId
|
|
288
|
+
});
|
|
289
|
+
const executionRequest = {
|
|
290
|
+
connectionId,
|
|
291
|
+
command,
|
|
292
|
+
timeout
|
|
293
|
+
};
|
|
294
|
+
const result = await this.commandExecutionService.executeCommand(executionRequest, clientId);
|
|
295
|
+
const statusCode = result.success ? 200 : (result.error?.code || 500);
|
|
296
|
+
res.status(statusCode).json({
|
|
297
|
+
success: result.success,
|
|
298
|
+
data: result,
|
|
299
|
+
timestamp: Date.now()
|
|
300
|
+
});
|
|
301
|
+
this.logger.debug(`CDP command execution completed: ${command.method}`, {
|
|
302
|
+
connectionId,
|
|
303
|
+
success: result.success,
|
|
304
|
+
executionTime: result.executionTime,
|
|
305
|
+
statusCode,
|
|
306
|
+
clientId
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.logger.error('Command execution API error:', error);
|
|
311
|
+
let statusCode = 500;
|
|
312
|
+
let errorMessage = error instanceof Error ? error.message : 'Command execution failed';
|
|
313
|
+
if (errorMessage.includes('Another CLI client')) {
|
|
314
|
+
statusCode = 409;
|
|
315
|
+
}
|
|
316
|
+
else if (errorMessage.includes('timeout')) {
|
|
317
|
+
statusCode = 408;
|
|
318
|
+
}
|
|
319
|
+
else if (errorMessage.includes('not found')) {
|
|
320
|
+
statusCode = 404;
|
|
321
|
+
}
|
|
322
|
+
else if (errorMessage.includes('not healthy')) {
|
|
323
|
+
statusCode = 503;
|
|
324
|
+
}
|
|
325
|
+
res.status(statusCode).json({
|
|
326
|
+
success: false,
|
|
327
|
+
error: errorMessage,
|
|
328
|
+
timestamp: Date.now()
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
248
332
|
async handleListConnections(_req, res) {
|
|
249
333
|
try {
|
|
250
334
|
const connections = this.connectionPool.getAllConnections();
|
|
@@ -665,6 +749,85 @@ class ProxyAPIServer {
|
|
|
665
749
|
}
|
|
666
750
|
next();
|
|
667
751
|
}
|
|
752
|
+
validateCommandExecutionRequest(req, res, next) {
|
|
753
|
+
const { command, timeout } = req.body;
|
|
754
|
+
if (!command || typeof command !== 'object') {
|
|
755
|
+
this.logger.logSecurityEvent('invalid_command_request', 'Missing or invalid command object', {
|
|
756
|
+
command,
|
|
757
|
+
ip: req.ip
|
|
758
|
+
});
|
|
759
|
+
res.status(400).json({
|
|
760
|
+
success: false,
|
|
761
|
+
error: 'Command is required and must be an object',
|
|
762
|
+
timestamp: Date.now()
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!command.method || typeof command.method !== 'string' || command.method.trim() === '') {
|
|
767
|
+
this.logger.logSecurityEvent('invalid_command_request', 'Missing or invalid command method', {
|
|
768
|
+
method: command.method,
|
|
769
|
+
ip: req.ip
|
|
770
|
+
});
|
|
771
|
+
res.status(400).json({
|
|
772
|
+
success: false,
|
|
773
|
+
error: 'Command method is required and must be a non-empty string',
|
|
774
|
+
timestamp: Date.now()
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const methodRegex = /^[A-Za-z][A-Za-z0-9]*\.[A-Za-z][A-Za-z0-9]*$/;
|
|
779
|
+
if (!methodRegex.test(command.method)) {
|
|
780
|
+
this.logger.logSecurityEvent('invalid_command_method', 'Command method has invalid format', {
|
|
781
|
+
method: command.method,
|
|
782
|
+
ip: req.ip
|
|
783
|
+
});
|
|
784
|
+
res.status(400).json({
|
|
785
|
+
success: false,
|
|
786
|
+
error: 'Command method must be in format "Domain.method" (e.g., "Runtime.evaluate")',
|
|
787
|
+
timestamp: Date.now()
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (command.id !== undefined && (typeof command.id !== 'number' && typeof command.id !== 'string')) {
|
|
792
|
+
this.logger.logSecurityEvent('invalid_command_request', 'Invalid command ID type', {
|
|
793
|
+
commandId: command.id,
|
|
794
|
+
ip: req.ip
|
|
795
|
+
});
|
|
796
|
+
res.status(400).json({
|
|
797
|
+
success: false,
|
|
798
|
+
error: 'Command ID must be a number or string if provided',
|
|
799
|
+
timestamp: Date.now()
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (timeout !== undefined) {
|
|
804
|
+
if (typeof timeout !== 'number' || timeout <= 0 || timeout > 300000) {
|
|
805
|
+
this.logger.logSecurityEvent('invalid_command_request', 'Invalid timeout value', {
|
|
806
|
+
timeout,
|
|
807
|
+
ip: req.ip
|
|
808
|
+
});
|
|
809
|
+
res.status(400).json({
|
|
810
|
+
success: false,
|
|
811
|
+
error: 'Timeout must be a positive number between 1 and 300000 (5 minutes)',
|
|
812
|
+
timestamp: Date.now()
|
|
813
|
+
});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (command.params !== undefined && (command.params === null || (typeof command.params !== 'object' && !Array.isArray(command.params)))) {
|
|
818
|
+
this.logger.logSecurityEvent('invalid_command_request', 'Invalid command params', {
|
|
819
|
+
paramsType: typeof command.params,
|
|
820
|
+
ip: req.ip
|
|
821
|
+
});
|
|
822
|
+
res.status(400).json({
|
|
823
|
+
success: false,
|
|
824
|
+
error: 'Command params must be an object or array if provided',
|
|
825
|
+
timestamp: Date.now()
|
|
826
|
+
});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
next();
|
|
830
|
+
}
|
|
668
831
|
parseConsoleMessageFilter(query) {
|
|
669
832
|
try {
|
|
670
833
|
const filter = {};
|
|
@@ -905,5 +1068,53 @@ class ProxyAPIServer {
|
|
|
905
1068
|
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
906
1069
|
}
|
|
907
1070
|
}
|
|
1071
|
+
async handleReleaseCLIClient(req, res) {
|
|
1072
|
+
try {
|
|
1073
|
+
const clientId = req.headers['x-client-id'] || `client_${req.ip}_${Date.now()}`;
|
|
1074
|
+
this.commandExecutionService.releaseActiveCLIClient(clientId);
|
|
1075
|
+
this.logger.info(`CLI client released: ${clientId}`);
|
|
1076
|
+
res.json({
|
|
1077
|
+
success: true,
|
|
1078
|
+
data: {
|
|
1079
|
+
message: 'CLI client released successfully',
|
|
1080
|
+
clientId
|
|
1081
|
+
},
|
|
1082
|
+
timestamp: Date.now()
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
this.logger.error('Release CLI client API error:', error);
|
|
1087
|
+
res.status(500).json({
|
|
1088
|
+
success: false,
|
|
1089
|
+
error: error instanceof Error ? error.message : 'Failed to release CLI client',
|
|
1090
|
+
timestamp: Date.now()
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async handleGetCLIClientStatus(_req, res) {
|
|
1095
|
+
try {
|
|
1096
|
+
const activeCLIClient = this.commandExecutionService.getActiveCLIClient();
|
|
1097
|
+
const hasActiveClient = this.commandExecutionService.hasActiveCLIClient();
|
|
1098
|
+
const pendingCommandsCount = this.commandExecutionService.getPendingCommandsCount();
|
|
1099
|
+
res.json({
|
|
1100
|
+
success: true,
|
|
1101
|
+
data: {
|
|
1102
|
+
hasActiveClient,
|
|
1103
|
+
activeCLIClient,
|
|
1104
|
+
pendingCommandsCount,
|
|
1105
|
+
singleClientMode: true
|
|
1106
|
+
},
|
|
1107
|
+
timestamp: Date.now()
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
this.logger.error('Get CLI client status API error:', error);
|
|
1112
|
+
res.status(500).json({
|
|
1113
|
+
success: false,
|
|
1114
|
+
error: error instanceof Error ? error.message : 'Failed to get CLI client status',
|
|
1115
|
+
timestamp: Date.now()
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
908
1119
|
}
|
|
909
1120
|
exports.ProxyAPIServer = ProxyAPIServer;
|
|
@@ -236,9 +236,11 @@ class WSProxy {
|
|
|
236
236
|
if (cdpConnection.connection.readyState === WebSocket.OPEN) {
|
|
237
237
|
cdpConnection.connection.send(message);
|
|
238
238
|
proxyConnection.messageCount++;
|
|
239
|
+
console.log(`[DEBUG] Forwarded CDP command from client ${proxyConnection.id}: ${cdpCommand.method} (id: ${cdpCommand.id})`);
|
|
239
240
|
this.logger.debug(`Forwarded CDP command from client ${proxyConnection.id}: ${cdpCommand.method} (id: ${cdpCommand.id})`);
|
|
240
241
|
}
|
|
241
242
|
else {
|
|
243
|
+
console.log(`[DEBUG] CDP connection ${connectionId} is not open, readyState: ${cdpConnection.connection.readyState}`);
|
|
242
244
|
this.logger.warn(`CDP connection ${connectionId} is not open, cannot forward command`);
|
|
243
245
|
clientWs.send(JSON.stringify({
|
|
244
246
|
error: { code: -32001, message: 'CDP connection unavailable' },
|
|
@@ -266,20 +268,30 @@ class WSProxy {
|
|
|
266
268
|
return;
|
|
267
269
|
}
|
|
268
270
|
if (this.isCDPResponse(cdpMessage)) {
|
|
271
|
+
console.log(`[DEBUG] Received CDP response for client ${proxyConnection.id}: id ${cdpMessage.id}`);
|
|
269
272
|
if (clientWs.readyState === WebSocket.OPEN) {
|
|
270
273
|
clientWs.send(message);
|
|
271
274
|
proxyConnection.messageCount++;
|
|
275
|
+
console.log(`[DEBUG] Forwarded CDP response to client ${proxyConnection.id}: id ${cdpMessage.id}`);
|
|
272
276
|
this.logger.debug(`Forwarded CDP response to client ${proxyConnection.id}: id ${cdpMessage.id}`);
|
|
273
277
|
}
|
|
278
|
+
else {
|
|
279
|
+
console.log(`[DEBUG] Client WebSocket is not open for ${proxyConnection.id}, readyState: ${clientWs.readyState}`);
|
|
280
|
+
}
|
|
274
281
|
}
|
|
275
282
|
else if (this.isCDPEvent(cdpMessage)) {
|
|
283
|
+
console.log(`[DEBUG] Received CDP event: ${cdpMessage.method}`);
|
|
276
284
|
this.forwardEventToSubscribedClients(connectionId, cdpMessage, message);
|
|
277
285
|
}
|
|
286
|
+
else {
|
|
287
|
+
console.log(`[DEBUG] Received unknown CDP message type:`, cdpMessage);
|
|
288
|
+
}
|
|
278
289
|
}
|
|
279
290
|
catch (error) {
|
|
280
291
|
this.logger.error(`Error forwarding message to client ${proxyConnection.id}:`, error);
|
|
281
292
|
}
|
|
282
293
|
};
|
|
294
|
+
console.log(`[DEBUG] Adding message handler to CDP connection ${connectionId} for proxy ${proxyConnection.id}`);
|
|
283
295
|
cdpConnection.connection.on('message', cdpMessageHandler);
|
|
284
296
|
clientWs.on('close', () => {
|
|
285
297
|
cdpConnection.connection.off('message', cdpMessageHandler);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-cdp-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "LLM-first browser automation CLI via Chrome DevTools Protocol. Eval-first design optimized for AI assistants - LLMs write JavaScript scripts for rapid validation. Features: JavaScript execution, element interaction, screenshots, DOM snapshots, console/network monitoring. Built-in IDE integration for Cursor and Claude.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|