fix(routing): unify route based architecture

This commit is contained in:
2025-05-13 12:48:41 +00:00
parent fe632bde67
commit fcc8cf9caa
46 changed files with 7943 additions and 1332 deletions

View File

@ -1,5 +1,7 @@
import * as plugins from '../../../plugins.js';
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
import type { IRouteConfig } from '../../smart-proxy/models/route-types.js';
import type { IRouteContext } from '../../../core/models/route-context.js';
/**
* Configuration options for NetworkProxy
@ -24,8 +26,15 @@ export interface INetworkProxyOptions {
// Protocol to use when proxying to backends: HTTP/1.x or HTTP/2
backendProtocol?: 'http1' | 'http2';
// Function cache options
functionCacheSize?: number; // Maximum number of cached function results (default: 1000)
functionCacheTtl?: number; // Time to live for cached function results in ms (default: 5000)
// ACME certificate management options
acme?: IAcmeOptions;
// Direct route configurations
routes?: IRouteConfig[];
}
/**
@ -38,20 +47,39 @@ export interface ICertificateEntry {
}
/**
* Interface for reverse proxy configuration
* @deprecated Use IRouteConfig instead. This interface will be removed in a future release.
*
* IMPORTANT: This is a legacy interface maintained only for backward compatibility.
* New code should use IRouteConfig for all configuration purposes.
*
* @see IRouteConfig for the modern, recommended configuration format
*/
export interface IReverseProxyConfig {
/** Target hostnames/IPs to proxy requests to */
destinationIps: string[];
/** Target ports to proxy requests to */
destinationPorts: number[];
/** Hostname to match for routing */
hostName: string;
/** SSL private key for this host (PEM format) */
privateKey: string;
/** SSL public key/certificate for this host (PEM format) */
publicKey: string;
/** Basic authentication configuration */
authentication?: {
type: 'Basic';
user: string;
pass: string;
};
/** Whether to rewrite the Host header to match the target */
rewriteHostHeader?: boolean;
/**
* Protocol to use when proxying to this backend: 'http1' or 'http2'.
* Overrides the global backendProtocol option if set.
@ -59,6 +87,231 @@ export interface IReverseProxyConfig {
backendProtocol?: 'http1' | 'http2';
}
/**
* Convert a legacy IReverseProxyConfig to the modern IRouteConfig format
*
* @deprecated This function is maintained for backward compatibility.
* New code should create IRouteConfig objects directly.
*
* @param legacyConfig The legacy configuration to convert
* @param proxyPort The port the proxy listens on
* @returns A modern route configuration equivalent to the legacy config
*/
export function convertLegacyConfigToRouteConfig(
legacyConfig: IReverseProxyConfig,
proxyPort: number
): IRouteConfig {
// Create basic route configuration
const routeConfig: IRouteConfig = {
// Match properties
match: {
ports: proxyPort,
domains: legacyConfig.hostName
},
// Action properties
action: {
type: 'forward',
target: {
host: legacyConfig.destinationIps,
port: legacyConfig.destinationPorts[0]
},
// TLS mode is always 'terminate' for legacy configs
tls: {
mode: 'terminate',
certificate: {
key: legacyConfig.privateKey,
cert: legacyConfig.publicKey
}
},
// Advanced options
advanced: {
// Rewrite host header if specified
headers: legacyConfig.rewriteHostHeader ? { 'host': '{domain}' } : {}
}
},
// Metadata
name: `Legacy Config - ${legacyConfig.hostName}`,
priority: 0, // Default priority
enabled: true
};
// Add authentication if present
if (legacyConfig.authentication) {
routeConfig.action.security = {
authentication: {
type: 'basic',
credentials: [{
username: legacyConfig.authentication.user,
password: legacyConfig.authentication.pass
}]
}
};
}
// Add backend protocol if specified
if (legacyConfig.backendProtocol) {
if (!routeConfig.action.options) {
routeConfig.action.options = {};
}
routeConfig.action.options.backendProtocol = legacyConfig.backendProtocol;
}
return routeConfig;
}
/**
* Route manager for NetworkProxy
* Handles route matching and configuration
*/
export class RouteManager {
private routes: IRouteConfig[] = [];
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
/**
* Update the routes configuration
*/
public updateRoutes(routes: IRouteConfig[]): void {
// Sort routes by priority (higher first)
this.routes = [...routes].sort((a, b) => {
const priorityA = a.priority ?? 0;
const priorityB = b.priority ?? 0;
return priorityB - priorityA;
});
this.logger.info(`Updated RouteManager with ${this.routes.length} routes`);
}
/**
* Get all routes
*/
public getRoutes(): IRouteConfig[] {
return [...this.routes];
}
/**
* Find the first matching route for a context
*/
public findMatchingRoute(context: IRouteContext): IRouteConfig | null {
for (const route of this.routes) {
if (this.matchesRoute(route, context)) {
return route;
}
}
return null;
}
/**
* Check if a route matches the given context
*/
private matchesRoute(route: IRouteConfig, context: IRouteContext): boolean {
// Skip disabled routes
if (route.enabled === false) {
return false;
}
// Check domain match if specified
if (route.match.domains && context.domain) {
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
if (!domains.some(domainPattern => this.matchDomain(domainPattern, context.domain!))) {
return false;
}
}
// Check path match if specified
if (route.match.path && context.path) {
if (!this.matchPath(route.match.path, context.path)) {
return false;
}
}
// Check client IP match if specified
if (route.match.clientIp && context.clientIp) {
if (!route.match.clientIp.some(ip => this.matchIp(ip, context.clientIp))) {
return false;
}
}
// Check TLS version match if specified
if (route.match.tlsVersion && context.tlsVersion) {
if (!route.match.tlsVersion.includes(context.tlsVersion)) {
return false;
}
}
// All criteria matched
return true;
}
/**
* Match a domain pattern against a domain
*/
private matchDomain(pattern: string, domain: string): boolean {
if (pattern === domain) {
return true;
}
if (pattern.includes('*')) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(domain);
}
return false;
}
/**
* Match a path pattern against a path
*/
private matchPath(pattern: string, path: string): boolean {
if (pattern === path) {
return true;
}
if (pattern.endsWith('*')) {
const prefix = pattern.slice(0, -1);
return path.startsWith(prefix);
}
return false;
}
/**
* Match an IP pattern against an IP
*/
private matchIp(pattern: string, ip: string): boolean {
if (pattern === ip) {
return true;
}
if (pattern.includes('*')) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(ip);
}
// TODO: Implement CIDR matching
return false;
}
}
/**
* Interface for connection tracking in the pool
*/