@serve.zone/dcrouter 13.43.0 → 13.43.2

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.
@@ -8,8 +8,7 @@ import type {
8
8
  IRoutePathPolicyBinding,
9
9
  IRouteMetadata,
10
10
  IRouteSecurity,
11
- IRouteSourcePolicy,
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?.sourcePolicy?.bindings || [];
39
+ const bindings = metadata?.sourceBindings || [];
41
40
  if (bindings.length === 0) {
42
41
  return [route];
43
42
  }
44
- if (this.validateSourcePolicyShape(metadata?.sourcePolicy, route)) {
43
+ if (this.validateSourceBindingsShape(bindings, route)) {
45
44
  return [];
46
45
  }
47
46
  if (!referenceResolver) {
48
47
  return [];
49
48
  }
50
- if (this.validateResolvedSourcePolicy(metadata?.sourcePolicy, referenceResolver)) {
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 validateSourcePolicyPayload(sourcePolicy?: Partial<IRouteSourcePolicy>): string | undefined {
147
- if (!sourcePolicy) {
153
+ public static validateSourceBindingsPayload(sourceBindings?: Partial<IRouteSourceBinding>[]): string | undefined {
154
+ if (sourceBindings === undefined) {
148
155
  return undefined;
149
156
  }
150
- if (!Array.isArray(sourcePolicy.bindings)) {
151
- return 'Source policy bindings must be an array';
157
+ if (!Array.isArray(sourceBindings)) {
158
+ return 'Source bindings must be an array';
152
159
  }
153
- if (sourcePolicy.bindings.length === 0) {
160
+ if (sourceBindings.length === 0) {
154
161
  return undefined;
155
162
  }
156
- if (sourcePolicy.bindings.length > sourcePolicyLimits.maxBindings) {
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 sourcePolicy.bindings) {
168
+ for (const binding of sourceBindings) {
162
169
  if (!binding || typeof binding !== 'object') {
163
- return 'Source policy binding must be an object';
170
+ return 'Source binding must be an object';
164
171
  }
165
172
  if (typeof binding.sourceProfileRef !== 'string') {
166
- return 'Source policy binding requires a source profile';
173
+ return 'Source binding requires a source profile';
167
174
  }
168
175
  if (binding.sourceProfileRef.length > sourcePolicyLimits.maxSourceProfileRefLength) {
169
- return `Source policy source profile ref exceeds ${sourcePolicyLimits.maxSourceProfileRefLength} characters`;
176
+ return `Source binding source profile ref exceeds ${sourcePolicyLimits.maxSourceProfileRefLength} characters`;
170
177
  }
171
178
  if (binding.sourceProfileRef.trim().length === 0) {
172
- return 'Source policy binding requires a source profile';
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 policy binding id exceeds ${sourcePolicyLimits.maxIdLength} characters`;
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
- sourcePolicy?: IRouteSourcePolicy,
278
+ sourceBindings?: IRouteSourceBinding[],
272
279
  route?: plugins.smartproxy.IRouteConfig,
273
280
  ): string | undefined {
274
- const payloadError = this.validateSourcePolicyPayload(sourcePolicy);
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 = sourcePolicy?.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
- sourcePolicy: IRouteSourcePolicy | undefined,
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 = sourcePolicy?.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 a source policy';
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: IRouteSourcePolicyBinding;
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 - 1,
473
- MIN_ROUTE_PRIORITY + routes.length - 1,
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: IRouteSourcePolicyBinding,
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: normalizedRoute,
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 = { ...route };
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
  }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.43.0',
6
+ version: '13.43.2',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }