@serve.zone/dcrouter 13.41.1 → 13.42.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/deno.json +3 -3
- package/dist_serve/bundle.js +1025 -983
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.db-seeder.js +29 -2
- package/dist_ts/config/classes.reference-resolver.d.ts +5 -0
- package/dist_ts/config/classes.reference-resolver.js +79 -15
- package/dist_ts/config/classes.route-config-manager.d.ts +5 -1
- package/dist_ts/config/classes.route-config-manager.js +136 -6
- package/dist_ts/config/classes.source-policy-compiler.d.ts +35 -0
- package/dist_ts/config/classes.source-policy-compiler.js +497 -0
- package/dist_ts/config/index.d.ts +1 -0
- package/dist_ts/config/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +39 -0
- package/dist_ts_interfaces/data/route-management.js +65 -1
- package/dist_ts_migrations/index.js +67 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +2 -0
- package/dist_ts_web/elements/network/ops-view-routes.js +237 -11
- package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +42 -5
- package/package.json +4 -4
- package/readme.md +74 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.db-seeder.ts +28 -1
- package/ts/config/classes.reference-resolver.ts +94 -14
- package/ts/config/classes.route-config-manager.ts +162 -5
- package/ts/config/classes.source-policy-compiler.ts +614 -0
- package/ts/config/index.ts +1 -0
- package/ts/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +257 -10
- package/ts_web/elements/network/ops-view-sourceprofiles.ts +41 -4
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
3
|
import { RouteDoc } from '../db/index.js';
|
|
4
|
+
import { routePathClasses } from '../../ts_interfaces/data/route-management.js';
|
|
4
5
|
import type {
|
|
5
6
|
IRoute,
|
|
6
7
|
IMergedRoute,
|
|
7
8
|
IRouteWarning,
|
|
8
9
|
IRouteMetadata,
|
|
10
|
+
IRoutePathPolicyBinding,
|
|
11
|
+
IRouteSourcePolicy,
|
|
12
|
+
IRouteSecurity,
|
|
9
13
|
} from '../../ts_interfaces/data/route-management.js';
|
|
10
14
|
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
|
11
15
|
import { type IHttp3Config, augmentRouteWithHttp3 } from '../http3/index.js';
|
|
12
16
|
import type { ReferenceResolver } from './classes.reference-resolver.js';
|
|
17
|
+
import { SourcePolicyCompiler } from './classes.source-policy-compiler.js';
|
|
13
18
|
|
|
14
19
|
export type TVpnClientAllowEntry = string | { clientId: string; domains: string[] };
|
|
15
20
|
|
|
@@ -131,6 +136,10 @@ export class RouteConfigManager {
|
|
|
131
136
|
): Promise<string> {
|
|
132
137
|
const id = plugins.uuid.v4();
|
|
133
138
|
const now = Date.now();
|
|
139
|
+
const sourcePolicyPayloadError = SourcePolicyCompiler.validateSourcePolicyPayload(metadata?.sourcePolicy);
|
|
140
|
+
if (sourcePolicyPayloadError) {
|
|
141
|
+
throw new Error(sourcePolicyPayloadError);
|
|
142
|
+
}
|
|
134
143
|
|
|
135
144
|
// Ensure route has a name
|
|
136
145
|
if (!route.name) {
|
|
@@ -144,6 +153,10 @@ export class RouteConfigManager {
|
|
|
144
153
|
route = resolved.route;
|
|
145
154
|
resolvedMetadata = this.normalizeRouteMetadata(resolved.metadata);
|
|
146
155
|
}
|
|
156
|
+
const sourcePolicyValidationError = this.validateSourcePolicy(resolvedMetadata?.sourcePolicy, route);
|
|
157
|
+
if (sourcePolicyValidationError) {
|
|
158
|
+
throw new Error(sourcePolicyValidationError);
|
|
159
|
+
}
|
|
147
160
|
|
|
148
161
|
const stored: IRoute = {
|
|
149
162
|
id,
|
|
@@ -174,8 +187,15 @@ export class RouteConfigManager {
|
|
|
174
187
|
if (!stored) {
|
|
175
188
|
return { success: false, message: 'Route not found' };
|
|
176
189
|
}
|
|
190
|
+
const sourcePolicyPayloadError = SourcePolicyCompiler.validateSourcePolicyPayload(patch.metadata?.sourcePolicy);
|
|
191
|
+
if (sourcePolicyPayloadError) {
|
|
192
|
+
return { success: false, message: sourcePolicyPayloadError };
|
|
193
|
+
}
|
|
177
194
|
|
|
178
195
|
const previousSourceProfileRef = stored.metadata?.sourceProfileRef;
|
|
196
|
+
const previousRoute = structuredClone(stored.route);
|
|
197
|
+
const previousMetadata = structuredClone(stored.metadata);
|
|
198
|
+
const previousEnabled = stored.enabled;
|
|
179
199
|
|
|
180
200
|
const isToggleOnlyPatch = patch.enabled !== undefined
|
|
181
201
|
&& patch.route === undefined
|
|
@@ -234,6 +254,14 @@ export class RouteConfigManager {
|
|
|
234
254
|
stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
|
|
235
255
|
}
|
|
236
256
|
|
|
257
|
+
const sourcePolicyValidationError = this.validateSourcePolicy(stored.metadata?.sourcePolicy, stored.route);
|
|
258
|
+
if (sourcePolicyValidationError) {
|
|
259
|
+
stored.route = previousRoute;
|
|
260
|
+
stored.metadata = previousMetadata;
|
|
261
|
+
stored.enabled = previousEnabled;
|
|
262
|
+
return { success: false, message: sourcePolicyValidationError };
|
|
263
|
+
}
|
|
264
|
+
|
|
237
265
|
stored.updatedAt = Date.now();
|
|
238
266
|
|
|
239
267
|
await this.persistRoute(stored);
|
|
@@ -454,6 +482,7 @@ export class RouteConfigManager {
|
|
|
454
482
|
|
|
455
483
|
const normalized: IRouteMetadata = {
|
|
456
484
|
sourceProfileRef: normalizeString(metadata.sourceProfileRef),
|
|
485
|
+
sourcePolicy: this.normalizeSourcePolicy(metadata.sourcePolicy),
|
|
457
486
|
networkTargetRef: normalizeString(metadata.networkTargetRef),
|
|
458
487
|
sourceProfileName: normalizeString(metadata.sourceProfileName),
|
|
459
488
|
networkTargetName: normalizeString(metadata.networkTargetName),
|
|
@@ -482,7 +511,7 @@ export class RouteConfigManager {
|
|
|
482
511
|
if (!normalized.networkTargetRef) {
|
|
483
512
|
normalized.networkTargetName = undefined;
|
|
484
513
|
}
|
|
485
|
-
if (!normalized.sourceProfileRef && !normalized.networkTargetRef) {
|
|
514
|
+
if (!normalized.sourceProfileRef && !normalized.sourcePolicy && !normalized.networkTargetRef) {
|
|
486
515
|
normalized.lastResolvedAt = undefined;
|
|
487
516
|
}
|
|
488
517
|
if (normalized.ownerType !== 'gatewayClient' && normalized.ownerType !== 'workhoster') {
|
|
@@ -507,6 +536,128 @@ export class RouteConfigManager {
|
|
|
507
536
|
return normalized;
|
|
508
537
|
}
|
|
509
538
|
|
|
539
|
+
private normalizeSourcePolicy(sourcePolicy?: Partial<IRouteSourcePolicy>): IRouteSourcePolicy | undefined {
|
|
540
|
+
const bindings = sourcePolicy?.bindings;
|
|
541
|
+
if (!Array.isArray(bindings)) {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const normalizedBindings: IRouteSourcePolicy['bindings'] = [];
|
|
546
|
+
for (const binding of bindings) {
|
|
547
|
+
const sourceProfileRef = typeof binding.sourceProfileRef === 'string'
|
|
548
|
+
? binding.sourceProfileRef.trim()
|
|
549
|
+
: '';
|
|
550
|
+
if (!sourceProfileRef) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const normalizedRateLimit = this.normalizeRateLimit(binding.rateLimit);
|
|
554
|
+
const normalizedPathPolicies = this.normalizePathPolicies(binding.pathPolicies);
|
|
555
|
+
|
|
556
|
+
normalizedBindings.push({
|
|
557
|
+
...(typeof binding.id === 'string' && binding.id.trim() ? { id: binding.id.trim() } : {}),
|
|
558
|
+
sourceProfileRef,
|
|
559
|
+
...(typeof binding.sourceProfileName === 'string' && binding.sourceProfileName.trim()
|
|
560
|
+
? { sourceProfileName: binding.sourceProfileName.trim() }
|
|
561
|
+
: {}),
|
|
562
|
+
...(normalizedRateLimit ? { rateLimit: normalizedRateLimit } : {}),
|
|
563
|
+
...(typeof binding.maxConnections === 'number' && Number.isFinite(binding.maxConnections) && binding.maxConnections >= 0
|
|
564
|
+
? { maxConnections: binding.maxConnections }
|
|
565
|
+
: {}),
|
|
566
|
+
...(binding.onExceeded?.type === '429'
|
|
567
|
+
? {
|
|
568
|
+
onExceeded: {
|
|
569
|
+
type: '429' as const,
|
|
570
|
+
...(typeof binding.onExceeded.errorMessage === 'string' && binding.onExceeded.errorMessage.trim()
|
|
571
|
+
? { errorMessage: binding.onExceeded.errorMessage.trim() }
|
|
572
|
+
: {}),
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
: {}),
|
|
576
|
+
...(normalizedPathPolicies ? { pathPolicies: normalizedPathPolicies } : {}),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return normalizedBindings.length > 0 ? { bindings: normalizedBindings } : undefined;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private normalizePathPolicies(
|
|
584
|
+
pathPolicies?: IRoutePathPolicyBinding[],
|
|
585
|
+
): IRoutePathPolicyBinding[] | undefined {
|
|
586
|
+
if (!Array.isArray(pathPolicies)) {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const validClasses = new Set<string>(routePathClasses);
|
|
591
|
+
const normalizedPathPolicies: IRoutePathPolicyBinding[] = [];
|
|
592
|
+
for (const pathPolicy of pathPolicies) {
|
|
593
|
+
if (!validClasses.has(pathPolicy.pathClass)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const normalizedRateLimit = this.normalizeRateLimit(pathPolicy.rateLimit);
|
|
598
|
+
const pathPatterns = Array.isArray(pathPolicy.pathPatterns)
|
|
599
|
+
? [...new Set(pathPolicy.pathPatterns
|
|
600
|
+
.map((pattern) => typeof pattern === 'string' ? pattern.trim() : '')
|
|
601
|
+
.filter(Boolean))]
|
|
602
|
+
: undefined;
|
|
603
|
+
|
|
604
|
+
normalizedPathPolicies.push({
|
|
605
|
+
...(typeof pathPolicy.id === 'string' && pathPolicy.id.trim() ? { id: pathPolicy.id.trim() } : {}),
|
|
606
|
+
pathClass: pathPolicy.pathClass,
|
|
607
|
+
...(pathPatterns?.length ? { pathPatterns } : {}),
|
|
608
|
+
...(normalizedRateLimit ? { rateLimit: normalizedRateLimit } : {}),
|
|
609
|
+
...(typeof pathPolicy.maxConnections === 'number' && Number.isFinite(pathPolicy.maxConnections) && pathPolicy.maxConnections >= 0
|
|
610
|
+
? { maxConnections: pathPolicy.maxConnections }
|
|
611
|
+
: {}),
|
|
612
|
+
...(pathPolicy.onExceeded?.type === '429'
|
|
613
|
+
? {
|
|
614
|
+
onExceeded: {
|
|
615
|
+
type: '429' as const,
|
|
616
|
+
...(typeof pathPolicy.onExceeded.errorMessage === 'string' && pathPolicy.onExceeded.errorMessage.trim()
|
|
617
|
+
? { errorMessage: pathPolicy.onExceeded.errorMessage.trim() }
|
|
618
|
+
: {}),
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
: {}),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return normalizedPathPolicies.length > 0 ? normalizedPathPolicies : undefined;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private validateSourcePolicy(
|
|
629
|
+
sourcePolicy: IRouteSourcePolicy | undefined,
|
|
630
|
+
route: IDcRouterRouteConfig,
|
|
631
|
+
): string | undefined {
|
|
632
|
+
const shapeError = SourcePolicyCompiler.validateSourcePolicyShape(sourcePolicy, route);
|
|
633
|
+
if (shapeError) {
|
|
634
|
+
return shapeError;
|
|
635
|
+
}
|
|
636
|
+
return SourcePolicyCompiler.validateResolvedSourcePolicy(sourcePolicy, this.referenceResolver);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private normalizeRateLimit(rateLimit?: IRouteSecurity['rateLimit']): IRouteSecurity['rateLimit'] | undefined {
|
|
640
|
+
if (!rateLimit || typeof rateLimit !== 'object') {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const maxRequests = Number(rateLimit.maxRequests);
|
|
645
|
+
const window = Number(rateLimit.window);
|
|
646
|
+
if (!Number.isFinite(maxRequests) || maxRequests < 0 || !Number.isFinite(window) || window < 0) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
enabled: rateLimit.enabled !== false,
|
|
652
|
+
maxRequests,
|
|
653
|
+
window,
|
|
654
|
+
keyBy: 'ip',
|
|
655
|
+
...(typeof rateLimit.errorMessage === 'string' && rateLimit.errorMessage.trim()
|
|
656
|
+
? { errorMessage: rateLimit.errorMessage.trim() }
|
|
657
|
+
: {}),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
510
661
|
// =========================================================================
|
|
511
662
|
// Private: warnings
|
|
512
663
|
// =========================================================================
|
|
@@ -569,10 +720,10 @@ export class RouteConfigManager {
|
|
|
569
720
|
|
|
570
721
|
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
571
722
|
|
|
572
|
-
// Add all enabled routes with HTTP/3 and
|
|
723
|
+
// Add all enabled routes with HTTP/3, VPN, and source-policy augmentation
|
|
573
724
|
for (const route of this.routes.values()) {
|
|
574
725
|
if (route.enabled) {
|
|
575
|
-
enabledRoutes.push(this.
|
|
726
|
+
enabledRoutes.push(...this.prepareStoredRoutesForApply(route));
|
|
576
727
|
}
|
|
577
728
|
}
|
|
578
729
|
|
|
@@ -592,9 +743,15 @@ export class RouteConfigManager {
|
|
|
592
743
|
});
|
|
593
744
|
}
|
|
594
745
|
|
|
595
|
-
private
|
|
746
|
+
private prepareStoredRoutesForApply(storedRoute: IRoute): plugins.smartproxy.IRouteConfig[] {
|
|
596
747
|
const hydratedRoute = this.hydrateStoredRoute?.(storedRoute);
|
|
597
|
-
|
|
748
|
+
const sourcePolicyRoutes = SourcePolicyCompiler.compileRoute(
|
|
749
|
+
hydratedRoute || storedRoute.route,
|
|
750
|
+
storedRoute.metadata,
|
|
751
|
+
this.referenceResolver,
|
|
752
|
+
storedRoute.id,
|
|
753
|
+
);
|
|
754
|
+
return sourcePolicyRoutes.map((route) => this.prepareRouteForApply(route, storedRoute.id));
|
|
598
755
|
}
|
|
599
756
|
|
|
600
757
|
private prepareRouteForApply(
|