@push.rocks/smartproxy 13.1.3 → 15.0.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.
Files changed (29) hide show
  1. package/dist_ts/00_commitinfo_data.js +3 -3
  2. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -3
  3. package/dist_ts/proxies/smart-proxy/index.js +9 -5
  4. package/dist_ts/proxies/smart-proxy/models/index.d.ts +2 -0
  5. package/dist_ts/proxies/smart-proxy/models/index.js +2 -1
  6. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +82 -15
  7. package/dist_ts/proxies/smart-proxy/models/interfaces.js +10 -1
  8. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +133 -0
  9. package/dist_ts/proxies/smart-proxy/models/route-types.js +2 -0
  10. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +55 -0
  11. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +804 -0
  12. package/dist_ts/proxies/smart-proxy/route-helpers.d.ts +127 -0
  13. package/dist_ts/proxies/smart-proxy/route-helpers.js +196 -0
  14. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +103 -0
  15. package/dist_ts/proxies/smart-proxy/route-manager.js +483 -0
  16. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +19 -8
  17. package/dist_ts/proxies/smart-proxy/smart-proxy.js +239 -46
  18. package/package.json +2 -2
  19. package/readme.md +675 -446
  20. package/readme.plan.md +311 -250
  21. package/ts/00_commitinfo_data.ts +2 -2
  22. package/ts/proxies/smart-proxy/index.ts +20 -4
  23. package/ts/proxies/smart-proxy/models/index.ts +4 -0
  24. package/ts/proxies/smart-proxy/models/interfaces.ts +91 -13
  25. package/ts/proxies/smart-proxy/models/route-types.ts +184 -0
  26. package/ts/proxies/smart-proxy/route-connection-handler.ts +1117 -0
  27. package/ts/proxies/smart-proxy/route-helpers.ts +344 -0
  28. package/ts/proxies/smart-proxy/route-manager.ts +587 -0
  29. package/ts/proxies/smart-proxy/smart-proxy.ts +300 -69
