fix(strcuture): refactor responsibilities
This commit is contained in:
9
ts/routing/index.ts
Normal file
9
ts/routing/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Routing functionality module
|
||||
*/
|
||||
|
||||
// Export types and models from HttpProxy
|
||||
export * from '../proxies/http-proxy/models/http-types.js';
|
||||
|
||||
// Export router functionality
|
||||
export * from './router/index.js';
|
6
ts/routing/models/http-types.ts
Normal file
6
ts/routing/models/http-types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* This file re-exports HTTP types from the HttpProxy module
|
||||
* for backward compatibility. All HTTP types are now consolidated
|
||||
* in the HttpProxy module.
|
||||
*/
|
||||
export * from '../../proxies/http-proxy/models/http-types.js';
|
12
ts/routing/router/index.ts
Normal file
12
ts/routing/router/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* HTTP routing
|
||||
*/
|
||||
|
||||
// Export selectively to avoid ambiguity between duplicate type names
|
||||
export { ProxyRouter } from './proxy-router.js';
|
||||
export type { IPathPatternConfig } from './proxy-router.js';
|
||||
// Re-export the RouterResult and PathPatternConfig from proxy-router.js (legacy names maintained for compatibility)
|
||||
export type { PathPatternConfig as ProxyPathPatternConfig, RouterResult as ProxyRouterResult } from './proxy-router.js';
|
||||
|
||||
export { RouteRouter } from './route-router.js';
|
||||
export type { PathPatternConfig as RoutePathPatternConfig, RouterResult as RouteRouterResult } from './route-router.js';
|
437
ts/routing/router/proxy-router.ts
Normal file
437
ts/routing/router/proxy-router.ts
Normal file
@ -0,0 +1,437 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IReverseProxyConfig } from '../../proxies/http-proxy/models/types.js';
|
||||
|
||||
/**
|
||||
* Optional path pattern configuration that can be added to proxy configs
|
||||
*/
|
||||
export interface PathPatternConfig {
|
||||
pathPattern?: string;
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
export type IPathPatternConfig = PathPatternConfig;
|
||||
|
||||
/**
|
||||
* Interface for router result with additional metadata
|
||||
*/
|
||||
export interface RouterResult {
|
||||
config: IReverseProxyConfig;
|
||||
pathMatch?: string;
|
||||
pathParams?: Record<string, string>;
|
||||
pathRemainder?: string;
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
export type IRouterResult = RouterResult;
|
||||
|
||||
/**
|
||||
* Router for HTTP reverse proxy requests
|
||||
*
|
||||
* Supports the following domain matching patterns:
|
||||
* - Exact matches: "example.com"
|
||||
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
|
||||
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
|
||||
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
|
||||
* - Default fallback: "*" (matches any unmatched domain)
|
||||
*
|
||||
* Also supports path pattern matching for each domain:
|
||||
* - Exact path: "/api/users"
|
||||
* - Wildcard paths: "/api/*"
|
||||
* - Path parameters: "/users/:id/profile"
|
||||
*/
|
||||
export class ProxyRouter {
|
||||
// Store original configs for reference
|
||||
private reverseProxyConfigs: IReverseProxyConfig[] = [];
|
||||
// Default config to use when no match is found (optional)
|
||||
private defaultConfig?: IReverseProxyConfig;
|
||||
// Store path patterns separately since they're not in the original interface
|
||||
private pathPatterns: Map<IReverseProxyConfig, string> = new Map();
|
||||
// Logger interface
|
||||
private logger: {
|
||||
error: (message: string, data?: any) => void;
|
||||
warn: (message: string, data?: any) => void;
|
||||
info: (message: string, data?: any) => void;
|
||||
debug: (message: string, data?: any) => void;
|
||||
};
|
||||
|
||||
constructor(
|
||||
configs?: IReverseProxyConfig[],
|
||||
logger?: {
|
||||
error: (message: string, data?: any) => void;
|
||||
warn: (message: string, data?: any) => void;
|
||||
info: (message: string, data?: any) => void;
|
||||
debug: (message: string, data?: any) => void;
|
||||
}
|
||||
) {
|
||||
this.logger = logger || console;
|
||||
if (configs) {
|
||||
this.setNewProxyConfigs(configs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new set of reverse configs to be routed to
|
||||
* @param reverseCandidatesArg Array of reverse proxy configurations
|
||||
*/
|
||||
public setNewProxyConfigs(reverseCandidatesArg: IReverseProxyConfig[]): void {
|
||||
this.reverseProxyConfigs = [...reverseCandidatesArg];
|
||||
|
||||
// Find default config if any (config with "*" as hostname)
|
||||
this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*');
|
||||
|
||||
this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a request based on hostname and path
|
||||
* @param req The incoming HTTP request
|
||||
* @returns The matching proxy config or undefined if no match found
|
||||
*/
|
||||
public routeReq(req: plugins.http.IncomingMessage): IReverseProxyConfig {
|
||||
const result = this.routeReqWithDetails(req);
|
||||
return result ? result.config : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a request with detailed matching information
|
||||
* @param req The incoming HTTP request
|
||||
* @returns Detailed routing result including matched config and path information
|
||||
*/
|
||||
public routeReqWithDetails(req: plugins.http.IncomingMessage): RouterResult | undefined {
|
||||
// Extract and validate host header
|
||||
const originalHost = req.headers.host;
|
||||
if (!originalHost) {
|
||||
this.logger.error('No host header found in request');
|
||||
return this.defaultConfig ? { config: this.defaultConfig } : undefined;
|
||||
}
|
||||
|
||||
// Parse URL for path matching
|
||||
const parsedUrl = plugins.url.parse(req.url || '/');
|
||||
const urlPath = parsedUrl.pathname || '/';
|
||||
|
||||
// Extract hostname without port
|
||||
const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
|
||||
|
||||
// First try exact hostname match
|
||||
const exactConfig = this.findConfigForHost(hostWithoutPort, urlPath);
|
||||
if (exactConfig) {
|
||||
return exactConfig;
|
||||
}
|
||||
|
||||
// Try various wildcard patterns
|
||||
if (hostWithoutPort.includes('.')) {
|
||||
const domainParts = hostWithoutPort.split('.');
|
||||
|
||||
// Try wildcard subdomain (*.example.com)
|
||||
if (domainParts.length > 2) {
|
||||
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
|
||||
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
|
||||
if (wildcardConfig) {
|
||||
return wildcardConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Try TLD wildcard (example.*)
|
||||
const baseDomain = domainParts.slice(0, -1).join('.');
|
||||
const tldWildcardDomain = `${baseDomain}.*`;
|
||||
const tldWildcardConfig = this.findConfigForHost(tldWildcardDomain, urlPath);
|
||||
if (tldWildcardConfig) {
|
||||
return tldWildcardConfig;
|
||||
}
|
||||
|
||||
// Try complex wildcard patterns
|
||||
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
|
||||
for (const pattern of wildcardPatterns) {
|
||||
const wildcardConfig = this.findConfigForHost(pattern, urlPath);
|
||||
if (wildcardConfig) {
|
||||
return wildcardConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default config if available
|
||||
if (this.defaultConfig) {
|
||||
this.logger.warn(`No specific config found for host: ${hostWithoutPort}, using default`);
|
||||
return { config: this.defaultConfig };
|
||||
}
|
||||
|
||||
this.logger.error(`No config found for host: ${hostWithoutPort}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find potential wildcard patterns that could match a given hostname
|
||||
* Handles complex patterns like "*.lossless*" or other partial matches
|
||||
* @param hostname The hostname to find wildcard matches for
|
||||
* @returns Array of potential wildcard patterns that could match
|
||||
*/
|
||||
private findWildcardMatches(hostname: string): string[] {
|
||||
const patterns: string[] = [];
|
||||
const hostnameParts = hostname.split('.');
|
||||
|
||||
// Find all configured hostnames that contain wildcards
|
||||
const wildcardConfigs = this.reverseProxyConfigs.filter(
|
||||
config => config.hostName.includes('*')
|
||||
);
|
||||
|
||||
// Extract unique wildcard patterns
|
||||
const wildcardPatterns = [...new Set(
|
||||
wildcardConfigs.map(config => config.hostName.toLowerCase())
|
||||
)];
|
||||
|
||||
// For each wildcard pattern, check if it could match the hostname
|
||||
// using simplified regex pattern matching
|
||||
for (const pattern of wildcardPatterns) {
|
||||
// Skip the default wildcard '*'
|
||||
if (pattern === '*') continue;
|
||||
|
||||
// Skip already checked patterns (*.domain.com and domain.*)
|
||||
if (pattern.startsWith('*.') && pattern.indexOf('*', 2) === -1) continue;
|
||||
if (pattern.endsWith('.*') && pattern.indexOf('*') === pattern.length - 1) continue;
|
||||
|
||||
// Convert wildcard pattern to regex
|
||||
const regexPattern = pattern
|
||||
.replace(/\./g, '\\.') // Escape dots
|
||||
.replace(/\*/g, '.*'); // Convert * to .* for regex
|
||||
|
||||
// Create regex object with case insensitive flag
|
||||
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
||||
|
||||
// If hostname matches this complex pattern, add it to the list
|
||||
if (regex.test(hostname)) {
|
||||
patterns.push(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a config for a specific host and path
|
||||
*/
|
||||
private findConfigForHost(hostname: string, path: string): RouterResult | undefined {
|
||||
// Find all configs for this hostname
|
||||
const configs = this.reverseProxyConfigs.filter(
|
||||
config => config.hostName.toLowerCase() === hostname.toLowerCase()
|
||||
);
|
||||
|
||||
if (configs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// First try configs with path patterns
|
||||
const configsWithPaths = configs.filter(config => this.pathPatterns.has(config));
|
||||
|
||||
// Sort by path pattern specificity - more specific first
|
||||
configsWithPaths.sort((a, b) => {
|
||||
const aPattern = this.pathPatterns.get(a) || '';
|
||||
const bPattern = this.pathPatterns.get(b) || '';
|
||||
|
||||
// Exact patterns come before wildcard patterns
|
||||
const aHasWildcard = aPattern.includes('*');
|
||||
const bHasWildcard = bPattern.includes('*');
|
||||
|
||||
if (aHasWildcard && !bHasWildcard) return 1;
|
||||
if (!aHasWildcard && bHasWildcard) return -1;
|
||||
|
||||
// Longer patterns are considered more specific
|
||||
return bPattern.length - aPattern.length;
|
||||
});
|
||||
|
||||
// Check each config with path pattern
|
||||
for (const config of configsWithPaths) {
|
||||
const pathPattern = this.pathPatterns.get(config);
|
||||
if (pathPattern) {
|
||||
const pathMatch = this.matchPath(path, pathPattern);
|
||||
if (pathMatch) {
|
||||
return {
|
||||
config,
|
||||
pathMatch: pathMatch.matched,
|
||||
pathParams: pathMatch.params,
|
||||
pathRemainder: pathMatch.remainder
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no path pattern matched, use the first config without a path pattern
|
||||
const configWithoutPath = configs.find(config => !this.pathPatterns.has(config));
|
||||
if (configWithoutPath) {
|
||||
return { config: configWithoutPath };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a URL path against a pattern
|
||||
* Supports:
|
||||
* - Exact matches: /users/profile
|
||||
* - Wildcards: /api/* (matches any path starting with /api/)
|
||||
* - Path parameters: /users/:id (captures id as a parameter)
|
||||
*
|
||||
* @param path The URL path to match
|
||||
* @param pattern The pattern to match against
|
||||
* @returns Match result with params and remainder, or null if no match
|
||||
*/
|
||||
private matchPath(path: string, pattern: string): {
|
||||
matched: string;
|
||||
params: Record<string, string>;
|
||||
remainder: string;
|
||||
} | null {
|
||||
// Handle exact match
|
||||
if (path === pattern) {
|
||||
return {
|
||||
matched: pattern,
|
||||
params: {},
|
||||
remainder: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Handle wildcard match
|
||||
if (pattern.endsWith('/*')) {
|
||||
const prefix = pattern.slice(0, -2);
|
||||
if (path === prefix || path.startsWith(`${prefix}/`)) {
|
||||
return {
|
||||
matched: prefix,
|
||||
params: {},
|
||||
remainder: path.slice(prefix.length)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle path parameters
|
||||
const patternParts = pattern.split('/').filter(p => p);
|
||||
const pathParts = path.split('/').filter(p => p);
|
||||
|
||||
// Too few path parts to match
|
||||
if (pathParts.length < patternParts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
// Compare each part
|
||||
for (let i = 0; i < patternParts.length; i++) {
|
||||
const patternPart = patternParts[i];
|
||||
const pathPart = pathParts[i];
|
||||
|
||||
// Handle parameter
|
||||
if (patternPart.startsWith(':')) {
|
||||
const paramName = patternPart.slice(1);
|
||||
params[paramName] = pathPart;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle wildcard at the end
|
||||
if (patternPart === '*' && i === patternParts.length - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle exact match for this part
|
||||
if (patternPart !== pathPart) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the remainder - the unmatched path parts
|
||||
const remainderParts = pathParts.slice(patternParts.length);
|
||||
const remainder = remainderParts.length ? '/' + remainderParts.join('/') : '';
|
||||
|
||||
// Calculate the matched path
|
||||
const matchedParts = patternParts.map((part, i) => {
|
||||
return part.startsWith(':') ? pathParts[i] : part;
|
||||
});
|
||||
const matched = '/' + matchedParts.join('/');
|
||||
|
||||
return {
|
||||
matched,
|
||||
params,
|
||||
remainder
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently active proxy configurations
|
||||
* @returns Array of all active configurations
|
||||
*/
|
||||
public getProxyConfigs(): IReverseProxyConfig[] {
|
||||
return [...this.reverseProxyConfigs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all hostnames that this router is configured to handle
|
||||
* @returns Array of hostnames
|
||||
*/
|
||||
public getHostnames(): string[] {
|
||||
const hostnames = new Set<string>();
|
||||
for (const config of this.reverseProxyConfigs) {
|
||||
if (config.hostName !== '*') {
|
||||
hostnames.add(config.hostName.toLowerCase());
|
||||
}
|
||||
}
|
||||
return Array.from(hostnames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single new proxy configuration
|
||||
* @param config The configuration to add
|
||||
* @param pathPattern Optional path pattern for route matching
|
||||
*/
|
||||
public addProxyConfig(
|
||||
config: IReverseProxyConfig,
|
||||
pathPattern?: string
|
||||
): void {
|
||||
this.reverseProxyConfigs.push(config);
|
||||
|
||||
// Store path pattern if provided
|
||||
if (pathPattern) {
|
||||
this.pathPatterns.set(config, pathPattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a path pattern for an existing config
|
||||
* @param config The existing configuration
|
||||
* @param pathPattern The path pattern to set
|
||||
* @returns Boolean indicating if the config was found and updated
|
||||
*/
|
||||
public setPathPattern(
|
||||
config: IReverseProxyConfig,
|
||||
pathPattern: string
|
||||
): boolean {
|
||||
const exists = this.reverseProxyConfigs.includes(config);
|
||||
if (exists) {
|
||||
this.pathPatterns.set(config, pathPattern);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a proxy configuration by hostname
|
||||
* @param hostname The hostname to remove
|
||||
* @returns Boolean indicating whether any configs were removed
|
||||
*/
|
||||
public removeProxyConfig(hostname: string): boolean {
|
||||
const initialCount = this.reverseProxyConfigs.length;
|
||||
|
||||
// Find configs to remove
|
||||
const configsToRemove = this.reverseProxyConfigs.filter(
|
||||
config => config.hostName === hostname
|
||||
);
|
||||
|
||||
// Remove them from the patterns map
|
||||
for (const config of configsToRemove) {
|
||||
this.pathPatterns.delete(config);
|
||||
}
|
||||
|
||||
// Filter them out of the configs array
|
||||
this.reverseProxyConfigs = this.reverseProxyConfigs.filter(
|
||||
config => config.hostName !== hostname
|
||||
);
|
||||
|
||||
return this.reverseProxyConfigs.length !== initialCount;
|
||||
}
|
||||
}
|
482
ts/routing/router/route-router.ts
Normal file
482
ts/routing/router/route-router.ts
Normal file
@ -0,0 +1,482 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
||||
import type { ILogger } from '../../proxies/http-proxy/models/types.js';
|
||||
|
||||
/**
|
||||
* Optional path pattern configuration that can be added to proxy configs
|
||||
*/
|
||||
export interface PathPatternConfig {
|
||||
pathPattern?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for router result with additional metadata
|
||||
*/
|
||||
export interface RouterResult {
|
||||
route: IRouteConfig;
|
||||
pathMatch?: string;
|
||||
pathParams?: Record<string, string>;
|
||||
pathRemainder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Router for HTTP reverse proxy requests based on route configurations
|
||||
*
|
||||
* Supports the following domain matching patterns:
|
||||
* - Exact matches: "example.com"
|
||||
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
|
||||
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
|
||||
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
|
||||
* - Default fallback: "*" (matches any unmatched domain)
|
||||
*
|
||||
* Also supports path pattern matching for each domain:
|
||||
* - Exact path: "/api/users"
|
||||
* - Wildcard paths: "/api/*"
|
||||
* - Path parameters: "/users/:id/profile"
|
||||
*/
|
||||
export class RouteRouter {
|
||||
// Store original routes for reference
|
||||
private routes: IRouteConfig[] = [];
|
||||
// Default route to use when no match is found (optional)
|
||||
private defaultRoute?: IRouteConfig;
|
||||
// Store path patterns separately since they're not in the original interface
|
||||
private pathPatterns: Map<IRouteConfig, string> = new Map();
|
||||
// Logger interface
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(
|
||||
routes?: IRouteConfig[],
|
||||
logger?: ILogger
|
||||
) {
|
||||
this.logger = logger || {
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
debug: console.debug
|
||||
};
|
||||
|
||||
if (routes) {
|
||||
this.setRoutes(routes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new set of routes to be routed to
|
||||
* @param routes Array of route configurations
|
||||
*/
|
||||
public setRoutes(routes: IRouteConfig[]): void {
|
||||
this.routes = [...routes];
|
||||
|
||||
// Sort routes by priority
|
||||
this.routes.sort((a, b) => {
|
||||
const priorityA = a.priority ?? 0;
|
||||
const priorityB = b.priority ?? 0;
|
||||
return priorityB - priorityA;
|
||||
});
|
||||
|
||||
// Find default route if any (route with "*" as domain)
|
||||
this.defaultRoute = this.routes.find(route => {
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
return domains.includes('*');
|
||||
});
|
||||
|
||||
// Extract path patterns from route match.path
|
||||
for (const route of this.routes) {
|
||||
if (route.match.path) {
|
||||
this.pathPatterns.set(route, route.match.path);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueDomains = this.getHostnames();
|
||||
this.logger.info(`Router initialized with ${this.routes.length} routes (${uniqueDomains.length} unique hosts)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a request based on hostname and path
|
||||
* @param req The incoming HTTP request
|
||||
* @returns The matching route or undefined if no match found
|
||||
*/
|
||||
public routeReq(req: plugins.http.IncomingMessage): IRouteConfig | undefined {
|
||||
const result = this.routeReqWithDetails(req);
|
||||
return result ? result.route : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes a request with detailed matching information
|
||||
* @param req The incoming HTTP request
|
||||
* @returns Detailed routing result including matched route and path information
|
||||
*/
|
||||
public routeReqWithDetails(req: plugins.http.IncomingMessage): RouterResult | undefined {
|
||||
// Extract and validate host header
|
||||
const originalHost = req.headers.host;
|
||||
if (!originalHost) {
|
||||
this.logger.error('No host header found in request');
|
||||
return this.defaultRoute ? { route: this.defaultRoute } : undefined;
|
||||
}
|
||||
|
||||
// Parse URL for path matching
|
||||
const parsedUrl = plugins.url.parse(req.url || '/');
|
||||
const urlPath = parsedUrl.pathname || '/';
|
||||
|
||||
// Extract hostname without port
|
||||
const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
|
||||
|
||||
// First try exact hostname match
|
||||
const exactRoute = this.findRouteForHost(hostWithoutPort, urlPath);
|
||||
if (exactRoute) {
|
||||
return exactRoute;
|
||||
}
|
||||
|
||||
// Try various wildcard patterns
|
||||
if (hostWithoutPort.includes('.')) {
|
||||
const domainParts = hostWithoutPort.split('.');
|
||||
|
||||
// Try wildcard subdomain (*.example.com)
|
||||
if (domainParts.length > 2) {
|
||||
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
|
||||
const wildcardRoute = this.findRouteForHost(wildcardDomain, urlPath);
|
||||
if (wildcardRoute) {
|
||||
return wildcardRoute;
|
||||
}
|
||||
}
|
||||
|
||||
// Try TLD wildcard (example.*)
|
||||
const baseDomain = domainParts.slice(0, -1).join('.');
|
||||
const tldWildcardDomain = `${baseDomain}.*`;
|
||||
const tldWildcardRoute = this.findRouteForHost(tldWildcardDomain, urlPath);
|
||||
if (tldWildcardRoute) {
|
||||
return tldWildcardRoute;
|
||||
}
|
||||
|
||||
// Try complex wildcard patterns
|
||||
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
|
||||
for (const pattern of wildcardPatterns) {
|
||||
const wildcardRoute = this.findRouteForHost(pattern, urlPath);
|
||||
if (wildcardRoute) {
|
||||
return wildcardRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default route if available
|
||||
if (this.defaultRoute) {
|
||||
this.logger.warn(`No specific route found for host: ${hostWithoutPort}, using default`);
|
||||
return { route: this.defaultRoute };
|
||||
}
|
||||
|
||||
this.logger.error(`No route found for host: ${hostWithoutPort}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find potential wildcard patterns that could match a given hostname
|
||||
* Handles complex patterns like "*.lossless*" or other partial matches
|
||||
* @param hostname The hostname to find wildcard matches for
|
||||
* @returns Array of potential wildcard patterns that could match
|
||||
*/
|
||||
private findWildcardMatches(hostname: string): string[] {
|
||||
const patterns: string[] = [];
|
||||
|
||||
// Find all routes with wildcard domains
|
||||
for (const route of this.routes) {
|
||||
if (!route.match.domains) continue;
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
// Filter to only wildcard domains
|
||||
const wildcardDomains = domains.filter(domain => domain.includes('*'));
|
||||
|
||||
// Convert each wildcard domain to a regex pattern and check if it matches
|
||||
for (const domain of wildcardDomains) {
|
||||
// Skip the default wildcard '*'
|
||||
if (domain === '*') continue;
|
||||
|
||||
// Skip already checked patterns (*.domain.com and domain.*)
|
||||
if (domain.startsWith('*.') && domain.indexOf('*', 2) === -1) continue;
|
||||
if (domain.endsWith('.*') && domain.indexOf('*') === domain.length - 1) continue;
|
||||
|
||||
// Convert wildcard pattern to regex
|
||||
const regexPattern = domain
|
||||
.replace(/\./g, '\\.') // Escape dots
|
||||
.replace(/\*/g, '.*'); // Convert * to .* for regex
|
||||
|
||||
// Create regex object with case insensitive flag
|
||||
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
||||
|
||||
// If hostname matches this complex pattern, add it to the list
|
||||
if (regex.test(hostname)) {
|
||||
patterns.push(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a route for a specific host and path
|
||||
*/
|
||||
private findRouteForHost(hostname: string, path: string): RouterResult | undefined {
|
||||
// Find all routes for this hostname
|
||||
const matchingRoutes = this.routes.filter(route => {
|
||||
if (!route.match.domains) return false;
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
return domains.some(domain => domain.toLowerCase() === hostname.toLowerCase());
|
||||
});
|
||||
|
||||
if (matchingRoutes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// First try routes with path patterns
|
||||
const routesWithPaths = matchingRoutes.filter(route => this.pathPatterns.has(route));
|
||||
|
||||
// Already sorted by priority during setRoutes
|
||||
|
||||
// Check each route with path pattern
|
||||
for (const route of routesWithPaths) {
|
||||
const pathPattern = this.pathPatterns.get(route);
|
||||
if (pathPattern) {
|
||||
const pathMatch = this.matchPath(path, pathPattern);
|
||||
if (pathMatch) {
|
||||
return {
|
||||
route,
|
||||
pathMatch: pathMatch.matched,
|
||||
pathParams: pathMatch.params,
|
||||
pathRemainder: pathMatch.remainder
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no path pattern matched, use the first route without a path pattern
|
||||
const routeWithoutPath = matchingRoutes.find(route => !this.pathPatterns.has(route));
|
||||
if (routeWithoutPath) {
|
||||
return { route: routeWithoutPath };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches a URL path against a pattern
|
||||
* Supports:
|
||||
* - Exact matches: /users/profile
|
||||
* - Wildcards: /api/* (matches any path starting with /api/)
|
||||
* - Path parameters: /users/:id (captures id as a parameter)
|
||||
*
|
||||
* @param path The URL path to match
|
||||
* @param pattern The pattern to match against
|
||||
* @returns Match result with params and remainder, or null if no match
|
||||
*/
|
||||
private matchPath(path: string, pattern: string): {
|
||||
matched: string;
|
||||
params: Record<string, string>;
|
||||
remainder: string;
|
||||
} | null {
|
||||
// Handle exact match
|
||||
if (path === pattern) {
|
||||
return {
|
||||
matched: pattern,
|
||||
params: {},
|
||||
remainder: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Handle wildcard match
|
||||
if (pattern.endsWith('/*')) {
|
||||
const prefix = pattern.slice(0, -2);
|
||||
if (path === prefix || path.startsWith(`${prefix}/`)) {
|
||||
return {
|
||||
matched: prefix,
|
||||
params: {},
|
||||
remainder: path.slice(prefix.length)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle path parameters
|
||||
const patternParts = pattern.split('/').filter(p => p);
|
||||
const pathParts = path.split('/').filter(p => p);
|
||||
|
||||
// Too few path parts to match
|
||||
if (pathParts.length < patternParts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
// Compare each part
|
||||
for (let i = 0; i < patternParts.length; i++) {
|
||||
const patternPart = patternParts[i];
|
||||
const pathPart = pathParts[i];
|
||||
|
||||
// Handle parameter
|
||||
if (patternPart.startsWith(':')) {
|
||||
const paramName = patternPart.slice(1);
|
||||
params[paramName] = pathPart;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle wildcard at the end
|
||||
if (patternPart === '*' && i === patternParts.length - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle exact match for this part
|
||||
if (patternPart !== pathPart) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the remainder - the unmatched path parts
|
||||
const remainderParts = pathParts.slice(patternParts.length);
|
||||
const remainder = remainderParts.length ? '/' + remainderParts.join('/') : '';
|
||||
|
||||
// Calculate the matched path
|
||||
const matchedParts = patternParts.map((part, i) => {
|
||||
return part.startsWith(':') ? pathParts[i] : part;
|
||||
});
|
||||
const matched = '/' + matchedParts.join('/');
|
||||
|
||||
return {
|
||||
matched,
|
||||
params,
|
||||
remainder
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all currently active route configurations
|
||||
* @returns Array of all active routes
|
||||
*/
|
||||
public getRoutes(): IRouteConfig[] {
|
||||
return [...this.routes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all hostnames that this router is configured to handle
|
||||
* @returns Array of hostnames
|
||||
*/
|
||||
public getHostnames(): string[] {
|
||||
const hostnames = new Set<string>();
|
||||
for (const route of this.routes) {
|
||||
if (!route.match.domains) continue;
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
for (const domain of domains) {
|
||||
if (domain !== '*') {
|
||||
hostnames.add(domain.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(hostnames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single new route configuration
|
||||
* @param route The route configuration to add
|
||||
*/
|
||||
public addRoute(route: IRouteConfig): void {
|
||||
this.routes.push(route);
|
||||
|
||||
// Store path pattern if present
|
||||
if (route.match.path) {
|
||||
this.pathPatterns.set(route, route.match.path);
|
||||
}
|
||||
|
||||
// Re-sort routes by priority
|
||||
this.routes.sort((a, b) => {
|
||||
const priorityA = a.priority ?? 0;
|
||||
const priorityB = b.priority ?? 0;
|
||||
return priorityB - priorityA;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes routes by domain pattern
|
||||
* @param domain The domain pattern to remove routes for
|
||||
* @returns Boolean indicating whether any routes were removed
|
||||
*/
|
||||
public removeRoutesByDomain(domain: string): boolean {
|
||||
const initialCount = this.routes.length;
|
||||
|
||||
// Find routes to remove
|
||||
const routesToRemove = this.routes.filter(route => {
|
||||
if (!route.match.domains) return false;
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
return domains.includes(domain);
|
||||
});
|
||||
|
||||
// Remove them from the patterns map
|
||||
for (const route of routesToRemove) {
|
||||
this.pathPatterns.delete(route);
|
||||
}
|
||||
|
||||
// Filter them out of the routes array
|
||||
this.routes = this.routes.filter(route => {
|
||||
if (!route.match.domains) return true;
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
return !domains.includes(domain);
|
||||
});
|
||||
|
||||
return this.routes.length !== initialCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method for compatibility with ProxyRouter
|
||||
* Converts IReverseProxyConfig to IRouteConfig and calls setRoutes
|
||||
*
|
||||
* @param configs Array of legacy proxy configurations
|
||||
*/
|
||||
public setNewProxyConfigs(configs: any[]): void {
|
||||
// Convert legacy configs to routes and add them
|
||||
const routes: IRouteConfig[] = configs.map(config => {
|
||||
// Create a basic route configuration from the legacy config
|
||||
return {
|
||||
match: {
|
||||
ports: config.destinationPorts[0], // Just use the first port
|
||||
domains: config.hostName
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: {
|
||||
host: config.destinationIps,
|
||||
port: config.destinationPorts[0]
|
||||
},
|
||||
tls: {
|
||||
mode: 'terminate',
|
||||
certificate: {
|
||||
key: config.privateKey,
|
||||
cert: config.publicKey
|
||||
}
|
||||
}
|
||||
},
|
||||
name: `Legacy Config - ${config.hostName}`,
|
||||
enabled: true
|
||||
};
|
||||
});
|
||||
|
||||
this.setRoutes(routes);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user