chrome-cdp-cli 1.5.0 → 1.6.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.
@@ -0,0 +1,909 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProxyAPIServer = void 0;
4
+ const SecurityManager_1 = require("./SecurityManager");
5
+ const logger_1 = require("../../utils/logger");
6
+ class ProxyAPIServer {
7
+ constructor(connectionPool, messageStore, healthMonitor, performanceMonitor, securityManager) {
8
+ this.connectionPool = connectionPool;
9
+ this.messageStore = messageStore;
10
+ this.healthMonitor = healthMonitor;
11
+ this.performanceMonitor = performanceMonitor;
12
+ this.securityManager = securityManager || new SecurityManager_1.SecurityManager();
13
+ this.logger = new logger_1.Logger();
14
+ }
15
+ setupRoutes(app) {
16
+ app.use('/api', this.securityManager.getRateLimiter());
17
+ app.post('/api/connect', this.securityManager.getStrictRateLimiter(), this.validateConnectRequest.bind(this), this.handleConnect.bind(this));
18
+ app.delete('/api/connection/:connectionId', this.securityManager.getStrictRateLimiter(), this.validateConnectionId.bind(this), this.handleCloseConnection.bind(this));
19
+ app.get('/api/connections', this.handleListConnections.bind(this));
20
+ app.get('/api/console/:connectionId', this.validateConnectionId.bind(this), this.handleGetConsoleMessages.bind(this));
21
+ app.get('/api/network/:connectionId', this.validateConnectionId.bind(this), this.handleGetNetworkRequests.bind(this));
22
+ app.get('/api/health/:connectionId', this.validateConnectionId.bind(this), this.handleHealthCheck.bind(this));
23
+ app.get('/api/health', this.handleServerHealth.bind(this));
24
+ app.get('/api/health/detailed', this.handleDetailedHealthCheck.bind(this));
25
+ app.get('/api/health/statistics', this.handleHealthStatistics.bind(this));
26
+ app.get('/api/health/connections', this.handleAllConnectionsHealth.bind(this));
27
+ app.post('/api/health/check/:connectionId', this.securityManager.getStrictRateLimiter(), this.validateConnectionId.bind(this), this.handleForceHealthCheck.bind(this));
28
+ app.get('/api/metrics/connections', this.handleConnectionMetrics.bind(this));
29
+ app.get('/api/metrics/reconnections', this.handleReconnectionMetrics.bind(this));
30
+ app.get('/api/performance/current', this.handleCurrentPerformance.bind(this));
31
+ app.get('/api/performance/history', this.handlePerformanceHistory.bind(this));
32
+ app.get('/api/performance/summary', this.handlePerformanceSummary.bind(this));
33
+ app.get('/api/status', this.handleServerStatus.bind(this));
34
+ this.logger.info('API routes configured with enhanced security measures');
35
+ }
36
+ async handleConnect(req, res) {
37
+ try {
38
+ const { host, port, targetId } = req.body;
39
+ const connection = await this.connectionPool.getOrCreateConnection(host, port, targetId);
40
+ const response = {
41
+ connectionId: connection.id,
42
+ targetInfo: {
43
+ id: connection.targetId,
44
+ title: 'Chrome Tab',
45
+ url: 'about:blank',
46
+ type: 'page'
47
+ },
48
+ isNewConnection: connection.createdAt === connection.lastUsed
49
+ };
50
+ this.logger.info(`Connection established: ${connection.id} (${host}:${port}${targetId ? ':' + targetId : ''})`);
51
+ res.json({
52
+ success: true,
53
+ data: response,
54
+ timestamp: Date.now()
55
+ });
56
+ }
57
+ catch (error) {
58
+ this.logger.error('Connect API error:', error);
59
+ let errorMessage = 'Connection failed';
60
+ let statusCode = 500;
61
+ if (error instanceof Error) {
62
+ if (error.message.includes('ECONNREFUSED')) {
63
+ errorMessage = 'Unable to connect to Chrome DevTools. Ensure Chrome is running with --remote-debugging-port';
64
+ statusCode = 503;
65
+ }
66
+ else if (error.message.includes('ENOTFOUND')) {
67
+ errorMessage = 'Host not found. Check the host address';
68
+ statusCode = 400;
69
+ }
70
+ else if (error.message.includes('timeout')) {
71
+ errorMessage = 'Connection timeout. Chrome may be unresponsive';
72
+ statusCode = 504;
73
+ }
74
+ else {
75
+ errorMessage = error.message;
76
+ }
77
+ }
78
+ res.status(statusCode).json({
79
+ success: false,
80
+ error: errorMessage,
81
+ timestamp: Date.now()
82
+ });
83
+ }
84
+ }
85
+ async handleGetConsoleMessages(req, res) {
86
+ try {
87
+ const { connectionId } = req.params;
88
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
89
+ if (!connection) {
90
+ res.status(404).json({
91
+ success: false,
92
+ error: 'Connection not found',
93
+ timestamp: Date.now()
94
+ });
95
+ return;
96
+ }
97
+ const filter = this.parseConsoleMessageFilter(req.query);
98
+ if (filter.error) {
99
+ res.status(400).json({
100
+ success: false,
101
+ error: filter.error,
102
+ timestamp: Date.now()
103
+ });
104
+ return;
105
+ }
106
+ const messages = this.messageStore.getConsoleMessages(connectionId, filter.data);
107
+ res.json({
108
+ success: true,
109
+ data: {
110
+ messages,
111
+ totalCount: messages.length,
112
+ connectionId,
113
+ source: 'proxy',
114
+ note: 'Includes all messages since connection established',
115
+ filter: filter.data
116
+ },
117
+ timestamp: Date.now()
118
+ });
119
+ }
120
+ catch (error) {
121
+ this.logger.error('Get console messages API error:', error);
122
+ res.status(500).json({
123
+ success: false,
124
+ error: error instanceof Error ? error.message : 'Failed to retrieve console messages',
125
+ timestamp: Date.now()
126
+ });
127
+ }
128
+ }
129
+ async handleGetNetworkRequests(req, res) {
130
+ try {
131
+ const { connectionId } = req.params;
132
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
133
+ if (!connection) {
134
+ res.status(404).json({
135
+ success: false,
136
+ error: 'Connection not found',
137
+ timestamp: Date.now()
138
+ });
139
+ return;
140
+ }
141
+ const filter = this.parseNetworkRequestFilter(req.query);
142
+ if (filter.error) {
143
+ res.status(400).json({
144
+ success: false,
145
+ error: filter.error,
146
+ timestamp: Date.now()
147
+ });
148
+ return;
149
+ }
150
+ const requests = this.messageStore.getNetworkRequests(connectionId, filter.data);
151
+ res.json({
152
+ success: true,
153
+ data: {
154
+ requests,
155
+ totalCount: requests.length,
156
+ connectionId,
157
+ source: 'proxy',
158
+ note: 'Includes all requests since connection established',
159
+ filter: filter.data
160
+ },
161
+ timestamp: Date.now()
162
+ });
163
+ }
164
+ catch (error) {
165
+ this.logger.error('Get network requests API error:', error);
166
+ res.status(500).json({
167
+ success: false,
168
+ error: error instanceof Error ? error.message : 'Failed to retrieve network requests',
169
+ timestamp: Date.now()
170
+ });
171
+ }
172
+ }
173
+ async handleHealthCheck(req, res) {
174
+ try {
175
+ const { connectionId } = req.params;
176
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
177
+ if (!connection) {
178
+ res.status(404).json({
179
+ success: false,
180
+ error: 'Connection not found',
181
+ timestamp: Date.now()
182
+ });
183
+ return;
184
+ }
185
+ const isHealthy = await this.connectionPool.healthCheck(connectionId);
186
+ const messageCount = this.messageStore.getConsoleMessageCount(connectionId);
187
+ const requestCount = this.messageStore.getNetworkRequestCount(connectionId);
188
+ res.json({
189
+ success: true,
190
+ data: {
191
+ connectionId,
192
+ isHealthy,
193
+ lastUsed: connection.lastUsed,
194
+ clientCount: connection.clientCount,
195
+ uptime: Date.now() - connection.createdAt,
196
+ target: {
197
+ host: connection.host,
198
+ port: connection.port,
199
+ targetId: connection.targetId
200
+ },
201
+ storage: {
202
+ consoleMessages: messageCount,
203
+ networkRequests: requestCount
204
+ }
205
+ },
206
+ timestamp: Date.now()
207
+ });
208
+ }
209
+ catch (error) {
210
+ this.logger.error('Health check API error:', error);
211
+ res.status(500).json({
212
+ success: false,
213
+ error: error instanceof Error ? error.message : 'Health check failed',
214
+ timestamp: Date.now()
215
+ });
216
+ }
217
+ }
218
+ async handleCloseConnection(req, res) {
219
+ try {
220
+ const { connectionId } = req.params;
221
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
222
+ if (!connection) {
223
+ res.status(404).json({
224
+ success: false,
225
+ error: 'Connection not found',
226
+ timestamp: Date.now()
227
+ });
228
+ return;
229
+ }
230
+ await this.connectionPool.closeConnection(connectionId);
231
+ this.messageStore.cleanupConnection(connectionId);
232
+ this.logger.info(`Connection closed: ${connectionId}`);
233
+ res.json({
234
+ success: true,
235
+ data: { connectionId, closed: true },
236
+ timestamp: Date.now()
237
+ });
238
+ }
239
+ catch (error) {
240
+ this.logger.error('Close connection API error:', error);
241
+ res.status(500).json({
242
+ success: false,
243
+ error: error instanceof Error ? error.message : 'Failed to close connection',
244
+ timestamp: Date.now()
245
+ });
246
+ }
247
+ }
248
+ async handleListConnections(_req, res) {
249
+ try {
250
+ const connections = this.connectionPool.getAllConnections();
251
+ const storageStats = this.messageStore.getStorageStats();
252
+ res.json({
253
+ success: true,
254
+ data: {
255
+ connections: connections.map(conn => ({
256
+ id: conn.id,
257
+ host: conn.host,
258
+ port: conn.port,
259
+ targetId: conn.targetId,
260
+ isHealthy: conn.isHealthy,
261
+ clientCount: conn.clientCount,
262
+ createdAt: conn.createdAt,
263
+ lastUsed: conn.lastUsed,
264
+ consoleMessages: storageStats.consoleMessagesByConnection[conn.id] || 0,
265
+ networkRequests: storageStats.networkRequestsByConnection[conn.id] || 0
266
+ })),
267
+ totalConnections: connections.length,
268
+ storageStats
269
+ },
270
+ timestamp: Date.now()
271
+ });
272
+ }
273
+ catch (error) {
274
+ this.logger.error('List connections API error:', error);
275
+ res.status(500).json({
276
+ success: false,
277
+ error: error instanceof Error ? error.message : 'Failed to list connections',
278
+ timestamp: Date.now()
279
+ });
280
+ }
281
+ }
282
+ async handleServerStatus(_req, res) {
283
+ try {
284
+ const connections = this.connectionPool.getAllConnections();
285
+ const storageStats = this.messageStore.getStorageStats();
286
+ res.json({
287
+ success: true,
288
+ data: {
289
+ status: 'running',
290
+ uptime: process.uptime() * 1000,
291
+ connections: connections.length,
292
+ totalConsoleMessages: storageStats.totalConsoleMessages,
293
+ totalNetworkRequests: storageStats.totalNetworkRequests,
294
+ memoryUsage: process.memoryUsage()
295
+ },
296
+ timestamp: Date.now()
297
+ });
298
+ }
299
+ catch (error) {
300
+ this.logger.error('Server status API error:', error);
301
+ res.status(500).json({
302
+ success: false,
303
+ error: error instanceof Error ? error.message : 'Failed to get server status',
304
+ timestamp: Date.now()
305
+ });
306
+ }
307
+ }
308
+ async handleServerHealth(_req, res) {
309
+ res.json({
310
+ success: true,
311
+ data: { status: 'healthy' },
312
+ timestamp: Date.now()
313
+ });
314
+ }
315
+ async handleDetailedHealthCheck(_req, res) {
316
+ try {
317
+ const connections = this.connectionPool.getAllConnections();
318
+ const healthStatuses = this.healthMonitor?.getAllHealthStatuses() || [];
319
+ const connectionMetrics = this.healthMonitor?.getAllConnectionMetrics() || [];
320
+ const reconnectionStatuses = this.connectionPool.getAllReconnectionStatuses();
321
+ const detailedHealth = {
322
+ server: {
323
+ status: 'healthy',
324
+ uptime: process.uptime() * 1000,
325
+ memoryUsage: process.memoryUsage(),
326
+ isHealthMonitoringActive: this.healthMonitor?.isMonitoring() || false
327
+ },
328
+ connections: connections.map(conn => {
329
+ const health = healthStatuses.find(h => h.connectionId === conn.id);
330
+ const metrics = connectionMetrics.find(m => m.connectionId === conn.id);
331
+ const reconnection = reconnectionStatuses.get(conn.id);
332
+ return {
333
+ id: conn.id,
334
+ host: conn.host,
335
+ port: conn.port,
336
+ targetId: conn.targetId,
337
+ isHealthy: conn.isHealthy,
338
+ clientCount: conn.clientCount,
339
+ uptime: Date.now() - conn.createdAt,
340
+ lastUsed: conn.lastUsed,
341
+ health: health ? {
342
+ lastCheck: health.lastCheck,
343
+ errorCount: health.errorCount,
344
+ lastError: health.lastError
345
+ } : null,
346
+ metrics: metrics ? {
347
+ messageCount: metrics.messageCount,
348
+ requestCount: metrics.requestCount,
349
+ lastActivity: metrics.lastActivity,
350
+ reconnectionCount: metrics.reconnectionCount
351
+ } : null,
352
+ reconnection: reconnection || null
353
+ };
354
+ }),
355
+ summary: this.healthMonitor?.getHealthStatistics() || {
356
+ totalConnections: connections.length,
357
+ healthyConnections: connections.filter(c => c.isHealthy).length,
358
+ unhealthyConnections: connections.filter(c => !c.isHealthy).length,
359
+ averageResponseTime: 0,
360
+ totalHealthChecks: 0
361
+ }
362
+ };
363
+ res.json({
364
+ success: true,
365
+ data: detailedHealth,
366
+ timestamp: Date.now()
367
+ });
368
+ }
369
+ catch (error) {
370
+ this.logger.error('Detailed health check API error:', error);
371
+ res.status(500).json({
372
+ success: false,
373
+ error: error instanceof Error ? error.message : 'Failed to get detailed health information',
374
+ timestamp: Date.now()
375
+ });
376
+ }
377
+ }
378
+ async handleHealthStatistics(_req, res) {
379
+ try {
380
+ const statistics = this.healthMonitor?.getHealthStatistics() || {
381
+ totalConnections: 0,
382
+ healthyConnections: 0,
383
+ unhealthyConnections: 0,
384
+ averageResponseTime: 0,
385
+ totalHealthChecks: 0
386
+ };
387
+ const storageStats = this.messageStore.getStorageStats();
388
+ res.json({
389
+ success: true,
390
+ data: {
391
+ health: statistics,
392
+ storage: storageStats,
393
+ server: {
394
+ uptime: process.uptime() * 1000,
395
+ memoryUsage: process.memoryUsage(),
396
+ isHealthMonitoringActive: this.healthMonitor?.isMonitoring() || false
397
+ }
398
+ },
399
+ timestamp: Date.now()
400
+ });
401
+ }
402
+ catch (error) {
403
+ this.logger.error('Health statistics API error:', error);
404
+ res.status(500).json({
405
+ success: false,
406
+ error: error instanceof Error ? error.message : 'Failed to get health statistics',
407
+ timestamp: Date.now()
408
+ });
409
+ }
410
+ }
411
+ async handleAllConnectionsHealth(_req, res) {
412
+ try {
413
+ const healthStatuses = this.healthMonitor?.getAllHealthStatuses() || [];
414
+ res.json({
415
+ success: true,
416
+ data: {
417
+ healthStatuses,
418
+ summary: {
419
+ total: healthStatuses.length,
420
+ healthy: healthStatuses.filter(h => h.isHealthy).length,
421
+ unhealthy: healthStatuses.filter(h => !h.isHealthy).length
422
+ }
423
+ },
424
+ timestamp: Date.now()
425
+ });
426
+ }
427
+ catch (error) {
428
+ this.logger.error('All connections health API error:', error);
429
+ res.status(500).json({
430
+ success: false,
431
+ error: error instanceof Error ? error.message : 'Failed to get connections health status',
432
+ timestamp: Date.now()
433
+ });
434
+ }
435
+ }
436
+ async handleForceHealthCheck(req, res) {
437
+ try {
438
+ const { connectionId } = req.params;
439
+ if (!this.healthMonitor) {
440
+ res.status(503).json({
441
+ success: false,
442
+ error: 'Health monitoring is not available',
443
+ timestamp: Date.now()
444
+ });
445
+ return;
446
+ }
447
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
448
+ if (!connection) {
449
+ res.status(404).json({
450
+ success: false,
451
+ error: 'Connection not found',
452
+ timestamp: Date.now()
453
+ });
454
+ return;
455
+ }
456
+ const healthResult = await this.healthMonitor.checkConnection(connectionId);
457
+ const metrics = this.healthMonitor.getConnectionMetrics(connectionId);
458
+ const reconnectionStatus = this.connectionPool.getReconnectionStatus(connectionId);
459
+ res.json({
460
+ success: true,
461
+ data: {
462
+ connectionId,
463
+ health: healthResult,
464
+ metrics,
465
+ reconnection: reconnectionStatus,
466
+ connection: {
467
+ host: connection.host,
468
+ port: connection.port,
469
+ targetId: connection.targetId,
470
+ clientCount: connection.clientCount,
471
+ uptime: Date.now() - connection.createdAt
472
+ }
473
+ },
474
+ timestamp: Date.now()
475
+ });
476
+ }
477
+ catch (error) {
478
+ this.logger.error('Force health check API error:', error);
479
+ res.status(500).json({
480
+ success: false,
481
+ error: error instanceof Error ? error.message : 'Force health check failed',
482
+ timestamp: Date.now()
483
+ });
484
+ }
485
+ }
486
+ async handleConnectionMetrics(_req, res) {
487
+ try {
488
+ const connections = this.connectionPool.getAllConnections();
489
+ const metrics = this.healthMonitor?.getAllConnectionMetrics() || [];
490
+ const storageStats = this.messageStore.getStorageStats();
491
+ const connectionMetrics = connections.map(conn => {
492
+ const metric = metrics.find(m => m.connectionId === conn.id);
493
+ const consoleMessages = storageStats.consoleMessagesByConnection[conn.id] || 0;
494
+ const networkRequests = storageStats.networkRequestsByConnection[conn.id] || 0;
495
+ return {
496
+ connectionId: conn.id,
497
+ host: conn.host,
498
+ port: conn.port,
499
+ targetId: conn.targetId,
500
+ uptime: Date.now() - conn.createdAt,
501
+ clientCount: conn.clientCount,
502
+ lastUsed: conn.lastUsed,
503
+ isHealthy: conn.isHealthy,
504
+ consoleMessages,
505
+ networkRequests,
506
+ metrics: metric || {
507
+ messageCount: 0,
508
+ requestCount: 0,
509
+ lastActivity: conn.lastUsed,
510
+ reconnectionCount: 0
511
+ }
512
+ };
513
+ });
514
+ res.json({
515
+ success: true,
516
+ data: {
517
+ connections: connectionMetrics,
518
+ summary: {
519
+ totalConnections: connections.length,
520
+ totalConsoleMessages: storageStats.totalConsoleMessages,
521
+ totalNetworkRequests: storageStats.totalNetworkRequests,
522
+ averageUptime: connections.length > 0
523
+ ? connections.reduce((sum, c) => sum + (Date.now() - c.createdAt), 0) / connections.length
524
+ : 0
525
+ }
526
+ },
527
+ timestamp: Date.now()
528
+ });
529
+ }
530
+ catch (error) {
531
+ this.logger.error('Connection metrics API error:', error);
532
+ res.status(500).json({
533
+ success: false,
534
+ error: error instanceof Error ? error.message : 'Failed to get connection metrics',
535
+ timestamp: Date.now()
536
+ });
537
+ }
538
+ }
539
+ async handleReconnectionMetrics(_req, res) {
540
+ try {
541
+ const reconnectionStatuses = this.connectionPool.getAllReconnectionStatuses();
542
+ const connectionMetrics = this.healthMonitor?.getAllConnectionMetrics() || [];
543
+ const reconnectionMetrics = Array.from(reconnectionStatuses.entries()).map(([connectionId, status]) => {
544
+ const metrics = connectionMetrics.find(m => m.connectionId === connectionId);
545
+ const connection = this.connectionPool.getConnectionInfo(connectionId);
546
+ return {
547
+ connectionId,
548
+ isReconnecting: status.isReconnecting,
549
+ attempts: status.attempts,
550
+ maxAttempts: status.maxAttempts,
551
+ reconnectionCount: metrics?.reconnectionCount || 0,
552
+ connection: connection ? {
553
+ host: connection.host,
554
+ port: connection.port,
555
+ targetId: connection.targetId,
556
+ isHealthy: connection.isHealthy
557
+ } : null
558
+ };
559
+ });
560
+ const summary = {
561
+ totalConnections: reconnectionStatuses.size,
562
+ activeReconnections: reconnectionMetrics.filter(r => r.isReconnecting).length,
563
+ totalReconnectionAttempts: reconnectionMetrics.reduce((sum, r) => sum + r.attempts, 0),
564
+ totalSuccessfulReconnections: reconnectionMetrics.reduce((sum, r) => sum + r.reconnectionCount, 0)
565
+ };
566
+ res.json({
567
+ success: true,
568
+ data: {
569
+ reconnections: reconnectionMetrics,
570
+ summary
571
+ },
572
+ timestamp: Date.now()
573
+ });
574
+ }
575
+ catch (error) {
576
+ this.logger.error('Reconnection metrics API error:', error);
577
+ res.status(500).json({
578
+ success: false,
579
+ error: error instanceof Error ? error.message : 'Failed to get reconnection metrics',
580
+ timestamp: Date.now()
581
+ });
582
+ }
583
+ }
584
+ validateConnectionId(req, res, next) {
585
+ const { connectionId } = req.params;
586
+ if (!connectionId || typeof connectionId !== 'string' || connectionId.trim() === '') {
587
+ this.logger.logSecurityEvent('invalid_connection_id', 'Invalid connection ID provided', {
588
+ connectionId,
589
+ ip: req.ip,
590
+ path: req.path
591
+ });
592
+ res.status(400).json({
593
+ success: false,
594
+ error: 'Connection ID is required and must be a non-empty string',
595
+ timestamp: Date.now()
596
+ });
597
+ return;
598
+ }
599
+ const connectionIdRegex = /^[a-zA-Z0-9-_]+$/;
600
+ if (!connectionIdRegex.test(connectionId)) {
601
+ this.logger.logSecurityEvent('malformed_connection_id', 'Connection ID contains invalid characters', {
602
+ connectionId,
603
+ ip: req.ip,
604
+ path: req.path
605
+ });
606
+ res.status(400).json({
607
+ success: false,
608
+ error: 'Connection ID contains invalid characters',
609
+ timestamp: Date.now()
610
+ });
611
+ return;
612
+ }
613
+ next();
614
+ }
615
+ validateConnectRequest(req, res, next) {
616
+ const { host, port, targetId } = req.body;
617
+ if (!host || typeof host !== 'string') {
618
+ this.logger.logSecurityEvent('invalid_connect_request', 'Missing or invalid host', {
619
+ host,
620
+ ip: req.ip
621
+ });
622
+ res.status(400).json({
623
+ success: false,
624
+ error: 'Host is required and must be a string',
625
+ timestamp: Date.now()
626
+ });
627
+ return;
628
+ }
629
+ if (!port || typeof port !== 'number' || port <= 0 || port > 65535) {
630
+ this.logger.logSecurityEvent('invalid_connect_request', 'Invalid port number', {
631
+ port,
632
+ ip: req.ip
633
+ });
634
+ res.status(400).json({
635
+ success: false,
636
+ error: 'Port is required and must be a valid number between 1 and 65535',
637
+ timestamp: Date.now()
638
+ });
639
+ return;
640
+ }
641
+ if (targetId !== undefined && (typeof targetId !== 'string' || targetId.trim() === '')) {
642
+ this.logger.logSecurityEvent('invalid_connect_request', 'Invalid target ID', {
643
+ targetId,
644
+ ip: req.ip
645
+ });
646
+ res.status(400).json({
647
+ success: false,
648
+ error: 'TargetId must be a non-empty string if provided',
649
+ timestamp: Date.now()
650
+ });
651
+ return;
652
+ }
653
+ const hostRegex = /^[a-zA-Z0-9.-]+$/;
654
+ if (!hostRegex.test(host)) {
655
+ this.logger.logSecurityEvent('invalid_host_format', 'Host contains invalid characters', {
656
+ host,
657
+ ip: req.ip
658
+ });
659
+ res.status(400).json({
660
+ success: false,
661
+ error: 'Host contains invalid characters',
662
+ timestamp: Date.now()
663
+ });
664
+ return;
665
+ }
666
+ next();
667
+ }
668
+ parseConsoleMessageFilter(query) {
669
+ try {
670
+ const filter = {};
671
+ if (query.types) {
672
+ const types = Array.isArray(query.types) ? query.types : [query.types];
673
+ const validTypes = ['log', 'info', 'warn', 'error', 'debug'];
674
+ for (const type of types) {
675
+ if (!validTypes.includes(type)) {
676
+ return { error: `Invalid message type: ${type}. Valid types are: ${validTypes.join(', ')}` };
677
+ }
678
+ }
679
+ filter.types = types;
680
+ }
681
+ if (query.textPattern) {
682
+ if (typeof query.textPattern !== 'string') {
683
+ return { error: 'textPattern must be a string' };
684
+ }
685
+ try {
686
+ new RegExp(query.textPattern, 'i');
687
+ filter.textPattern = query.textPattern;
688
+ }
689
+ catch (e) {
690
+ return { error: 'Invalid regex pattern in textPattern' };
691
+ }
692
+ }
693
+ if (query.maxMessages) {
694
+ const maxMessages = parseInt(query.maxMessages, 10);
695
+ if (isNaN(maxMessages) || maxMessages <= 0) {
696
+ return { error: 'maxMessages must be a positive integer' };
697
+ }
698
+ filter.maxMessages = maxMessages;
699
+ }
700
+ if (query.startTime) {
701
+ const startTime = parseInt(query.startTime, 10);
702
+ if (isNaN(startTime) || startTime < 0) {
703
+ return { error: 'startTime must be a valid timestamp' };
704
+ }
705
+ filter.startTime = startTime;
706
+ }
707
+ if (query.endTime) {
708
+ const endTime = parseInt(query.endTime, 10);
709
+ if (isNaN(endTime) || endTime < 0) {
710
+ return { error: 'endTime must be a valid timestamp' };
711
+ }
712
+ filter.endTime = endTime;
713
+ }
714
+ if (filter.startTime && filter.endTime && filter.startTime > filter.endTime) {
715
+ return { error: 'startTime must be less than or equal to endTime' };
716
+ }
717
+ if (query.source) {
718
+ const validSources = ['Runtime.consoleAPICalled', 'Log.entryAdded'];
719
+ if (!validSources.includes(query.source)) {
720
+ return { error: `Invalid source: ${query.source}. Valid sources are: ${validSources.join(', ')}` };
721
+ }
722
+ filter.source = query.source;
723
+ }
724
+ return { data: filter };
725
+ }
726
+ catch (error) {
727
+ return { error: 'Failed to parse console message filter' };
728
+ }
729
+ }
730
+ parseNetworkRequestFilter(query) {
731
+ try {
732
+ const filter = {};
733
+ if (query.methods) {
734
+ const methods = Array.isArray(query.methods) ? query.methods : [query.methods];
735
+ const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
736
+ for (const method of methods) {
737
+ const upperMethod = method.toUpperCase();
738
+ if (!validMethods.includes(upperMethod)) {
739
+ return { error: `Invalid HTTP method: ${method}. Valid methods are: ${validMethods.join(', ')}` };
740
+ }
741
+ }
742
+ filter.methods = methods.map((m) => m.toUpperCase());
743
+ }
744
+ if (query.statusCodes) {
745
+ const statusCodes = Array.isArray(query.statusCodes) ? query.statusCodes : [query.statusCodes];
746
+ for (const code of statusCodes) {
747
+ const statusCode = parseInt(code, 10);
748
+ if (isNaN(statusCode) || statusCode < 100 || statusCode > 599) {
749
+ return { error: `Invalid status code: ${code}. Must be between 100 and 599` };
750
+ }
751
+ }
752
+ filter.statusCodes = statusCodes.map((c) => parseInt(c, 10));
753
+ }
754
+ if (query.urlPattern) {
755
+ if (typeof query.urlPattern !== 'string') {
756
+ return { error: 'urlPattern must be a string' };
757
+ }
758
+ try {
759
+ new RegExp(query.urlPattern, 'i');
760
+ filter.urlPattern = query.urlPattern;
761
+ }
762
+ catch (e) {
763
+ return { error: 'Invalid regex pattern in urlPattern' };
764
+ }
765
+ }
766
+ if (query.maxRequests) {
767
+ const maxRequests = parseInt(query.maxRequests, 10);
768
+ if (isNaN(maxRequests) || maxRequests <= 0) {
769
+ return { error: 'maxRequests must be a positive integer' };
770
+ }
771
+ filter.maxRequests = maxRequests;
772
+ }
773
+ if (query.startTime) {
774
+ const startTime = parseInt(query.startTime, 10);
775
+ if (isNaN(startTime) || startTime < 0) {
776
+ return { error: 'startTime must be a valid timestamp' };
777
+ }
778
+ filter.startTime = startTime;
779
+ }
780
+ if (query.endTime) {
781
+ const endTime = parseInt(query.endTime, 10);
782
+ if (isNaN(endTime) || endTime < 0) {
783
+ return { error: 'endTime must be a valid timestamp' };
784
+ }
785
+ filter.endTime = endTime;
786
+ }
787
+ if (filter.startTime && filter.endTime && filter.startTime > filter.endTime) {
788
+ return { error: 'startTime must be less than or equal to endTime' };
789
+ }
790
+ if (query.includeResponseBody !== undefined) {
791
+ if (query.includeResponseBody === 'true' || query.includeResponseBody === true) {
792
+ filter.includeResponseBody = true;
793
+ }
794
+ else if (query.includeResponseBody === 'false' || query.includeResponseBody === false) {
795
+ filter.includeResponseBody = false;
796
+ }
797
+ else {
798
+ return { error: 'includeResponseBody must be true or false' };
799
+ }
800
+ }
801
+ return { data: filter };
802
+ }
803
+ catch (error) {
804
+ return { error: 'Failed to parse network request filter' };
805
+ }
806
+ }
807
+ async handleCurrentPerformance(_req, res) {
808
+ try {
809
+ if (!this.performanceMonitor) {
810
+ res.status(503).json({
811
+ success: false,
812
+ error: 'Performance monitoring not available',
813
+ timestamp: Date.now()
814
+ });
815
+ return;
816
+ }
817
+ const metrics = this.performanceMonitor.getCurrentMetrics();
818
+ res.json({
819
+ success: true,
820
+ data: metrics,
821
+ timestamp: Date.now()
822
+ });
823
+ }
824
+ catch (error) {
825
+ this.logger.error('Current performance API error:', error);
826
+ res.status(500).json({
827
+ success: false,
828
+ error: 'Failed to get current performance metrics',
829
+ timestamp: Date.now()
830
+ });
831
+ }
832
+ }
833
+ async handlePerformanceHistory(req, res) {
834
+ try {
835
+ if (!this.performanceMonitor) {
836
+ res.status(503).json({
837
+ success: false,
838
+ error: 'Performance monitoring not available',
839
+ timestamp: Date.now()
840
+ });
841
+ return;
842
+ }
843
+ const count = req.query.count ? parseInt(req.query.count) : undefined;
844
+ const history = this.performanceMonitor.getMetricsHistory(count);
845
+ res.json({
846
+ success: true,
847
+ data: {
848
+ history,
849
+ count: history.length
850
+ },
851
+ timestamp: Date.now()
852
+ });
853
+ }
854
+ catch (error) {
855
+ this.logger.error('Performance history API error:', error);
856
+ res.status(500).json({
857
+ success: false,
858
+ error: 'Failed to get performance history',
859
+ timestamp: Date.now()
860
+ });
861
+ }
862
+ }
863
+ async handlePerformanceSummary(req, res) {
864
+ try {
865
+ if (!this.performanceMonitor) {
866
+ res.status(503).json({
867
+ success: false,
868
+ error: 'Performance monitoring not available',
869
+ timestamp: Date.now()
870
+ });
871
+ return;
872
+ }
873
+ const periodMs = req.query.period ? parseInt(req.query.period) : 3600000;
874
+ const summary = this.performanceMonitor.getPerformanceSummary(periodMs);
875
+ res.json({
876
+ success: true,
877
+ data: {
878
+ summary,
879
+ periodMs,
880
+ periodDescription: this.formatPeriod(periodMs)
881
+ },
882
+ timestamp: Date.now()
883
+ });
884
+ }
885
+ catch (error) {
886
+ this.logger.error('Performance summary API error:', error);
887
+ res.status(500).json({
888
+ success: false,
889
+ error: 'Failed to get performance summary',
890
+ timestamp: Date.now()
891
+ });
892
+ }
893
+ }
894
+ formatPeriod(periodMs) {
895
+ const minutes = Math.floor(periodMs / 60000);
896
+ const hours = Math.floor(minutes / 60);
897
+ const days = Math.floor(hours / 24);
898
+ if (days > 0) {
899
+ return `${days} day${days > 1 ? 's' : ''}`;
900
+ }
901
+ else if (hours > 0) {
902
+ return `${hours} hour${hours > 1 ? 's' : ''}`;
903
+ }
904
+ else {
905
+ return `${minutes} minute${minutes > 1 ? 's' : ''}`;
906
+ }
907
+ }
908
+ }
909
+ exports.ProxyAPIServer = ProxyAPIServer;