@@ -0,0 +1,587 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type {
3
+ IRouteConfig,
4
+ IRouteMatch,
5
+ IRouteAction,
6
+ TPortRange
7
+ } from './models/route-types.js';
8
+ import type {
9
+ ISmartProxyOptions,
10
+ IRoutedSmartProxyOptions,
11
+ IDomainConfig
12
+ } from './models/interfaces.js';
13
+ import {
14
+ isRoutedOptions,
15
+ isLegacyOptions
16
+ } from './models/interfaces.js';
17
+
18
+ /**
19
+ * Result of route matching
20
+ */
21
+ export interface IRouteMatchResult {
22
+ route: IRouteConfig;
23
+ // Additional match parameters (path, query, etc.)
24
+ params?: Record<string, string>;
25
+ }
26
+
27
+ /**
28
+ * The RouteManager handles all routing decisions based on connections and attributes
29
+ */
30
+ export class RouteManager extends plugins.EventEmitter {
31
+ private routes: IRouteConfig[] = [];
32
+ private portMap: Map<number, IRouteConfig[]> = new Map();
33
+ private options: IRoutedSmartProxyOptions;
34
+
35
+ constructor(options: ISmartProxyOptions) {
36
+ super();
37
+
38
+ // We no longer support legacy options, always use provided options
39
+ this.options = options;
40
+
41
+ // Initialize routes from either source
42
+ this.updateRoutes(this.options.routes);
43
+ }
44
+
45
+ /**
46
+ * Update routes with new configuration
47
+ */
48
+ public updateRoutes(routes: IRouteConfig[] = []): void {
49
+ // Sort routes by priority (higher first)
50
+ this.routes = [...(routes || [])].sort((a, b) => {
51
+ const priorityA = a.priority ?? 0;
52
+ const priorityB = b.priority ?? 0;
53
+ return priorityB - priorityA;
54
+ });
55
+
56
+ // Rebuild port mapping for fast lookups
57
+ this.rebuildPortMap();
58
+ }
59
+
60
+ /**
61
+ * Rebuild the port mapping for fast lookups
62
+ */
63
+ private rebuildPortMap(): void {
64
+ this.portMap.clear();
65
+
66
+ for (const route of this.routes) {
67
+ const ports = this.expandPortRange(route.match.ports);
68
+
69
+ for (const port of ports) {
70
+ if (!this.portMap.has(port)) {
71
+ this.portMap.set(port, []);
72
+ }
73
+ this.portMap.get(port)!.push(route);
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Expand a port range specification into an array of individual ports
80
+ */
81
+ private expandPortRange(portRange: TPortRange): number[] {
82
+ if (typeof portRange === 'number') {
83
+ return [portRange];
84
+ }
85
+
86
+ if (Array.isArray(portRange)) {
87
+ // Handle array of port objects or numbers
88
+ return portRange.flatMap(item => {
89
+ if (typeof item === 'number') {
90
+ return [item];
91
+ } else if (typeof item === 'object' && 'from' in item && 'to' in item) {
92
+ // Handle port range object
93
+ const ports: number[] = [];
94
+ for (let p = item.from; p <= item.to; p++) {
95
+ ports.push(p);
96
+ }
97
+ return ports;
98
+ }
99
+ return [];
100
+ });
101
+ }
102
+
103
+ return [];
104
+ }
105
+
106
+ /**
107
+ * Get all ports that should be listened on
108
+ */
109
+ public getListeningPorts(): number[] {
110
+ return Array.from(this.portMap.keys());
111
+ }
112
+
113
+ /**
114
+ * Get all routes for a given port
115
+ */
116
+ public getRoutesForPort(port: number): IRouteConfig[] {
117
+ return this.portMap.get(port) || [];
118
+ }
119
+
120
+ /**
121
+ * Test if a pattern matches a domain using glob matching
122
+ */
123
+ private matchDomain(pattern: string, domain: string): boolean {
124
+ // Convert glob pattern to regex
125
+ const regexPattern = pattern
126
+ .replace(/\./g, '\\.') // Escape dots
127
+ .replace(/\*/g, '.*'); // Convert * to .*
128
+
129
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
130
+ return regex.test(domain);
131
+ }
132
+
133
+ /**
134
+ * Match a domain against all patterns in a route
135
+ */
136
+ private matchRouteDomain(route: IRouteConfig, domain: string): boolean {
137
+ if (!route.match.domains) {
138
+ // If no domains specified, match all domains
139
+ return true;
140
+ }
141
+
142
+ const patterns = Array.isArray(route.match.domains)
143
+ ? route.match.domains
144
+ : [route.match.domains];
145
+
146
+ return patterns.some(pattern => this.matchDomain(pattern, domain));
147
+ }
148
+
149
+ /**
150
+ * Check if a client IP is allowed by a route's security settings
151
+ */
152
+ private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean {
153
+ const security = route.action.security;
154
+
155
+ if (!security) {
156
+ return true; // No security settings means allowed
157
+ }
158
+
159
+ // Check blocked IPs first
160
+ if (security.blockedIps && security.blockedIps.length > 0) {
161
+ for (const pattern of security.blockedIps) {
162
+ if (this.matchIpPattern(pattern, clientIp)) {
163
+ return false; // IP is blocked
164
+ }
165
+ }
166
+ }
167
+
168
+ // If there are allowed IPs, check them
169
+ if (security.allowedIps && security.allowedIps.length > 0) {
170
+ for (const pattern of security.allowedIps) {
171
+ if (this.matchIpPattern(pattern, clientIp)) {
172
+ return true; // IP is allowed
173
+ }
174
+ }
175
+ return false; // IP not in allowed list
176
+ }
177
+
178
+ // No allowed IPs specified, so IP is allowed
179
+ return true;
180
+ }
181
+
182
+ /**
183
+ * Match an IP against a pattern
184
+ */
185
+ private matchIpPattern(pattern: string, ip: string): boolean {
186
+ // Handle exact match
187
+ if (pattern === ip) {
188
+ return true;
189
+ }
190
+
191
+ // Handle CIDR notation (e.g., 192.168.1.0/24)
192
+ if (pattern.includes('/')) {
193
+ return this.matchIpCidr(pattern, ip);
194
+ }
195
+
196
+ // Handle glob pattern (e.g., 192.168.1.*)
197
+ if (pattern.includes('*')) {
198
+ const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
199
+ const regex = new RegExp(`^${regexPattern}$`);
200
+ return regex.test(ip);
201
+ }
202
+
203
+ return false;
204
+ }
205
+
206
+ /**
207
+ * Match an IP against a CIDR pattern
208
+ */
209
+ private matchIpCidr(cidr: string, ip: string): boolean {
210
+ try {
211
+ // In a real implementation, you'd use a proper IP library
212
+ // This is a simplified implementation
213
+ const [subnet, bits] = cidr.split('/');
214
+ const mask = parseInt(bits, 10);
215
+
216
+ // Convert IP addresses to numeric values
217
+ const ipNum = this.ipToNumber(ip);
218
+ const subnetNum = this.ipToNumber(subnet);
219
+
220
+ // Calculate subnet mask
221
+ const maskNum = ~(2 ** (32 - mask) - 1);
222
+
223
+ // Check if IP is in subnet
224
+ return (ipNum & maskNum) === (subnetNum & maskNum);
225
+ } catch (e) {
226
+ console.error(`Error matching IP ${ip} against CIDR ${cidr}:`, e);
227
+ return false;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Convert an IP address to a numeric value
233
+ */
234
+ private ipToNumber(ip: string): number {
235
+ const parts = ip.split('.').map(part => parseInt(part, 10));
236
+ return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
237
+ }
238
+
239
+ /**
240
+ * Find the matching route for a connection
241
+ */
242
+ public findMatchingRoute(options: {
243
+ port: number;
244
+ domain?: string;
245
+ clientIp: string;
246
+ path?: string;
247
+ tlsVersion?: string;
248
+ }): IRouteMatchResult | null {
249
+ const { port, domain, clientIp, path, tlsVersion } = options;
250
+
251
+ // Get all routes for this port
252
+ const routesForPort = this.getRoutesForPort(port);
253
+
254
+ // Find the first matching route based on priority order
255
+ for (const route of routesForPort) {
256
+ // Check domain match if specified
257
+ if (domain && !this.matchRouteDomain(route, domain)) {
258
+ continue;
259
+ }
260
+
261
+ // Check path match if specified in both route and request
262
+ if (path && route.match.path) {
263
+ if (!this.matchPath(route.match.path, path)) {
264
+ continue;
265
+ }
266
+ }
267
+
268
+ // Check client IP match
269
+ if (route.match.clientIp && !route.match.clientIp.some(pattern =>
270
+ this.matchIpPattern(pattern, clientIp))) {
271
+ continue;
272
+ }
273
+
274
+ // Check TLS version match
275
+ if (tlsVersion && route.match.tlsVersion &&
276
+ !route.match.tlsVersion.includes(tlsVersion)) {
277
+ continue;
278
+ }
279
+
280
+ // Check security settings
281
+ if (!this.isClientIpAllowed(route, clientIp)) {
282
+ continue;
283
+ }
284
+
285
+ // All checks passed, this route matches
286
+ return { route };
287
+ }
288
+
289
+ return null;
290
+ }
291
+
292
+ /**
293
+ * Match a path against a pattern
294
+ */
295
+ private matchPath(pattern: string, path: string): boolean {
296
+ // Convert the glob pattern to a regex
297
+ const regexPattern = pattern
298
+ .replace(/\./g, '\\.') // Escape dots
299
+ .replace(/\*/g, '.*') // Convert * to .*
300
+ .replace(/\//g, '\\/'); // Escape slashes
301
+
302
+ const regex = new RegExp(`^${regexPattern}$`);
303
+ return regex.test(path);
304
+ }
305
+
306
+ /**
307
+ * Convert a domain config to routes
308
+ * (For backward compatibility with code that still uses domainConfigs)
309
+ */
310
+ public domainConfigToRoutes(domainConfig: IDomainConfig): IRouteConfig[] {
311
+ const routes: IRouteConfig[] = [];
312
+ const { domains, forwarding } = domainConfig;
313
+
314
+ // Determine the action based on forwarding type
315
+ let action: IRouteAction = {
316
+ type: 'forward',
317
+ target: {
318
+ host: forwarding.target.host,
319
+ port: forwarding.target.port
320
+ }
321
+ };
322
+
323
+ // Set TLS mode based on forwarding type
324
+ switch (forwarding.type) {
325
+ case 'http-only':
326
+ // No TLS settings needed
327
+ break;
328
+ case 'https-passthrough':
329
+ action.tls = { mode: 'passthrough' };
330
+ break;
331
+ case 'https-terminate-to-http':
332
+ action.tls = {
333
+ mode: 'terminate',
334
+ certificate: forwarding.https?.customCert ? {
335
+ key: forwarding.https.customCert.key,
336
+ cert: forwarding.https.customCert.cert
337
+ } : 'auto'
338
+ };
339
+ break;
340
+ case 'https-terminate-to-https':
341
+ action.tls = {
342
+ mode: 'terminate-and-reencrypt',
343
+ certificate: forwarding.https?.customCert ? {
344
+ key: forwarding.https.customCert.key,
345
+ cert: forwarding.https.customCert.cert
346
+ } : 'auto'
347
+ };
348
+ break;
349
+ }
350
+
351
+ // Add security settings if present
352
+ if (forwarding.security) {
353
+ action.security = {
354
+ allowedIps: forwarding.security.allowedIps,
355
+ blockedIps: forwarding.security.blockedIps,
356
+ maxConnections: forwarding.security.maxConnections
357
+ };
358
+ }
359
+
360
+ // Add advanced settings if present
361
+ if (forwarding.advanced) {
362
+ action.advanced = {
363
+ timeout: forwarding.advanced.timeout,
364
+ headers: forwarding.advanced.headers,
365
+ keepAlive: forwarding.advanced.keepAlive
366
+ };
367
+ }
368
+
369
+ // Determine which port to use based on forwarding type
370
+ const defaultPort = forwarding.type.startsWith('https') ? 443 : 80;
371
+
372
+ // Add the main route
373
+ routes.push({
374
+ match: {
375
+ ports: defaultPort,
376
+ domains
377
+ },
378
+ action,
379
+ name: `Route for ${domains.join(', ')}`
380
+ });
381
+
382
+ // Add HTTP redirect if needed
383
+ if (forwarding.http?.redirectToHttps) {
384
+ routes.push({
385
+ match: {
386
+ ports: 80,
387
+ domains
388
+ },
389
+ action: {
390
+ type: 'redirect',
391
+ redirect: {
392
+ to: 'https://{domain}{path}',
393
+ status: 301
394
+ }
395
+ },
396
+ name: `HTTP Redirect for ${domains.join(', ')}`,
397
+ priority: 100 // Higher priority for redirects
398
+ });
399
+ }
400
+
401
+ // Add port ranges if specified
402
+ if (forwarding.advanced?.portRanges) {
403
+ for (const range of forwarding.advanced.portRanges) {
404
+ routes.push({
405
+ match: {
406
+ ports: [{ from: range.from, to: range.to }],
407
+ domains
408
+ },
409
+ action,
410
+ name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')}`
411
+ });
412
+ }
413
+ }
414
+
415
+ return routes;
416
+ }
417
+
418
+ /**
419
+ * Update routes based on domain configs
420
+ * (For backward compatibility with code that still uses domainConfigs)
421
+ */
422
+ public updateFromDomainConfigs(domainConfigs: IDomainConfig[]): void {
423
+ const routes: IRouteConfig[] = [];
424
+
425
+ // Convert each domain config to routes
426
+ for (const config of domainConfigs) {
427
+ routes.push(...this.domainConfigToRoutes(config));
428
+ }
429
+
430
+ // Merge with existing routes that aren't derived from domain configs
431
+ const nonDomainRoutes = this.routes.filter(r =>
432
+ !r.name || !r.name.includes('for '));
433
+
434
+ this.updateRoutes([...nonDomainRoutes, ...routes]);
435
+ }
436
+
437
+ /**
438
+ * Validate the route configuration and return any warnings
439
+ */
440
+ public validateConfiguration(): string[] {
441
+ const warnings: string[] = [];
442
+ const duplicatePorts = new Map<number, number>();
443
+
444
+ // Check for routes with the same exact match criteria
445
+ for (let i = 0; i < this.routes.length; i++) {
446
+ for (let j = i + 1; j < this.routes.length; j++) {
447
+ const route1 = this.routes[i];
448
+ const route2 = this.routes[j];
449
+
450
+ // Check if route match criteria are the same
451
+ if (this.areMatchesSimilar(route1.match, route2.match)) {
452
+ warnings.push(
453
+ `Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` +
454
+ `The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.`
455
+ );
456
+ }
457
+ }
458
+ }
459
+
460
+ // Check for routes that may never be matched due to priority
461
+ for (let i = 0; i < this.routes.length; i++) {
462
+ const route = this.routes[i];
463
+ const higherPriorityRoutes = this.routes.filter(r =>
464
+ (r.priority || 0) > (route.priority || 0));
465
+
466
+ for (const higherRoute of higherPriorityRoutes) {
467
+ if (this.isRouteShadowed(route, higherRoute)) {
468
+ warnings.push(
469
+ `Route "${route.name || i}" may never be matched because it is shadowed by ` +
470
+ `higher priority route "${higherRoute.name || 'unnamed'}"`
471
+ );
472
+ break;
473
+ }
474
+ }
475
+ }
476
+
477
+ return warnings;
478
+ }
479
+
480
+ /**
481
+ * Check if two route matches are similar (potential conflict)
482
+ */
483
+ private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean {
484
+ // Check port overlap
485
+ const ports1 = new Set(this.expandPortRange(match1.ports));
486
+ const ports2 = new Set(this.expandPortRange(match2.ports));
487
+
488
+ let havePortOverlap = false;
489
+ for (const port of ports1) {
490
+ if (ports2.has(port)) {
491
+ havePortOverlap = true;
492
+ break;
493
+ }
494
+ }
495
+
496
+ if (!havePortOverlap) {
497
+ return false;
498
+ }
499
+
500
+ // Check domain overlap
501
+ if (match1.domains && match2.domains) {
502
+ const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains];
503
+ const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains];
504
+
505
+ // Check if any domain pattern from match1 could match any from match2
506
+ let haveDomainOverlap = false;
507
+ for (const domain1 of domains1) {
508
+ for (const domain2 of domains2) {
509
+ if (domain1 === domain2 ||
510
+ (domain1.includes('*') || domain2.includes('*'))) {
511
+ haveDomainOverlap = true;
512
+ break;
513
+ }
514
+ }
515
+ if (haveDomainOverlap) break;
516
+ }
517
+
518
+ if (!haveDomainOverlap) {
519
+ return false;
520
+ }
521
+ } else if (match1.domains || match2.domains) {
522
+ // One has domains, the other doesn't - they could overlap
523
+ // The one with domains is more specific, so it's not exactly a conflict
524
+ return false;
525
+ }
526
+
527
+ // Check path overlap
528
+ if (match1.path && match2.path) {
529
+ // This is a simplified check - in a real implementation,
530
+ // you'd need to check if the path patterns could match the same paths
531
+ return match1.path === match2.path ||
532
+ match1.path.includes('*') ||
533
+ match2.path.includes('*');
534
+ } else if (match1.path || match2.path) {
535
+ // One has a path, the other doesn't
536
+ return false;
537
+ }
538
+
539
+ // If we get here, the matches have significant overlap
540
+ return true;
541
+ }
542
+
543
+ /**
544
+ * Check if a route is completely shadowed by a higher priority route
545
+ */
546
+ private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean {
547
+ // If they don't have similar match criteria, no shadowing occurs
548
+ if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) {
549
+ return false;
550
+ }
551
+
552
+ // If higher priority route has more specific criteria, no shadowing
553
+ if (this.isRouteMoreSpecific(higherPriorityRoute.match, route.match)) {
554
+ return false;
555
+ }
556
+
557
+ // If higher priority route is equally or less specific but has higher priority,
558
+ // it shadows the lower priority route
559
+ return true;
560
+ }
561
+
562
+ /**
563
+ * Check if route1 is more specific than route2
564
+ */
565
+ private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean {
566
+ // Check if match1 has more specific criteria
567
+ let match1Points = 0;
568
+ let match2Points = 0;
569
+
570
+ // Path is the most specific
571
+ if (match1.path) match1Points += 3;
572
+ if (match2.path) match2Points += 3;
573
+
574
+ // Domain is next most specific
575
+ if (match1.domains) match1Points += 2;
576
+ if (match2.domains) match2Points += 2;
577
+
578
+ // Client IP and TLS version are least specific
579
+ if (match1.clientIp) match1Points += 1;
580
+ if (match2.clientIp) match2Points += 1;
581
+
582
+ if (match1.tlsVersion) match1Points += 1;
583
+ if (match2.tlsVersion) match2Points += 1;
584
+
585
+ return match1Points > match2Points;
586
+ }
587
+ }