269 lines
8.0 KiB
TypeScript
269 lines
8.0 KiB
TypeScript
/**
|
|
* Route Validators
|
|
*
|
|
* This file provides utility functions for validating route configurations.
|
|
* These validators help ensure that route configurations are valid and correctly structured.
|
|
*/
|
|
|
|
import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange } from '../models/route-types.js';
|
|
|
|
/**
|
|
* Validates a port range or port number
|
|
* @param port Port number or port range
|
|
* @returns True if valid, false otherwise
|
|
*/
|
|
export function isValidPort(port: TPortRange): boolean {
|
|
if (typeof port === 'number') {
|
|
return port > 0 && port < 65536; // Valid port range is 1-65535
|
|
} else if (Array.isArray(port)) {
|
|
return port.every(p => typeof p === 'number' && p > 0 && p < 65536);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validates a domain string
|
|
* @param domain Domain string to validate
|
|
* @returns True if valid, false otherwise
|
|
*/
|
|
export function isValidDomain(domain: string): boolean {
|
|
// Basic domain validation regex - allows wildcards (*.example.com)
|
|
const domainRegex = /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
return domainRegex.test(domain);
|
|
}
|
|
|
|
/**
|
|
* Validates a route match configuration
|
|
* @param match Route match configuration to validate
|
|
* @returns { valid: boolean, errors: string[] } Validation result
|
|
*/
|
|
export function validateRouteMatch(match: IRouteMatch): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
// Validate ports
|
|
if (match.ports !== undefined) {
|
|
if (!isValidPort(match.ports)) {
|
|
errors.push('Invalid port number or port range in match.ports');
|
|
}
|
|
}
|
|
|
|
// Validate domains
|
|
if (match.domains !== undefined) {
|
|
if (typeof match.domains === 'string') {
|
|
if (!isValidDomain(match.domains)) {
|
|
errors.push(`Invalid domain format: ${match.domains}`);
|
|
}
|
|
} else if (Array.isArray(match.domains)) {
|
|
for (const domain of match.domains) {
|
|
if (!isValidDomain(domain)) {
|
|
errors.push(`Invalid domain format: ${domain}`);
|
|
}
|
|
}
|
|
} else {
|
|
errors.push('Domains must be a string or an array of strings');
|
|
}
|
|
}
|
|
|
|
// Validate path
|
|
if (match.path !== undefined) {
|
|
if (typeof match.path !== 'string' || !match.path.startsWith('/')) {
|
|
errors.push('Path must be a string starting with /');
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validates a route action configuration
|
|
* @param action Route action configuration to validate
|
|
* @returns { valid: boolean, errors: string[] } Validation result
|
|
*/
|
|
export function validateRouteAction(action: IRouteAction): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
// Validate action type
|
|
if (!action.type) {
|
|
errors.push('Action type is required');
|
|
} else if (!['forward', 'redirect', 'static', 'block'].includes(action.type)) {
|
|
errors.push(`Invalid action type: ${action.type}`);
|
|
}
|
|
|
|
// Validate target for 'forward' action
|
|
if (action.type === 'forward') {
|
|
if (!action.target) {
|
|
errors.push('Target is required for forward action');
|
|
} else {
|
|
// Validate target host
|
|
if (!action.target.host) {
|
|
errors.push('Target host is required');
|
|
}
|
|
|
|
// Validate target port
|
|
if (!action.target.port || !isValidPort(action.target.port)) {
|
|
errors.push('Valid target port is required');
|
|
}
|
|
}
|
|
|
|
// Validate TLS options for forward actions
|
|
if (action.tls) {
|
|
if (!['passthrough', 'terminate', 'terminate-and-reencrypt'].includes(action.tls.mode)) {
|
|
errors.push(`Invalid TLS mode: ${action.tls.mode}`);
|
|
}
|
|
|
|
// For termination modes, validate certificate
|
|
if (['terminate', 'terminate-and-reencrypt'].includes(action.tls.mode)) {
|
|
if (action.tls.certificate !== 'auto' &&
|
|
(!action.tls.certificate || !action.tls.certificate.key || !action.tls.certificate.cert)) {
|
|
errors.push('Certificate must be "auto" or an object with key and cert properties');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate redirect for 'redirect' action
|
|
if (action.type === 'redirect') {
|
|
if (!action.redirect) {
|
|
errors.push('Redirect configuration is required for redirect action');
|
|
} else {
|
|
if (!action.redirect.to) {
|
|
errors.push('Redirect target (to) is required');
|
|
}
|
|
|
|
if (action.redirect.status &&
|
|
![301, 302, 303, 307, 308].includes(action.redirect.status)) {
|
|
errors.push('Invalid redirect status code');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate static file config for 'static' action
|
|
if (action.type === 'static') {
|
|
if (!action.static) {
|
|
errors.push('Static file configuration is required for static action');
|
|
} else {
|
|
if (!action.static.root) {
|
|
errors.push('Static file root directory is required');
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validates a complete route configuration
|
|
* @param route Route configuration to validate
|
|
* @returns { valid: boolean, errors: string[] } Validation result
|
|
*/
|
|
export function validateRouteConfig(route: IRouteConfig): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
// Check for required properties
|
|
if (!route.match) {
|
|
errors.push('Route match configuration is required');
|
|
}
|
|
|
|
if (!route.action) {
|
|
errors.push('Route action configuration is required');
|
|
}
|
|
|
|
// Validate match configuration
|
|
if (route.match) {
|
|
const matchValidation = validateRouteMatch(route.match);
|
|
if (!matchValidation.valid) {
|
|
errors.push(...matchValidation.errors.map(err => `Match: ${err}`));
|
|
}
|
|
}
|
|
|
|
// Validate action configuration
|
|
if (route.action) {
|
|
const actionValidation = validateRouteAction(route.action);
|
|
if (!actionValidation.valid) {
|
|
errors.push(...actionValidation.errors.map(err => `Action: ${err}`));
|
|
}
|
|
}
|
|
|
|
// Ensure the route has a unique identifier
|
|
if (!route.id && !route.name) {
|
|
errors.push('Route should have either an id or a name for identification');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate an array of route configurations
|
|
* @param routes Array of route configurations to validate
|
|
* @returns { valid: boolean, errors: { index: number, errors: string[] }[] } Validation result
|
|
*/
|
|
export function validateRoutes(routes: IRouteConfig[]): {
|
|
valid: boolean;
|
|
errors: { index: number; errors: string[] }[]
|
|
} {
|
|
const results: { index: number; errors: string[] }[] = [];
|
|
|
|
routes.forEach((route, index) => {
|
|
const validation = validateRouteConfig(route);
|
|
if (!validation.valid) {
|
|
results.push({
|
|
index,
|
|
errors: validation.errors
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
valid: results.length === 0,
|
|
errors: results
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if a route configuration has the required properties for a specific action type
|
|
* @param route Route configuration to check
|
|
* @param actionType Expected action type
|
|
* @returns True if the route has the necessary properties, false otherwise
|
|
*/
|
|
export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType: string): boolean {
|
|
if (!route.action || route.action.type !== actionType) {
|
|
return false;
|
|
}
|
|
|
|
switch (actionType) {
|
|
case 'forward':
|
|
return !!route.action.target && !!route.action.target.host && !!route.action.target.port;
|
|
case 'redirect':
|
|
return !!route.action.redirect && !!route.action.redirect.to;
|
|
case 'static':
|
|
return !!route.action.static && !!route.action.static.root;
|
|
case 'block':
|
|
return true; // Block action doesn't require additional properties
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws an error if the route config is invalid, returns the config if valid
|
|
* Useful for immediate validation when creating routes
|
|
* @param route Route configuration to validate
|
|
* @returns The validated route configuration
|
|
* @throws Error if the route configuration is invalid
|
|
*/
|
|
export function assertValidRoute(route: IRouteConfig): IRouteConfig {
|
|
const validation = validateRouteConfig(route);
|
|
if (!validation.valid) {
|
|
throw new Error(`Invalid route configuration: ${validation.errors.join(', ')}`);
|
|
}
|
|
return route;
|
|
} |