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,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SecurityManager = void 0;
|
|
7
|
+
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
8
|
+
const logger_1 = require("../../utils/logger");
|
|
9
|
+
class SecurityManager {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.logger = (0, logger_1.createLogger)({ component: 'SecurityManager' });
|
|
12
|
+
this.config = {
|
|
13
|
+
enableRateLimit: true,
|
|
14
|
+
rateLimitWindowMs: 60 * 1000,
|
|
15
|
+
rateLimitMaxRequests: 100,
|
|
16
|
+
enableRequestValidation: true,
|
|
17
|
+
enableInputSanitization: true,
|
|
18
|
+
allowedHosts: ['localhost', '127.0.0.1'],
|
|
19
|
+
maxRequestBodySize: '10mb',
|
|
20
|
+
enableSecurityHeaders: true,
|
|
21
|
+
...config
|
|
22
|
+
};
|
|
23
|
+
this.setupRateLimiters();
|
|
24
|
+
this.logger.info('Security manager initialized', { config: this.config });
|
|
25
|
+
}
|
|
26
|
+
setupRateLimiters() {
|
|
27
|
+
this.rateLimiter = (0, express_rate_limit_1.default)({
|
|
28
|
+
windowMs: this.config.rateLimitWindowMs,
|
|
29
|
+
max: this.config.rateLimitMaxRequests,
|
|
30
|
+
message: {
|
|
31
|
+
success: false,
|
|
32
|
+
error: 'Too many requests, please try again later',
|
|
33
|
+
timestamp: Date.now()
|
|
34
|
+
},
|
|
35
|
+
standardHeaders: true,
|
|
36
|
+
legacyHeaders: false,
|
|
37
|
+
skip: (req) => {
|
|
38
|
+
return req.path === '/api/health' || req.path === '/api/status';
|
|
39
|
+
},
|
|
40
|
+
handler: (req, res) => {
|
|
41
|
+
this.logger.logSecurityEvent('rate_limit_exceeded', 'Rate limit exceeded', {
|
|
42
|
+
ip: req.ip,
|
|
43
|
+
path: req.path,
|
|
44
|
+
method: req.method,
|
|
45
|
+
userAgent: req.get('user-agent')
|
|
46
|
+
});
|
|
47
|
+
res.status(429).json({
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'Too many requests, please try again later',
|
|
50
|
+
timestamp: Date.now()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.strictRateLimiter = (0, express_rate_limit_1.default)({
|
|
55
|
+
windowMs: this.config.rateLimitWindowMs,
|
|
56
|
+
max: Math.floor(this.config.rateLimitMaxRequests / 4),
|
|
57
|
+
message: {
|
|
58
|
+
success: false,
|
|
59
|
+
error: 'Too many requests for this operation, please try again later',
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
},
|
|
62
|
+
standardHeaders: true,
|
|
63
|
+
legacyHeaders: false,
|
|
64
|
+
handler: (req, res) => {
|
|
65
|
+
this.logger.logSecurityEvent('strict_rate_limit_exceeded', 'Strict rate limit exceeded', {
|
|
66
|
+
ip: req.ip,
|
|
67
|
+
path: req.path,
|
|
68
|
+
method: req.method,
|
|
69
|
+
userAgent: req.get('user-agent')
|
|
70
|
+
});
|
|
71
|
+
res.status(429).json({
|
|
72
|
+
success: false,
|
|
73
|
+
error: 'Too many requests for this operation, please try again later',
|
|
74
|
+
timestamp: Date.now()
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
getRateLimiter() {
|
|
80
|
+
return this.config.enableRateLimit ? this.rateLimiter : this.noOpMiddleware;
|
|
81
|
+
}
|
|
82
|
+
getStrictRateLimiter() {
|
|
83
|
+
return this.config.enableRateLimit ? this.strictRateLimiter : this.noOpMiddleware;
|
|
84
|
+
}
|
|
85
|
+
getSecurityHeadersMiddleware() {
|
|
86
|
+
return (_req, res, next) => {
|
|
87
|
+
if (!this.config.enableSecurityHeaders) {
|
|
88
|
+
return next();
|
|
89
|
+
}
|
|
90
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
91
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
92
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
93
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
94
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'");
|
|
95
|
+
res.removeHeader('X-Powered-By');
|
|
96
|
+
next();
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
getRequestValidationMiddleware() {
|
|
100
|
+
return (req, res, next) => {
|
|
101
|
+
if (!this.config.enableRequestValidation) {
|
|
102
|
+
return next();
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
|
|
106
|
+
if (!allowedMethods.includes(req.method)) {
|
|
107
|
+
this.logger.logSecurityEvent('invalid_method', 'Invalid HTTP method', {
|
|
108
|
+
method: req.method,
|
|
109
|
+
ip: req.ip,
|
|
110
|
+
path: req.path
|
|
111
|
+
});
|
|
112
|
+
return res.status(405).json({
|
|
113
|
+
success: false,
|
|
114
|
+
error: 'Method not allowed',
|
|
115
|
+
timestamp: Date.now()
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (['POST', 'PUT'].includes(req.method)) {
|
|
119
|
+
const contentType = req.get('content-type');
|
|
120
|
+
if (!contentType || !contentType.includes('application/json')) {
|
|
121
|
+
this.logger.logSecurityEvent('invalid_content_type', 'Invalid content type', {
|
|
122
|
+
contentType,
|
|
123
|
+
method: req.method,
|
|
124
|
+
ip: req.ip,
|
|
125
|
+
path: req.path
|
|
126
|
+
});
|
|
127
|
+
return res.status(400).json({
|
|
128
|
+
success: false,
|
|
129
|
+
error: 'Content-Type must be application/json',
|
|
130
|
+
timestamp: Date.now()
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const contentLength = req.get('content-length');
|
|
135
|
+
if (contentLength) {
|
|
136
|
+
const maxSize = this.parseSize(this.config.maxRequestBodySize);
|
|
137
|
+
if (parseInt(contentLength) > maxSize) {
|
|
138
|
+
this.logger.logSecurityEvent('request_too_large', 'Request body too large', {
|
|
139
|
+
contentLength: parseInt(contentLength),
|
|
140
|
+
maxSize,
|
|
141
|
+
ip: req.ip,
|
|
142
|
+
path: req.path
|
|
143
|
+
});
|
|
144
|
+
return res.status(413).json({
|
|
145
|
+
success: false,
|
|
146
|
+
error: 'Request body too large',
|
|
147
|
+
timestamp: Date.now()
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const userAgent = req.get('user-agent');
|
|
152
|
+
if (!userAgent || this.isSuspiciousUserAgent(userAgent)) {
|
|
153
|
+
this.logger.logSecurityEvent('suspicious_user_agent', 'Suspicious user agent', {
|
|
154
|
+
userAgent,
|
|
155
|
+
ip: req.ip,
|
|
156
|
+
path: req.path
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
next();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.logger.error('Request validation error:', error);
|
|
163
|
+
res.status(500).json({
|
|
164
|
+
success: false,
|
|
165
|
+
error: 'Request validation failed',
|
|
166
|
+
timestamp: Date.now()
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
getInputSanitizationMiddleware() {
|
|
172
|
+
return (req, res, next) => {
|
|
173
|
+
if (!this.config.enableInputSanitization) {
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
if (req.body && typeof req.body === 'object') {
|
|
178
|
+
req.body = this.sanitizeObject(req.body);
|
|
179
|
+
}
|
|
180
|
+
if (req.query && typeof req.query === 'object') {
|
|
181
|
+
req.query = this.sanitizeObject(req.query);
|
|
182
|
+
}
|
|
183
|
+
if (req.params && typeof req.params === 'object') {
|
|
184
|
+
req.params = this.sanitizeObject(req.params);
|
|
185
|
+
}
|
|
186
|
+
next();
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
this.logger.error('Input sanitization error:', error);
|
|
190
|
+
res.status(500).json({
|
|
191
|
+
success: false,
|
|
192
|
+
error: 'Input sanitization failed',
|
|
193
|
+
timestamp: Date.now()
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
getHostValidationMiddleware() {
|
|
199
|
+
return (req, res, next) => {
|
|
200
|
+
if (req.path === '/api/connect' && req.method === 'POST') {
|
|
201
|
+
const { host } = req.body;
|
|
202
|
+
if (host && !this.isAllowedHost(host)) {
|
|
203
|
+
this.logger.logSecurityEvent('unauthorized_host', 'Unauthorized host connection attempt', {
|
|
204
|
+
requestedHost: host,
|
|
205
|
+
clientIP: req.ip,
|
|
206
|
+
allowedHosts: this.config.allowedHosts
|
|
207
|
+
});
|
|
208
|
+
res.status(403).json({
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'Connection to this host is not allowed for security reasons',
|
|
211
|
+
timestamp: Date.now()
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
next();
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
getSecurityLoggingMiddleware() {
|
|
220
|
+
return (req, res, next) => {
|
|
221
|
+
const startTime = Date.now();
|
|
222
|
+
this.logger.debug('Security: Request received', {
|
|
223
|
+
method: req.method,
|
|
224
|
+
path: req.path,
|
|
225
|
+
ip: req.ip,
|
|
226
|
+
userAgent: req.get('user-agent'),
|
|
227
|
+
contentType: req.get('content-type'),
|
|
228
|
+
contentLength: req.get('content-length')
|
|
229
|
+
});
|
|
230
|
+
res.on('finish', () => {
|
|
231
|
+
const duration = Date.now() - startTime;
|
|
232
|
+
if (res.statusCode >= 400) {
|
|
233
|
+
this.logger.logSecurityEvent('error_response', 'Error response sent', {
|
|
234
|
+
method: req.method,
|
|
235
|
+
path: req.path,
|
|
236
|
+
statusCode: res.statusCode,
|
|
237
|
+
duration,
|
|
238
|
+
ip: req.ip,
|
|
239
|
+
userAgent: req.get('user-agent')
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
next();
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
isAllowedHost(host) {
|
|
247
|
+
if (host === 'localhost' || host === '127.0.0.1') {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
if (this.config.allowedHosts.includes(host)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if (host.startsWith('192.168.') ||
|
|
254
|
+
host.startsWith('10.') ||
|
|
255
|
+
host.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
isSuspiciousUserAgent(userAgent) {
|
|
261
|
+
const suspiciousPatterns = [
|
|
262
|
+
/bot/i,
|
|
263
|
+
/crawler/i,
|
|
264
|
+
/spider/i,
|
|
265
|
+
/scraper/i,
|
|
266
|
+
/curl/i,
|
|
267
|
+
/wget/i,
|
|
268
|
+
/python/i,
|
|
269
|
+
/^$/
|
|
270
|
+
];
|
|
271
|
+
return suspiciousPatterns.some(pattern => pattern.test(userAgent));
|
|
272
|
+
}
|
|
273
|
+
sanitizeObject(obj) {
|
|
274
|
+
if (obj === null || obj === undefined) {
|
|
275
|
+
return obj;
|
|
276
|
+
}
|
|
277
|
+
if (typeof obj === 'string') {
|
|
278
|
+
return this.sanitizeString(obj);
|
|
279
|
+
}
|
|
280
|
+
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
|
281
|
+
return obj;
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(obj)) {
|
|
284
|
+
return obj.map(item => this.sanitizeObject(item));
|
|
285
|
+
}
|
|
286
|
+
if (typeof obj === 'object') {
|
|
287
|
+
const sanitized = {};
|
|
288
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
289
|
+
const sanitizedKey = this.sanitizeString(key);
|
|
290
|
+
sanitized[sanitizedKey] = this.sanitizeObject(value);
|
|
291
|
+
}
|
|
292
|
+
return sanitized;
|
|
293
|
+
}
|
|
294
|
+
return obj;
|
|
295
|
+
}
|
|
296
|
+
sanitizeString(str) {
|
|
297
|
+
if (typeof str !== 'string') {
|
|
298
|
+
return str;
|
|
299
|
+
}
|
|
300
|
+
let sanitized = str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
301
|
+
if (sanitized.length > 10000) {
|
|
302
|
+
sanitized = sanitized.substring(0, 10000);
|
|
303
|
+
this.logger.warn('String truncated during sanitization', {
|
|
304
|
+
originalLength: str.length,
|
|
305
|
+
truncatedLength: sanitized.length
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return sanitized;
|
|
309
|
+
}
|
|
310
|
+
parseSize(sizeStr) {
|
|
311
|
+
const match = sizeStr.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/i);
|
|
312
|
+
if (!match) {
|
|
313
|
+
return 10 * 1024 * 1024;
|
|
314
|
+
}
|
|
315
|
+
const value = parseFloat(match[1]);
|
|
316
|
+
const unit = match[2].toLowerCase();
|
|
317
|
+
switch (unit) {
|
|
318
|
+
case 'b': return value;
|
|
319
|
+
case 'kb': return value * 1024;
|
|
320
|
+
case 'mb': return value * 1024 * 1024;
|
|
321
|
+
case 'gb': return value * 1024 * 1024 * 1024;
|
|
322
|
+
default: return 10 * 1024 * 1024;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
noOpMiddleware(_req, _res, next) {
|
|
326
|
+
next();
|
|
327
|
+
}
|
|
328
|
+
updateConfig(newConfig) {
|
|
329
|
+
this.config = { ...this.config, ...newConfig };
|
|
330
|
+
this.setupRateLimiters();
|
|
331
|
+
this.logger.info('Security configuration updated', { config: this.config });
|
|
332
|
+
}
|
|
333
|
+
getConfig() {
|
|
334
|
+
return { ...this.config };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
exports.SecurityManager = SecurityManager;
|