@push.rocks/smartproxy 27.22.0 → 28.1.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/changelog.md +47 -0
- package/dist_rust/rustproxy_linux_amd64 +0 -0
- package/dist_rust/rustproxy_linux_amd64.tsrust-build.json +5 -5
- package/dist_rust/rustproxy_linux_arm64 +0 -0
- package/dist_rust/rustproxy_linux_arm64.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +4 -2
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +12 -6
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/models/rust-types.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/models/rust-types.js +3 -2
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +28 -4
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +75 -6
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +46 -41
- package/dist_ts/proxies/smart-proxy/utils/index.d.ts +2 -0
- package/dist_ts/proxies/smart-proxy/utils/index.js +3 -1
- package/dist_ts/proxies/smart-proxy/utils/route-validation-error.d.ts +24 -0
- package/dist_ts/proxies/smart-proxy/utils/route-validation-error.js +41 -0
- package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +14 -2
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +178 -43
- package/package.json +3 -3
- package/readme.md +74 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +4 -2
- package/ts/proxies/smart-proxy/models/route-types.ts +14 -6
- package/ts/proxies/smart-proxy/models/rust-types.ts +20 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +106 -13
- package/ts/proxies/smart-proxy/smart-proxy.ts +41 -40
- package/ts/proxies/smart-proxy/utils/index.ts +4 -0
- package/ts/proxies/smart-proxy/utils/route-validation-error.ts +60 -0
- package/ts/proxies/smart-proxy/utils/route-validator.ts +208 -51
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
// Export route validator (class-based and functional API)
|
|
9
9
|
export * from './route-validator.js';
|
|
10
10
|
|
|
11
|
+
// Export the error SmartProxy throws when it refuses a route set
|
|
12
|
+
export { RouteValidationError } from './route-validation-error.js';
|
|
13
|
+
export type { IRouteValidationErrorEntry } from './route-validation-error.js';
|
|
14
|
+
|
|
11
15
|
// Export route utilities for route operations
|
|
12
16
|
export * from './route-utils.js';
|
|
13
17
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The validation failures of one route, as the route validators report them.
|
|
3
|
+
*/
|
|
4
|
+
export interface IRouteValidationErrorEntry {
|
|
5
|
+
/** The route's position in the validated route list. */
|
|
6
|
+
index: number;
|
|
7
|
+
/**
|
|
8
|
+
* The route's label from `RouteValidator.getRouteLabels()`: its name, else its id,
|
|
9
|
+
* else `route[<index>]`; a name or id that several routes share gets `[<index>]` appended.
|
|
10
|
+
*/
|
|
11
|
+
route: string;
|
|
12
|
+
/** Every reason the route was rejected. */
|
|
13
|
+
messages: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MAX_ROUTES_IN_MESSAGE = 10;
|
|
17
|
+
const MAX_MESSAGES_PER_ROUTE_IN_MESSAGE = 5;
|
|
18
|
+
const MAX_TEXT_LENGTH_IN_MESSAGE = 200;
|
|
19
|
+
|
|
20
|
+
function truncateText(textArg: string): string {
|
|
21
|
+
return textArg.length > MAX_TEXT_LENGTH_IN_MESSAGE
|
|
22
|
+
? `${textArg.slice(0, MAX_TEXT_LENGTH_IN_MESSAGE)}…`
|
|
23
|
+
: textArg;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatEntry(entryArg: IRouteValidationErrorEntry): string {
|
|
27
|
+
const shownMessages = entryArg.messages
|
|
28
|
+
.slice(0, MAX_MESSAGES_PER_ROUTE_IN_MESSAGE)
|
|
29
|
+
.map(truncateText);
|
|
30
|
+
const hiddenMessageCount = entryArg.messages.length - shownMessages.length;
|
|
31
|
+
if (hiddenMessageCount > 0) {
|
|
32
|
+
shownMessages.push(`and ${hiddenMessageCount} more`);
|
|
33
|
+
}
|
|
34
|
+
return `route '${truncateText(entryArg.route)}': ${shownMessages.join('; ')}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatMessage(summaryArg: string, errorsArg: IRouteValidationErrorEntry[]): string {
|
|
38
|
+
const shownRoutes = errorsArg.slice(0, MAX_ROUTES_IN_MESSAGE).map(formatEntry);
|
|
39
|
+
const hiddenRouteCount = errorsArg.length - shownRoutes.length;
|
|
40
|
+
if (hiddenRouteCount > 0) {
|
|
41
|
+
shownRoutes.push(`and ${hiddenRouteCount} more route(s)`);
|
|
42
|
+
}
|
|
43
|
+
return `${summaryArg}: ${errorsArg.length} route(s) have errors: ${shownRoutes.join(' | ')}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Thrown when SmartProxy refuses a route set because it failed validation.
|
|
48
|
+
*
|
|
49
|
+
* `errors` lists every rejected route with all its reasons. The message names the
|
|
50
|
+
* first routes and reasons only, so a large route set cannot produce an unbounded message.
|
|
51
|
+
*/
|
|
52
|
+
export class RouteValidationError extends Error {
|
|
53
|
+
public readonly errors: IRouteValidationErrorEntry[];
|
|
54
|
+
|
|
55
|
+
constructor(summaryArg: string, errorsArg: IRouteValidationErrorEntry[]) {
|
|
56
|
+
super(formatMessage(summaryArg, errorsArg));
|
|
57
|
+
this.name = 'RouteValidationError';
|
|
58
|
+
this.errors = errorsArg;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { logger } from '../../../core/utils/logger.js';
|
|
2
2
|
import type { IInboundProxyProtocolPolicy, IRouteConfig, IRouteMatch, IRouteAction, TPortRange, TTransportProtocol } from '../models/route-types.js';
|
|
3
|
+
import { RouteValidationError, type IRouteValidationErrorEntry } from './route-validation-error.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Validates route configurations for correctness and safety
|
|
6
7
|
*/
|
|
7
8
|
export class RouteValidator {
|
|
8
9
|
private static readonly VALID_TLS_MODES = ['terminate', 'passthrough', 'terminate-and-reencrypt'];
|
|
9
|
-
private static readonly VALID_ACTION_TYPES = ['forward', 'socket-handler'];
|
|
10
|
+
private static readonly VALID_ACTION_TYPES = ['forward', 'socket-handler', 'respond'];
|
|
10
11
|
private static readonly VALID_PROTOCOLS = ['tcp', 'http', 'https', 'ws', 'wss', 'udp', 'quic', 'http3'];
|
|
11
12
|
private static readonly MAX_PORTS = 100;
|
|
12
13
|
private static readonly MAX_DOMAINS = 1000;
|
|
@@ -149,6 +150,14 @@ export class RouteValidator {
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// Validate fixed response
|
|
154
|
+
if (route.action.type === 'respond') {
|
|
155
|
+
errors.push(...getRespondActionErrors(route.action));
|
|
156
|
+
if (route.match) {
|
|
157
|
+
errors.push(...getRespondMatchErrors(route.match));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
152
161
|
// Validate forward target
|
|
153
162
|
if (route.action.type === 'forward') {
|
|
154
163
|
if (!route.action.targets || route.action.targets.length === 0) {
|
|
@@ -315,62 +324,84 @@ export class RouteValidator {
|
|
|
315
324
|
}
|
|
316
325
|
|
|
317
326
|
/**
|
|
318
|
-
*
|
|
327
|
+
* Label every route for validation reports: its name, else its id, else `route[<index>]`.
|
|
328
|
+
* A name or id that several routes share gets `[<index>]` appended, so every label is unique.
|
|
329
|
+
*/
|
|
330
|
+
public static getRouteLabels(routes: IRouteConfig[]): string[] {
|
|
331
|
+
const baseLabels = routes.map((route, index) => route.name || route.id || `route[${index}]`);
|
|
332
|
+
const labelCounts = new Map<string, number>();
|
|
333
|
+
for (const label of baseLabels) {
|
|
334
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
335
|
+
}
|
|
336
|
+
return baseLabels.map((label, index) => (labelCounts.get(label)! > 1 ? `${label}[${index}]` : label));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Validate multiple route configurations.
|
|
341
|
+
*
|
|
342
|
+
* `entries` lists every invalid route by position and label; `errors` maps the same
|
|
343
|
+
* labels to the reasons.
|
|
319
344
|
*/
|
|
320
|
-
public static validateRoutes(routes: IRouteConfig[]): {
|
|
321
|
-
|
|
322
|
-
|
|
345
|
+
public static validateRoutes(routes: IRouteConfig[]): {
|
|
346
|
+
valid: boolean;
|
|
347
|
+
errors: Map<string, string[]>;
|
|
348
|
+
entries: IRouteValidationErrorEntry[];
|
|
349
|
+
} {
|
|
350
|
+
const messagesByIndex: string[][] = routes.map(() => []);
|
|
323
351
|
|
|
324
352
|
// Check for duplicate route names
|
|
325
353
|
const routeNames = new Set<string>();
|
|
326
|
-
|
|
354
|
+
routes.forEach((route, index) => {
|
|
327
355
|
if (route.name && routeNames.has(route.name)) {
|
|
328
|
-
|
|
329
|
-
existingErrors.push('Duplicate route name');
|
|
330
|
-
errorMap.set(route.name, existingErrors);
|
|
331
|
-
valid = false;
|
|
356
|
+
messagesByIndex[index].push('Duplicate route name');
|
|
332
357
|
}
|
|
333
358
|
if (route.name) {
|
|
334
359
|
routeNames.add(route.name);
|
|
335
360
|
}
|
|
336
|
-
}
|
|
361
|
+
});
|
|
337
362
|
|
|
338
363
|
// Validate each route
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
}
|
|
364
|
+
routes.forEach((route, index) => {
|
|
365
|
+
messagesByIndex[index].push(...this.validateRoute(route).errors);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const labels = this.getRouteLabels(routes);
|
|
346
369
|
|
|
347
370
|
// Check for conflicting routes
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
for (const conflict of conflicts) {
|
|
351
|
-
const existingErrors = errorMap.get(conflict.route) || [];
|
|
352
|
-
existingErrors.push(conflict.message);
|
|
353
|
-
errorMap.set(conflict.route, existingErrors);
|
|
354
|
-
}
|
|
355
|
-
valid = false;
|
|
371
|
+
for (const conflict of this.findRouteConflicts(routes, labels)) {
|
|
372
|
+
messagesByIndex[conflict.index].push(conflict.message);
|
|
356
373
|
}
|
|
357
374
|
|
|
358
|
-
|
|
375
|
+
const entries: IRouteValidationErrorEntry[] = [];
|
|
376
|
+
messagesByIndex.forEach((messages, index) => {
|
|
377
|
+
if (messages.length > 0) {
|
|
378
|
+
entries.push({ index, route: labels[index], messages });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
valid: entries.length === 0,
|
|
384
|
+
errors: new Map(entries.map((entry) => [entry.route, entry.messages])),
|
|
385
|
+
entries,
|
|
386
|
+
};
|
|
359
387
|
}
|
|
360
388
|
|
|
361
389
|
/**
|
|
362
390
|
* Find potential conflicts between routes
|
|
363
391
|
*/
|
|
364
|
-
private static findRouteConflicts(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
392
|
+
private static findRouteConflicts(
|
|
393
|
+
routes: IRouteConfig[],
|
|
394
|
+
labels: string[],
|
|
395
|
+
): Array<{ index: number; message: string }> {
|
|
396
|
+
const conflicts: Array<{ index: number; message: string }> = [];
|
|
397
|
+
|
|
398
|
+
// Group route indices by port
|
|
399
|
+
const portMap = new Map<number, number[]>();
|
|
400
|
+
|
|
401
|
+
routes.forEach((route, index) => {
|
|
371
402
|
if (route.match?.ports) {
|
|
372
403
|
const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
|
|
373
|
-
|
|
404
|
+
|
|
374
405
|
// Expand port ranges to individual ports
|
|
375
406
|
const expandedPorts: number[] = [];
|
|
376
407
|
for (const port of ports) {
|
|
@@ -382,25 +413,25 @@ export class RouteValidator {
|
|
|
382
413
|
}
|
|
383
414
|
}
|
|
384
415
|
}
|
|
385
|
-
|
|
416
|
+
|
|
386
417
|
for (const port of expandedPorts) {
|
|
387
418
|
const routesOnPort = portMap.get(port) || [];
|
|
388
|
-
routesOnPort.push(
|
|
419
|
+
routesOnPort.push(index);
|
|
389
420
|
portMap.set(port, routesOnPort);
|
|
390
421
|
}
|
|
391
422
|
}
|
|
392
|
-
}
|
|
423
|
+
});
|
|
393
424
|
|
|
394
425
|
// Check for conflicting catch-all routes on the same port
|
|
395
426
|
for (const [port, routesOnPort] of portMap) {
|
|
396
|
-
const catchAllRoutes = routesOnPort.filter(
|
|
397
|
-
this.isCatchAllDomainMatch(
|
|
427
|
+
const catchAllRoutes = routesOnPort.filter((index) =>
|
|
428
|
+
this.isCatchAllDomainMatch(routes[index].match) && !this.hasSourceScope(routes[index].match)
|
|
398
429
|
);
|
|
399
|
-
|
|
430
|
+
|
|
400
431
|
if (catchAllRoutes.length > 1) {
|
|
401
|
-
for (const
|
|
432
|
+
for (const index of catchAllRoutes) {
|
|
402
433
|
conflicts.push({
|
|
403
|
-
|
|
434
|
+
index,
|
|
404
435
|
message: `Multiple catch-all routes on port ${port}`
|
|
405
436
|
});
|
|
406
437
|
}
|
|
@@ -411,9 +442,9 @@ export class RouteValidator {
|
|
|
411
442
|
// a shorthand for the full port/transport listener policy, so every route
|
|
412
443
|
// sharing that listener must either all omit it or all use an identical one.
|
|
413
444
|
const listenerPolicies = new Map<string, { route: string; fingerprint: string }>();
|
|
414
|
-
|
|
445
|
+
routes.forEach((route, index) => {
|
|
415
446
|
if (!route.match?.ports) {
|
|
416
|
-
|
|
447
|
+
return;
|
|
417
448
|
}
|
|
418
449
|
|
|
419
450
|
for (const port of this.expandPorts(route.match.ports)) {
|
|
@@ -423,18 +454,18 @@ export class RouteValidator {
|
|
|
423
454
|
const existing = listenerPolicies.get(key);
|
|
424
455
|
if (!existing) {
|
|
425
456
|
listenerPolicies.set(key, {
|
|
426
|
-
route:
|
|
457
|
+
route: labels[index],
|
|
427
458
|
fingerprint,
|
|
428
459
|
});
|
|
429
460
|
} else if (existing.fingerprint !== fingerprint) {
|
|
430
461
|
conflicts.push({
|
|
431
|
-
|
|
462
|
+
index,
|
|
432
463
|
message: `Conflicting inbound PROXY protocol policy on ${transport} port ${port} (differs from ${existing.route})`,
|
|
433
464
|
});
|
|
434
465
|
}
|
|
435
466
|
}
|
|
436
467
|
}
|
|
437
|
-
}
|
|
468
|
+
});
|
|
438
469
|
|
|
439
470
|
return conflicts;
|
|
440
471
|
}
|
|
@@ -719,6 +750,120 @@ export function validateRouteMatch(match: IRouteMatch): { valid: boolean; errors
|
|
|
719
750
|
};
|
|
720
751
|
}
|
|
721
752
|
|
|
753
|
+
/** Largest fixed-response body a `respond` route may carry, in UTF-8 bytes. */
|
|
754
|
+
export const MAX_FIXED_RESPONSE_BODY_BYTES = 65536;
|
|
755
|
+
|
|
756
|
+
/** Hop-by-hop or computed headers a fixed response must not set. */
|
|
757
|
+
const FIXED_RESPONSE_FORBIDDEN_HEADERS = [
|
|
758
|
+
'connection',
|
|
759
|
+
'keep-alive',
|
|
760
|
+
'transfer-encoding',
|
|
761
|
+
'upgrade',
|
|
762
|
+
'te',
|
|
763
|
+
'trailer',
|
|
764
|
+
'proxy-connection',
|
|
765
|
+
'content-length',
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
const HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
769
|
+
const HTTP_HEADER_VALUE_PATTERN = /^[\t\x20-\x7E]*$/;
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Returns the action-level errors of a `respond` route: the fixed response itself,
|
|
773
|
+
* and the forwarding, handler, UDP and TLS options a fixed response cannot use.
|
|
774
|
+
*/
|
|
775
|
+
function getRespondActionErrors(action: IRouteAction): string[] {
|
|
776
|
+
const errors: string[] = [];
|
|
777
|
+
|
|
778
|
+
if (action.targets !== undefined) {
|
|
779
|
+
errors.push('respond action must not define targets');
|
|
780
|
+
}
|
|
781
|
+
if (action.socketHandler !== undefined || action.datagramHandler !== undefined) {
|
|
782
|
+
errors.push('respond action must not define a socketHandler or datagramHandler');
|
|
783
|
+
}
|
|
784
|
+
if (action.forwardingEngine !== undefined || action.nftables !== undefined) {
|
|
785
|
+
errors.push('respond action must not define forwardingEngine or nftables options');
|
|
786
|
+
}
|
|
787
|
+
if (action.udp !== undefined) {
|
|
788
|
+
errors.push('respond action does not support UDP or HTTP/3 (action.udp)');
|
|
789
|
+
}
|
|
790
|
+
if (action.tls && action.tls.mode !== 'terminate') {
|
|
791
|
+
errors.push(`respond action supports only TLS mode "terminate", got "${action.tls.mode}"`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const response = action.response;
|
|
795
|
+
if (!response || typeof response !== 'object') {
|
|
796
|
+
errors.push('respond action requires a response object');
|
|
797
|
+
return errors;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!Number.isInteger(response.status) || response.status < 200 || response.status > 599) {
|
|
801
|
+
errors.push(`response.status must be an integer from 200 to 599, got ${response.status}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const seenHeaders = new Set<string>();
|
|
805
|
+
let hasContentType = false;
|
|
806
|
+
if (response.headers !== undefined) {
|
|
807
|
+
if (!response.headers || typeof response.headers !== 'object' || Array.isArray(response.headers)) {
|
|
808
|
+
errors.push('response.headers must be an object of header names to string values');
|
|
809
|
+
} else {
|
|
810
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
811
|
+
const lowerName = name.toLowerCase();
|
|
812
|
+
if (!HTTP_TOKEN_PATTERN.test(name)) {
|
|
813
|
+
errors.push(`response.headers has an invalid header name: ${name}`);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (FIXED_RESPONSE_FORBIDDEN_HEADERS.includes(lowerName)) {
|
|
817
|
+
errors.push(`response.headers must not set ${lowerName}`);
|
|
818
|
+
}
|
|
819
|
+
if (seenHeaders.has(lowerName)) {
|
|
820
|
+
errors.push(`response.headers sets ${lowerName} more than once`);
|
|
821
|
+
}
|
|
822
|
+
seenHeaders.add(lowerName);
|
|
823
|
+
if (typeof value !== 'string' || !HTTP_HEADER_VALUE_PATTERN.test(value)) {
|
|
824
|
+
errors.push(`response.headers.${name} must be a string of visible ASCII characters`);
|
|
825
|
+
}
|
|
826
|
+
if (lowerName === 'content-type') {
|
|
827
|
+
hasContentType = true;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (response.body !== undefined) {
|
|
834
|
+
if (typeof response.body !== 'string') {
|
|
835
|
+
errors.push('response.body must be a string');
|
|
836
|
+
} else if (response.body.length > 0) {
|
|
837
|
+
const bodyBytes = new TextEncoder().encode(response.body).length;
|
|
838
|
+
if (bodyBytes > MAX_FIXED_RESPONSE_BODY_BYTES) {
|
|
839
|
+
errors.push(`response.body is ${bodyBytes} bytes, the maximum is ${MAX_FIXED_RESPONSE_BODY_BYTES}`);
|
|
840
|
+
}
|
|
841
|
+
if (response.status === 204 || response.status === 304) {
|
|
842
|
+
errors.push(`response.status ${response.status} must not carry a body`);
|
|
843
|
+
}
|
|
844
|
+
if (!hasContentType) {
|
|
845
|
+
errors.push('response.body requires an explicit content-type header');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return errors;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Returns the match-level errors of a `respond` route: a fixed response is HTTP over TCP only.
|
|
855
|
+
*/
|
|
856
|
+
function getRespondMatchErrors(match: IRouteMatch): string[] {
|
|
857
|
+
const errors: string[] = [];
|
|
858
|
+
if (match.transport === 'udp' || match.transport === 'all') {
|
|
859
|
+
errors.push(`respond routes support only TCP transport, got "${match.transport}"`);
|
|
860
|
+
}
|
|
861
|
+
if (match.protocol !== undefined && match.protocol !== 'http') {
|
|
862
|
+
errors.push(`respond routes support only protocol "http", got "${match.protocol}"`);
|
|
863
|
+
}
|
|
864
|
+
return errors;
|
|
865
|
+
}
|
|
866
|
+
|
|
722
867
|
/**
|
|
723
868
|
* Validates a route action configuration
|
|
724
869
|
* @param action Route action configuration to validate
|
|
@@ -729,10 +874,14 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
|
|
|
729
874
|
|
|
730
875
|
if (!action.type) {
|
|
731
876
|
errors.push('Action type is required');
|
|
732
|
-
} else if (!['forward', 'socket-handler'].includes(action.type)) {
|
|
877
|
+
} else if (!['forward', 'socket-handler', 'respond'].includes(action.type)) {
|
|
733
878
|
errors.push(`Invalid action type: ${action.type}`);
|
|
734
879
|
}
|
|
735
880
|
|
|
881
|
+
if (action.type === 'respond') {
|
|
882
|
+
errors.push(...getRespondActionErrors(action));
|
|
883
|
+
}
|
|
884
|
+
|
|
736
885
|
if (action.type === 'forward') {
|
|
737
886
|
if (!action.targets || !Array.isArray(action.targets) || action.targets.length === 0) {
|
|
738
887
|
errors.push('Targets array is required for forward action');
|
|
@@ -836,6 +985,10 @@ export function validateRouteConfig(route: IRouteConfig): { valid: boolean; erro
|
|
|
836
985
|
}
|
|
837
986
|
}
|
|
838
987
|
|
|
988
|
+
if (route.match && route.action?.type === 'respond') {
|
|
989
|
+
errors.push(...getRespondMatchErrors(route.match).map(err => `Match: ${err}`));
|
|
990
|
+
}
|
|
991
|
+
|
|
839
992
|
return {
|
|
840
993
|
valid: errors.length === 0,
|
|
841
994
|
errors
|
|
@@ -889,6 +1042,8 @@ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType:
|
|
|
889
1042
|
case 'socket-handler':
|
|
890
1043
|
return (!!route.action.socketHandler && typeof route.action.socketHandler === 'function') ||
|
|
891
1044
|
(!!route.action.datagramHandler && typeof route.action.datagramHandler === 'function');
|
|
1045
|
+
case 'respond':
|
|
1046
|
+
return !!route.action.response && typeof route.action.response.status === 'number';
|
|
892
1047
|
default:
|
|
893
1048
|
return false;
|
|
894
1049
|
}
|
|
@@ -899,12 +1054,14 @@ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType:
|
|
|
899
1054
|
* Useful for immediate validation when creating routes
|
|
900
1055
|
* @param route Route configuration to validate
|
|
901
1056
|
* @returns The validated route configuration
|
|
902
|
-
* @throws
|
|
1057
|
+
* @throws RouteValidationError if the route configuration is invalid
|
|
903
1058
|
*/
|
|
904
1059
|
export function assertValidRoute(route: IRouteConfig): IRouteConfig {
|
|
905
1060
|
const validation = validateRouteConfig(route);
|
|
906
1061
|
if (!validation.valid) {
|
|
907
|
-
throw new
|
|
1062
|
+
throw new RouteValidationError('Invalid route configuration', [
|
|
1063
|
+
{ index: 0, route: RouteValidator.getRouteLabels([route])[0], messages: validation.errors },
|
|
1064
|
+
]);
|
|
908
1065
|
}
|
|
909
1066
|
return route;
|
|
910
1067
|
}
|