update
This commit is contained in:
40
ts/proxies/smart-proxy/utils/index.ts
Normal file
40
ts/proxies/smart-proxy/utils/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* SmartProxy Route Utilities
|
||||
*
|
||||
* This file exports all route-related utilities for the SmartProxy module,
|
||||
* including helpers, validators, utilities, and patterns for working with routes.
|
||||
*/
|
||||
|
||||
// Export route helpers for creating routes
|
||||
export * from './route-helpers.js';
|
||||
|
||||
// Export route validators for validating route configurations
|
||||
export * from './route-validators.js';
|
||||
|
||||
// Export route utilities for route operations
|
||||
export * from './route-utils.js';
|
||||
|
||||
// Export route patterns with renamed exports to avoid conflicts
|
||||
import {
|
||||
createWebSocketRoute as createWebSocketPatternRoute,
|
||||
createLoadBalancerRoute as createLoadBalancerPatternRoute,
|
||||
createApiGatewayRoute,
|
||||
createStaticFileServerRoute,
|
||||
addRateLimiting,
|
||||
addBasicAuth,
|
||||
addJwtAuth
|
||||
} from './route-patterns.js';
|
||||
|
||||
export {
|
||||
createWebSocketPatternRoute,
|
||||
createLoadBalancerPatternRoute,
|
||||
createApiGatewayRoute,
|
||||
createStaticFileServerRoute,
|
||||
addRateLimiting,
|
||||
addBasicAuth,
|
||||
addJwtAuth
|
||||
};
|
||||
|
||||
// Export migration utilities for transitioning from domain-based to route-based configs
|
||||
// Note: These will be removed in a future version once migration is complete
|
||||
export * from './route-migration-utils.js';
|
455
ts/proxies/smart-proxy/utils/route-helpers.ts
Normal file
455
ts/proxies/smart-proxy/utils/route-helpers.ts
Normal file
@ -0,0 +1,455 @@
|
||||
/**
|
||||
* Route Helper Functions
|
||||
*
|
||||
* This file provides utility functions for creating route configurations for common scenarios.
|
||||
* These functions aim to simplify the creation of route configurations for typical use cases.
|
||||
*
|
||||
* This module includes helper functions for creating:
|
||||
* - HTTP routes (createHttpRoute)
|
||||
* - HTTPS routes with TLS termination (createHttpsTerminateRoute)
|
||||
* - HTTP to HTTPS redirects (createHttpToHttpsRedirect)
|
||||
* - HTTPS passthrough routes (createHttpsPassthroughRoute)
|
||||
* - Complete HTTPS servers with redirects (createCompleteHttpsServer)
|
||||
* - Load balancer routes (createLoadBalancerRoute)
|
||||
* - Static file server routes (createStaticFileRoute)
|
||||
* - API routes (createApiRoute)
|
||||
* - WebSocket routes (createWebSocketRoute)
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange } from '../models/route-types.js';
|
||||
|
||||
/**
|
||||
* Create an HTTP-only route configuration
|
||||
* @param domains Domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createHttpRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number },
|
||||
options: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.match?.ports || 80,
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `HTTP Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTPS route with TLS termination (including HTTP redirect to HTTPS)
|
||||
* @param domains Domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createHttpsTerminateRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number },
|
||||
options: {
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
httpPort?: number | number[];
|
||||
httpsPort?: number | number[];
|
||||
reencrypt?: boolean;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.httpsPort || 443,
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target,
|
||||
tls: {
|
||||
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
}
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `HTTPS Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP to HTTPS redirect route
|
||||
* @param domains Domain(s) to match
|
||||
* @param httpsPort HTTPS port to redirect to (default: 443)
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createHttpToHttpsRedirect(
|
||||
domains: string | string[],
|
||||
httpsPort: number = 443,
|
||||
options: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.match?.ports || 80,
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'redirect',
|
||||
redirect: {
|
||||
to: `https://{domain}:${httpsPort}{path}`,
|
||||
status: 301
|
||||
}
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTPS passthrough route (SNI-based forwarding without TLS termination)
|
||||
* @param domains Domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createHttpsPassthroughRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number },
|
||||
options: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.match?.ports || 443,
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target,
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
}
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `HTTPS Passthrough for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a complete HTTPS server with HTTP to HTTPS redirects
|
||||
* @param domains Domain(s) to match
|
||||
* @param target Target host and port
|
||||
* @param options Additional configuration options
|
||||
* @returns Array of two route configurations (HTTPS and HTTP redirect)
|
||||
*/
|
||||
export function createCompleteHttpsServer(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number },
|
||||
options: {
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
httpPort?: number | number[];
|
||||
httpsPort?: number | number[];
|
||||
reencrypt?: boolean;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig[] {
|
||||
// Create the HTTPS route
|
||||
const httpsRoute = createHttpsTerminateRoute(domains, target, options);
|
||||
|
||||
// Create the HTTP redirect route
|
||||
const httpRedirectRoute = createHttpToHttpsRedirect(
|
||||
domains,
|
||||
// Extract the HTTPS port from the HTTPS route - ensure it's a number
|
||||
typeof options.httpsPort === 'number' ? options.httpsPort :
|
||||
Array.isArray(options.httpsPort) ? options.httpsPort[0] : 443,
|
||||
{
|
||||
// Set the HTTP port
|
||||
match: {
|
||||
ports: options.httpPort || 80,
|
||||
domains
|
||||
},
|
||||
name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
||||
}
|
||||
);
|
||||
|
||||
return [httpsRoute, httpRedirectRoute];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a load balancer route (round-robin between multiple backend hosts)
|
||||
* @param domains Domain(s) to match
|
||||
* @param hosts Array of backend hosts to load balance between
|
||||
* @param port Backend port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createLoadBalancerRoute(
|
||||
domains: string | string[],
|
||||
hosts: string[],
|
||||
port: number,
|
||||
options: {
|
||||
tls?: {
|
||||
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
};
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.match?.ports || (options.tls ? 443 : 80),
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route target
|
||||
const target: IRouteTarget = {
|
||||
host: hosts,
|
||||
port
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target
|
||||
};
|
||||
|
||||
// Add TLS configuration if provided
|
||||
if (options.tls) {
|
||||
action.tls = {
|
||||
mode: options.tls.mode,
|
||||
certificate: options.tls.certificate || 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a static file server route
|
||||
* @param domains Domain(s) to match
|
||||
* @param rootDir Root directory path for static files
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createStaticFileRoute(
|
||||
domains: string | string[],
|
||||
rootDir: string,
|
||||
options: {
|
||||
indexFiles?: string[];
|
||||
serveOnHttps?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
httpPort?: number | number[];
|
||||
httpsPort?: number | number[];
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.serveOnHttps
|
||||
? (options.httpsPort || 443)
|
||||
: (options.httpPort || 80),
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'static',
|
||||
static: {
|
||||
root: rootDir,
|
||||
index: options.indexFiles || ['index.html', 'index.htm']
|
||||
}
|
||||
};
|
||||
|
||||
// Add TLS configuration if serving on HTTPS
|
||||
if (options.serveOnHttps) {
|
||||
action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `Static Files for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an API route configuration
|
||||
* @param domains Domain(s) to match
|
||||
* @param apiPath API base path (e.g., "/api")
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createApiRoute(
|
||||
domains: string | string[],
|
||||
apiPath: string,
|
||||
target: { host: string | string[]; port: number },
|
||||
options: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
addCorsHeaders?: boolean;
|
||||
httpPort?: number | number[];
|
||||
httpsPort?: number | number[];
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Normalize API path
|
||||
const normalizedPath = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
||||
const pathWithWildcard = normalizedPath.endsWith('/')
|
||||
? `${normalizedPath}*`
|
||||
: `${normalizedPath}/*`;
|
||||
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.useTls
|
||||
? (options.httpsPort || 443)
|
||||
: (options.httpPort || 80),
|
||||
domains,
|
||||
path: pathWithWildcard
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target
|
||||
};
|
||||
|
||||
// Add TLS configuration if using HTTPS
|
||||
if (options.useTls) {
|
||||
action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
// Add CORS headers if requested
|
||||
const headers: Record<string, Record<string, string>> = {};
|
||||
if (options.addCorsHeaders) {
|
||||
headers.response = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400'
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
name: options.name || `API Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
priority: options.priority || 100, // Higher priority for specific path matches
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a WebSocket route configuration
|
||||
* @param domains Domain(s) to match
|
||||
* @param wsPath WebSocket path (e.g., "/ws")
|
||||
* @param target Target WebSocket server host and port
|
||||
* @param options Additional route options
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createWebSocketRoute(
|
||||
domains: string | string[],
|
||||
wsPath: string,
|
||||
target: { host: string | string[]; port: number },
|
||||
options: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
httpPort?: number | number[];
|
||||
httpsPort?: number | number[];
|
||||
pingInterval?: number;
|
||||
pingTimeout?: number;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Normalize WebSocket path
|
||||
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
||||
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: options.useTls
|
||||
? (options.httpsPort || 443)
|
||||
: (options.httpPort || 80),
|
||||
domains,
|
||||
path: normalizedPath
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target,
|
||||
websocket: {
|
||||
enabled: true,
|
||||
pingInterval: options.pingInterval || 30000, // 30 seconds
|
||||
pingTimeout: options.pingTimeout || 5000 // 5 seconds
|
||||
}
|
||||
};
|
||||
|
||||
// Add TLS configuration if using HTTPS
|
||||
if (options.useTls) {
|
||||
action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
priority: options.priority || 100, // Higher priority for WebSocket routes
|
||||
...options
|
||||
};
|
||||
}
|
165
ts/proxies/smart-proxy/utils/route-migration-utils.ts
Normal file
165
ts/proxies/smart-proxy/utils/route-migration-utils.ts
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Route Migration Utilities
|
||||
*
|
||||
* This file provides utility functions for migrating from legacy domain-based
|
||||
* configuration to the new route-based configuration system. These functions
|
||||
* are temporary and will be removed after the migration is complete.
|
||||
*/
|
||||
|
||||
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
|
||||
|
||||
/**
|
||||
* Legacy domain config interface (for migration only)
|
||||
* @deprecated This interface will be removed in a future version
|
||||
*/
|
||||
export interface ILegacyDomainConfig {
|
||||
domains: string[];
|
||||
forwarding: {
|
||||
type: TForwardingType;
|
||||
target: {
|
||||
host: string | string[];
|
||||
port: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a legacy domain config to a route-based config
|
||||
* @param domainConfig Legacy domain configuration
|
||||
* @param additionalOptions Additional options to add to the route
|
||||
* @returns Route configuration
|
||||
* @deprecated This function will be removed in a future version
|
||||
*/
|
||||
export function domainConfigToRouteConfig(
|
||||
domainConfig: ILegacyDomainConfig,
|
||||
additionalOptions: Partial<IRouteConfig> = {}
|
||||
): IRouteConfig {
|
||||
// Default port based on forwarding type
|
||||
let defaultPort = 80;
|
||||
let tlsMode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt' | undefined;
|
||||
|
||||
switch (domainConfig.forwarding.type) {
|
||||
case 'http-only':
|
||||
defaultPort = 80;
|
||||
break;
|
||||
case 'https-passthrough':
|
||||
defaultPort = 443;
|
||||
tlsMode = 'passthrough';
|
||||
break;
|
||||
case 'https-terminate-to-http':
|
||||
defaultPort = 443;
|
||||
tlsMode = 'terminate';
|
||||
break;
|
||||
case 'https-terminate-to-https':
|
||||
defaultPort = 443;
|
||||
tlsMode = 'terminate-and-reencrypt';
|
||||
break;
|
||||
}
|
||||
|
||||
// Create route match criteria
|
||||
const match: IRouteMatch = {
|
||||
ports: additionalOptions.match?.ports || defaultPort,
|
||||
domains: domainConfig.domains
|
||||
};
|
||||
|
||||
// Create route target
|
||||
const target: IRouteTarget = {
|
||||
host: domainConfig.forwarding.target.host,
|
||||
port: domainConfig.forwarding.target.port
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
target
|
||||
};
|
||||
|
||||
// Add TLS configuration if needed
|
||||
if (tlsMode) {
|
||||
action.tls = {
|
||||
mode: tlsMode,
|
||||
certificate: 'auto'
|
||||
};
|
||||
|
||||
// If the legacy config has custom certificates, use them
|
||||
if (domainConfig.forwarding.https?.customCert) {
|
||||
action.tls.certificate = {
|
||||
key: domainConfig.forwarding.https.customCert.key,
|
||||
cert: domainConfig.forwarding.https.customCert.cert
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add security options if present
|
||||
if (domainConfig.forwarding.security) {
|
||||
action.security = domainConfig.forwarding.security;
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
const routeConfig: IRouteConfig = {
|
||||
match,
|
||||
action,
|
||||
// Include a name based on domains if not provided
|
||||
name: additionalOptions.name || `Legacy route for ${domainConfig.domains.join(', ')}`,
|
||||
// Include a note that this was converted from a legacy config
|
||||
description: additionalOptions.description || 'Converted from legacy domain configuration'
|
||||
};
|
||||
|
||||
// Add optional properties if provided
|
||||
if (additionalOptions.priority !== undefined) {
|
||||
routeConfig.priority = additionalOptions.priority;
|
||||
}
|
||||
|
||||
if (additionalOptions.tags) {
|
||||
routeConfig.tags = additionalOptions.tags;
|
||||
}
|
||||
|
||||
return routeConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of legacy domain configs to route configurations
|
||||
* @param domainConfigs Array of legacy domain configurations
|
||||
* @returns Array of route configurations
|
||||
* @deprecated This function will be removed in a future version
|
||||
*/
|
||||
export function domainConfigsToRouteConfigs(
|
||||
domainConfigs: ILegacyDomainConfig[]
|
||||
): IRouteConfig[] {
|
||||
return domainConfigs.map(config => domainConfigToRouteConfig(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domains from a route configuration
|
||||
* @param route Route configuration
|
||||
* @returns Array of domains
|
||||
*/
|
||||
export function extractDomainsFromRoute(route: IRouteConfig): string[] {
|
||||
if (!route.match.domains) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domains from an array of route configurations
|
||||
* @param routes Array of route configurations
|
||||
* @returns Array of unique domains
|
||||
*/
|
||||
export function extractDomainsFromRoutes(routes: IRouteConfig[]): string[] {
|
||||
const domains = new Set<string>();
|
||||
|
||||
for (const route of routes) {
|
||||
const routeDomains = extractDomainsFromRoute(route);
|
||||
for (const domain of routeDomains) {
|
||||
domains.add(domain);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(domains);
|
||||
}
|
309
ts/proxies/smart-proxy/utils/route-patterns.ts
Normal file
309
ts/proxies/smart-proxy/utils/route-patterns.ts
Normal file
@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Route Patterns
|
||||
*
|
||||
* This file provides pre-defined route patterns for common use cases.
|
||||
* These patterns can be used as templates for creating route configurations.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig } from '../models/route-types.js';
|
||||
import { createHttpRoute, createHttpsTerminateRoute, createHttpsPassthroughRoute, createCompleteHttpsServer } from './route-helpers.js';
|
||||
import { mergeRouteConfigs } from './route-utils.js';
|
||||
|
||||
/**
|
||||
* Create an API Gateway route pattern
|
||||
* @param domains Domain(s) to match
|
||||
* @param apiBasePath Base path for API endpoints (e.g., '/api')
|
||||
* @param target Target host and port
|
||||
* @param options Additional route options
|
||||
* @returns API route configuration
|
||||
*/
|
||||
export function createApiGatewayRoute(
|
||||
domains: string | string[],
|
||||
apiBasePath: string,
|
||||
target: { host: string | string[]; port: number },
|
||||
options: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
addCorsHeaders?: boolean;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Normalize apiBasePath to ensure it starts with / and doesn't end with /
|
||||
const normalizedPath = apiBasePath.startsWith('/')
|
||||
? apiBasePath
|
||||
: `/${apiBasePath}`;
|
||||
|
||||
// Add wildcard to path to match all API endpoints
|
||||
const apiPath = normalizedPath.endsWith('/')
|
||||
? `${normalizedPath}*`
|
||||
: `${normalizedPath}/*`;
|
||||
|
||||
// Create base route
|
||||
const baseRoute = options.useTls
|
||||
? createHttpsTerminateRoute(domains, target, {
|
||||
certificate: options.certificate || 'auto'
|
||||
})
|
||||
: createHttpRoute(domains, target);
|
||||
|
||||
// Add API-specific configurations
|
||||
const apiRoute: Partial<IRouteConfig> = {
|
||||
match: {
|
||||
...baseRoute.match,
|
||||
path: apiPath
|
||||
},
|
||||
name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
|
||||
priority: options.priority || 100 // Higher priority for specific path matching
|
||||
};
|
||||
|
||||
// Add CORS headers if requested
|
||||
if (options.addCorsHeaders) {
|
||||
apiRoute.headers = {
|
||||
response: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return mergeRouteConfigs(baseRoute, apiRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a static file server route pattern
|
||||
* @param domains Domain(s) to match
|
||||
* @param rootDirectory Root directory for static files
|
||||
* @param options Additional route options
|
||||
* @returns Static file server route configuration
|
||||
*/
|
||||
export function createStaticFileServerRoute(
|
||||
domains: string | string[],
|
||||
rootDirectory: string,
|
||||
options: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
indexFiles?: string[];
|
||||
cacheControl?: string;
|
||||
path?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Create base route with static action
|
||||
const baseRoute: IRouteConfig = {
|
||||
match: {
|
||||
domains,
|
||||
ports: options.useTls ? 443 : 80,
|
||||
path: options.path || '/'
|
||||
},
|
||||
action: {
|
||||
type: 'static',
|
||||
static: {
|
||||
root: rootDirectory,
|
||||
index: options.indexFiles || ['index.html', 'index.htm'],
|
||||
headers: {
|
||||
'Cache-Control': options.cacheControl || 'public, max-age=3600'
|
||||
}
|
||||
}
|
||||
},
|
||||
name: options.name || `Static Server: ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
priority: options.priority || 50
|
||||
};
|
||||
|
||||
// Add TLS configuration if requested
|
||||
if (options.useTls) {
|
||||
baseRoute.action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: options.certificate || 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
return baseRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a WebSocket route pattern
|
||||
* @param domains Domain(s) to match
|
||||
* @param target WebSocket server host and port
|
||||
* @param options Additional route options
|
||||
* @returns WebSocket route configuration
|
||||
*/
|
||||
export function createWebSocketRoute(
|
||||
domains: string | string[],
|
||||
target: { host: string | string[]; port: number },
|
||||
options: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
path?: string;
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Create base route
|
||||
const baseRoute = options.useTls
|
||||
? createHttpsTerminateRoute(domains, target, {
|
||||
certificate: options.certificate || 'auto'
|
||||
})
|
||||
: createHttpRoute(domains, target);
|
||||
|
||||
// Add WebSocket-specific configurations
|
||||
const wsRoute: Partial<IRouteConfig> = {
|
||||
match: {
|
||||
...baseRoute.match,
|
||||
path: options.path || '/ws',
|
||||
headers: {
|
||||
'Upgrade': 'websocket'
|
||||
}
|
||||
},
|
||||
action: {
|
||||
...baseRoute.action,
|
||||
websocket: {
|
||||
enabled: true,
|
||||
pingInterval: options.pingInterval || 30000, // 30 seconds
|
||||
pingTimeout: options.pingTimeout || 5000 // 5 seconds
|
||||
}
|
||||
},
|
||||
name: options.name || `WebSocket: ${Array.isArray(domains) ? domains.join(', ') : domains} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
|
||||
priority: options.priority || 100 // Higher priority for WebSocket routes
|
||||
};
|
||||
|
||||
return mergeRouteConfigs(baseRoute, wsRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a load balancer route pattern
|
||||
* @param domains Domain(s) to match
|
||||
* @param backends Array of backend servers
|
||||
* @param options Additional route options
|
||||
* @returns Load balancer route configuration
|
||||
*/
|
||||
export function createLoadBalancerRoute(
|
||||
domains: string | string[],
|
||||
backends: Array<{ host: string; port: number }>,
|
||||
options: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
algorithm?: 'round-robin' | 'least-connections' | 'ip-hash';
|
||||
healthCheck?: {
|
||||
path: string;
|
||||
interval: number;
|
||||
timeout: number;
|
||||
unhealthyThreshold: number;
|
||||
healthyThreshold: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
} = {}
|
||||
): IRouteConfig {
|
||||
// Extract hosts and ensure all backends use the same port
|
||||
const port = backends[0].port;
|
||||
const hosts = backends.map(backend => backend.host);
|
||||
|
||||
// Create route with multiple hosts for load balancing
|
||||
const baseRoute = options.useTls
|
||||
? createHttpsTerminateRoute(domains, { host: hosts, port }, {
|
||||
certificate: options.certificate || 'auto'
|
||||
})
|
||||
: createHttpRoute(domains, { host: hosts, port });
|
||||
|
||||
// Add load balancing specific configurations
|
||||
const lbRoute: Partial<IRouteConfig> = {
|
||||
action: {
|
||||
...baseRoute.action,
|
||||
loadBalancing: {
|
||||
algorithm: options.algorithm || 'round-robin',
|
||||
healthCheck: options.healthCheck
|
||||
}
|
||||
},
|
||||
name: options.name || `Load Balancer: ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
priority: options.priority || 50
|
||||
};
|
||||
|
||||
return mergeRouteConfigs(baseRoute, lbRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rate limiting route pattern
|
||||
* @param baseRoute Base route to add rate limiting to
|
||||
* @param rateLimit Rate limiting configuration
|
||||
* @returns Route with rate limiting
|
||||
*/
|
||||
export function addRateLimiting(
|
||||
baseRoute: IRouteConfig,
|
||||
rateLimit: {
|
||||
maxRequests: number;
|
||||
window: number; // Time window in seconds
|
||||
keyBy?: 'ip' | 'path' | 'header';
|
||||
headerName?: string; // Required if keyBy is 'header'
|
||||
errorMessage?: string;
|
||||
}
|
||||
): IRouteConfig {
|
||||
return mergeRouteConfigs(baseRoute, {
|
||||
security: {
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
maxRequests: rateLimit.maxRequests,
|
||||
window: rateLimit.window,
|
||||
keyBy: rateLimit.keyBy || 'ip',
|
||||
headerName: rateLimit.headerName,
|
||||
errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic authentication route pattern
|
||||
* @param baseRoute Base route to add authentication to
|
||||
* @param auth Authentication configuration
|
||||
* @returns Route with basic authentication
|
||||
*/
|
||||
export function addBasicAuth(
|
||||
baseRoute: IRouteConfig,
|
||||
auth: {
|
||||
users: Array<{ username: string; password: string }>;
|
||||
realm?: string;
|
||||
excludePaths?: string[];
|
||||
}
|
||||
): IRouteConfig {
|
||||
return mergeRouteConfigs(baseRoute, {
|
||||
security: {
|
||||
basicAuth: {
|
||||
enabled: true,
|
||||
users: auth.users,
|
||||
realm: auth.realm || 'Restricted Area',
|
||||
excludePaths: auth.excludePaths || []
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JWT authentication route pattern
|
||||
* @param baseRoute Base route to add JWT authentication to
|
||||
* @param jwt JWT authentication configuration
|
||||
* @returns Route with JWT authentication
|
||||
*/
|
||||
export function addJwtAuth(
|
||||
baseRoute: IRouteConfig,
|
||||
jwt: {
|
||||
secret: string;
|
||||
algorithm?: string;
|
||||
issuer?: string;
|
||||
audience?: string;
|
||||
expiresIn?: number; // Time in seconds
|
||||
excludePaths?: string[];
|
||||
}
|
||||
): IRouteConfig {
|
||||
return mergeRouteConfigs(baseRoute, {
|
||||
security: {
|
||||
jwtAuth: {
|
||||
enabled: true,
|
||||
secret: jwt.secret,
|
||||
algorithm: jwt.algorithm || 'HS256',
|
||||
issuer: jwt.issuer,
|
||||
audience: jwt.audience,
|
||||
expiresIn: jwt.expiresIn,
|
||||
excludePaths: jwt.excludePaths || []
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
330
ts/proxies/smart-proxy/utils/route-utils.ts
Normal file
330
ts/proxies/smart-proxy/utils/route-utils.ts
Normal file
@ -0,0 +1,330 @@
|
||||
/**
|
||||
* Route Utilities
|
||||
*
|
||||
* This file provides utility functions for working with route configurations,
|
||||
* including merging, finding, and managing route collections.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch } from '../models/route-types.js';
|
||||
import { validateRouteConfig } from './route-validators.js';
|
||||
|
||||
/**
|
||||
* Merge two route configurations
|
||||
* The second route's properties will override the first route's properties where they exist
|
||||
* @param baseRoute The base route configuration
|
||||
* @param overrideRoute The route configuration with overriding properties
|
||||
* @returns A new merged route configuration
|
||||
*/
|
||||
export function mergeRouteConfigs(
|
||||
baseRoute: IRouteConfig,
|
||||
overrideRoute: Partial<IRouteConfig>
|
||||
): IRouteConfig {
|
||||
// Create deep copies to avoid modifying original objects
|
||||
const mergedRoute: IRouteConfig = JSON.parse(JSON.stringify(baseRoute));
|
||||
|
||||
// Apply overrides at the top level
|
||||
if (overrideRoute.id) mergedRoute.id = overrideRoute.id;
|
||||
if (overrideRoute.name) mergedRoute.name = overrideRoute.name;
|
||||
if (overrideRoute.enabled !== undefined) mergedRoute.enabled = overrideRoute.enabled;
|
||||
if (overrideRoute.priority !== undefined) mergedRoute.priority = overrideRoute.priority;
|
||||
|
||||
// Merge match configuration
|
||||
if (overrideRoute.match) {
|
||||
mergedRoute.match = { ...mergedRoute.match };
|
||||
|
||||
if (overrideRoute.match.ports !== undefined) {
|
||||
mergedRoute.match.ports = overrideRoute.match.ports;
|
||||
}
|
||||
|
||||
if (overrideRoute.match.domains !== undefined) {
|
||||
mergedRoute.match.domains = overrideRoute.match.domains;
|
||||
}
|
||||
|
||||
if (overrideRoute.match.path !== undefined) {
|
||||
mergedRoute.match.path = overrideRoute.match.path;
|
||||
}
|
||||
|
||||
if (overrideRoute.match.headers !== undefined) {
|
||||
mergedRoute.match.headers = overrideRoute.match.headers;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge action configuration
|
||||
if (overrideRoute.action) {
|
||||
// If action types are different, replace the entire action
|
||||
if (overrideRoute.action.type && overrideRoute.action.type !== mergedRoute.action.type) {
|
||||
mergedRoute.action = JSON.parse(JSON.stringify(overrideRoute.action));
|
||||
} else {
|
||||
// Otherwise merge the action properties
|
||||
mergedRoute.action = { ...mergedRoute.action };
|
||||
|
||||
// Merge target
|
||||
if (overrideRoute.action.target) {
|
||||
mergedRoute.action.target = {
|
||||
...mergedRoute.action.target,
|
||||
...overrideRoute.action.target
|
||||
};
|
||||
}
|
||||
|
||||
// Merge TLS options
|
||||
if (overrideRoute.action.tls) {
|
||||
mergedRoute.action.tls = {
|
||||
...mergedRoute.action.tls,
|
||||
...overrideRoute.action.tls
|
||||
};
|
||||
}
|
||||
|
||||
// Merge redirect options
|
||||
if (overrideRoute.action.redirect) {
|
||||
mergedRoute.action.redirect = {
|
||||
...mergedRoute.action.redirect,
|
||||
...overrideRoute.action.redirect
|
||||
};
|
||||
}
|
||||
|
||||
// Merge static options
|
||||
if (overrideRoute.action.static) {
|
||||
mergedRoute.action.static = {
|
||||
...mergedRoute.action.static,
|
||||
...overrideRoute.action.static
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route matches a domain
|
||||
* @param route The route to check
|
||||
* @param domain The domain to match against
|
||||
* @returns True if the route matches the domain, false otherwise
|
||||
*/
|
||||
export function routeMatchesDomain(route: IRouteConfig, domain: string): boolean {
|
||||
if (!route.match?.domains) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
return domains.some(d => {
|
||||
// Handle wildcard domains
|
||||
if (d.startsWith('*.')) {
|
||||
const suffix = d.substring(2);
|
||||
return domain.endsWith(suffix) && domain.split('.').length > suffix.split('.').length;
|
||||
}
|
||||
return d.toLowerCase() === domain.toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route matches a port
|
||||
* @param route The route to check
|
||||
* @param port The port to match against
|
||||
* @returns True if the route matches the port, false otherwise
|
||||
*/
|
||||
export function routeMatchesPort(route: IRouteConfig, port: number): boolean {
|
||||
if (!route.match?.ports) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof route.match.ports === 'number') {
|
||||
return route.match.ports === port;
|
||||
}
|
||||
|
||||
if (Array.isArray(route.match.ports)) {
|
||||
// Simple case - array of numbers
|
||||
if (typeof route.match.ports[0] === 'number') {
|
||||
return (route.match.ports as number[]).includes(port);
|
||||
}
|
||||
|
||||
// Complex case - array of port ranges
|
||||
if (typeof route.match.ports[0] === 'object') {
|
||||
return (route.match.ports as Array<{ from: number; to: number }>).some(
|
||||
range => port >= range.from && port <= range.to
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route matches a path
|
||||
* @param route The route to check
|
||||
* @param path The path to match against
|
||||
* @returns True if the route matches the path, false otherwise
|
||||
*/
|
||||
export function routeMatchesPath(route: IRouteConfig, path: string): boolean {
|
||||
if (!route.match?.path) {
|
||||
return true; // No path specified means it matches any path
|
||||
}
|
||||
|
||||
// Handle exact path
|
||||
if (route.match.path === path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle path prefix with trailing slash (e.g., /api/)
|
||||
if (route.match.path.endsWith('/') && path.startsWith(route.match.path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle exact path match without trailing slash
|
||||
if (!route.match.path.endsWith('/') && path === route.match.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle wildcard paths (e.g., /api/*)
|
||||
if (route.match.path.endsWith('*')) {
|
||||
const prefix = route.match.path.slice(0, -1);
|
||||
return path.startsWith(prefix);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route matches headers
|
||||
* @param route The route to check
|
||||
* @param headers The headers to match against
|
||||
* @returns True if the route matches the headers, false otherwise
|
||||
*/
|
||||
export function routeMatchesHeaders(
|
||||
route: IRouteConfig,
|
||||
headers: Record<string, string>
|
||||
): boolean {
|
||||
if (!route.match?.headers || Object.keys(route.match.headers).length === 0) {
|
||||
return true; // No headers specified means it matches any headers
|
||||
}
|
||||
|
||||
// Check each header in the route's match criteria
|
||||
return Object.entries(route.match.headers).every(([key, value]) => {
|
||||
// If the header isn't present in the request, it doesn't match
|
||||
if (!headers[key]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle exact match
|
||||
if (typeof value === 'string') {
|
||||
return headers[key] === value;
|
||||
}
|
||||
|
||||
// Handle regex match
|
||||
if (value instanceof RegExp) {
|
||||
return value.test(headers[key]);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all routes that match the given criteria
|
||||
* @param routes Array of routes to search
|
||||
* @param criteria Matching criteria
|
||||
* @returns Array of matching routes sorted by priority
|
||||
*/
|
||||
export function findMatchingRoutes(
|
||||
routes: IRouteConfig[],
|
||||
criteria: {
|
||||
domain?: string;
|
||||
port?: number;
|
||||
path?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): IRouteConfig[] {
|
||||
// Filter routes that are enabled and match all provided criteria
|
||||
const matchingRoutes = routes.filter(route => {
|
||||
// Skip disabled routes
|
||||
if (route.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check domain match if specified
|
||||
if (criteria.domain && !routeMatchesDomain(route, criteria.domain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check port match if specified
|
||||
if (criteria.port !== undefined && !routeMatchesPort(route, criteria.port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check path match if specified
|
||||
if (criteria.path && !routeMatchesPath(route, criteria.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check headers match if specified
|
||||
if (criteria.headers && !routeMatchesHeaders(route, criteria.headers)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort matching routes by priority (higher priority first)
|
||||
return matchingRoutes.sort((a, b) => {
|
||||
const priorityA = a.priority || 0;
|
||||
const priorityB = b.priority || 0;
|
||||
return priorityB - priorityA; // Higher priority first
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best matching route for the given criteria
|
||||
* @param routes Array of routes to search
|
||||
* @param criteria Matching criteria
|
||||
* @returns The best matching route or undefined if no match
|
||||
*/
|
||||
export function findBestMatchingRoute(
|
||||
routes: IRouteConfig[],
|
||||
criteria: {
|
||||
domain?: string;
|
||||
port?: number;
|
||||
path?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
): IRouteConfig | undefined {
|
||||
const matchingRoutes = findMatchingRoutes(routes, criteria);
|
||||
return matchingRoutes.length > 0 ? matchingRoutes[0] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a route ID based on route properties
|
||||
* @param route Route configuration
|
||||
* @returns Generated route ID
|
||||
*/
|
||||
export function generateRouteId(route: IRouteConfig): string {
|
||||
// Create a deterministic ID based on route properties
|
||||
const domains = Array.isArray(route.match?.domains)
|
||||
? route.match.domains.join('-')
|
||||
: route.match?.domains || 'any';
|
||||
|
||||
let portsStr = 'any';
|
||||
if (route.match?.ports) {
|
||||
if (Array.isArray(route.match.ports)) {
|
||||
portsStr = route.match.ports.join('-');
|
||||
} else if (typeof route.match.ports === 'number') {
|
||||
portsStr = route.match.ports.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const path = route.match?.path || 'any';
|
||||
const action = route.action?.type || 'unknown';
|
||||
|
||||
return `route-${domains}-${portsStr}-${path}-${action}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a route configuration
|
||||
* @param route Route to clone
|
||||
* @returns Deep copy of the route
|
||||
*/
|
||||
export function cloneRoute(route: IRouteConfig): IRouteConfig {
|
||||
return JSON.parse(JSON.stringify(route));
|
||||
}
|
269
ts/proxies/smart-proxy/utils/route-validators.ts
Normal file
269
ts/proxies/smart-proxy/utils/route-validators.ts
Normal file
@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
Reference in New Issue
Block a user