@push.rocks/smartproxy 19.6.17 → 20.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.
- package/dist_ts/core/utils/shared-security-manager.js +30 -5
- package/dist_ts/proxies/http-proxy/request-handler.d.ts +4 -0
- package/dist_ts/proxies/http-proxy/request-handler.js +104 -21
- package/dist_ts/proxies/http-proxy/websocket-handler.d.ts +4 -0
- package/dist_ts/proxies/http-proxy/websocket-handler.js +78 -8
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +19 -2
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/nftables-manager.js +14 -11
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +112 -28
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +23 -23
- package/dist_ts/proxies/smart-proxy/utils/route-patterns.js +13 -13
- package/dist_ts/proxies/smart-proxy/utils/route-utils.js +4 -7
- package/dist_ts/proxies/smart-proxy/utils/route-validators.js +41 -25
- package/package.json +1 -1
- package/readme.plan.md +139 -266
- package/ts/core/utils/shared-security-manager.ts +33 -4
- package/ts/proxies/http-proxy/request-handler.ts +124 -21
- package/ts/proxies/http-proxy/websocket-handler.ts +96 -8
- package/ts/proxies/smart-proxy/models/route-types.ts +34 -8
- package/ts/proxies/smart-proxy/nftables-manager.ts +14 -10
- package/ts/proxies/smart-proxy/route-connection-handler.ts +132 -28
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +14 -14
- package/ts/proxies/smart-proxy/utils/route-patterns.ts +6 -6
- package/ts/proxies/smart-proxy/utils/route-utils.ts +3 -6
- 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
|
-
//
|
|
735
|
-
if (!action.
|
|
736
|
-
logger.log('error', `Forward action missing
|
|
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, '
|
|
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
|
|
859
|
+
if (typeof selectedTarget.host === 'function') {
|
|
763
860
|
try {
|
|
764
|
-
targetHost =
|
|
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 =
|
|
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
|
|
890
|
+
if (typeof selectedTarget.port === 'function') {
|
|
794
891
|
try {
|
|
795
|
-
targetPort =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
829
|
-
switch (
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
935
|
-
targetPort:
|
|
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
|
|
1047
|
+
if (typeof selectedTarget.host === 'function') {
|
|
944
1048
|
// For function-based host, use the same routeContext created earlier
|
|
945
|
-
const hostResult =
|
|
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(
|
|
952
|
-
?
|
|
953
|
-
:
|
|
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
|
|
959
|
-
targetPort =
|
|
960
|
-
} else if (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
if (overrideRoute.action.
|
|
71
|
-
mergedRoute.action.
|
|
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
|
|
105
|
+
// Validate targets for 'forward' action
|
|
106
106
|
if (action.type === 'forward') {
|
|
107
|
-
if (!action.
|
|
108
|
-
errors.push('
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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:
|