@push.rocks/smartproxy 19.6.17 → 20.0.1

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 (27) hide show
  1. package/changelog.md +142 -0
  2. package/dist_ts/core/utils/shared-security-manager.js +30 -5
  3. package/dist_ts/proxies/http-proxy/request-handler.d.ts +4 -0
  4. package/dist_ts/proxies/http-proxy/request-handler.js +104 -21
  5. package/dist_ts/proxies/http-proxy/websocket-handler.d.ts +4 -0
  6. package/dist_ts/proxies/http-proxy/websocket-handler.js +78 -8
  7. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +19 -2
  8. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  9. package/dist_ts/proxies/smart-proxy/nftables-manager.js +14 -11
  10. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -0
  11. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +112 -28
  12. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +23 -23
  13. package/dist_ts/proxies/smart-proxy/utils/route-patterns.js +13 -13
  14. package/dist_ts/proxies/smart-proxy/utils/route-utils.js +4 -7
  15. package/dist_ts/proxies/smart-proxy/utils/route-validators.js +41 -25
  16. package/package.json +3 -2
  17. package/readme.plan.md +139 -266
  18. package/ts/core/utils/shared-security-manager.ts +33 -4
  19. package/ts/proxies/http-proxy/request-handler.ts +124 -21
  20. package/ts/proxies/http-proxy/websocket-handler.ts +96 -8
  21. package/ts/proxies/smart-proxy/models/route-types.ts +34 -8
  22. package/ts/proxies/smart-proxy/nftables-manager.ts +14 -10
  23. package/ts/proxies/smart-proxy/route-connection-handler.ts +132 -28
  24. package/ts/proxies/smart-proxy/utils/route-helpers.ts +14 -14
  25. package/ts/proxies/smart-proxy/utils/route-patterns.ts +6 -6
  26. package/ts/proxies/smart-proxy/utils/route-utils.ts +3 -6
  27. package/ts/proxies/smart-proxy/utils/route-validators.ts +38 -21
@@ -3,7 +3,7 @@ import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.
3
3
  import { logger } from '../../core/utils/logger.js';
4
4
  import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
5
5
  // Route checking functions have been removed
6
- import type { IRouteConfig, IRouteAction } from './models/route-types.js';
6
+ import type { IRouteConfig, IRouteAction, IRouteTarget } from './models/route-types.js';
7
7
  import type { IRouteContext } from '../../core/models/route-context.js';
8
8
  import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
9
9
  import { WrappedSocket } from '../../core/models/wrapped-socket.js';
@@ -657,6 +657,80 @@ export class RouteConnectionHandler {
657
657
  }
658
658
  }
659
659
 
660
+ /**
661
+ * Select the appropriate target from the targets array based on sub-matching criteria
662
+ */
663
+ private selectTarget(
664
+ targets: IRouteTarget[],
665
+ context: {
666
+ port: number;
667
+ path?: string;
668
+ headers?: Record<string, string>;
669
+ method?: string;
670
+ }
671
+ ): IRouteTarget | null {
672
+ // Sort targets by priority (higher first)
673
+ const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
674
+
675
+ // Find the first matching target
676
+ for (const target of sortedTargets) {
677
+ if (!target.match) {
678
+ // No match criteria means this is a default/fallback target
679
+ return target;
680
+ }
681
+
682
+ // Check port match
683
+ if (target.match.ports && !target.match.ports.includes(context.port)) {
684
+ continue;
685
+ }
686
+
687
+ // Check path match (supports wildcards)
688
+ if (target.match.path && context.path) {
689
+ const pathPattern = target.match.path.replace(/\*/g, '.*');
690
+ const pathRegex = new RegExp(`^${pathPattern}$`);
691
+ if (!pathRegex.test(context.path)) {
692
+ continue;
693
+ }
694
+ }
695
+
696
+ // Check method match
697
+ if (target.match.method && context.method && !target.match.method.includes(context.method)) {
698
+ continue;
699
+ }
700
+
701
+ // Check headers match
702
+ if (target.match.headers && context.headers) {
703
+ let headersMatch = true;
704
+ for (const [key, pattern] of Object.entries(target.match.headers)) {
705
+ const headerValue = context.headers[key.toLowerCase()];
706
+ if (!headerValue) {
707
+ headersMatch = false;
708
+ break;
709
+ }
710
+
711
+ if (pattern instanceof RegExp) {
712
+ if (!pattern.test(headerValue)) {
713
+ headersMatch = false;
714
+ break;
715
+ }
716
+ } else if (headerValue !== pattern) {
717
+ headersMatch = false;
718
+ break;
719
+ }
720
+ }
721
+ if (!headersMatch) {
722
+ continue;
723
+ }
724
+ }
725
+
726
+ // All criteria matched
727
+ return target;
728
+ }
729
+
730
+ // No matching target found, return the first target without match criteria (default)
731
+ return sortedTargets.find(t => !t.match) || null;
732
+ }
733
+
660
734
  /**
661
735
  * Handle a forward action for a route
662
736
  */
