@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
|
@@ -10,7 +10,7 @@ import { ConnectionPool } from './connection-pool.js';
|
|
|
10
10
|
import { ContextCreator } from './context-creator.js';
|
|
11
11
|
import { HttpRequestHandler } from './http-request-handler.js';
|
|
12
12
|
import { Http2RequestHandler } from './http2-request-handler.js';
|
|
13
|
-
import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
|
|
13
|
+
import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
|
|
14
14
|
import type { IRouteContext, IHttpRouteContext } from '../../core/models/route-context.js';
|
|
15
15
|
import { toBaseContext } from '../../core/models/route-context.js';
|
|
16
16
|
import { TemplateUtils } from '../../core/utils/template-utils.js';
|
|
@@ -99,6 +99,80 @@ export class RequestHandler {
|
|
|
99
99
|
return { ...this.defaultHeaders };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Select the appropriate target from the targets array based on sub-matching criteria
|
|
104
|
+
*/
|
|
105
|
+
private selectTarget(
|
|
106
|
+
targets: IRouteTarget[],
|
|
107
|
+
context: {
|
|
108
|
+
port: number;
|
|
109
|
+
path?: string;
|
|
110
|
+
headers?: Record<string, string>;
|
|
111
|
+
method?: string;
|
|
112
|
+
}
|
|
113
|
+
): IRouteTarget | null {
|
|
114
|
+
// Sort targets by priority (higher first)
|
|
115
|
+
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
116
|
+
|
|
117
|
+
// Find the first matching target
|
|
118
|
+
for (const target of sortedTargets) {
|
|
119
|
+
if (!target.match) {
|
|
120
|
+
// No match criteria means this is a default/fallback target
|
|
121
|
+
return target;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check port match
|
|
125
|
+
if (target.match.ports && !target.match.ports.includes(context.port)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check path match (supports wildcards)
|
|
130
|
+
if (target.match.path && context.path) {
|
|
131
|
+
const pathPattern = target.match.path.replace(/\*/g, '.*');
|
|
132
|
+
const pathRegex = new RegExp(`^${pathPattern}$`);
|
|
133
|
+
if (!pathRegex.test(context.path)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check method match
|
|
139
|
+
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check headers match
|
|
144
|
+
if (target.match.headers && context.headers) {
|
|
145
|
+
let headersMatch = true;
|
|
146
|
+
for (const [key, pattern] of Object.entries(target.match.headers)) {
|
|
147
|
+
const headerValue = context.headers[key.toLowerCase()];
|
|
148
|
+
if (!headerValue) {
|
|
149
|
+
headersMatch = false;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (pattern instanceof RegExp) {
|
|
154
|
+
if (!pattern.test(headerValue)) {
|
|
155
|
+
headersMatch = false;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
} else if (headerValue !== pattern) {
|
|
159
|
+
headersMatch = false;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!headersMatch) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// All criteria matched
|
|
169
|
+
return target;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// No matching target found, return the first target without match criteria (default)
|
|
173
|
+
return sortedTargets.find(t => !t.match) || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
102
176
|
/**
|
|
103
177
|
* Apply CORS headers to response if configured
|
|
104
178
|
* Implements Phase 5.5: Context-aware CORS handling
|
|
@@ -480,17 +554,31 @@ export class RequestHandler {
|
|
|
480
554
|
}
|
|
481
555
|
}
|
|
482
556
|
|
|
483
|
-
// If we found a matching route with
|
|
484
|
-
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.
|
|
557
|
+
// If we found a matching route with forward action, select appropriate target
|
|
558
|
+
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
|
|
485
559
|
this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`);
|
|
486
560
|
|
|
561
|
+
// Select the appropriate target from the targets array
|
|
562
|
+
const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
|
|
563
|
+
port: routeContext.port,
|
|
564
|
+
path: routeContext.path,
|
|
565
|
+
headers: routeContext.headers,
|
|
566
|
+
method: routeContext.method
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
if (!selectedTarget) {
|
|
570
|
+
this.logger.error(`No matching target found for route ${matchingRoute.name}`);
|
|
571
|
+
req.socket.end();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
487
575
|
// Extract target information, resolving functions if needed
|
|
488
576
|
let targetHost: string | string[];
|
|
489
577
|
let targetPort: number;
|
|
490
578
|
|
|
491
579
|
try {
|
|
492
580
|
// Check function cache for host and resolve or use cached value
|
|
493
|
-
if (typeof
|
|
581
|
+
if (typeof selectedTarget.host === 'function') {
|
|
494
582
|
// Generate a function ID for caching (use route name or ID if available)
|
|
495
583
|
const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
|
496
584
|
|
|
@@ -502,7 +590,7 @@ export class RequestHandler {
|
|
|
502
590
|
this.logger.debug(`Using cached host value for ${functionId}`);
|
|
503
591
|
} else {
|
|
504
592
|
// Resolve the function and cache the result
|
|
505
|
-
const resolvedHost =
|
|
593
|
+
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
|
|
506
594
|
targetHost = resolvedHost;
|
|
507
595
|
|
|
508
596
|
// Cache the result
|
|
@@ -511,16 +599,16 @@ export class RequestHandler {
|
|
|
511
599
|
}
|
|
512
600
|
} else {
|
|
513
601
|
// No cache available, just resolve
|
|
514
|
-
const resolvedHost =
|
|
602
|
+
const resolvedHost = selectedTarget.host(routeContext);
|
|
515
603
|
targetHost = resolvedHost;
|
|
516
604
|
this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
|
517
605
|
}
|
|
518
606
|
} else {
|
|
519
|
-
targetHost =
|
|
607
|
+
targetHost = selectedTarget.host;
|
|
520
608
|
}
|
|
521
609
|
|
|
522
610
|
// Check function cache for port and resolve or use cached value
|
|
523
|
-
if (typeof
|
|
611
|
+
if (typeof selectedTarget.port === 'function') {
|
|
524
612
|
// Generate a function ID for caching
|
|
525
613
|
const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
|
526
614
|
|
|
@@ -532,7 +620,7 @@ export class RequestHandler {
|
|
|
532
620
|
this.logger.debug(`Using cached port value for ${functionId}`);
|
|
533
621
|
} else {
|
|
534
622
|
// Resolve the function and cache the result
|
|
535
|
-
const resolvedPort =
|
|
623
|
+
const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
|
|
536
624
|
targetPort = resolvedPort;
|
|
537
625
|
|
|
538
626
|
// Cache the result
|
|
@@ -541,12 +629,12 @@ export class RequestHandler {
|
|
|
541
629
|
}
|
|
542
630
|
} else {
|
|
543
631
|
// No cache available, just resolve
|
|
544
|
-
const resolvedPort =
|
|
632
|
+
const resolvedPort = selectedTarget.port(routeContext);
|
|
545
633
|
targetPort = resolvedPort;
|
|
546
634
|
this.logger.debug(`Resolved function-based port to: ${resolvedPort}`);
|
|
547
635
|
}
|
|
548
636
|
} else {
|
|
549
|
-
targetPort =
|
|
637
|
+
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
|
|
550
638
|
}
|
|
551
639
|
|
|
552
640
|
// Select a single host if an array was provided
|
|
@@ -626,17 +714,32 @@ export class RequestHandler {
|
|
|
626
714
|
}
|
|
627
715
|
}
|
|
628
716
|
|
|
629
|
-
// If we found a matching route with
|
|
630
|
-
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.
|
|
717
|
+
// If we found a matching route with forward action, select appropriate target
|
|
718
|
+
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
|
|
631
719
|
this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`);
|
|
632
720
|
|
|
721
|
+
// Select the appropriate target from the targets array
|
|
722
|
+
const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
|
|
723
|
+
port: routeContext.port,
|
|
724
|
+
path: routeContext.path,
|
|
725
|
+
headers: routeContext.headers,
|
|
726
|
+
method: routeContext.method
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
if (!selectedTarget) {
|
|
730
|
+
this.logger.error(`No matching target found for route ${matchingRoute.name}`);
|
|
731
|
+
stream.respond({ ':status': 502 });
|
|
732
|
+
stream.end();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
633
736
|
// Extract target information, resolving functions if needed
|
|
634
737
|
let targetHost: string | string[];
|
|
635
738
|
let targetPort: number;
|
|
636
739
|
|
|
637
740
|
try {
|
|
638
741
|
// Check function cache for host and resolve or use cached value
|
|
639
|
-
if (typeof
|
|
742
|
+
if (typeof selectedTarget.host === 'function') {
|
|
640
743
|
// Generate a function ID for caching (use route name or ID if available)
|
|
641
744
|
const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
|
642
745
|
|
|
@@ -648,7 +751,7 @@ export class RequestHandler {
|
|
|
648
751
|
this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`);
|
|
649
752
|
} else {
|
|
650
753
|
// Resolve the function and cache the result
|
|
651
|
-
const resolvedHost =
|
|
754
|
+
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
|
|
652
755
|
targetHost = resolvedHost;
|
|
653
756
|
|
|
654
757
|
// Cache the result
|
|
@@ -657,16 +760,16 @@ export class RequestHandler {
|
|
|
657
760
|
}
|
|
658
761
|
} else {
|
|
659
762
|
// No cache available, just resolve
|
|
660
|
-
const resolvedHost =
|
|
763
|
+
const resolvedHost = selectedTarget.host(routeContext);
|
|
661
764
|
targetHost = resolvedHost;
|
|
662
765
|
this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
|
663
766
|
}
|
|
664
767
|
} else {
|
|
665
|
-
targetHost =
|
|
768
|
+
targetHost = selectedTarget.host;
|
|
666
769
|
}
|
|
667
770
|
|
|
668
771
|
// Check function cache for port and resolve or use cached value
|
|
669
|
-
if (typeof
|
|
772
|
+
if (typeof selectedTarget.port === 'function') {
|
|
670
773
|
// Generate a function ID for caching
|
|
671
774
|
const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
|
672
775
|
|
|
@@ -678,7 +781,7 @@ export class RequestHandler {
|
|
|
678
781
|
this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`);
|
|
679
782
|
} else {
|
|
680
783
|
// Resolve the function and cache the result
|
|
681
|
-
const resolvedPort =
|
|
784
|
+
const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
|
|
682
785
|
targetPort = resolvedPort;
|
|
683
786
|
|
|
684
787
|
// Cache the result
|
|
@@ -687,12 +790,12 @@ export class RequestHandler {
|
|
|
687
790
|
}
|
|
688
791
|
} else {
|
|
689
792
|
// No cache available, just resolve
|
|
690
|
-
const resolvedPort =
|
|
793
|
+
const resolvedPort = selectedTarget.port(routeContext);
|
|
691
794
|
targetPort = resolvedPort;
|
|
692
795
|
this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`);
|
|
693
796
|
}
|
|
694
797
|
} else {
|
|
695
|
-
targetPort =
|
|
798
|
+
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
|
|
696
799
|
}
|
|
697
800
|
|
|
698
801
|
// Select a single host if an array was provided
|
|
@@ -3,7 +3,7 @@ import '../../core/models/socket-augmentation.js';
|
|
|
3
3
|
import { type IHttpProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger } from './models/types.js';
|
|
4
4
|
import { ConnectionPool } from './connection-pool.js';
|
|
5
5
|
import { HttpRouter } from '../../routing/router/index.js';
|
|
6
|
-
import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
|
|
6
|
+
import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
|
|
7
7
|
import type { IRouteContext } from '../../core/models/route-context.js';
|
|
8
8
|
import { toBaseContext } from '../../core/models/route-context.js';
|
|
9
9
|
import { ContextCreator } from './context-creator.js';
|
|
@@ -53,6 +53,80 @@ export class WebSocketHandler {
|
|
|
53
53
|
this.securityManager.setRoutes(routes);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Select the appropriate target from the targets array based on sub-matching criteria
|
|
58
|
+
*/
|
|
59
|
+
private selectTarget(
|
|
60
|
+
targets: IRouteTarget[],
|
|
61
|
+
context: {
|
|
62
|
+
port: number;
|
|
63
|
+
path?: string;
|
|
64
|
+
headers?: Record<string, string>;
|
|
65
|
+
method?: string;
|
|
66
|
+
}
|
|
67
|
+
): IRouteTarget | null {
|
|
68
|
+
// Sort targets by priority (higher first)
|
|
69
|
+
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
70
|
+
|
|
71
|
+
// Find the first matching target
|
|
72
|
+
for (const target of sortedTargets) {
|
|
73
|
+
if (!target.match) {
|
|
74
|
+
// No match criteria means this is a default/fallback target
|
|
75
|
+
return target;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check port match
|
|
79
|
+
if (target.match.ports && !target.match.ports.includes(context.port)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check path match (supports wildcards)
|
|
84
|
+
if (target.match.path && context.path) {
|
|
85
|
+
const pathPattern = target.match.path.replace(/\*/g, '.*');
|
|
86
|
+
const pathRegex = new RegExp(`^${pathPattern}$`);
|
|
87
|
+
if (!pathRegex.test(context.path)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check method match
|
|
93
|
+
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check headers match
|
|
98
|
+
if (target.match.headers && context.headers) {
|
|
99
|
+
let headersMatch = true;
|
|
100
|
+
for (const [key, pattern] of Object.entries(target.match.headers)) {
|
|
101
|
+
const headerValue = context.headers[key.toLowerCase()];
|
|
102
|
+
if (!headerValue) {
|
|
103
|
+
headersMatch = false;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pattern instanceof RegExp) {
|
|
108
|
+
if (!pattern.test(headerValue)) {
|
|
109
|
+
headersMatch = false;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
} else if (headerValue !== pattern) {
|
|
113
|
+
headersMatch = false;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!headersMatch) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// All criteria matched
|
|
123
|
+
return target;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// No matching target found, return the first target without match criteria (default)
|
|
127
|
+
return sortedTargets.find(t => !t.match) || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
56
130
|
/**
|
|
57
131
|
* Initialize WebSocket server on an existing HTTPS server
|
|
58
132
|
*/
|
|
@@ -146,9 +220,23 @@ export class WebSocketHandler {
|
|
|
146
220
|
let destination: { host: string; port: number };
|
|
147
221
|
|
|
148
222
|
// If we found a route with the modern router, use it
|
|
149
|
-
if (route && route.action.type === 'forward' && route.action.
|
|
223
|
+
if (route && route.action.type === 'forward' && route.action.targets && route.action.targets.length > 0) {
|
|
150
224
|
this.logger.debug(`Found matching WebSocket route: ${route.name || 'unnamed'}`);
|
|
151
225
|
|
|
226
|
+
// Select the appropriate target from the targets array
|
|
227
|
+
const selectedTarget = this.selectTarget(route.action.targets, {
|
|
228
|
+
port: routeContext.port,
|
|
229
|
+
path: routeContext.path,
|
|
230
|
+
headers: routeContext.headers,
|
|
231
|
+
method: routeContext.method
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!selectedTarget) {
|
|
235
|
+
this.logger.error(`No matching target found for route ${route.name}`);
|
|
236
|
+
wsIncoming.close(1003, 'No matching target');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
152
240
|
// Check if WebSockets are enabled for this route
|
|
153
241
|
if (route.action.websocket?.enabled === false) {
|
|
154
242
|
this.logger.debug(`WebSockets are disabled for route: ${route.name || 'unnamed'}`);
|
|
@@ -192,20 +280,20 @@ export class WebSocketHandler {
|
|
|
192
280
|
|
|
193
281
|
try {
|
|
194
282
|
// Resolve host if it's a function
|
|
195
|
-
if (typeof
|
|
196
|
-
const resolvedHost =
|
|
283
|
+
if (typeof selectedTarget.host === 'function') {
|
|
284
|
+
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
|
|
197
285
|
targetHost = resolvedHost;
|
|
198
286
|
this.logger.debug(`Resolved function-based host for WebSocket: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
|
199
287
|
} else {
|
|
200
|
-
targetHost =
|
|
288
|
+
targetHost = selectedTarget.host;
|
|
201
289
|
}
|
|
202
290
|
|
|
203
291
|
// Resolve port if it's a function
|
|
204
|
-
if (typeof
|
|
205
|
-
targetPort =
|
|
292
|
+
if (typeof selectedTarget.port === 'function') {
|
|
293
|
+
targetPort = selectedTarget.port(toBaseContext(routeContext));
|
|
206
294
|
this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`);
|
|
207
295
|
} else {
|
|
208
|
-
targetPort =
|
|
296
|
+
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
|
|
209
297
|
}
|
|
210
298
|
|
|
211
299
|
// Select a single host if an array was provided
|
|
@@ -46,11 +46,36 @@ export interface IRouteMatch {
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Target
|
|
49
|
+
* Target-specific match criteria for sub-routing within a route
|
|
50
|
+
*/
|
|
51
|
+
export interface ITargetMatch {
|
|
52
|
+
ports?: number[]; // Match specific ports from the route
|
|
53
|
+
path?: string; // Match specific paths (supports wildcards like /api/*)
|
|
54
|
+
headers?: Record<string, string | RegExp>; // Match specific HTTP headers
|
|
55
|
+
method?: string[]; // Match specific HTTP methods (GET, POST, etc.)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Target configuration for forwarding with sub-matching and overrides
|
|
50
60
|
*/
|
|
51
61
|
export interface IRouteTarget {
|
|
62
|
+
// Optional sub-matching criteria within the route
|
|
63
|
+
match?: ITargetMatch;
|
|
64
|
+
|
|
65
|
+
// Target destination
|
|
52
66
|
host: string | string[] | ((context: IRouteContext) => string | string[]); // Host or hosts with optional function for dynamic resolution
|
|
53
67
|
port: number | 'preserve' | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping (use 'preserve' to keep the incoming port)
|
|
68
|
+
|
|
69
|
+
// Optional target-specific overrides (these override route-level settings)
|
|
70
|
+
tls?: IRouteTls; // Override route-level TLS settings
|
|
71
|
+
websocket?: IRouteWebSocket; // Override route-level WebSocket settings
|
|
72
|
+
loadBalancing?: IRouteLoadBalancing; // Override route-level load balancing
|
|
73
|
+
sendProxyProtocol?: boolean; // Override route-level proxy protocol setting
|
|
74
|
+
headers?: IRouteHeaders; // Override route-level headers
|
|
75
|
+
advanced?: IRouteAdvanced; // Override route-level advanced settings
|
|
76
|
+
|
|
77
|
+
// Priority for matching (higher values are checked first, default: 0)
|
|
78
|
+
priority?: number;
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
/**
|
|
@@ -221,19 +246,20 @@ export interface IRouteAction {
|
|
|
221
246
|
// Basic routing
|
|
222
247
|
type: TRouteActionType;
|
|
223
248
|
|
|
224
|
-
//
|
|
225
|
-
|
|
249
|
+
// Targets for forwarding (array supports multiple targets with sub-matching)
|
|
250
|
+
// Required for 'forward' action type
|
|
251
|
+
targets?: IRouteTarget[];
|
|
226
252
|
|
|
227
|
-
// TLS handling
|
|
253
|
+
// TLS handling (default for all targets, can be overridden per target)
|
|
228
254
|
tls?: IRouteTls;
|
|
229
255
|
|
|
230
|
-
// WebSocket support
|
|
256
|
+
// WebSocket support (default for all targets, can be overridden per target)
|
|
231
257
|
websocket?: IRouteWebSocket;
|
|
232
258
|
|
|
233
|
-
// Load balancing options
|
|
259
|
+
// Load balancing options (default for all targets, can be overridden per target)
|
|
234
260
|
loadBalancing?: IRouteLoadBalancing;
|
|
235
261
|
|
|
236
|
-
// Advanced options
|
|
262
|
+
// Advanced options (default for all targets, can be overridden per target)
|
|
237
263
|
advanced?: IRouteAdvanced;
|
|
238
264
|
|
|
239
265
|
// Additional options for backend-specific settings
|
|
@@ -251,7 +277,7 @@ export interface IRouteAction {
|
|
|
251
277
|
// Socket handler function (when type is 'socket-handler')
|
|
252
278
|
socketHandler?: TSocketHandler;
|
|
253
279
|
|
|
254
|
-
// PROXY protocol support
|
|
280
|
+
// PROXY protocol support (default for all targets, can be overridden per target)
|
|
255
281
|
sendProxyProtocol?: boolean;
|
|
256
282
|
}
|
|
257
283
|
|
|
@@ -123,39 +123,43 @@ export class NFTablesManager {
|
|
|
123
123
|
private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
|
|
124
124
|
const { action } = route;
|
|
125
125
|
|
|
126
|
-
// Ensure we have
|
|
127
|
-
if (!action.
|
|
128
|
-
throw new Error('Route must have
|
|
126
|
+
// Ensure we have targets
|
|
127
|
+
if (!action.targets || action.targets.length === 0) {
|
|
128
|
+
throw new Error('Route must have targets to use NFTables forwarding');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// NFTables can only handle a single target, so we use the first target without match criteria
|
|
132
|
+
// or the first target if all have match criteria
|
|
133
|
+
const defaultTarget = action.targets.find(t => !t.match) || action.targets[0];
|
|
134
|
+
|
|
131
135
|
// Convert port specifications
|
|
132
136
|
const fromPorts = this.expandPortRange(route.match.ports);
|
|
133
137
|
|
|
134
138
|
// Determine target port
|
|
135
139
|
let toPorts: number | PortRange | Array<number | PortRange>;
|
|
136
140
|
|
|
137
|
-
if (
|
|
141
|
+
if (defaultTarget.port === 'preserve') {
|
|
138
142
|
// 'preserve' means use the same ports as the source
|
|
139
143
|
toPorts = fromPorts;
|
|
140
|
-
} else if (typeof
|
|
144
|
+
} else if (typeof defaultTarget.port === 'function') {
|
|
141
145
|
// For function-based ports, we can't determine at setup time
|
|
142
146
|
// Use the "preserve" approach and let NFTables handle it
|
|
143
147
|
toPorts = fromPorts;
|
|
144
148
|
} else {
|
|
145
|
-
toPorts =
|
|
149
|
+
toPorts = defaultTarget.port;
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
// Determine target host
|
|
149
153
|
let toHost: string;
|
|
150
|
-
if (typeof
|
|
154
|
+
if (typeof defaultTarget.host === 'function') {
|
|
151
155
|
// Can't determine at setup time, use localhost as a placeholder
|
|
152
156
|
// and rely on run-time handling
|
|
153
157
|
toHost = 'localhost';
|
|
154
|
-
} else if (Array.isArray(
|
|
158
|
+
} else if (Array.isArray(defaultTarget.host)) {
|
|
155
159
|
// Use first host for now - NFTables will do simple round-robin
|
|
156
|
-
toHost =
|
|
160
|
+
toHost = defaultTarget.host[0];
|
|
157
161
|
} else {
|
|
158
|
-
toHost =
|
|
162
|
+
toHost = defaultTarget.host;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
// Create options
|