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.
- package/README.md +12 -0
- package/dist/cli/CLIApplication.js +21 -0
- package/dist/cli/CLIInterface.js +65 -1
- package/dist/cli/CommandRouter.js +1 -0
- package/dist/client/ProxyClient.js +254 -0
- package/dist/client/index.js +1 -0
- package/dist/handlers/EvaluateScriptHandler.js +126 -1
- package/dist/handlers/GetConsoleMessageHandler.js +80 -10
- package/dist/handlers/GetNetworkRequestHandler.js +68 -10
- package/dist/handlers/ListConsoleMessagesHandler.js +83 -12
- package/dist/handlers/ListNetworkRequestsHandler.js +67 -11
- package/dist/monitors/ConsoleMonitor.js +47 -0
- package/dist/proxy/ProxyManager.js +267 -0
- package/dist/proxy/index.js +60 -0
- package/dist/proxy/server/CDPEventMonitor.js +263 -0
- package/dist/proxy/server/CDPProxyServer.js +436 -0
- package/dist/proxy/server/ConnectionPool.js +430 -0
- package/dist/proxy/server/FileSystemSecurity.js +358 -0
- package/dist/proxy/server/HealthMonitor.js +242 -0
- package/dist/proxy/server/MessageStore.js +360 -0
- package/dist/proxy/server/PerformanceMonitor.js +277 -0
- package/dist/proxy/server/ProxyAPIServer.js +909 -0
- package/dist/proxy/server/SecurityManager.js +337 -0
- package/dist/proxy/server/WSProxy.js +456 -0
- package/dist/proxy/types/ProxyTypes.js +2 -0
- package/dist/utils/logger.js +256 -18
- package/package.json +7 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileSystemSecurity = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const logger_1 = require("../../utils/logger");
|
|
41
|
+
class FileSystemSecurity {
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.logger = (0, logger_1.createLogger)({ component: 'FileSystemSecurity' });
|
|
44
|
+
const homeDir = os.homedir();
|
|
45
|
+
const configDir = path.join(homeDir, '.chrome-cdp-cli');
|
|
46
|
+
this.config = {
|
|
47
|
+
allowedDirectories: [
|
|
48
|
+
configDir,
|
|
49
|
+
path.join(configDir, 'logs'),
|
|
50
|
+
path.join(configDir, 'config'),
|
|
51
|
+
'/tmp',
|
|
52
|
+
os.tmpdir()
|
|
53
|
+
],
|
|
54
|
+
configDirectory: configDir,
|
|
55
|
+
logDirectory: path.join(configDir, 'logs'),
|
|
56
|
+
enablePermissionChecks: true,
|
|
57
|
+
enableDataSanitization: true,
|
|
58
|
+
maxFileSize: 100 * 1024 * 1024,
|
|
59
|
+
...config
|
|
60
|
+
};
|
|
61
|
+
this.allowedPaths = new Set();
|
|
62
|
+
this.initializeAllowedPaths();
|
|
63
|
+
this.ensureDirectoriesExist();
|
|
64
|
+
this.logger.info('File system security initialized', {
|
|
65
|
+
config: this.sanitizeConfigForLogging(this.config)
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
initializeAllowedPaths() {
|
|
69
|
+
for (const dir of this.config.allowedDirectories) {
|
|
70
|
+
try {
|
|
71
|
+
const resolvedPath = path.resolve(dir);
|
|
72
|
+
this.allowedPaths.add(resolvedPath);
|
|
73
|
+
this.allowedPaths.add(path.join(resolvedPath, 'proxy'));
|
|
74
|
+
this.allowedPaths.add(path.join(resolvedPath, 'temp'));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.logger.warn(`Failed to resolve allowed directory: ${dir}`, { error });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ensureDirectoriesExist() {
|
|
82
|
+
const requiredDirs = [
|
|
83
|
+
this.config.configDirectory,
|
|
84
|
+
this.config.logDirectory
|
|
85
|
+
];
|
|
86
|
+
for (const dir of requiredDirs) {
|
|
87
|
+
try {
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
90
|
+
this.logger.info(`Created directory with secure permissions: ${dir}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.checkAndFixDirectoryPermissions(dir);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.logger.error(`Failed to create/secure directory: ${dir}`, error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
isPathAllowed(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
const resolvedPath = path.resolve(filePath);
|
|
104
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
105
|
+
for (const allowedPath of this.allowedPaths) {
|
|
106
|
+
if (normalizedPath.startsWith(allowedPath)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.logger.logSecurityEvent('unauthorized_path_access', 'Attempted access to unauthorized path', {
|
|
111
|
+
requestedPath: filePath,
|
|
112
|
+
resolvedPath,
|
|
113
|
+
allowedPaths: Array.from(this.allowedPaths)
|
|
114
|
+
});
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
this.logger.logSecurityEvent('path_validation_error', 'Error validating file path', {
|
|
119
|
+
requestedPath: filePath,
|
|
120
|
+
error: error.message
|
|
121
|
+
});
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async readFileSecurely(filePath) {
|
|
126
|
+
if (!this.isPathAllowed(filePath)) {
|
|
127
|
+
throw new Error(`Access denied: Path not allowed - ${filePath}`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const stats = await fs.promises.stat(filePath);
|
|
131
|
+
if (stats.size > this.config.maxFileSize) {
|
|
132
|
+
throw new Error(`File too large: ${stats.size} bytes (max: ${this.config.maxFileSize})`);
|
|
133
|
+
}
|
|
134
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
135
|
+
this.logger.debug('Secure file read completed', {
|
|
136
|
+
filePath: this.sanitizePath(filePath),
|
|
137
|
+
fileSize: stats.size
|
|
138
|
+
});
|
|
139
|
+
return this.config.enableDataSanitization ? this.sanitizeFileContent(content) : content;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
this.logger.logSecurityEvent('file_read_error', 'Secure file read failed', {
|
|
143
|
+
filePath: this.sanitizePath(filePath),
|
|
144
|
+
error: error.message
|
|
145
|
+
});
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async writeFileSecurely(filePath, content) {
|
|
150
|
+
if (!this.isPathAllowed(filePath)) {
|
|
151
|
+
throw new Error(`Access denied: Path not allowed - ${filePath}`);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const sanitizedContent = this.config.enableDataSanitization
|
|
155
|
+
? this.sanitizeFileContent(content)
|
|
156
|
+
: content;
|
|
157
|
+
const contentSize = Buffer.byteLength(sanitizedContent, 'utf8');
|
|
158
|
+
if (contentSize > this.config.maxFileSize) {
|
|
159
|
+
throw new Error(`Content too large: ${contentSize} bytes (max: ${this.config.maxFileSize})`);
|
|
160
|
+
}
|
|
161
|
+
const dir = path.dirname(filePath);
|
|
162
|
+
if (!fs.existsSync(dir)) {
|
|
163
|
+
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
164
|
+
}
|
|
165
|
+
await fs.promises.writeFile(filePath, sanitizedContent, {
|
|
166
|
+
encoding: 'utf8',
|
|
167
|
+
mode: 0o600
|
|
168
|
+
});
|
|
169
|
+
this.logger.debug('Secure file write completed', {
|
|
170
|
+
filePath: this.sanitizePath(filePath),
|
|
171
|
+
contentSize
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.logger.logSecurityEvent('file_write_error', 'Secure file write failed', {
|
|
176
|
+
filePath: this.sanitizePath(filePath),
|
|
177
|
+
error: error.message
|
|
178
|
+
});
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async checkConfigurationSecurity(configPath) {
|
|
183
|
+
const issues = [];
|
|
184
|
+
const recommendations = [];
|
|
185
|
+
try {
|
|
186
|
+
if (!this.isPathAllowed(configPath)) {
|
|
187
|
+
issues.push('Configuration file is not in an allowed directory');
|
|
188
|
+
recommendations.push(`Move configuration to: ${this.config.configDirectory}`);
|
|
189
|
+
return { isSecure: false, issues, recommendations };
|
|
190
|
+
}
|
|
191
|
+
if (!fs.existsSync(configPath)) {
|
|
192
|
+
issues.push('Configuration file does not exist');
|
|
193
|
+
recommendations.push('Create configuration file with secure permissions');
|
|
194
|
+
return { isSecure: false, issues, recommendations };
|
|
195
|
+
}
|
|
196
|
+
const stats = await fs.promises.stat(configPath);
|
|
197
|
+
const mode = stats.mode & parseInt('777', 8);
|
|
198
|
+
if (mode > parseInt('644', 8)) {
|
|
199
|
+
issues.push(`Configuration file has overly permissive permissions: ${mode.toString(8)}`);
|
|
200
|
+
recommendations.push('Set file permissions to 600 (owner read/write only)');
|
|
201
|
+
}
|
|
202
|
+
if (mode & parseInt('004', 8)) {
|
|
203
|
+
issues.push('Configuration file is world-readable');
|
|
204
|
+
recommendations.push('Remove world-read permissions');
|
|
205
|
+
}
|
|
206
|
+
if (mode & parseInt('020', 8)) {
|
|
207
|
+
issues.push('Configuration file is group-writable');
|
|
208
|
+
recommendations.push('Remove group-write permissions');
|
|
209
|
+
}
|
|
210
|
+
if (stats.size > 1024 * 1024) {
|
|
211
|
+
issues.push('Configuration file is unusually large');
|
|
212
|
+
recommendations.push('Review configuration file for unnecessary content');
|
|
213
|
+
}
|
|
214
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
215
|
+
const uid = process.getuid();
|
|
216
|
+
if (stats.uid !== uid) {
|
|
217
|
+
issues.push('Configuration file is not owned by current user');
|
|
218
|
+
recommendations.push('Change file ownership to current user');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const isSecure = issues.length === 0;
|
|
222
|
+
this.logger.info('Configuration security check completed', {
|
|
223
|
+
configPath: this.sanitizePath(configPath),
|
|
224
|
+
isSecure,
|
|
225
|
+
issueCount: issues.length
|
|
226
|
+
});
|
|
227
|
+
return { isSecure, issues, recommendations };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
this.logger.logSecurityEvent('config_security_check_error', 'Configuration security check failed', {
|
|
231
|
+
configPath: this.sanitizePath(configPath),
|
|
232
|
+
error: error.message
|
|
233
|
+
});
|
|
234
|
+
issues.push(`Security check failed: ${error.message}`);
|
|
235
|
+
return { isSecure: false, issues, recommendations };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async fixConfigurationPermissions(configPath) {
|
|
239
|
+
if (!this.isPathAllowed(configPath)) {
|
|
240
|
+
throw new Error(`Access denied: Path not allowed - ${configPath}`);
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await fs.promises.chmod(configPath, 0o600);
|
|
244
|
+
this.logger.info('Configuration file permissions fixed', {
|
|
245
|
+
configPath: this.sanitizePath(configPath),
|
|
246
|
+
newPermissions: '600'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
this.logger.logSecurityEvent('permission_fix_error', 'Failed to fix configuration permissions', {
|
|
251
|
+
configPath: this.sanitizePath(configPath),
|
|
252
|
+
error: error.message
|
|
253
|
+
});
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
sanitizeFileContent(content) {
|
|
258
|
+
let sanitized = content;
|
|
259
|
+
const sensitivePatterns = [
|
|
260
|
+
/['"](api[_-]?key|token|secret|password)['"]\s*:\s*['"][^'"]+['"]/gi,
|
|
261
|
+
/https?:\/\/[^:]+:[^@]+@[^\s]+/gi,
|
|
262
|
+
/-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(RSA\s+)?PRIVATE\s+KEY-----/gi,
|
|
263
|
+
/eyJ[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g,
|
|
264
|
+
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
265
|
+
/\b\d{3}-\d{2}-\d{4}\b/g
|
|
266
|
+
];
|
|
267
|
+
for (const pattern of sensitivePatterns) {
|
|
268
|
+
sanitized = sanitized.replace(pattern, (match) => {
|
|
269
|
+
this.logger.logSecurityEvent('sensitive_data_sanitized', 'Sensitive data pattern detected and sanitized', {
|
|
270
|
+
patternLength: match.length,
|
|
271
|
+
patternStart: match.substring(0, 10) + '...'
|
|
272
|
+
});
|
|
273
|
+
return '[REDACTED]';
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return sanitized;
|
|
277
|
+
}
|
|
278
|
+
sanitizePath(filePath) {
|
|
279
|
+
const homeDir = os.homedir();
|
|
280
|
+
return filePath.replace(homeDir, '~');
|
|
281
|
+
}
|
|
282
|
+
sanitizeConfigForLogging(config) {
|
|
283
|
+
return {
|
|
284
|
+
...config,
|
|
285
|
+
allowedDirectories: config.allowedDirectories.map(dir => this.sanitizePath(dir)),
|
|
286
|
+
configDirectory: this.sanitizePath(config.configDirectory),
|
|
287
|
+
logDirectory: this.sanitizePath(config.logDirectory)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
checkAndFixDirectoryPermissions(dirPath) {
|
|
291
|
+
try {
|
|
292
|
+
const stats = fs.statSync(dirPath);
|
|
293
|
+
const mode = stats.mode & parseInt('777', 8);
|
|
294
|
+
if (mode !== parseInt('700', 8)) {
|
|
295
|
+
fs.chmodSync(dirPath, 0o700);
|
|
296
|
+
this.logger.info('Fixed directory permissions', {
|
|
297
|
+
directory: this.sanitizePath(dirPath),
|
|
298
|
+
oldMode: mode.toString(8),
|
|
299
|
+
newMode: '700'
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
this.logger.warn('Failed to check/fix directory permissions', {
|
|
305
|
+
directory: this.sanitizePath(dirPath),
|
|
306
|
+
error: error.message
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
getConfig() {
|
|
311
|
+
return { ...this.config };
|
|
312
|
+
}
|
|
313
|
+
updateConfig(newConfig) {
|
|
314
|
+
this.config = { ...this.config, ...newConfig };
|
|
315
|
+
this.allowedPaths.clear();
|
|
316
|
+
this.initializeAllowedPaths();
|
|
317
|
+
this.ensureDirectoriesExist();
|
|
318
|
+
this.logger.info('File system security configuration updated', {
|
|
319
|
+
config: this.sanitizeConfigForLogging(this.config)
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
addAllowedDirectory(directory) {
|
|
323
|
+
try {
|
|
324
|
+
const resolvedPath = path.resolve(directory);
|
|
325
|
+
this.allowedPaths.add(resolvedPath);
|
|
326
|
+
this.config.allowedDirectories.push(directory);
|
|
327
|
+
this.logger.info('Added allowed directory', {
|
|
328
|
+
directory: this.sanitizePath(directory),
|
|
329
|
+
resolvedPath: this.sanitizePath(resolvedPath)
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
this.logger.error('Failed to add allowed directory', error, {
|
|
334
|
+
data: { directory: this.sanitizePath(directory) }
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
removeAllowedDirectory(directory) {
|
|
339
|
+
try {
|
|
340
|
+
const resolvedPath = path.resolve(directory);
|
|
341
|
+
this.allowedPaths.delete(resolvedPath);
|
|
342
|
+
const index = this.config.allowedDirectories.indexOf(directory);
|
|
343
|
+
if (index > -1) {
|
|
344
|
+
this.config.allowedDirectories.splice(index, 1);
|
|
345
|
+
}
|
|
346
|
+
this.logger.info('Removed allowed directory', {
|
|
347
|
+
directory: this.sanitizePath(directory),
|
|
348
|
+
resolvedPath: this.sanitizePath(resolvedPath)
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
this.logger.error('Failed to remove allowed directory', error, {
|
|
353
|
+
data: { directory: this.sanitizePath(directory) }
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
exports.FileSystemSecurity = FileSystemSecurity;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealthMonitor = void 0;
|
|
4
|
+
const logger_1 = require("../../utils/logger");
|
|
5
|
+
class HealthMonitor {
|
|
6
|
+
constructor(connectionPool) {
|
|
7
|
+
this.healthChecks = new Map();
|
|
8
|
+
this.connectionMetrics = new Map();
|
|
9
|
+
this.isRunning = false;
|
|
10
|
+
this.healthCheckTimeout = 5000;
|
|
11
|
+
this.maxConsecutiveFailures = 3;
|
|
12
|
+
this.connectionPool = connectionPool;
|
|
13
|
+
this.logger = new logger_1.Logger();
|
|
14
|
+
}
|
|
15
|
+
start(intervalMs = 30000) {
|
|
16
|
+
if (this.checkInterval) {
|
|
17
|
+
this.logger.warn('Health monitoring already running, stopping previous instance');
|
|
18
|
+
this.stop();
|
|
19
|
+
}
|
|
20
|
+
this.logger.info(`Starting health monitoring with ${intervalMs}ms interval`);
|
|
21
|
+
this.isRunning = true;
|
|
22
|
+
this.performHealthChecks().catch(error => {
|
|
23
|
+
this.logger.error('Initial health check failed:', error);
|
|
24
|
+
});
|
|
25
|
+
this.checkInterval = setInterval(async () => {
|
|
26
|
+
await this.performHealthChecks();
|
|
27
|
+
}, intervalMs);
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
if (this.checkInterval) {
|
|
31
|
+
clearInterval(this.checkInterval);
|
|
32
|
+
this.checkInterval = undefined;
|
|
33
|
+
this.isRunning = false;
|
|
34
|
+
this.logger.info('Health monitoring stopped');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
isMonitoring() {
|
|
38
|
+
return this.isRunning;
|
|
39
|
+
}
|
|
40
|
+
async checkConnection(connectionId) {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
try {
|
|
43
|
+
const connectionInfo = this.connectionPool.getConnectionInfo(connectionId);
|
|
44
|
+
if (!connectionInfo) {
|
|
45
|
+
const result = {
|
|
46
|
+
connectionId,
|
|
47
|
+
isHealthy: false,
|
|
48
|
+
lastCheck: Date.now(),
|
|
49
|
+
errorCount: 0,
|
|
50
|
+
lastError: 'Connection not found'
|
|
51
|
+
};
|
|
52
|
+
this.healthChecks.set(connectionId, result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const isHealthy = await Promise.race([
|
|
56
|
+
this.connectionPool.healthCheck(connectionId),
|
|
57
|
+
new Promise((_, reject) => {
|
|
58
|
+
setTimeout(() => reject(new Error('Health check timeout')), this.healthCheckTimeout);
|
|
59
|
+
})
|
|
60
|
+
]);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const existingResult = this.healthChecks.get(connectionId);
|
|
63
|
+
const result = {
|
|
64
|
+
connectionId,
|
|
65
|
+
isHealthy,
|
|
66
|
+
lastCheck: now,
|
|
67
|
+
errorCount: isHealthy ? 0 : (existingResult?.errorCount || 0) + 1,
|
|
68
|
+
lastError: isHealthy ? undefined : existingResult?.lastError || 'Health check failed'
|
|
69
|
+
};
|
|
70
|
+
this.healthChecks.set(connectionId, result);
|
|
71
|
+
this.updateConnectionMetrics(connectionId, connectionInfo, now - startTime);
|
|
72
|
+
if (!isHealthy) {
|
|
73
|
+
this.logger.warn(`Health check failed for connection ${connectionId} (${result.errorCount} consecutive failures)`);
|
|
74
|
+
}
|
|
75
|
+
else if (existingResult && !existingResult.isHealthy) {
|
|
76
|
+
this.logger.info(`Connection ${connectionId} recovered and is now healthy`);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const existingResult = this.healthChecks.get(connectionId);
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown health check error';
|
|
84
|
+
const result = {
|
|
85
|
+
connectionId,
|
|
86
|
+
isHealthy: false,
|
|
87
|
+
lastCheck: now,
|
|
88
|
+
errorCount: (existingResult?.errorCount || 0) + 1,
|
|
89
|
+
lastError: errorMessage
|
|
90
|
+
};
|
|
91
|
+
this.healthChecks.set(connectionId, result);
|
|
92
|
+
this.logger.error(`Health check error for connection ${connectionId}:`, error);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async attemptReconnection(connectionId) {
|
|
97
|
+
this.logger.info(`Attempting reconnection for connection ${connectionId}`);
|
|
98
|
+
try {
|
|
99
|
+
this.incrementReconnectionCount(connectionId);
|
|
100
|
+
const success = await this.connectionPool.reconnect(connectionId);
|
|
101
|
+
if (success) {
|
|
102
|
+
const result = {
|
|
103
|
+
connectionId,
|
|
104
|
+
isHealthy: true,
|
|
105
|
+
lastCheck: Date.now(),
|
|
106
|
+
errorCount: 0
|
|
107
|
+
};
|
|
108
|
+
this.healthChecks.set(connectionId, result);
|
|
109
|
+
this.logger.info(`Reconnection successful for connection ${connectionId}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const existingResult = this.healthChecks.get(connectionId);
|
|
113
|
+
const result = {
|
|
114
|
+
connectionId,
|
|
115
|
+
isHealthy: false,
|
|
116
|
+
lastCheck: Date.now(),
|
|
117
|
+
errorCount: (existingResult?.errorCount || 0) + 1,
|
|
118
|
+
lastError: 'Reconnection failed'
|
|
119
|
+
};
|
|
120
|
+
this.healthChecks.set(connectionId, result);
|
|
121
|
+
this.logger.warn(`Reconnection failed for connection ${connectionId}`);
|
|
122
|
+
}
|
|
123
|
+
return success;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const existingResult = this.healthChecks.get(connectionId);
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown reconnection error';
|
|
128
|
+
const result = {
|
|
129
|
+
connectionId,
|
|
130
|
+
isHealthy: false,
|
|
131
|
+
lastCheck: Date.now(),
|
|
132
|
+
errorCount: (existingResult?.errorCount || 0) + 1,
|
|
133
|
+
lastError: `Reconnection error: ${errorMessage}`
|
|
134
|
+
};
|
|
135
|
+
this.healthChecks.set(connectionId, result);
|
|
136
|
+
this.logger.error(`Reconnection error for connection ${connectionId}:`, error);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
getHealthStatus(connectionId) {
|
|
141
|
+
return this.healthChecks.get(connectionId) || null;
|
|
142
|
+
}
|
|
143
|
+
getAllHealthStatuses() {
|
|
144
|
+
return Array.from(this.healthChecks.values());
|
|
145
|
+
}
|
|
146
|
+
getConnectionMetrics(connectionId) {
|
|
147
|
+
return this.connectionMetrics.get(connectionId) || null;
|
|
148
|
+
}
|
|
149
|
+
getAllConnectionMetrics() {
|
|
150
|
+
return Array.from(this.connectionMetrics.values());
|
|
151
|
+
}
|
|
152
|
+
getHealthStatistics() {
|
|
153
|
+
const healthStatuses = this.getAllHealthStatuses();
|
|
154
|
+
const metrics = this.getAllConnectionMetrics();
|
|
155
|
+
const totalConnections = healthStatuses.length;
|
|
156
|
+
const healthyConnections = healthStatuses.filter(h => h.isHealthy).length;
|
|
157
|
+
const unhealthyConnections = totalConnections - healthyConnections;
|
|
158
|
+
const recentMetrics = metrics.filter(m => m.lastActivity > Date.now() - 300000);
|
|
159
|
+
const averageResponseTime = recentMetrics.length > 0
|
|
160
|
+
? recentMetrics.reduce((sum, m) => sum + (m.lastActivity || 0), 0) / recentMetrics.length
|
|
161
|
+
: 0;
|
|
162
|
+
const totalHealthChecks = healthStatuses.reduce((sum, h) => sum + (h.errorCount || 0), 0) + healthyConnections;
|
|
163
|
+
return {
|
|
164
|
+
totalConnections,
|
|
165
|
+
healthyConnections,
|
|
166
|
+
unhealthyConnections,
|
|
167
|
+
averageResponseTime,
|
|
168
|
+
totalHealthChecks
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async forceHealthCheckAll() {
|
|
172
|
+
const connections = this.connectionPool.getAllConnections();
|
|
173
|
+
const results = [];
|
|
174
|
+
for (const connection of connections) {
|
|
175
|
+
try {
|
|
176
|
+
const result = await this.checkConnection(connection.id);
|
|
177
|
+
results.push(result);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
this.logger.error(`Force health check failed for connection ${connection.id}:`, error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
cleanupHealthData(connectionId) {
|
|
186
|
+
this.healthChecks.delete(connectionId);
|
|
187
|
+
this.connectionMetrics.delete(connectionId);
|
|
188
|
+
this.logger.debug(`Cleaned up health data for connection ${connectionId}`);
|
|
189
|
+
}
|
|
190
|
+
async performHealthChecks() {
|
|
191
|
+
if (!this.isRunning) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const connections = this.connectionPool.getAllConnections();
|
|
195
|
+
this.logger.debug(`Performing health checks for ${connections.length} connections`);
|
|
196
|
+
const healthCheckPromises = connections.map(async (connection) => {
|
|
197
|
+
try {
|
|
198
|
+
const result = await this.checkConnection(connection.id);
|
|
199
|
+
if (!result.isHealthy && result.errorCount >= this.maxConsecutiveFailures) {
|
|
200
|
+
this.logger.warn(`Connection ${connection.id} has ${result.errorCount} consecutive failures, attempting reconnection`);
|
|
201
|
+
await this.attemptReconnection(connection.id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
this.logger.error(`Health check error for connection ${connection.id}:`, error);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
await Promise.allSettled(healthCheckPromises);
|
|
209
|
+
const activeConnectionIds = new Set(connections.map(c => c.id));
|
|
210
|
+
for (const connectionId of this.healthChecks.keys()) {
|
|
211
|
+
if (!activeConnectionIds.has(connectionId)) {
|
|
212
|
+
this.cleanupHealthData(connectionId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const stats = this.getHealthStatistics();
|
|
216
|
+
if (stats.totalConnections > 0) {
|
|
217
|
+
this.logger.debug(`Health check summary: ${stats.healthyConnections}/${stats.totalConnections} healthy connections`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
updateConnectionMetrics(connectionId, connectionInfo, _responseTime) {
|
|
221
|
+
const existing = this.connectionMetrics.get(connectionId);
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
const metrics = {
|
|
224
|
+
connectionId,
|
|
225
|
+
uptime: now - connectionInfo.createdAt,
|
|
226
|
+
messageCount: existing?.messageCount || 0,
|
|
227
|
+
requestCount: existing?.requestCount || 0,
|
|
228
|
+
clientCount: connectionInfo.clientCount,
|
|
229
|
+
lastActivity: now,
|
|
230
|
+
reconnectionCount: existing?.reconnectionCount || 0
|
|
231
|
+
};
|
|
232
|
+
this.connectionMetrics.set(connectionId, metrics);
|
|
233
|
+
}
|
|
234
|
+
incrementReconnectionCount(connectionId) {
|
|
235
|
+
const existing = this.connectionMetrics.get(connectionId);
|
|
236
|
+
if (existing) {
|
|
237
|
+
existing.reconnectionCount++;
|
|
238
|
+
this.connectionMetrics.set(connectionId, existing);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.HealthMonitor = HealthMonitor;
|