@push.rocks/smartproxy 21.1.3 → 21.1.5

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.
Files changed (38) hide show
  1. package/changelog.md +23 -0
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/core/models/socket-augmentation.d.ts +3 -0
  4. package/dist_ts/core/models/socket-augmentation.js +1 -1
  5. package/dist_ts/core/utils/socket-tracker.d.ts +16 -0
  6. package/dist_ts/core/utils/socket-tracker.js +49 -0
  7. package/dist_ts/detection/detectors/http-detector.js +14 -2
  8. package/dist_ts/detection/protocol-detector.js +2 -1
  9. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +5 -1
  10. package/dist_ts/proxies/http-proxy/http-proxy.js +63 -39
  11. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +5 -0
  12. package/dist_ts/proxies/smart-proxy/certificate-manager.js +20 -6
  13. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
  14. package/dist_ts/proxies/smart-proxy/index.js +2 -1
  15. package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +4 -0
  16. package/dist_ts/proxies/smart-proxy/metrics-collector.js +52 -7
  17. package/dist_ts/proxies/smart-proxy/route-orchestrator.d.ts +56 -0
  18. package/dist_ts/proxies/smart-proxy/route-orchestrator.js +204 -0
  19. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -11
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -237
  21. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +42 -7
  22. package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +58 -0
  23. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +405 -0
  24. package/package.json +3 -2
  25. package/readme.md +321 -828
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/core/models/socket-augmentation.ts +5 -0
  28. package/ts/core/utils/socket-tracker.ts +63 -0
  29. package/ts/detection/detectors/http-detector.ts +14 -1
  30. package/ts/detection/protocol-detector.ts +1 -0
  31. package/ts/proxies/http-proxy/http-proxy.ts +73 -48
  32. package/ts/proxies/smart-proxy/certificate-manager.ts +21 -5
  33. package/ts/proxies/smart-proxy/index.ts +1 -0
  34. package/ts/proxies/smart-proxy/metrics-collector.ts +58 -6
  35. package/ts/proxies/smart-proxy/route-orchestrator.ts +297 -0
  36. package/ts/proxies/smart-proxy/smart-proxy.ts +66 -270
  37. package/ts/proxies/smart-proxy/utils/route-helpers.ts +45 -6
  38. package/ts/proxies/smart-proxy/utils/route-validator.ts +453 -0
