update
This commit is contained in:
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));
|
||||
}
|
Reference in New Issue
Block a user