/** * 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) * - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute) * - Dynamic routing (createDynamicRoute, createSmartLoadBalancer) * - NFTables routes (createNfTablesRoute, createNfTablesTerminateRoute) */ import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } 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 { // 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 { // 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 { // 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> = {}; 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 }; } /** * Create a helper function that applies a port offset * @param offset The offset to apply to the matched port * @returns A function that adds the offset to the matched port */ export function createPortOffset(offset: number): (context: IRouteContext) => number { return (context: IRouteContext) => context.port + offset; } /** * Create a port mapping route with context-based port function * @param options Port mapping route options * @returns Route configuration object */ export function createPortMappingRoute(options: { sourcePortRange: TPortRange; targetHost: string | string[] | ((context: IRouteContext) => string | string[]); portMapper: (context: IRouteContext) => number; name?: string; domains?: string | string[]; priority?: number; [key: string]: any; }): IRouteConfig { // Create route match const match: IRouteMatch = { ports: options.sourcePortRange, domains: options.domains }; // Create route action const action: IRouteAction = { type: 'forward', target: { host: options.targetHost, port: options.portMapper } }; // Create the route config return { match, action, name: options.name || `Port Mapping Route for ${options.domains || 'all domains'}`, priority: options.priority, ...options }; } /** * Create a simple offset port mapping route * @param options Offset port mapping route options * @returns Route configuration object */ export function createOffsetPortMappingRoute(options: { ports: TPortRange; targetHost: string | string[]; offset: number; name?: string; domains?: string | string[]; priority?: number; [key: string]: any; }): IRouteConfig { return createPortMappingRoute({ sourcePortRange: options.ports, targetHost: options.targetHost, portMapper: (context) => context.port + options.offset, name: options.name || `Offset Mapping (${options.offset > 0 ? '+' : ''}${options.offset}) for ${options.domains || 'all domains'}`, domains: options.domains, priority: options.priority, ...options }); } /** * Create a dynamic route with context-based host and port mapping * @param options Dynamic route options * @returns Route configuration object */ export function createDynamicRoute(options: { ports: TPortRange; targetHost: (context: IRouteContext) => string | string[]; portMapper: (context: IRouteContext) => number; name?: string; domains?: string | string[]; path?: string; clientIp?: string[]; priority?: number; [key: string]: any; }): IRouteConfig { // Create route match const match: IRouteMatch = { ports: options.ports, domains: options.domains, path: options.path, clientIp: options.clientIp }; // Create route action const action: IRouteAction = { type: 'forward', target: { host: options.targetHost, port: options.portMapper } }; // Create the route config return { match, action, name: options.name || `Dynamic Route for ${options.domains || 'all domains'}`, priority: options.priority, ...options }; } /** * Create a smart load balancer with dynamic domain-based backend selection * @param options Smart load balancer options * @returns Route configuration object */ export function createSmartLoadBalancer(options: { ports: TPortRange; domainTargets: Record; portMapper: (context: IRouteContext) => number; name?: string; defaultTarget?: string | string[]; priority?: number; [key: string]: any; }): IRouteConfig { // Extract all domain keys to create the match criteria const domains = Object.keys(options.domainTargets); // Create the smart host selector function const hostSelector = (context: IRouteContext) => { const domain = context.domain || ''; return options.domainTargets[domain] || options.defaultTarget || 'localhost'; }; // Create route match const match: IRouteMatch = { ports: options.ports, domains }; // Create route action const action: IRouteAction = { type: 'forward', target: { host: hostSelector, port: options.portMapper } }; // Create the route config return { match, action, name: options.name || `Smart Load Balancer for ${domains.join(', ')}`, priority: options.priority, ...options }; } /** * Create an NFTables-based route for high-performance packet forwarding * @param nameOrDomains Name or domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createNfTablesRoute( nameOrDomains: string | string[], target: { host: string; port: number | 'preserve' }, options: { ports?: TPortRange; protocol?: 'tcp' | 'udp' | 'all'; preserveSourceIP?: boolean; ipAllowList?: string[]; ipBlockList?: string[]; maxRate?: string; priority?: number; useTls?: boolean; tableName?: string; useIPSets?: boolean; useAdvancedNAT?: boolean; } = {} ): IRouteConfig { // Determine if this is a name or domain let name: string; let domains: string | string[] | undefined; if (Array.isArray(nameOrDomains) || (typeof nameOrDomains === 'string' && nameOrDomains.includes('.'))) { domains = nameOrDomains; name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains; } else { name = nameOrDomains; domains = undefined; // No domains } // Create route match const match: IRouteMatch = { domains, ports: options.ports || 80 }; // Create route action const action: IRouteAction = { type: 'forward', target: { host: target.host, port: target.port }, forwardingEngine: 'nftables', nftables: { protocol: options.protocol || 'tcp', preserveSourceIP: options.preserveSourceIP, maxRate: options.maxRate, priority: options.priority, tableName: options.tableName, useIPSets: options.useIPSets, useAdvancedNAT: options.useAdvancedNAT } }; // Add security if allowed or blocked IPs are specified if (options.ipAllowList?.length || options.ipBlockList?.length) { action.security = { ipAllowList: options.ipAllowList, ipBlockList: options.ipBlockList }; } // Add TLS options if needed if (options.useTls) { action.tls = { mode: 'passthrough' }; } // Create the route config return { name, match, action }; } /** * Create an NFTables-based TLS termination route * @param nameOrDomains Name or domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Route configuration object */ export function createNfTablesTerminateRoute( nameOrDomains: string | string[], target: { host: string; port: number | 'preserve' }, options: { ports?: TPortRange; protocol?: 'tcp' | 'udp' | 'all'; preserveSourceIP?: boolean; ipAllowList?: string[]; ipBlockList?: string[]; maxRate?: string; priority?: number; tableName?: string; useIPSets?: boolean; useAdvancedNAT?: boolean; certificate?: 'auto' | { key: string; cert: string }; } = {} ): IRouteConfig { // Create basic NFTables route const route = createNfTablesRoute( nameOrDomains, target, { ...options, ports: options.ports || 443, useTls: false } ); // Set TLS termination route.action.tls = { mode: 'terminate', certificate: options.certificate || 'auto' }; return route; } /** * Create a complete NFTables-based HTTPS setup with HTTP redirect * @param nameOrDomains Name or domain(s) to match * @param target Target host and port * @param options Additional route options * @returns Array of two route configurations (HTTPS and HTTP redirect) */ export function createCompleteNfTablesHttpsServer( nameOrDomains: string | string[], target: { host: string; port: number | 'preserve' }, options: { httpPort?: TPortRange; httpsPort?: TPortRange; protocol?: 'tcp' | 'udp' | 'all'; preserveSourceIP?: boolean; ipAllowList?: string[]; ipBlockList?: string[]; maxRate?: string; priority?: number; tableName?: string; useIPSets?: boolean; useAdvancedNAT?: boolean; certificate?: 'auto' | { key: string; cert: string }; } = {} ): IRouteConfig[] { // Create the HTTPS route using NFTables const httpsRoute = createNfTablesTerminateRoute( nameOrDomains, target, { ...options, ports: options.httpsPort || 443 } ); // Determine the domain(s) for HTTP redirect const domains = typeof nameOrDomains === 'string' && !nameOrDomains.includes('.') ? undefined : nameOrDomains; // Extract the HTTPS port for the redirect destination const httpsPort = typeof options.httpsPort === 'number' ? options.httpsPort : Array.isArray(options.httpsPort) && typeof options.httpsPort[0] === 'number' ? options.httpsPort[0] : 443; // Create the HTTP redirect route (this uses standard forwarding, not NFTables) const httpRedirectRoute = createHttpToHttpsRedirect( domains as any, // Type cast needed since domains can be undefined now httpsPort, { match: { ports: options.httpPort || 80, domains: domains as any // Type cast needed since domains can be undefined now }, name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains || 'all domains'}` } ); return [httpsRoute, httpRedirectRoute]; }