@serve.zone/dcrouter 13.43.1 → 13.43.3
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 +2 -2
- package/dist_serve/bundle.js +894 -896
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.reference-resolver.d.ts +3 -3
- package/dist_ts/config/classes.reference-resolver.js +16 -40
- package/dist_ts/config/classes.route-config-manager.d.ts +3 -2
- package/dist_ts/config/classes.route-config-manager.js +38 -36
- package/dist_ts/config/classes.source-policy-compiler.d.ts +9 -4
- package/dist_ts/config/classes.source-policy-compiler.js +92 -26
- package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/workhoster.handler.js +25 -3
- package/dist_ts_interfaces/data/route-management.d.ts +7 -8
- package/dist_ts_migrations/index.js +102 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.js +111 -134
- package/package.json +2 -2
- package/readme.md +44 -45
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.reference-resolver.ts +16 -40
- package/ts/config/classes.route-config-manager.ts +41 -40
- package/ts/config/classes.source-policy-compiler.ts +115 -30
- package/ts/opsserver/handlers/workhoster.handler.ts +26 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +112 -136
|
@@ -8,8 +8,7 @@ import type {
|
|
|
8
8
|
IRoutePathPolicyBinding,
|
|
9
9
|
IRouteMetadata,
|
|
10
10
|
IRouteSecurity,
|
|
11
|
-
|
|
12
|
-
IRouteSourcePolicyBinding,
|
|
11
|
+
IRouteSourceBinding,
|
|
13
12
|
} from '../../ts_interfaces/data/route-management.js';
|
|
14
13
|
import type { ReferenceResolver } from './classes.reference-resolver.js';
|
|
15
14
|
|
|
@@ -37,22 +36,23 @@ export class SourcePolicyCompiler {
|
|
|
37
36
|
referenceResolver: ReferenceResolver | undefined,
|
|
38
37
|
routeId?: string,
|
|
39
38
|
): plugins.smartproxy.IRouteConfig[] {
|
|
40
|
-
const bindings = metadata?.
|
|
39
|
+
const bindings = metadata?.sourceBindings || [];
|
|
41
40
|
if (bindings.length === 0) {
|
|
42
41
|
return [route];
|
|
43
42
|
}
|
|
44
|
-
if (this.
|
|
43
|
+
if (this.validateSourceBindingsShape(bindings, route)) {
|
|
45
44
|
return [];
|
|
46
45
|
}
|
|
47
46
|
if (!referenceResolver) {
|
|
48
47
|
return [];
|
|
49
48
|
}
|
|
50
|
-
if (this.
|
|
49
|
+
if (this.validateResolvedSourceBindings(bindings, referenceResolver)) {
|
|
51
50
|
return [];
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
const compiledRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
55
54
|
const basePriority = route.priority ?? 0;
|
|
55
|
+
let hasAllSourcesBinding = false;
|
|
56
56
|
|
|
57
57
|
bindings.forEach((binding, index) => {
|
|
58
58
|
const profile = referenceResolver.getProfile(binding.sourceProfileRef);
|
|
@@ -65,6 +65,9 @@ export class SourcePolicyCompiler {
|
|
|
65
65
|
if (sourceMatches.length === 0) {
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
+
if (this.matchesAllSources(sourceMatches)) {
|
|
69
|
+
hasAllSourcesBinding = true;
|
|
70
|
+
}
|
|
68
71
|
const sourcePriority = this.calculateSourcePriority(basePriority, index, bindings.length);
|
|
69
72
|
const sourceMatch = this.matchesAllSources(sourceMatches)
|
|
70
73
|
? { ...route.match }
|
|
@@ -140,39 +143,43 @@ export class SourcePolicyCompiler {
|
|
|
140
143
|
}
|
|
141
144
|
});
|
|
142
145
|
|
|
146
|
+
if (compiledRoutes.length > 0 && !hasAllSourcesBinding) {
|
|
147
|
+
compiledRoutes.push(this.buildDenyFallbackRoute(route, basePriority, routeId));
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
return this.applyIntegerPriorities(compiledRoutes, basePriority);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
public static
|
|
147
|
-
if (
|
|
153
|
+
public static validateSourceBindingsPayload(sourceBindings?: Partial<IRouteSourceBinding>[]): string | undefined {
|
|
154
|
+
if (sourceBindings === undefined) {
|
|
148
155
|
return undefined;
|
|
149
156
|
}
|
|
150
|
-
if (!Array.isArray(
|
|
151
|
-
return 'Source
|
|
157
|
+
if (!Array.isArray(sourceBindings)) {
|
|
158
|
+
return 'Source bindings must be an array';
|
|
152
159
|
}
|
|
153
|
-
if (
|
|
160
|
+
if (sourceBindings.length === 0) {
|
|
154
161
|
return undefined;
|
|
155
162
|
}
|
|
156
|
-
if (
|
|
163
|
+
if (sourceBindings.length > sourcePolicyLimits.maxBindings) {
|
|
157
164
|
return `Source policy exceeds ${sourcePolicyLimits.maxBindings} bindings`;
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
const validClasses = new Set<string>(routePathClasses);
|
|
161
|
-
for (const binding of
|
|
168
|
+
for (const binding of sourceBindings) {
|
|
162
169
|
if (!binding || typeof binding !== 'object') {
|
|
163
|
-
return 'Source
|
|
170
|
+
return 'Source binding must be an object';
|
|
164
171
|
}
|
|
165
172
|
if (typeof binding.sourceProfileRef !== 'string') {
|
|
166
|
-
return 'Source
|
|
173
|
+
return 'Source binding requires a source profile';
|
|
167
174
|
}
|
|
168
175
|
if (binding.sourceProfileRef.length > sourcePolicyLimits.maxSourceProfileRefLength) {
|
|
169
|
-
return `Source
|
|
176
|
+
return `Source binding source profile ref exceeds ${sourcePolicyLimits.maxSourceProfileRefLength} characters`;
|
|
170
177
|
}
|
|
171
178
|
if (binding.sourceProfileRef.trim().length === 0) {
|
|
172
|
-
return 'Source
|
|
179
|
+
return 'Source binding requires a source profile';
|
|
173
180
|
}
|
|
174
181
|
if (typeof binding.id === 'string' && binding.id.length > sourcePolicyLimits.maxIdLength) {
|
|
175
|
-
return `Source
|
|
182
|
+
return `Source binding id exceeds ${sourcePolicyLimits.maxIdLength} characters`;
|
|
176
183
|
}
|
|
177
184
|
if (typeof binding.maxConnections === 'number' && binding.maxConnections < 0) {
|
|
178
185
|
return 'Source policy maxConnections must be non-negative';
|
|
@@ -268,14 +275,21 @@ export class SourcePolicyCompiler {
|
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
public static validateSourcePolicyShape(
|
|
271
|
-
|
|
278
|
+
sourceBindings?: IRouteSourceBinding[],
|
|
272
279
|
route?: plugins.smartproxy.IRouteConfig,
|
|
273
280
|
): string | undefined {
|
|
274
|
-
|
|
281
|
+
return this.validateSourceBindingsShape(sourceBindings, route);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public static validateSourceBindingsShape(
|
|
285
|
+
sourceBindings?: IRouteSourceBinding[],
|
|
286
|
+
route?: plugins.smartproxy.IRouteConfig,
|
|
287
|
+
): string | undefined {
|
|
288
|
+
const payloadError = this.validateSourceBindingsPayload(sourceBindings);
|
|
275
289
|
if (payloadError) {
|
|
276
290
|
return payloadError;
|
|
277
291
|
}
|
|
278
|
-
const bindings =
|
|
292
|
+
const bindings = sourceBindings || [];
|
|
279
293
|
if (bindings.length === 0) {
|
|
280
294
|
return undefined;
|
|
281
295
|
}
|
|
@@ -310,19 +324,36 @@ export class SourcePolicyCompiler {
|
|
|
310
324
|
}
|
|
311
325
|
}
|
|
312
326
|
|
|
327
|
+
// Private-only source bindings add one terminal deny route to prevent fall-through
|
|
328
|
+
// to broader routes with the same host/path/port scope.
|
|
329
|
+
estimatedCompiledRoutes++;
|
|
330
|
+
|
|
313
331
|
const expandedPortCount = route ? this.getExpandedPortCount(route.match?.ports) : 1;
|
|
314
332
|
if (estimatedCompiledRoutes * expandedPortCount > sourcePolicyLimits.maxCompiledVariantsPerRoute) {
|
|
315
333
|
return `Source policy exceeds ${sourcePolicyLimits.maxCompiledVariantsPerRoute} compiled route-port variants`;
|
|
316
334
|
}
|
|
335
|
+
if (route && typeof route.priority === 'number' && Number.isFinite(route.priority)) {
|
|
336
|
+
const integerBasePriority = Math.trunc(this.clampPriority(route.priority));
|
|
337
|
+
if (integerBasePriority + estimatedCompiledRoutes > MAX_ROUTE_PRIORITY) {
|
|
338
|
+
return `Source policy route priority leaves no priority headroom for ${estimatedCompiledRoutes} compiled variants`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
317
341
|
|
|
318
342
|
return undefined;
|
|
319
343
|
}
|
|
320
344
|
|
|
321
345
|
public static validateResolvedSourcePolicy(
|
|
322
|
-
|
|
346
|
+
sourceBindings: IRouteSourceBinding[] | undefined,
|
|
347
|
+
referenceResolver: ReferenceResolver | undefined,
|
|
348
|
+
): string | undefined {
|
|
349
|
+
return this.validateResolvedSourceBindings(sourceBindings, referenceResolver);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public static validateResolvedSourceBindings(
|
|
353
|
+
sourceBindings: IRouteSourceBinding[] | undefined,
|
|
323
354
|
referenceResolver: ReferenceResolver | undefined,
|
|
324
355
|
): string | undefined {
|
|
325
|
-
const bindings =
|
|
356
|
+
const bindings = sourceBindings || [];
|
|
326
357
|
if (bindings.length === 0) {
|
|
327
358
|
return undefined;
|
|
328
359
|
}
|
|
@@ -346,10 +377,7 @@ export class SourcePolicyCompiler {
|
|
|
346
377
|
}
|
|
347
378
|
const matchesAllSources = this.matchesAllSources(sourceMatches);
|
|
348
379
|
if (matchesAllSources && index < bindings.length - 1) {
|
|
349
|
-
return 'Wildcard source profile bindings must be last in
|
|
350
|
-
}
|
|
351
|
-
if (index === bindings.length - 1 && !matchesAllSources) {
|
|
352
|
-
return 'Source policy must end with an all-source fallback profile';
|
|
380
|
+
return 'Wildcard source profile bindings must be last in source bindings';
|
|
353
381
|
}
|
|
354
382
|
}
|
|
355
383
|
|
|
@@ -361,7 +389,7 @@ export class SourcePolicyCompiler {
|
|
|
361
389
|
sourceMatch: plugins.smartproxy.IRouteConfig['match'];
|
|
362
390
|
profileName: string;
|
|
363
391
|
profileSecurity: IRouteSecurity;
|
|
364
|
-
binding:
|
|
392
|
+
binding: IRouteSourceBinding;
|
|
365
393
|
pathPolicy?: IRoutePathPolicyBinding;
|
|
366
394
|
pathPattern?: string;
|
|
367
395
|
sourcePriority: number;
|
|
@@ -414,6 +442,63 @@ export class SourcePolicyCompiler {
|
|
|
414
442
|
};
|
|
415
443
|
}
|
|
416
444
|
|
|
445
|
+
private static buildDenyFallbackRoute(
|
|
446
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
447
|
+
basePriority: number,
|
|
448
|
+
routeId?: string,
|
|
449
|
+
): plugins.smartproxy.IRouteConfig {
|
|
450
|
+
const routeKey = route.id || routeId || route.name || 'route';
|
|
451
|
+
return {
|
|
452
|
+
...route,
|
|
453
|
+
id: `${routeKey}:source:deny-fallback`,
|
|
454
|
+
name: `${route.name || routeKey}:source:deny-fallback`,
|
|
455
|
+
match: { ...route.match },
|
|
456
|
+
priority: this.clampPriority(basePriority - SOURCE_PRIORITY_BAND - PATH_PRIORITY_BAND),
|
|
457
|
+
action: {
|
|
458
|
+
type: 'socket-handler',
|
|
459
|
+
socketHandler: (socket) => this.denySocket(socket),
|
|
460
|
+
},
|
|
461
|
+
security: undefined,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private static denySocket(socket: plugins.net.Socket): void {
|
|
466
|
+
let timeout: ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
467
|
+
const cleanup = () => {
|
|
468
|
+
clearTimeout(timeout);
|
|
469
|
+
socket.removeListener('data', handleData);
|
|
470
|
+
socket.removeListener('error', cleanup);
|
|
471
|
+
socket.removeListener('close', cleanup);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const handleData = (chunk: string | Uint8Array) => {
|
|
475
|
+
cleanup();
|
|
476
|
+
if (this.looksLikeHttpRequest(chunk)) {
|
|
477
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: close\r\n\r\nForbidden');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
socket.destroy();
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
timeout = setTimeout(() => {
|
|
484
|
+
cleanup();
|
|
485
|
+
socket.destroy();
|
|
486
|
+
}, 2000) as ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
487
|
+
timeout.unref?.();
|
|
488
|
+
|
|
489
|
+
socket.once('data', handleData);
|
|
490
|
+
socket.once('error', cleanup);
|
|
491
|
+
socket.once('close', cleanup);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private static looksLikeHttpRequest(chunk: string | Uint8Array): boolean {
|
|
495
|
+
const prefix = typeof chunk === 'string'
|
|
496
|
+
? chunk.slice(0, 16)
|
|
497
|
+
: String.fromCharCode(...chunk.subarray(0, 16));
|
|
498
|
+
return /^(GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS|TRACE|CONNECT)\s/.test(prefix)
|
|
499
|
+
|| prefix.startsWith('PRI * HTTP/2.0');
|
|
500
|
+
}
|
|
501
|
+
|
|
417
502
|
private static getPathPatterns(pathPolicy: IRoutePathPolicyBinding): string[] {
|
|
418
503
|
const patterns: string[] = pathPolicy.pathPatterns?.length
|
|
419
504
|
? pathPolicy.pathPatterns
|
|
@@ -469,8 +554,8 @@ export class SourcePolicyCompiler {
|
|
|
469
554
|
}))
|
|
470
555
|
.sort((a, b) => (b.priority - a.priority) || (a.originalIndex - b.originalIndex));
|
|
471
556
|
const topPriority = Math.trunc(this.clampPriority(
|
|
472
|
-
basePriority + routes.length
|
|
473
|
-
MIN_ROUTE_PRIORITY + routes.length
|
|
557
|
+
basePriority + routes.length,
|
|
558
|
+
MIN_ROUTE_PRIORITY + routes.length,
|
|
474
559
|
MAX_ROUTE_PRIORITY,
|
|
475
560
|
));
|
|
476
561
|
const integerPriorities = new Map<number, number>();
|
|
@@ -589,7 +674,7 @@ export class SourcePolicyCompiler {
|
|
|
589
674
|
private static buildBindingSecurity(
|
|
590
675
|
routeSecurity: IRouteSecurity | undefined,
|
|
591
676
|
profileSecurity: IRouteSecurity,
|
|
592
|
-
binding:
|
|
677
|
+
binding: IRouteSourceBinding,
|
|
593
678
|
pathPolicy?: IRoutePathPolicyBinding,
|
|
594
679
|
): IRouteSecurity | undefined {
|
|
595
680
|
const baseSecurity = this.omitSourceMatchFields(routeSecurity || {});
|
|
@@ -587,7 +587,13 @@ export class WorkHosterHandler {
|
|
|
587
587
|
return { success: false, message: 'route is required unless delete=true' };
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
const sourceBindings = this.getManagedRouteSourceBindings();
|
|
591
|
+
if (!sourceBindings) {
|
|
592
|
+
return { success: false, message: 'STANDARD source profile not found' };
|
|
593
|
+
}
|
|
594
|
+
|
|
590
595
|
const metadata: interfaces.data.IRouteMetadata = {
|
|
596
|
+
sourceBindings,
|
|
591
597
|
ownerType: 'gatewayClient',
|
|
592
598
|
gatewayClientType: resolvedOwnership.gatewayClientType,
|
|
593
599
|
gatewayClientId: resolvedOwnership.gatewayClientId,
|
|
@@ -600,8 +606,10 @@ export class WorkHosterHandler {
|
|
|
600
606
|
const normalizedRoute = this.normalizeGatewayClientRoute(route, resolvedOwnership, externalKey);
|
|
601
607
|
|
|
602
608
|
if (existingRoute) {
|
|
609
|
+
const routePatch: Partial<interfaces.data.IDcRouterRouteConfig> = { ...normalizedRoute };
|
|
610
|
+
(routePatch as any).security = null;
|
|
603
611
|
const result = await manager.updateRoute(existingRoute.id, {
|
|
604
|
-
route:
|
|
612
|
+
route: routePatch,
|
|
605
613
|
enabled: enabled ?? true,
|
|
606
614
|
metadata,
|
|
607
615
|
});
|
|
@@ -640,10 +648,26 @@ export class WorkHosterHandler {
|
|
|
640
648
|
ownership: Required<interfaces.data.IGatewayClientOwnership>,
|
|
641
649
|
externalKey: string,
|
|
642
650
|
): interfaces.data.IDcRouterRouteConfig {
|
|
643
|
-
const normalizedRoute =
|
|
651
|
+
const normalizedRoute = structuredClone(route);
|
|
652
|
+
delete normalizedRoute.security;
|
|
644
653
|
if (!normalizedRoute.name) {
|
|
645
654
|
normalizedRoute.name = `gateway-client-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`;
|
|
646
655
|
}
|
|
647
656
|
return normalizedRoute;
|
|
648
657
|
}
|
|
658
|
+
|
|
659
|
+
private getManagedRouteSourceBindings(): interfaces.data.IRouteSourceBinding[] | undefined {
|
|
660
|
+
const resolver = this.opsServerRef.dcRouterRef.referenceResolver;
|
|
661
|
+
const standardProfile = resolver?.listProfiles().find((profile: interfaces.data.ISourceProfile) => {
|
|
662
|
+
return profile.id.trim().toLowerCase() === 'standard'
|
|
663
|
+
|| profile.name.trim().toLowerCase() === 'standard';
|
|
664
|
+
});
|
|
665
|
+
if (!standardProfile) {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
return [{
|
|
669
|
+
sourceProfileRef: standardProfile.id,
|
|
670
|
+
sourceProfileName: standardProfile.name,
|
|
671
|
+
}];
|
|
672
|
+
}
|
|
649
673
|
}
|