- Removed deprecated route utility functions in favor of direct matcher usage. - Updated imports to reflect new module structure for routing utilities. - Consolidated route manager functionality into SharedRouteManager for better consistency. - Eliminated legacy routing methods and interfaces, streamlining the HttpProxy and associated components. - Enhanced WebSocket and HTTP request handling to utilize the new unified HttpRouter. - Updated route matching logic to leverage matcher classes for domain, path, and header checks. - Cleaned up legacy compatibility code across various modules, ensuring a more maintainable codebase.
266 lines
7.6 KiB
TypeScript
266 lines
7.6 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
|
import { DomainMatcher, PathMatcher } from '../../core/routing/matchers/index.js';
|
|
|
|
/**
|
|
* Interface for router result with additional metadata
|
|
*/
|
|
export interface RouterResult {
|
|
route: IRouteConfig;
|
|
pathMatch?: string;
|
|
pathParams?: Record<string, string>;
|
|
pathRemainder?: string;
|
|
}
|
|
|
|
|
|
/**
|
|
* Logger interface for HttpRouter
|
|
*/
|
|
export interface ILogger {
|
|
debug?: (message: string, data?: any) => void;
|
|
info: (message: string, data?: any) => void;
|
|
warn: (message: string, data?: any) => void;
|
|
error: (message: string, data?: any) => void;
|
|
}
|
|
|
|
/**
|
|
* Unified HTTP Router for reverse proxy requests
|
|
*
|
|
* 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)
|
|
*
|
|
* Path pattern matching:
|
|
* - Exact path: "/api/users"
|
|
* - Wildcard paths: "/api/*"
|
|
* - Path parameters: "/users/:id/profile"
|
|
*/
|
|
export class HttpRouter {
|
|
// Store routes sorted by priority
|
|
private routes: IRouteConfig[] = [];
|
|
// Default route to use when no match is found (optional)
|
|
private defaultRoute?: IRouteConfig;
|
|
// Logger interface
|
|
private logger: ILogger;
|
|
|
|
constructor(
|
|
routes?: IRouteConfig[],
|
|
logger?: ILogger
|
|
) {
|
|
this.logger = logger || {
|
|
error: console.error.bind(console),
|
|
warn: console.warn.bind(console),
|
|
info: console.info.bind(console),
|
|
debug: console.debug?.bind(console)
|
|
};
|
|
|
|
if (routes) {
|
|
this.setRoutes(routes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a new set of routes
|
|
* @param routes Array of route configurations
|
|
*/
|
|
public setRoutes(routes: IRouteConfig[]): void {
|
|
this.routes = [...routes];
|
|
|
|
// Sort routes by priority (higher priority first)
|
|
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 ? [route.match.domains] : [];
|
|
return domains.includes('*');
|
|
});
|
|
|
|
const uniqueDomains = this.getHostnames();
|
|
this.logger.info(`HttpRouter 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();
|
|
|
|
// Find matching route
|
|
const matchingRoute = this.findMatchingRoute(hostWithoutPort, urlPath);
|
|
|
|
if (matchingRoute) {
|
|
return matchingRoute;
|
|
}
|
|
|
|
// 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 the best matching route for a given hostname and path
|
|
*/
|
|
private findMatchingRoute(hostname: string, path: string): RouterResult | undefined {
|
|
// Try each route in priority order
|
|
for (const route of this.routes) {
|
|
// Skip disabled routes
|
|
if (route.enabled === false) {
|
|
continue;
|
|
}
|
|
|
|
// Check domain match
|
|
if (route.match.domains) {
|
|
const domains = Array.isArray(route.match.domains)
|
|
? route.match.domains
|
|
: [route.match.domains];
|
|
|
|
// Check if any domain pattern matches
|
|
const domainMatches = domains.some(domain =>
|
|
DomainMatcher.match(domain, hostname)
|
|
);
|
|
|
|
if (!domainMatches) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Check path match if specified
|
|
if (route.match.path) {
|
|
const pathResult = PathMatcher.match(route.match.path, path);
|
|
if (pathResult.matches) {
|
|
return {
|
|
route,
|
|
pathMatch: path,
|
|
pathParams: pathResult.params,
|
|
pathRemainder: pathResult.pathRemainder
|
|
};
|
|
}
|
|
} else {
|
|
// No path specified, so domain match is sufficient
|
|
return { route };
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* 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 unique 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);
|
|
|
|
// 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;
|
|
|
|
// Filter out routes that match the domain
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Remove a specific route by reference
|
|
* @param route The route to remove
|
|
* @returns Boolean indicating if the route was found and removed
|
|
*/
|
|
public removeRoute(route: IRouteConfig): boolean {
|
|
const index = this.routes.indexOf(route);
|
|
if (index !== -1) {
|
|
this.routes.splice(index, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} |