@@ -0,0 +1,453 @@
1
+ import { logger } from '../../../core/utils/logger.js';
2
+ import type { IRouteConfig } from '../models/route-types.js';
3
+
4
+ /**
5
+ * Validates route configurations for correctness and safety
6
+ */
7
+ export class RouteValidator {
8
+ private static readonly VALID_TLS_MODES = ['terminate', 'passthrough', 'terminate-and-reencrypt'];
9
+ private static readonly VALID_ACTION_TYPES = ['forward', 'socket-handler'];
10
+ private static readonly VALID_PROTOCOLS = ['tcp', 'http', 'https', 'ws', 'wss'];
11
+ private static readonly MAX_PORTS = 100;
12
+ private static readonly MAX_DOMAINS = 1000;
13
+ private static readonly MAX_HEADER_SIZE = 8192;
14
+
15
+ /**
16
+ * Validate a single route configuration
17
+ */
18
+ public static validateRoute(route: IRouteConfig): { valid: boolean; errors: string[] } {
19
+ const errors: string[] = [];
20
+
21
+ // Validate route has a name
22
+ if (!route.name || typeof route.name !== 'string') {
23
+ errors.push('Route must have a valid name');
24
+ }
25
+
26
+ // Validate match criteria
27
+ if (!route.match) {
28
+ errors.push('Route must have match criteria');
29
+ } else {
30
+ // Validate ports
31
+ if (route.match.ports) {
32
+ const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
33
+
34
+ if (ports.length > this.MAX_PORTS) {
35
+ errors.push(`Too many ports specified (max ${this.MAX_PORTS})`);
36
+ }
37
+
38
+ for (const port of ports) {
39
+ if (typeof port === 'number') {
40
+ if (!this.isValidPort(port)) {
41
+ errors.push(`Invalid port: ${port}. Must be between 1 and 65535`);
42
+ }
43
+ } else if (typeof port === 'object' && 'from' in port && 'to' in port) {
44
+ if (!this.isValidPort(port.from)) {
45
+ errors.push(`Invalid port range start: ${port.from}. Must be between 1 and 65535`);
46
+ }
47
+ if (!this.isValidPort(port.to)) {
48
+ errors.push(`Invalid port range end: ${port.to}. Must be between 1 and 65535`);
49
+ }
50
+ if (port.from > port.to) {
51
+ errors.push(`Invalid port range: ${port.from}-${port.to} (start > end)`);
52
+ }
53
+ } else {
54
+ errors.push(`Invalid port configuration: ${JSON.stringify(port)}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ // Validate domains
60
+ if (route.match.domains) {
61
+ const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
62
+
63
+ if (domains.length > this.MAX_DOMAINS) {
64
+ errors.push(`Too many domains specified (max ${this.MAX_DOMAINS})`);
65
+ }
66
+
67
+ for (const domain of domains) {
68
+ if (!this.isValidDomain(domain)) {
69
+ errors.push(`Invalid domain pattern: ${domain}`);
70
+ }
71
+ }
72
+ }
73
+
74
+ // Validate paths
75
+ if (route.match.path) {
76
+ const paths = Array.isArray(route.match.path) ? route.match.path : [route.match.path];
77
+
78
+ for (const path of paths) {
79
+ if (!this.isValidPath(path)) {
80
+ errors.push(`Invalid path pattern: ${path}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ // Validate client IPs
86
+ if (route.match.clientIp) {
87
+ const ips = Array.isArray(route.match.clientIp) ? route.match.clientIp : [route.match.clientIp];
88
+
89
+ for (const ip of ips) {
90
+ if (!this.isValidIPPattern(ip)) {
91
+ errors.push(`Invalid IP pattern: ${ip}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ // Validate headers
97
+ if (route.match.headers) {
98
+ for (const [key, value] of Object.entries(route.match.headers)) {
99
+ if (key.length > 256) {
100
+ errors.push(`Header name too long: ${key}`);
101
+ }
102
+
103
+ const headerValue = String(value);
104
+ if (headerValue.length > this.MAX_HEADER_SIZE) {
105
+ errors.push(`Header value too long for ${key} (max ${this.MAX_HEADER_SIZE} bytes)`);
106
+ }
107
+
108
+ if (!/^[\x20-\x7E]+$/.test(key)) {
109
+ errors.push(`Invalid header name: ${key} (must be printable ASCII)`);
110
+ }
111
+ }
112
+ }
113
+
114
+ // Protocol validation removed - not part of IRouteMatch interface
115
+ }
116
+
117
+ // Validate action
118
+ if (!route.action) {
119
+ errors.push('Route must have an action');
120
+ } else {
121
+ // Validate action type
122
+ if (!route.action.type || !this.VALID_ACTION_TYPES.includes(route.action.type)) {
123
+ errors.push(`Invalid action type: ${route.action.type}. Must be one of: ${this.VALID_ACTION_TYPES.join(', ')}`);
124
+ }
125
+
126
+ // Validate socket-handler
127
+ if (route.action.type === 'socket-handler') {
128
+ if (typeof route.action.socketHandler !== 'function') {
129
+ errors.push('socket-handler action requires a socketHandler function');
130
+ }
131
+ }
132
+
133
+ // Validate forward target
134
+ if (route.action.type === 'forward') {
135
+ if (!route.action.targets || route.action.targets.length === 0) {
136
+ errors.push('Forward action must have at least one target');
137
+ } else {
138
+ for (const target of route.action.targets) {
139
+ if (!target.host) {
140
+ errors.push('Target must have a host');
141
+ } else if (typeof target.host !== 'string' && !Array.isArray(target.host) && typeof target.host !== 'function') {
142
+ errors.push('Target host must be a string, array of strings, or function');
143
+ }
144
+
145
+ if (target.port) {
146
+ if (typeof target.port === 'number' && !this.isValidPort(target.port)) {
147
+ errors.push(`Invalid target port: ${target.port}`);
148
+ } else if (target.port !== 'preserve' && typeof target.port !== 'function' && typeof target.port !== 'number') {
149
+ errors.push(`Invalid target port configuration: ${target.port}`);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // Validate TLS settings
157
+ if (route.action.tls) {
158
+ if (route.action.tls.mode && !this.VALID_TLS_MODES.includes(route.action.tls.mode)) {
159
+ errors.push(`Invalid TLS mode: ${route.action.tls.mode}. Must be one of: ${this.VALID_TLS_MODES.join(', ')}`);
160
+ }
161
+
162
+ if (route.action.tls.certificate) {
163
+ if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate !== 'object') {
164
+ errors.push('TLS certificate must be "auto" or a certificate configuration object');
165
+ }
166
+ }
167
+
168
+ if (route.action.tls.versions) {
169
+ for (const version of route.action.tls.versions) {
170
+ if (!['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'].includes(version)) {
171
+ errors.push(`Invalid TLS version: ${version}`);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ // Validate security settings
179
+ if (route.security) {
180
+ // Validate IP allow/block lists
181
+ if (route.security.ipAllowList) {
182
+ const allowList = Array.isArray(route.security.ipAllowList) ? route.security.ipAllowList : [route.security.ipAllowList];
183
+
184
+ for (const ip of allowList) {
185
+ if (!this.isValidIPPattern(ip)) {
186
+ errors.push(`Invalid IP pattern in allow list: ${ip}`);
187
+ }
188
+ }
189
+ }
190
+
191
+ if (route.security.ipBlockList) {
192
+ const blockList = Array.isArray(route.security.ipBlockList) ? route.security.ipBlockList : [route.security.ipBlockList];
193
+
194
+ for (const ip of blockList) {
195
+ if (!this.isValidIPPattern(ip)) {
196
+ errors.push(`Invalid IP pattern in block list: ${ip}`);
197
+ }
198
+ }
199
+ }
200
+
201
+ // Validate rate limits
202
+ if (route.security.rateLimit) {
203
+ if (route.security.rateLimit.maxRequests && route.security.rateLimit.maxRequests < 0) {
204
+ errors.push('Rate limit maxRequests must be positive');
205
+ }
206
+
207
+ if (route.security.rateLimit.window && route.security.rateLimit.window < 0) {
208
+ errors.push('Rate limit window must be positive');
209
+ }
210
+ }
211
+
212
+ // Validate connection limits
213
+ if (route.security.maxConnections && route.security.maxConnections < 0) {
214
+ errors.push('Max connections must be positive');
215
+ }
216
+ }
217
+
218
+ // Validate priority
219
+ if (route.priority !== undefined && (route.priority < 0 || route.priority > 10000)) {
220
+ errors.push('Priority must be between 0 and 10000');
221
+ }
222
+
223
+ return {
224
+ valid: errors.length === 0,
225
+ errors
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Validate multiple route configurations
231
+ */
232
+ public static validateRoutes(routes: IRouteConfig[]): { valid: boolean; errors: Map<string, string[]> } {
233
+ const errorMap = new Map<string, string[]>();
234
+ let valid = true;
235
+
236
+ // Check for duplicate route names
237
+ const routeNames = new Set<string>();
238
+ for (const route of routes) {
239
+ if (route.name && routeNames.has(route.name)) {
240
+ const existingErrors = errorMap.get(route.name) || [];
241
+ existingErrors.push('Duplicate route name');
242
+ errorMap.set(route.name, existingErrors);
243
+ valid = false;
244
+ }
245
+ routeNames.add(route.name);
246
+ }
247
+
248
+ // Validate each route
249
+ for (const route of routes) {
250
+ const result = this.validateRoute(route);
251
+ if (!result.valid) {
252
+ errorMap.set(route.name || 'unnamed', result.errors);
253
+ valid = false;
254
+ }
255
+ }
256
+
257
+ // Check for conflicting routes
258
+ const conflicts = this.findRouteConflicts(routes);
259
+ if (conflicts.length > 0) {
260
+ for (const conflict of conflicts) {
261
+ const existingErrors = errorMap.get(conflict.route) || [];
262
+ existingErrors.push(conflict.message);
263
+ errorMap.set(conflict.route, existingErrors);
264
+ }
265
+ valid = false;
266
+ }
267
+
268
+ return { valid, errors: errorMap };
269
+ }
270
+
271
+ /**
272
+ * Find potential conflicts between routes
273
+ */
274
+ private static findRouteConflicts(routes: IRouteConfig[]): Array<{ route: string; message: string }> {
275
+ const conflicts: Array<{ route: string; message: string }> = [];
276
+
277
+ // Group routes by port
278
+ const portMap = new Map<number, IRouteConfig[]>();
279
+
280
+ for (const route of routes) {
281
+ if (route.match?.ports) {
282
+ const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
283
+
284
+ // Expand port ranges to individual ports
285
+ const expandedPorts: number[] = [];
286
+ for (const port of ports) {
287
+ if (typeof port === 'number') {
288
+ expandedPorts.push(port);
289
+ } else if (typeof port === 'object' && 'from' in port && 'to' in port) {
290
+ for (let p = port.from; p <= port.to; p++) {
291
+ expandedPorts.push(p);
292
+ }
293
+ }
294
+ }
295
+
296
+ for (const port of expandedPorts) {
297
+ const routesOnPort = portMap.get(port) || [];
298
+ routesOnPort.push(route);
299
+ portMap.set(port, routesOnPort);
300
+ }
301
+ }
302
+ }
303
+
304
+ // Check for conflicting catch-all routes on the same port
305
+ for (const [port, routesOnPort] of portMap) {
306
+ const catchAllRoutes = routesOnPort.filter(r =>
307
+ !r.match.domains ||
308
+ (Array.isArray(r.match.domains) && r.match.domains.includes('*')) ||
309
+ r.match.domains === '*'
310
+ );
311
+
312
+ if (catchAllRoutes.length > 1) {
313
+ for (const route of catchAllRoutes) {
314
+ conflicts.push({
315
+ route: route.name,
316
+ message: `Multiple catch-all routes on port ${port}`
317
+ });
318
+ }
319
+ }
320
+ }
321
+
322
+ return conflicts;
323
+ }
324
+
325
+ /**
326
+ * Validate port number
327
+ */
328
+ private static isValidPort(port: number): boolean {
329
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
330
+ }
331
+
332
+ /**
333
+ * Validate domain pattern
334
+ */
335
+ private static isValidDomain(domain: string): boolean {
336
+ if (!domain || typeof domain !== 'string') return false;
337
+ if (domain === '*') return true;
338
+
339
+ // Basic domain pattern validation
340
+ const domainPattern = /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
341
+ return domainPattern.test(domain) || domain === 'localhost';
342
+ }
343
+
344
+ /**
345
+ * Validate path pattern
346
+ */
347
+ private static isValidPath(path: string): boolean {
348
+ if (!path || typeof path !== 'string') return false;
349
+ if (!path.startsWith('/')) return false;
350
+
351
+ // Check for invalid characters
352
+ if (!/^[a-zA-Z0-9/_*:{}.-]+$/.test(path)) return false;
353
+
354
+ // Validate parameter syntax
355
+ const paramPattern = /\{[a-zA-Z_][a-zA-Z0-9_]*\}/g;
356
+ const params = path.match(paramPattern) || [];
357
+
358
+ for (const param of params) {
359
+ if (param.length > 32) return false;
360
+ }
361
+
362
+ return true;
363
+ }
364
+
365
+ /**
366
+ * Validate IP pattern
367
+ */
368
+ private static isValidIPPattern(ip: string): boolean {
369
+ if (!ip || typeof ip !== 'string') return false;
370
+ if (ip === '*') return true;
371
+
372
+ // Check for CIDR notation
373
+ if (ip.includes('/')) {
374
+ const [addr, prefix] = ip.split('/');
375
+ const prefixNum = parseInt(prefix, 10);
376
+
377
+ if (addr.includes(':')) {
378
+ // IPv6 CIDR
379
+ return this.isValidIPv6(addr) && prefixNum >= 0 && prefixNum <= 128;
380
+ } else {
381
+ // IPv4 CIDR
382
+ return this.isValidIPv4(addr) && prefixNum >= 0 && prefixNum <= 32;
383
+ }
384
+ }
385
+
386
+ // Check for range
387
+ if (ip.includes('-')) {
388
+ const [start, end] = ip.split('-');
389
+ return (this.isValidIPv4(start) && this.isValidIPv4(end)) ||
390
+ (this.isValidIPv6(start) && this.isValidIPv6(end));
391
+ }
392
+
393
+ // Check for wildcards in IPv4
394
+ if (ip.includes('*') && !ip.includes(':')) {
395
+ const parts = ip.split('.');
396
+ if (parts.length !== 4) return false;
397
+
398
+ for (const part of parts) {
399
+ if (part !== '*' && !/^\d{1,3}$/.test(part)) return false;
400
+ if (part !== '*' && parseInt(part, 10) > 255) return false;
401
+ }
402
+
403
+ return true;
404
+ }
405
+
406
+ // Regular IP address
407
+ return this.isValidIPv4(ip) || this.isValidIPv6(ip);
408
+ }
409
+
410
+ /**
411
+ * Validate IPv4 address
412
+ */
413
+ private static isValidIPv4(ip: string): boolean {
414
+ const parts = ip.split('.');
415
+ if (parts.length !== 4) return false;
416
+
417
+ for (const part of parts) {
418
+ const num = parseInt(part, 10);
419
+ if (isNaN(num) || num < 0 || num > 255) return false;
420
+ }
421
+
422
+ return true;
423
+ }
424
+
425
+ /**
426
+ * Validate IPv6 address
427
+ */
428
+ private static isValidIPv6(ip: string): boolean {
429
+ // Simple IPv6 validation
430
+ const ipv6Pattern = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|::[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{1,4}){0,6}|::1|::)$/;
431
+ return ipv6Pattern.test(ip);
432
+ }
433
+
434
+ /**
435
+ * Log validation errors
436
+ */
437
+ public static logValidationErrors(errors: Map<string, string[]>): void {
438
+ for (const [routeName, routeErrors] of errors) {
439
+ logger.log('error', `Route validation failed for ${routeName}:`, {
440
+ route: routeName,
441
+ errors: routeErrors,
442
+ component: 'route-validator'
443
+ });
444
+
445
+ for (const error of routeErrors) {
446
+ logger.log('error', ` - ${error}`, {
447
+ route: routeName,
448
+ component: 'route-validator'
449
+ });
450
+ }
451
+ }
452
+ }
453
+ }