@@ -731,14 +805,37 @@ export class RouteConnectionHandler {
731
805
  return;
732
806
  }
733
807
 
734
- // We should have a target configuration for forwarding
735
- if (!action.target) {
736
- logger.log('error', `Forward action missing target configuration for connection ${connectionId}`, {
808
+ // Select the appropriate target from the targets array
809
+ if (!action.targets || action.targets.length === 0) {
810
+ logger.log('error', `Forward action missing targets configuration for connection ${connectionId}`, {
811
+ connectionId,
812
+ component: 'route-handler'
813
+ });
814
+ socket.end();
815
+ this.smartProxy.connectionManager.cleanupConnection(record, 'missing_targets');
816
+ return;
817
+ }
818
+
819
+ // Create context for target selection
820
+ const targetSelectionContext = {
821
+ port: record.localPort,
822
+ path: undefined, // Will be populated from HTTP headers if available
823
+ headers: undefined, // Will be populated from HTTP headers if available
824
+ method: undefined // Will be populated from HTTP headers if available
825
+ };
826
+
827
+ // TODO: Extract path, headers, and method from initialChunk if it's HTTP
828
+ // For now, we'll select based on port only
829
+
830
+ const selectedTarget = this.selectTarget(action.targets, targetSelectionContext);
831
+ if (!selectedTarget) {
832
+ logger.log('error', `No matching target found for connection ${connectionId}`, {
737
833
  connectionId,
834
+ port: targetSelectionContext.port,
738
835
  component: 'route-handler'
739
836
  });
740
837
  socket.end();
741
- this.smartProxy.connectionManager.cleanupConnection(record, 'missing_target');
838
+ this.smartProxy.connectionManager.cleanupConnection(record, 'no_matching_target');
742
839
  return;
743
840
  }
744
841
 
@@ -759,9 +856,9 @@ export class RouteConnectionHandler {
759
856
 
760
857
  // Determine host using function or static value
761
858
  let targetHost: string | string[];
762
- if (typeof action.target.host === 'function') {
859
+ if (typeof selectedTarget.host === 'function') {
763
860
  try {
764
- targetHost = action.target.host(routeContext);
861
+ targetHost = selectedTarget.host(routeContext);
765
862
  if (this.smartProxy.settings.enableDetailedLogging) {
766
863
  logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
767
864
  connectionId,
@@ -780,7 +877,7 @@ export class RouteConnectionHandler {
780
877
  return;
781
878
  }
782
879
  } else {
783
- targetHost = action.target.host;
880
+ targetHost = selectedTarget.host;
784
881
  }
785
882
 
786
883
  // If an array of hosts, select one randomly for load balancing
@@ -790,9 +887,9 @@ export class RouteConnectionHandler {
790
887
 
791
888
  // Determine port using function or static value
792
889
  let targetPort: number;
793
- if (typeof action.target.port === 'function') {
890
+ if (typeof selectedTarget.port === 'function') {
794
891
  try {
795
- targetPort = action.target.port(routeContext);
892
+ targetPort = selectedTarget.port(routeContext);
796
893
  if (this.smartProxy.settings.enableDetailedLogging) {
797
894
  logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
798
895
  connectionId,
@@ -813,20 +910,27 @@ export class RouteConnectionHandler {
813
910
  this.smartProxy.connectionManager.cleanupConnection(record, 'port_mapping_error');
814
911
  return;
815
912
  }
816
- } else if (action.target.port === 'preserve') {
913
+ } else if (selectedTarget.port === 'preserve') {
817
914
  // Use incoming port if port is 'preserve'
818
915
  targetPort = record.localPort;
819
916
  } else {
820
917
  // Use static port from configuration
821
- targetPort = action.target.port;
918
+ targetPort = selectedTarget.port;
822
919
  }
823
920
 
824
921
  // Store the resolved host in the context
825
922
  routeContext.targetHost = selectedHost;
826
923
 
924
+ // Get effective settings (target overrides route-level settings)
925
+ const effectiveTls = selectedTarget.tls || action.tls;
926
+ const effectiveWebsocket = selectedTarget.websocket || action.websocket;
927
+ const effectiveSendProxyProtocol = selectedTarget.sendProxyProtocol !== undefined
928
+ ? selectedTarget.sendProxyProtocol
929
+ : action.sendProxyProtocol;
930
+
827
931
  // Determine if this needs TLS handling
828
- if (action.tls) {
829
- switch (action.tls.mode) {
932
+ if (effectiveTls) {
933
+ switch (effectiveTls.mode) {
830
934
  case 'passthrough':
831
935
  // For TLS passthrough, just forward directly
832
936
  if (this.smartProxy.settings.enableDetailedLogging) {
@@ -853,9 +957,9 @@ export class RouteConnectionHandler {
853
957
  // For TLS termination, use HttpProxy
854
958
  if (this.smartProxy.httpProxyBridge.getHttpProxy()) {
855
959
  if (this.smartProxy.settings.enableDetailedLogging) {
856
- logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host} for connection ${connectionId}`, {
960
+ logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(selectedTarget.host) ? selectedTarget.host.join(', ') : selectedTarget.host} for connection ${connectionId}`, {
857
961
  connectionId,
858
- targetHost: action.target.host,
962
+ targetHost: selectedTarget.host,
859
963
  component: 'route-handler'
860
964
  });
861
965
  }
@@ -929,10 +1033,10 @@ export class RouteConnectionHandler {
929
1033
  } else {
930
1034
  // Basic forwarding
931
1035
  if (this.smartProxy.settings.enableDetailedLogging) {
932
- logger.log('info', `Using basic forwarding to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host}:${action.target.port} for connection ${connectionId}`, {
1036
+ logger.log('info', `Using basic forwarding to ${Array.isArray(selectedTarget.host) ? selectedTarget.host.join(', ') : selectedTarget.host}:${selectedTarget.port} for connection ${connectionId}`, {
933
1037
  connectionId,
934
- targetHost: action.target.host,
935
- targetPort: action.target.port,
1038
+ targetHost: selectedTarget.host,
1039
+ targetPort: selectedTarget.port,
936
1040
  component: 'route-handler'
937
1041
  });
938
1042
  }
@@ -940,27 +1044,27 @@ export class RouteConnectionHandler {
940
1044
  // Get the appropriate host value
941
1045
  let targetHost: string;
942
1046
 
943
- if (typeof action.target.host === 'function') {
1047
+ if (typeof selectedTarget.host === 'function') {
944
1048
  // For function-based host, use the same routeContext created earlier
945
- const hostResult = action.target.host(routeContext);
1049
+ const hostResult = selectedTarget.host(routeContext);
946
1050
  targetHost = Array.isArray(hostResult)
947
1051
  ? hostResult[Math.floor(Math.random() * hostResult.length)]
948
1052
  : hostResult;
949
1053
  } else {
950
1054
  // For static host value
951
- targetHost = Array.isArray(action.target.host)
952
- ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
953
- : action.target.host;
1055
+ targetHost = Array.isArray(selectedTarget.host)
1056
+ ? selectedTarget.host[Math.floor(Math.random() * selectedTarget.host.length)]
1057
+ : selectedTarget.host;
954
1058
  }
955
1059
 
956
1060
  // Determine port - either function-based, static, or preserve incoming port
957
1061
  let targetPort: number;
958
- if (typeof action.target.port === 'function') {
959
- targetPort = action.target.port(routeContext);
960
- } else if (action.target.port === 'preserve') {
1062
+ if (typeof selectedTarget.port === 'function') {
1063
+ targetPort = selectedTarget.port(routeContext);
1064
+ } else if (selectedTarget.port === 'preserve') {
961
1065
  targetPort = record.localPort;
962
1066
  } else {
963
- targetPort = action.target.port;
1067
+ targetPort = selectedTarget.port;
964
1068
  }
965
1069
 
966
1070
  // Update the connection record and context with resolved values
@@ -42,7 +42,7 @@ export function createHttpRoute(
42
42
  // Create route action
43
43
  const action: IRouteAction = {
44
44
  type: 'forward',
45
- target
45
+ targets: [target]
46
46
  };
47
47
 
48
48
  // Create the route config
@@ -82,7 +82,7 @@ export function createHttpsTerminateRoute(
82
82
  // Create route action
83
83
  const action: IRouteAction = {
84
84
  type: 'forward',
85
- target,
85
+ targets: [target],
86
86
  tls: {
87
87
  mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
88
88
  certificate: options.certificate || 'auto'
@@ -152,7 +152,7 @@ export function createHttpsPassthroughRoute(
152
152
  // Create route action
153
153
  const action: IRouteAction = {
154
154
  type: 'forward',
155
- target,
155
+ targets: [target],
156
156
  tls: {
157
157
  mode: 'passthrough'
158
158
  }
@@ -243,7 +243,7 @@ export function createLoadBalancerRoute(
243
243
  // Create route action
244
244
  const action: IRouteAction = {
245
245
  type: 'forward',
246
- target
246
+ targets: [target]
247
247
  };
248
248
 
249
249
  // Add TLS configuration if provided
@@ -303,7 +303,7 @@ export function createApiRoute(
303
303
  // Create route action
304
304
  const action: IRouteAction = {
305
305
  type: 'forward',
306
- target
306
+ targets: [target]
307
307
  };
308
308
 
309
309
  // Add TLS configuration if using HTTPS
@@ -374,7 +374,7 @@ export function createWebSocketRoute(
374
374
  // Create route action
375
375
  const action: IRouteAction = {
376
376
  type: 'forward',
377
- target,
377
+ targets: [target],
378
378
  websocket: {
379
379
  enabled: true,
380
380
  pingInterval: options.pingInterval || 30000, // 30 seconds
@@ -432,10 +432,10 @@ export function createPortMappingRoute(options: {
432
432
  // Create route action
433
433
  const action: IRouteAction = {
434
434
  type: 'forward',
435
- target: {
435
+ targets: [{
436
436
  host: options.targetHost,
437
437
  port: options.portMapper
438
- }
438
+ }]
439
439
  };
440
440
 
441
441
  // Create the route config
@@ -500,10 +500,10 @@ export function createDynamicRoute(options: {
500
500
  // Create route action
501
501
  const action: IRouteAction = {
502
502
  type: 'forward',
503
- target: {
503
+ targets: [{
504
504
  host: options.targetHost,
505
505
  port: options.portMapper
506
- }
506
+ }]
507
507
  };
508
508
 
509
509
  // Create the route config
@@ -548,10 +548,10 @@ export function createSmartLoadBalancer(options: {
548
548
  // Create route action
549
549
  const action: IRouteAction = {
550
550
  type: 'forward',
551
- target: {
551
+ targets: [{
552
552
  host: hostSelector,
553
553
  port: options.portMapper
554
- }
554
+ }]
555
555
  };
556
556
 
557
557
  // Create the route config
@@ -609,10 +609,10 @@ export function createNfTablesRoute(
609
609
  // Create route action
610
610
  const action: IRouteAction = {
611
611
  type: 'forward',
612
- target: {
612
+ targets: [{
613
613
  host: target.host,
614
614
  port: target.port
615
- },
615
+ }],
616
616
  forwardingEngine: 'nftables',
617
617
  nftables: {
618
618
  protocol: options.protocol || 'tcp',
@@ -24,10 +24,10 @@ export function createHttpRoute(
24
24
  },
25
25
  action: {
26
26
  type: 'forward',
27
- target: {
27
+ targets: [{
28
28
  host: target.host,
29
29
  port: target.port
30
- }
30
+ }]
31
31
  },
32
32
  name: options.name || `HTTP: ${Array.isArray(domains) ? domains.join(', ') : domains}`
33
33
  };
@@ -53,10 +53,10 @@ export function createHttpsTerminateRoute(
53
53
  },
54
54
  action: {
55
55
  type: 'forward',
56
- target: {
56
+ targets: [{
57
57
  host: target.host,
58
58
  port: target.port
59
- },
59
+ }],
60
60
  tls: {
61
61
  mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
62
62
  certificate: options.certificate || 'auto'
@@ -83,10 +83,10 @@ export function createHttpsPassthroughRoute(
83
83
  },
84
84
  action: {
85
85
  type: 'forward',
86
- target: {
86
+ targets: [{
87
87
  host: target.host,
88
88
  port: target.port
89
- },
89
+ }],
90
90
  tls: {
91
91
  mode: 'passthrough'
92
92
  }
@@ -66,12 +66,9 @@ export function mergeRouteConfigs(
66
66
  // Otherwise merge the action properties
67
67
  mergedRoute.action = { ...mergedRoute.action };
68
68
 
69
- // Merge target
70
- if (overrideRoute.action.target) {
71
- mergedRoute.action.target = {
72
- ...mergedRoute.action.target,
73
- ...overrideRoute.action.target
74
- };
69
+ // Merge targets
70
+ if (overrideRoute.action.targets) {
71
+ mergedRoute.action.targets = overrideRoute.action.targets;
75
72
  }
76
73
 
77
74
  // Merge TLS options
@@ -102,29 +102,43 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
102
102
  errors.push(`Invalid action type: ${action.type}`);
103
103
  }
104
104
 
105
- // Validate target for 'forward' action
105
+ // Validate targets for 'forward' action
106
106
  if (action.type === 'forward') {
107
- if (!action.target) {
108
- errors.push('Target is required for forward action');
107
+ if (!action.targets || !Array.isArray(action.targets) || action.targets.length === 0) {
108
+ errors.push('Targets array is required for forward action');
109
109
  } else {
110
- // Validate target host
111
- if (!action.target.host) {
112
- errors.push('Target host is required');
113
- } else if (typeof action.target.host !== 'string' &&
114
- !Array.isArray(action.target.host) &&
115
- typeof action.target.host !== 'function') {
116
- errors.push('Target host must be a string, array of strings, or function');
117
- }
110
+ // Validate each target
111
+ action.targets.forEach((target, index) => {
112
+ // Validate target host
113
+ if (!target.host) {
114
+ errors.push(`Target[${index}] host is required`);
115
+ } else if (typeof target.host !== 'string' &&
116
+ !Array.isArray(target.host) &&
117
+ typeof target.host !== 'function') {
118
+ errors.push(`Target[${index}] host must be a string, array of strings, or function`);
119
+ }
118
120
 
119
- // Validate target port
120
- if (action.target.port === undefined) {
121
- errors.push('Target port is required');
122
- } else if (typeof action.target.port !== 'number' &&
123
- typeof action.target.port !== 'function') {
124
- errors.push('Target port must be a number or a function');
125
- } else if (typeof action.target.port === 'number' && !isValidPort(action.target.port)) {
126
- errors.push('Target port must be between 1 and 65535');
127
- }
121
+ // Validate target port
122
+ if (target.port === undefined) {
123
+ errors.push(`Target[${index}] port is required`);
124
+ } else if (typeof target.port !== 'number' &&
125
+ typeof target.port !== 'function' &&
126
+ target.port !== 'preserve') {
127
+ errors.push(`Target[${index}] port must be a number, 'preserve', or a function`);
128
+ } else if (typeof target.port === 'number' && !isValidPort(target.port)) {
129
+ errors.push(`Target[${index}] port must be between 1 and 65535`);
130
+ }
131
+
132
+ // Validate match criteria if present
133
+ if (target.match) {
134
+ if (target.match.ports && !Array.isArray(target.match.ports)) {
135
+ errors.push(`Target[${index}] match.ports must be an array`);
136
+ }
137
+ if (target.match.method && !Array.isArray(target.match.method)) {
138
+ errors.push(`Target[${index}] match.method must be an array`);
139
+ }
140
+ }
141
+ });
128
142
  }
129
143
 
130
144
  // Validate TLS options for forward actions
@@ -242,7 +256,10 @@ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType:
242
256
 
243
257
  switch (actionType) {
244
258
  case 'forward':
245
- return !!route.action.target && !!route.action.target.host && !!route.action.target.port;
259
+ return !!route.action.targets &&
260
+ Array.isArray(route.action.targets) &&
261
+ route.action.targets.length > 0 &&
262
+ route.action.targets.every(t => t.host && t.port !== undefined);
246
263
  case 'socket-handler':
247
264
  return !!route.action.socketHandler && typeof route.action.socketHandler === 'function';
248
265
  default: