fix(routing): unify route based architecture
This commit is contained in:
@ -1,441 +0,0 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IDomainConfig, ISmartProxyOptions } from './models/interfaces.js';
|
||||
import type { TForwardingType, IForwardConfig } from '../../forwarding/config/forwarding-types.js';
|
||||
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
|
||||
import { ForwardingHandlerFactory } from '../../forwarding/factory/forwarding-factory.js';
|
||||
import type { IRouteConfig } from './models/route-types.js';
|
||||
import { RouteManager } from './route-manager.js';
|
||||
|
||||
/**
|
||||
* Manages domain configurations and target selection
|
||||
*/
|
||||
export class DomainConfigManager {
|
||||
// Track round-robin indices for domain configs
|
||||
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
||||
|
||||
// Cache forwarding handlers for each domain config
|
||||
private forwardingHandlers: Map<IDomainConfig, ForwardingHandler> = new Map();
|
||||
|
||||
// Store derived domain configs from routes
|
||||
private derivedDomainConfigs: IDomainConfig[] = [];
|
||||
|
||||
// Reference to RouteManager for route-based configuration
|
||||
private routeManager?: RouteManager;
|
||||
|
||||
constructor(private settings: ISmartProxyOptions) {
|
||||
// Initialize with derived domain configs if using route-based configuration
|
||||
if (settings.routes && !settings.domainConfigs) {
|
||||
this.generateDomainConfigsFromRoutes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the route manager reference for route-based queries
|
||||
*/
|
||||
public setRouteManager(routeManager: RouteManager): void {
|
||||
this.routeManager = routeManager;
|
||||
|
||||
// Regenerate domain configs from routes if needed
|
||||
if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) {
|
||||
this.generateDomainConfigsFromRoutes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate domain configs from routes
|
||||
*/
|
||||
public generateDomainConfigsFromRoutes(): void {
|
||||
this.derivedDomainConfigs = [];
|
||||
|
||||
if (!this.settings.routes) return;
|
||||
|
||||
for (const route of this.settings.routes) {
|
||||
if (route.action.type !== 'forward' || !route.match.domains) continue;
|
||||
|
||||
// Convert route to domain config
|
||||
const domainConfig = this.routeToDomainConfig(route);
|
||||
if (domainConfig) {
|
||||
this.derivedDomainConfigs.push(domainConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a route to a domain config
|
||||
*/
|
||||
private routeToDomainConfig(route: IRouteConfig): IDomainConfig | null {
|
||||
if (route.action.type !== 'forward' || !route.action.target) return null;
|
||||
|
||||
// Get domains from route
|
||||
const domains = Array.isArray(route.match.domains) ?
|
||||
route.match.domains :
|
||||
(route.match.domains ? [route.match.domains] : []);
|
||||
|
||||
if (domains.length === 0) return null;
|
||||
|
||||
// Determine forwarding type based on TLS mode
|
||||
let forwardingType: TForwardingType = 'http-only';
|
||||
if (route.action.tls) {
|
||||
switch (route.action.tls.mode) {
|
||||
case 'passthrough':
|
||||
forwardingType = 'https-passthrough';
|
||||
break;
|
||||
case 'terminate':
|
||||
forwardingType = 'https-terminate-to-http';
|
||||
break;
|
||||
case 'terminate-and-reencrypt':
|
||||
forwardingType = 'https-terminate-to-https';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create domain config
|
||||
return {
|
||||
domains,
|
||||
forwarding: {
|
||||
type: forwardingType,
|
||||
target: {
|
||||
host: route.action.target.host,
|
||||
port: route.action.target.port
|
||||
},
|
||||
security: route.action.security ? {
|
||||
allowedIps: route.action.security.allowedIps,
|
||||
blockedIps: route.action.security.blockedIps,
|
||||
maxConnections: route.action.security.maxConnections
|
||||
} : undefined,
|
||||
https: route.action.tls && route.action.tls.certificate !== 'auto' ? {
|
||||
customCert: route.action.tls.certificate
|
||||
} : undefined,
|
||||
advanced: route.action.advanced
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the domain configurations
|
||||
*/
|
||||
public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void {
|
||||
// If we're using domainConfigs property, update it
|
||||
if (this.settings.domainConfigs) {
|
||||
this.settings.domainConfigs = newDomainConfigs;
|
||||
} else {
|
||||
// Otherwise update our derived configs
|
||||
this.derivedDomainConfigs = newDomainConfigs;
|
||||
}
|
||||
|
||||
// Reset target indices for removed configs
|
||||
const currentConfigSet = new Set(newDomainConfigs);
|
||||
for (const [config] of this.domainTargetIndices) {
|
||||
if (!currentConfigSet.has(config)) {
|
||||
this.domainTargetIndices.delete(config);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear handlers for removed configs and create handlers for new configs
|
||||
const handlersToRemove: IDomainConfig[] = [];
|
||||
for (const [config] of this.forwardingHandlers) {
|
||||
if (!currentConfigSet.has(config)) {
|
||||
handlersToRemove.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove handlers that are no longer needed
|
||||
for (const config of handlersToRemove) {
|
||||
this.forwardingHandlers.delete(config);
|
||||
}
|
||||
|
||||
// Create handlers for new configs
|
||||
for (const config of newDomainConfigs) {
|
||||
if (!this.forwardingHandlers.has(config)) {
|
||||
try {
|
||||
const handler = this.createForwardingHandler(config);
|
||||
this.forwardingHandlers.set(config, handler);
|
||||
} catch (err) {
|
||||
console.log(`Error creating forwarding handler for domain ${config.domains.join(', ')}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all domain configurations
|
||||
*/
|
||||
public getDomainConfigs(): IDomainConfig[] {
|
||||
// Use domainConfigs from settings if available, otherwise use derived configs
|
||||
return this.settings.domainConfigs || this.derivedDomainConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find domain config matching a server name
|
||||
*/
|
||||
public findDomainConfig(serverName: string): IDomainConfig | undefined {
|
||||
if (!serverName) return undefined;
|
||||
|
||||
// Get domain configs from the appropriate source
|
||||
const domainConfigs = this.getDomainConfigs();
|
||||
|
||||
// Check for direct match
|
||||
for (const config of domainConfigs) {
|
||||
if (config.domains.some(d => plugins.minimatch(serverName, d))) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
// No match found
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find domain config for a specific port
|
||||
*/
|
||||
public findDomainConfigForPort(port: number): IDomainConfig | undefined {
|
||||
// Get domain configs from the appropriate source
|
||||
const domainConfigs = this.getDomainConfigs();
|
||||
|
||||
// Check if any domain config has a matching port range
|
||||
for (const domain of domainConfigs) {
|
||||
const portRanges = domain.forwarding?.advanced?.portRanges;
|
||||
if (portRanges && portRanges.length > 0 && this.isPortInRanges(port, portRanges)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're in route-based mode, also check routes for this port
|
||||
if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) {
|
||||
const routesForPort = this.settings.routes.filter(route => {
|
||||
// Check if this port is in the route's ports
|
||||
if (typeof route.match.ports === 'number') {
|
||||
return route.match.ports === port;
|
||||
} else if (Array.isArray(route.match.ports)) {
|
||||
return route.match.ports.some(p => {
|
||||
if (typeof p === 'number') {
|
||||
return p === port;
|
||||
} else if (p.from && p.to) {
|
||||
return port >= p.from && port <= p.to;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// If we found any routes for this port, convert the first one to a domain config
|
||||
if (routesForPort.length > 0 && routesForPort[0].action.type === 'forward') {
|
||||
const domainConfig = this.routeToDomainConfig(routesForPort[0]);
|
||||
if (domainConfig) {
|
||||
return domainConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is within any of the given ranges
|
||||
*/
|
||||
public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean {
|
||||
return ranges.some((range) => port >= range.from && port <= range.to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target IP with round-robin support
|
||||
*/
|
||||
public getTargetIP(domainConfig: IDomainConfig): string {
|
||||
const targetHosts = Array.isArray(domainConfig.forwarding.target.host)
|
||||
? domainConfig.forwarding.target.host
|
||||
: [domainConfig.forwarding.target.host];
|
||||
|
||||
if (targetHosts.length > 0) {
|
||||
const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
|
||||
const ip = targetHosts[currentIndex % targetHosts.length];
|
||||
this.domainTargetIndices.set(domainConfig, currentIndex + 1);
|
||||
return ip;
|
||||
}
|
||||
|
||||
return this.settings.targetIP || 'localhost';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target host with round-robin support (for tests)
|
||||
* This is just an alias for getTargetIP for easier test compatibility
|
||||
*/
|
||||
public getTargetHost(domainConfig: IDomainConfig): string {
|
||||
return this.getTargetIP(domainConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target port from domain config
|
||||
*/
|
||||
public getTargetPort(domainConfig: IDomainConfig, defaultPort: number): number {
|
||||
return domainConfig.forwarding.target.port || defaultPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a domain should use NetworkProxy
|
||||
*/
|
||||
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
|
||||
const forwardingType = this.getForwardingType(domainConfig);
|
||||
return forwardingType === 'https-terminate-to-http' ||
|
||||
forwardingType === 'https-terminate-to-https';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NetworkProxy port for a domain
|
||||
*/
|
||||
public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined {
|
||||
// First check if we should use NetworkProxy at all
|
||||
if (!this.shouldUseNetworkProxy(domainConfig)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return domainConfig.forwarding.advanced?.networkProxyPort || this.settings.networkProxyPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective allowed and blocked IPs for a domain
|
||||
*
|
||||
* This method combines domain-specific security rules from the forwarding configuration
|
||||
* with global security defaults when necessary.
|
||||
*/
|
||||
public getEffectiveIPRules(domainConfig: IDomainConfig): {
|
||||
allowedIPs: string[],
|
||||
blockedIPs: string[]
|
||||
} {
|
||||
// Start with empty arrays
|
||||
const allowedIPs: string[] = [];
|
||||
const blockedIPs: string[] = [];
|
||||
|
||||
// Add IPs from forwarding security settings if available
|
||||
if (domainConfig.forwarding?.security?.allowedIps) {
|
||||
allowedIPs.push(...domainConfig.forwarding.security.allowedIps);
|
||||
} else {
|
||||
// If no allowed IPs are specified in forwarding config and global defaults exist, use them
|
||||
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||
allowedIPs.push(...this.settings.defaultAllowedIPs);
|
||||
} else {
|
||||
// Default to allow all if no specific rules
|
||||
allowedIPs.push('*');
|
||||
}
|
||||
}
|
||||
|
||||
// Add blocked IPs from forwarding security settings if available
|
||||
if (domainConfig.forwarding?.security?.blockedIps) {
|
||||
blockedIPs.push(...domainConfig.forwarding.security.blockedIps);
|
||||
}
|
||||
|
||||
// Always add global blocked IPs, even if domain has its own rules
|
||||
// This ensures that global blocks take precedence
|
||||
if (this.settings.defaultBlockedIPs && this.settings.defaultBlockedIPs.length > 0) {
|
||||
// Add only unique IPs that aren't already in the list
|
||||
for (const ip of this.settings.defaultBlockedIPs) {
|
||||
if (!blockedIPs.includes(ip)) {
|
||||
blockedIPs.push(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowedIPs,
|
||||
blockedIPs
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection timeout for a domain
|
||||
*/
|
||||
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
|
||||
if (domainConfig?.forwarding.advanced?.timeout) {
|
||||
return domainConfig.forwarding.advanced.timeout;
|
||||
}
|
||||
|
||||
return this.settings.maxConnectionLifetime || 86400000; // 24 hours default
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a forwarding handler for a domain configuration
|
||||
*/
|
||||
private createForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler {
|
||||
// Create a new handler using the factory
|
||||
const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding);
|
||||
|
||||
// Initialize the handler
|
||||
handler.initialize().catch(err => {
|
||||
console.log(`Error initializing forwarding handler for ${domainConfig.domains.join(', ')}: ${err}`);
|
||||
});
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a forwarding handler for a domain config
|
||||
* If no handler exists, creates one
|
||||
*/
|
||||
public getForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler {
|
||||
// If we already have a handler, return it
|
||||
if (this.forwardingHandlers.has(domainConfig)) {
|
||||
return this.forwardingHandlers.get(domainConfig)!;
|
||||
}
|
||||
|
||||
// Otherwise create a new handler
|
||||
const handler = this.createForwardingHandler(domainConfig);
|
||||
this.forwardingHandlers.set(domainConfig, handler);
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the forwarding type for a domain config
|
||||
*/
|
||||
public getForwardingType(domainConfig?: IDomainConfig): TForwardingType | undefined {
|
||||
if (!domainConfig?.forwarding) return undefined;
|
||||
return domainConfig.forwarding.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the forwarding type requires TLS termination
|
||||
*/
|
||||
public requiresTlsTermination(domainConfig?: IDomainConfig): boolean {
|
||||
if (!domainConfig) return false;
|
||||
|
||||
const forwardingType = this.getForwardingType(domainConfig);
|
||||
return forwardingType === 'https-terminate-to-http' ||
|
||||
forwardingType === 'https-terminate-to-https';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the forwarding type supports HTTP
|
||||
*/
|
||||
public supportsHttp(domainConfig?: IDomainConfig): boolean {
|
||||
if (!domainConfig) return false;
|
||||
|
||||
const forwardingType = this.getForwardingType(domainConfig);
|
||||
|
||||
// HTTP-only always supports HTTP
|
||||
if (forwardingType === 'http-only') return true;
|
||||
|
||||
// For termination types, check the HTTP settings
|
||||
if (forwardingType === 'https-terminate-to-http' ||
|
||||
forwardingType === 'https-terminate-to-https') {
|
||||
// HTTP is supported by default for termination types
|
||||
return domainConfig.forwarding?.http?.enabled !== false;
|
||||
}
|
||||
|
||||
// HTTPS-passthrough doesn't support HTTP
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if HTTP requests should be redirected to HTTPS
|
||||
*/
|
||||
public shouldRedirectToHttps(domainConfig?: IDomainConfig): boolean {
|
||||
if (!domainConfig?.forwarding) return false;
|
||||
|
||||
// Only check for redirect if HTTP is enabled
|
||||
if (this.supportsHttp(domainConfig)) {
|
||||
return !!domainConfig.forwarding.http?.redirectToHttps;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -20,15 +20,5 @@ export { NetworkProxyBridge } from './network-proxy-bridge.js';
|
||||
export { RouteManager } from './route-manager.js';
|
||||
export { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
|
||||
// Export route helpers for configuration
|
||||
export {
|
||||
createRoute,
|
||||
createHttpRoute,
|
||||
createHttpsRoute,
|
||||
createPassthroughRoute,
|
||||
createRedirectRoute,
|
||||
createHttpToHttpsRedirect,
|
||||
createBlockRoute,
|
||||
createLoadBalancerRoute,
|
||||
createHttpsServer
|
||||
} from './route-helpers.js';
|
||||
// Export all helper functions from the utils directory
|
||||
export * from './utils/index.js';
|
||||
|
@ -33,10 +33,8 @@ export interface ISmartProxyOptions {
|
||||
// The unified configuration array (required)
|
||||
routes: IRouteConfig[];
|
||||
|
||||
// Port range configuration
|
||||
globalPortRanges?: Array<{ from: number; to: number }>;
|
||||
forwardAllGlobalRanges?: boolean;
|
||||
preserveSourceIP?: boolean;
|
||||
// Port configuration
|
||||
preserveSourceIP?: boolean; // Preserve client IP when forwarding
|
||||
|
||||
// Global/default settings
|
||||
defaults?: {
|
||||
@ -140,6 +138,11 @@ export interface IConnectionRecord {
|
||||
hasReceivedInitialData: boolean; // Whether initial data has been received
|
||||
routeConfig?: IRouteConfig; // Associated route config for this connection
|
||||
|
||||
// Target information (for dynamic port/host mapping)
|
||||
targetHost?: string; // Resolved target host
|
||||
targetPort?: number; // Resolved target port
|
||||
tlsVersion?: string; // TLS version (for routing context)
|
||||
|
||||
// Keep-alive tracking
|
||||
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
|
||||
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
||||
|
@ -34,13 +34,43 @@ export interface IRouteMatch {
|
||||
headers?: Record<string, string | RegExp>; // Match specific HTTP headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Context provided to port and host mapping functions
|
||||
*/
|
||||
export interface IRouteContext {
|
||||
// Connection information
|
||||
port: number; // The matched incoming port
|
||||
domain?: string; // The domain from SNI or Host header
|
||||
clientIp: string; // The client's IP address
|
||||
serverIp: string; // The server's IP address
|
||||
path?: string; // URL path (for HTTP connections)
|
||||
query?: string; // Query string (for HTTP connections)
|
||||
headers?: Record<string, string>; // HTTP headers (for HTTP connections)
|
||||
|
||||
// TLS information
|
||||
isTls: boolean; // Whether the connection is TLS
|
||||
tlsVersion?: string; // TLS version if applicable
|
||||
|
||||
// Route information
|
||||
routeName?: string; // The name of the matched route
|
||||
routeId?: string; // The ID of the matched route
|
||||
|
||||
// Target information (resolved from dynamic mapping)
|
||||
targetHost?: string; // The resolved target host
|
||||
targetPort?: number; // The resolved target port
|
||||
|
||||
// Additional properties
|
||||
timestamp: number; // The request timestamp
|
||||
connectionId: string; // Unique connection identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Target configuration for forwarding
|
||||
*/
|
||||
export interface IRouteTarget {
|
||||
host: string | string[]; // Support single host or round-robin
|
||||
port: number;
|
||||
preservePort?: boolean; // Use incoming port as target port
|
||||
host: string | string[] | ((context: any) => string | string[]); // Support static or dynamic host selection with any compatible context
|
||||
port: number | ((context: any) => number); // Support static or dynamic port mapping with any compatible context
|
||||
preservePort?: boolean; // Use incoming port as target port (ignored if port is a function)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,6 +145,16 @@ export interface IRouteTestResponse {
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL rewriting configuration
|
||||
*/
|
||||
export interface IRouteUrlRewrite {
|
||||
pattern: string; // RegExp pattern to match in URL
|
||||
target: string; // Replacement pattern (supports template variables like {domain})
|
||||
flags?: string; // RegExp flags like 'g' for global replacement
|
||||
onlyRewritePath?: boolean; // Only apply to path, not query string
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced options for route actions
|
||||
*/
|
||||
@ -124,6 +164,7 @@ export interface IRouteAdvanced {
|
||||
keepAlive?: boolean;
|
||||
staticFiles?: IRouteStaticFiles;
|
||||
testResponse?: IRouteTestResponse;
|
||||
urlRewrite?: IRouteUrlRewrite; // URL rewriting configuration
|
||||
// Additional advanced options would go here
|
||||
}
|
||||
|
||||
@ -131,10 +172,15 @@ export interface IRouteAdvanced {
|
||||
* WebSocket configuration
|
||||
*/
|
||||
export interface IRouteWebSocket {
|
||||
enabled: boolean;
|
||||
pingInterval?: number;
|
||||
pingTimeout?: number;
|
||||
maxPayloadSize?: number;
|
||||
enabled: boolean; // Whether WebSockets are enabled for this route
|
||||
pingInterval?: number; // Interval for sending ping frames (ms)
|
||||
pingTimeout?: number; // Timeout for pong response (ms)
|
||||
maxPayloadSize?: number; // Maximum message size in bytes
|
||||
customHeaders?: Record<string, string>; // Custom headers for WebSocket handshake
|
||||
subprotocols?: string[]; // Supported subprotocols
|
||||
rewritePath?: string; // Path rewriting for WebSocket connections
|
||||
allowedOrigins?: string[]; // Allowed origins for WebSocket connections
|
||||
authenticateRequest?: boolean; // Whether to apply route security to WebSocket connections
|
||||
}
|
||||
|
||||
/**
|
||||
@ -181,6 +227,12 @@ export interface IRouteAction {
|
||||
|
||||
// Advanced options
|
||||
advanced?: IRouteAdvanced;
|
||||
|
||||
// Additional options for backend-specific settings
|
||||
options?: {
|
||||
backendProtocol?: 'http1' | 'http2';
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -219,12 +271,27 @@ export interface IRouteSecurity {
|
||||
ipBlockList?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS configuration for a route
|
||||
*/
|
||||
export interface IRouteCors {
|
||||
enabled: boolean; // Whether CORS is enabled for this route
|
||||
allowOrigin?: string | string[]; // Allowed origins (*,domain.com,[domain1,domain2])
|
||||
allowMethods?: string; // Allowed methods (GET,POST,etc.)
|
||||
allowHeaders?: string; // Allowed headers
|
||||
allowCredentials?: boolean; // Whether to allow credentials
|
||||
exposeHeaders?: string; // Headers to expose to the client
|
||||
maxAge?: number; // Preflight cache duration in seconds
|
||||
preflight?: boolean; // Whether to respond to preflight requests
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers configuration
|
||||
*/
|
||||
export interface IRouteHeaders {
|
||||
request?: Record<string, string>;
|
||||
response?: Record<string, string>;
|
||||
request?: Record<string, string>; // Headers to add/modify for requests to backend
|
||||
response?: Record<string, string>; // Headers to add/modify for responses to client
|
||||
cors?: IRouteCors; // CORS configuration
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { NetworkProxy } from '../network-proxy/index.js';
|
||||
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
||||
import { Port80HandlerEvents } from '../../core/models/common-types.js';
|
||||
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
|
||||
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||
@ -11,8 +10,8 @@ import type { IRouteConfig } from './models/route-types.js';
|
||||
* Manages NetworkProxy integration for TLS termination
|
||||
*
|
||||
* NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination.
|
||||
* It directly maps route configurations to NetworkProxy configuration format and manages
|
||||
* certificate provisioning through Port80Handler when ACME is enabled.
|
||||
* It directly passes route configurations to NetworkProxy and manages the physical
|
||||
* connection piping between SmartProxy and NetworkProxy for TLS termination.
|
||||
*
|
||||
* It is used by SmartProxy for routes that have:
|
||||
* - TLS mode of 'terminate' or 'terminate-and-reencrypt'
|
||||
@ -49,7 +48,7 @@ export class NetworkProxyBridge {
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
||||
// Configure NetworkProxy options based on PortProxy settings
|
||||
// Configure NetworkProxy options based on SmartProxy settings
|
||||
const networkProxyOptions: any = {
|
||||
port: this.settings.networkProxyPort!,
|
||||
portProxyIntegration: true,
|
||||
@ -57,7 +56,6 @@ export class NetworkProxyBridge {
|
||||
useExternalPort80Handler: !!this.port80Handler // Use Port80Handler if available
|
||||
};
|
||||
|
||||
|
||||
this.networkProxy = new NetworkProxy(networkProxyOptions);
|
||||
|
||||
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
||||
@ -80,29 +78,8 @@ export class NetworkProxyBridge {
|
||||
|
||||
console.log(`Received certificate for ${data.domain} from Port80Handler, updating NetworkProxy`);
|
||||
|
||||
try {
|
||||
// Find existing config for this domain
|
||||
const existingConfigs = this.networkProxy.getProxyConfigs()
|
||||
.filter(config => config.hostName === data.domain);
|
||||
|
||||
if (existingConfigs.length > 0) {
|
||||
// Update existing configs with new certificate
|
||||
for (const config of existingConfigs) {
|
||||
config.privateKey = data.privateKey;
|
||||
config.publicKey = data.certificate;
|
||||
}
|
||||
|
||||
// Apply updated configs
|
||||
this.networkProxy.updateProxyConfigs(existingConfigs)
|
||||
.then(() => console.log(`Updated certificate for ${data.domain} in NetworkProxy`))
|
||||
.catch(err => console.log(`Error updating certificate in NetworkProxy: ${err}`));
|
||||
} else {
|
||||
// Create a new config for this domain
|
||||
console.log(`No existing config found for ${data.domain}, creating new config in NetworkProxy`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Error handling certificate event: ${err}`);
|
||||
}
|
||||
// Apply certificate directly to NetworkProxy
|
||||
this.networkProxy.updateCertificate(data.domain, data.certificate, data.privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,7 +90,9 @@ export class NetworkProxyBridge {
|
||||
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
||||
return;
|
||||
}
|
||||
this.handleCertificateEvent(data);
|
||||
|
||||
// Apply certificate directly to NetworkProxy
|
||||
this.networkProxy.updateCertificate(data.domain, data.certificate, data.privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -155,92 +134,6 @@ export class NetworkProxyBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register domains from routes with Port80Handler for certificate management
|
||||
*
|
||||
* Extracts domains from routes that require TLS termination and registers them
|
||||
* with the Port80Handler for certificate issuance and renewal.
|
||||
*
|
||||
* @param routes The route configurations to extract domains from
|
||||
*/
|
||||
public registerDomainsWithPort80Handler(routes: IRouteConfig[]): void {
|
||||
if (!this.port80Handler) {
|
||||
console.log('Cannot register domains - Port80Handler not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract domains from routes that require TLS termination
|
||||
const domainsToRegister = new Set<string>();
|
||||
|
||||
for (const route of routes) {
|
||||
// Skip routes without domains or TLS configuration
|
||||
if (!route.match.domains || !route.action.tls) continue;
|
||||
|
||||
// Only register domains for routes that terminate TLS
|
||||
if (route.action.tls.mode !== 'terminate' && route.action.tls.mode !== 'terminate-and-reencrypt') continue;
|
||||
|
||||
// Extract domains from route
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
// Add each domain to the set (avoiding duplicates)
|
||||
for (const domain of domains) {
|
||||
// Skip wildcards
|
||||
if (domain.includes('*')) {
|
||||
console.log(`Skipping wildcard domain for ACME: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
domainsToRegister.add(domain);
|
||||
}
|
||||
}
|
||||
|
||||
// Register each unique domain with Port80Handler
|
||||
for (const domain of domainsToRegister) {
|
||||
try {
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true,
|
||||
// Include route reference if we can find it
|
||||
routeReference: this.findRouteReferenceForDomain(domain, routes)
|
||||
});
|
||||
|
||||
console.log(`Registered domain with Port80Handler: ${domain}`);
|
||||
} catch (err) {
|
||||
console.log(`Error registering domain ${domain} with Port80Handler: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the route reference for a given domain
|
||||
*
|
||||
* @param domain The domain to find a route reference for
|
||||
* @param routes The routes to search
|
||||
* @returns The route reference if found, undefined otherwise
|
||||
*/
|
||||
private findRouteReferenceForDomain(domain: string, routes: IRouteConfig[]): { routeId?: string; routeName?: string } | undefined {
|
||||
// Find the first route that matches this domain
|
||||
for (const route of routes) {
|
||||
if (!route.match.domains) continue;
|
||||
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
if (domains.includes(domain)) {
|
||||
return {
|
||||
routeId: undefined, // No explicit IDs in our current routes
|
||||
routeName: route.name
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards a TLS connection to a NetworkProxy for handling
|
||||
*/
|
||||
@ -305,7 +198,6 @@ export class NetworkProxyBridge {
|
||||
socket.pipe(proxySocket);
|
||||
proxySocket.pipe(socket);
|
||||
|
||||
// Update activity on data transfer (caller should handle this)
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
|
||||
}
|
||||
@ -315,13 +207,8 @@ export class NetworkProxyBridge {
|
||||
/**
|
||||
* Synchronizes routes to NetworkProxy
|
||||
*
|
||||
* This method directly maps route configurations to NetworkProxy format and updates
|
||||
* the NetworkProxy with these configurations. It handles:
|
||||
*
|
||||
* - Extracting domain, target, and certificate information from routes
|
||||
* - Converting TLS mode settings to NetworkProxy configuration
|
||||
* - Applying security and advanced settings
|
||||
* - Registering domains for ACME certificate provisioning when needed
|
||||
* This method directly passes route configurations to NetworkProxy without any
|
||||
* intermediate conversion. NetworkProxy natively understands route configurations.
|
||||
*
|
||||
* @param routes The route configurations to sync to NetworkProxy
|
||||
*/
|
||||
@ -332,140 +219,22 @@ export class NetworkProxyBridge {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get SSL certificates from assets
|
||||
// Import fs directly since it's not in plugins
|
||||
const fs = await import('fs');
|
||||
|
||||
let defaultCertPair;
|
||||
try {
|
||||
defaultCertPair = {
|
||||
key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
|
||||
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'),
|
||||
};
|
||||
} catch (certError) {
|
||||
console.log(`Warning: Could not read default certificates: ${certError}`);
|
||||
console.log(
|
||||
'Using empty certificate placeholders - ACME will generate proper certificates if enabled'
|
||||
// Filter only routes that are applicable to NetworkProxy (TLS termination)
|
||||
const networkProxyRoutes = routes.filter(route => {
|
||||
return (
|
||||
route.action.type === 'forward' &&
|
||||
route.action.tls &&
|
||||
(route.action.tls.mode === 'terminate' || route.action.tls.mode === 'terminate-and-reencrypt')
|
||||
);
|
||||
});
|
||||
|
||||
// Use empty placeholders - NetworkProxy will use its internal defaults
|
||||
// or ACME will generate proper ones if enabled
|
||||
defaultCertPair = {
|
||||
key: '',
|
||||
cert: '',
|
||||
};
|
||||
}
|
||||
|
||||
// Map routes directly to NetworkProxy configs
|
||||
const proxyConfigs = this.mapRoutesToNetworkProxyConfigs(routes, defaultCertPair);
|
||||
|
||||
// Update the proxy configs
|
||||
await this.networkProxy.updateProxyConfigs(proxyConfigs);
|
||||
console.log(`Synced ${proxyConfigs.length} configurations to NetworkProxy`);
|
||||
|
||||
// Register domains with Port80Handler for certificate issuance
|
||||
if (this.port80Handler) {
|
||||
this.registerDomainsWithPort80Handler(routes);
|
||||
}
|
||||
// Pass routes directly to NetworkProxy
|
||||
await this.networkProxy.updateRouteConfigs(networkProxyRoutes);
|
||||
console.log(`Synced ${networkProxyRoutes.length} routes directly to NetworkProxy`);
|
||||
} catch (err) {
|
||||
console.log(`Error syncing routes to NetworkProxy: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map routes directly to NetworkProxy configuration format
|
||||
*
|
||||
* This method directly maps route configurations to NetworkProxy's format
|
||||
* without any intermediate domain-based representation. It processes each route
|
||||
* and creates appropriate NetworkProxy configs for domains that require TLS termination.
|
||||
*
|
||||
* @param routes Array of route configurations to map
|
||||
* @param defaultCertPair Default certificate to use if no custom certificate is specified
|
||||
* @returns Array of NetworkProxy configurations
|
||||
*/
|
||||
public mapRoutesToNetworkProxyConfigs(
|
||||
routes: IRouteConfig[],
|
||||
defaultCertPair: { key: string; cert: string }
|
||||
): plugins.tsclass.network.IReverseProxyConfig[] {
|
||||
const configs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
// Skip routes without domains
|
||||
if (!route.match.domains) continue;
|
||||
|
||||
// Skip non-forward routes
|
||||
if (route.action.type !== 'forward') continue;
|
||||
|
||||
// Skip routes without TLS configuration
|
||||
if (!route.action.tls || !route.action.target) continue;
|
||||
|
||||
// Skip routes that don't require TLS termination
|
||||
if (route.action.tls.mode !== 'terminate' && route.action.tls.mode !== 'terminate-and-reencrypt') continue;
|
||||
|
||||
// Get domains from route
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains
|
||||
: [route.match.domains];
|
||||
|
||||
// Create a config for each domain
|
||||
for (const domain of domains) {
|
||||
// Get certificate
|
||||
let certKey = defaultCertPair.key;
|
||||
let certCert = defaultCertPair.cert;
|
||||
|
||||
// Use custom certificate if specified
|
||||
if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate === 'object') {
|
||||
certKey = route.action.tls.certificate.key;
|
||||
certCert = route.action.tls.certificate.cert;
|
||||
}
|
||||
|
||||
// Determine target hosts and ports
|
||||
const targetHosts = Array.isArray(route.action.target.host)
|
||||
? route.action.target.host
|
||||
: [route.action.target.host];
|
||||
|
||||
const targetPort = route.action.target.port;
|
||||
|
||||
// Create the NetworkProxy config
|
||||
const config: plugins.tsclass.network.IReverseProxyConfig = {
|
||||
hostName: domain,
|
||||
privateKey: certKey,
|
||||
publicKey: certCert,
|
||||
destinationIps: targetHosts,
|
||||
destinationPorts: [targetPort]
|
||||
// Note: We can't include additional metadata as it's not supported in the interface
|
||||
};
|
||||
|
||||
configs.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is kept for backward compatibility.
|
||||
* Use mapRoutesToNetworkProxyConfigs() instead.
|
||||
*/
|
||||
public convertRoutesToNetworkProxyConfigs(
|
||||
routes: IRouteConfig[],
|
||||
defaultCertPair: { key: string; cert: string }
|
||||
): plugins.tsclass.network.IReverseProxyConfig[] {
|
||||
return this.mapRoutesToNetworkProxyConfigs(routes, defaultCertPair);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is deprecated and will be removed in a future version.
|
||||
* Use syncRoutesToNetworkProxy() instead.
|
||||
*
|
||||
* This legacy method exists only for backward compatibility and
|
||||
* simply forwards to syncRoutesToNetworkProxy().
|
||||
*/
|
||||
public async syncDomainConfigsToNetworkProxy(): Promise<void> {
|
||||
console.log('DEPRECATED: Method syncDomainConfigsToNetworkProxy will be removed in a future version.');
|
||||
console.log('Please use syncRoutesToNetworkProxy() instead for direct route-based configuration.');
|
||||
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a certificate for a specific domain
|
||||
@ -496,12 +265,6 @@ export class NetworkProxyBridge {
|
||||
domainOptions.routeReference = {
|
||||
routeName
|
||||
};
|
||||
} else {
|
||||
// Try to find a route reference from the current routes
|
||||
const routeReference = this.findRouteReferenceForDomain(domain, this.settings.routes || []);
|
||||
if (routeReference) {
|
||||
domainOptions.routeReference = routeReference;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the domain for certificate issuance
|
||||
|
195
ts/proxies/smart-proxy/port-manager.ts
Normal file
195
ts/proxies/smart-proxy/port-manager.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||
import { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
|
||||
/**
|
||||
* PortManager handles the dynamic creation and removal of port listeners
|
||||
*
|
||||
* This class provides methods to add and remove listening ports at runtime,
|
||||
* allowing SmartProxy to adapt to configuration changes without requiring
|
||||
* a full restart.
|
||||
*/
|
||||
export class PortManager {
|
||||
private servers: Map<number, plugins.net.Server> = new Map();
|
||||
private settings: ISmartProxyOptions;
|
||||
private routeConnectionHandler: RouteConnectionHandler;
|
||||
private isShuttingDown: boolean = false;
|
||||
|
||||
/**
|
||||
* Create a new PortManager
|
||||
*
|
||||
* @param settings The SmartProxy settings
|
||||
* @param routeConnectionHandler The handler for new connections
|
||||
*/
|
||||
constructor(
|
||||
settings: ISmartProxyOptions,
|
||||
routeConnectionHandler: RouteConnectionHandler
|
||||
) {
|
||||
this.settings = settings;
|
||||
this.routeConnectionHandler = routeConnectionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening on a specific port
|
||||
*
|
||||
* @param port The port number to listen on
|
||||
* @returns Promise that resolves when the server is listening or rejects on error
|
||||
*/
|
||||
public async addPort(port: number): Promise<void> {
|
||||
// Check if we're already listening on this port
|
||||
if (this.servers.has(port)) {
|
||||
console.log(`PortManager: Already listening on port ${port}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a server for this port
|
||||
const server = plugins.net.createServer((socket) => {
|
||||
// Check if shutting down
|
||||
if (this.isShuttingDown) {
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to route connection handler
|
||||
this.routeConnectionHandler.handleConnection(socket);
|
||||
}).on('error', (err: Error) => {
|
||||
console.log(`Server Error on port ${port}: ${err.message}`);
|
||||
});
|
||||
|
||||
// Start listening on the port
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
server.listen(port, () => {
|
||||
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
||||
console.log(
|
||||
`SmartProxy -> OK: Now listening on port ${port}${
|
||||
isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''
|
||||
}`
|
||||
);
|
||||
|
||||
// Store the server reference
|
||||
this.servers.set(port, server);
|
||||
resolve();
|
||||
}).on('error', (err) => {
|
||||
console.log(`Failed to listen on port ${port}: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening on a specific port
|
||||
*
|
||||
* @param port The port to stop listening on
|
||||
* @returns Promise that resolves when the server is closed
|
||||
*/
|
||||
public async removePort(port: number): Promise<void> {
|
||||
// Get the server for this port
|
||||
const server = this.servers.get(port);
|
||||
if (!server) {
|
||||
console.log(`PortManager: Not listening on port ${port}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the server
|
||||
return new Promise<void>((resolve) => {
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
console.log(`Error closing server on port ${port}: ${err.message}`);
|
||||
} else {
|
||||
console.log(`SmartProxy -> Stopped listening on port ${port}`);
|
||||
}
|
||||
|
||||
// Remove the server reference
|
||||
this.servers.delete(port);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple ports at once
|
||||
*
|
||||
* @param ports Array of ports to add
|
||||
* @returns Promise that resolves when all servers are listening
|
||||
*/
|
||||
public async addPorts(ports: number[]): Promise<void> {
|
||||
const uniquePorts = [...new Set(ports)];
|
||||
await Promise.all(uniquePorts.map(port => this.addPort(port)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove multiple ports at once
|
||||
*
|
||||
* @param ports Array of ports to remove
|
||||
* @returns Promise that resolves when all servers are closed
|
||||
*/
|
||||
public async removePorts(ports: number[]): Promise<void> {
|
||||
const uniquePorts = [...new Set(ports)];
|
||||
await Promise.all(uniquePorts.map(port => this.removePort(port)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update listening ports to match the provided list
|
||||
*
|
||||
* This will add any ports that aren't currently listening,
|
||||
* and remove any ports that are no longer needed.
|
||||
*
|
||||
* @param ports Array of ports that should be listening
|
||||
* @returns Promise that resolves when all operations are complete
|
||||
*/
|
||||
public async updatePorts(ports: number[]): Promise<void> {
|
||||
const targetPorts = new Set(ports);
|
||||
const currentPorts = new Set(this.servers.keys());
|
||||
|
||||
// Find ports to add and remove
|
||||
const portsToAdd = ports.filter(port => !currentPorts.has(port));
|
||||
const portsToRemove = Array.from(currentPorts).filter(port => !targetPorts.has(port));
|
||||
|
||||
// Log the changes
|
||||
if (portsToAdd.length > 0) {
|
||||
console.log(`PortManager: Adding new listeners for ports: ${portsToAdd.join(', ')}`);
|
||||
}
|
||||
|
||||
if (portsToRemove.length > 0) {
|
||||
console.log(`PortManager: Removing listeners for ports: ${portsToRemove.join(', ')}`);
|
||||
}
|
||||
|
||||
// Add and remove ports
|
||||
await this.removePorts(portsToRemove);
|
||||
await this.addPorts(portsToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all ports that are currently listening
|
||||
*
|
||||
* @returns Array of port numbers
|
||||
*/
|
||||
public getListeningPorts(): number[] {
|
||||
return Array.from(this.servers.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the port manager as shutting down
|
||||
*/
|
||||
public setShuttingDown(isShuttingDown: boolean): void {
|
||||
this.isShuttingDown = isShuttingDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all listening servers
|
||||
*
|
||||
* @returns Promise that resolves when all servers are closed
|
||||
*/
|
||||
public async closeAll(): Promise<void> {
|
||||
const allPorts = Array.from(this.servers.keys());
|
||||
await this.removePorts(allPorts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all server instances (for testing or debugging)
|
||||
*/
|
||||
public getServers(): Map<number, plugins.net.Server> {
|
||||
return new Map(this.servers);
|
||||
}
|
||||
}
|
@ -8,7 +8,8 @@ import {
|
||||
} from './models/interfaces.js';
|
||||
import type {
|
||||
IRouteConfig,
|
||||
IRouteAction
|
||||
IRouteAction,
|
||||
IRouteContext
|
||||
} from './models/route-types.js';
|
||||
import { ConnectionManager } from './connection-manager.js';
|
||||
import { SecurityManager } from './security-manager.js';
|
||||
@ -24,6 +25,9 @@ import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.j
|
||||
export class RouteConnectionHandler {
|
||||
private settings: ISmartProxyOptions;
|
||||
|
||||
// Cache for route contexts to avoid recreation
|
||||
private routeContextCache: Map<string, IRouteContext> = new Map();
|
||||
|
||||
constructor(
|
||||
settings: ISmartProxyOptions,
|
||||
private connectionManager: ConnectionManager,
|
||||
@ -36,6 +40,47 @@ export class RouteConnectionHandler {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a route context object for port and host mapping functions
|
||||
*/
|
||||
private createRouteContext(options: {
|
||||
connectionId: string;
|
||||
port: number;
|
||||
domain?: string;
|
||||
clientIp: string;
|
||||
serverIp: string;
|
||||
isTls: boolean;
|
||||
tlsVersion?: string;
|
||||
routeName?: string;
|
||||
routeId?: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
headers?: Record<string, string>;
|
||||
}): IRouteContext {
|
||||
return {
|
||||
// Connection information
|
||||
port: options.port,
|
||||
domain: options.domain,
|
||||
clientIp: options.clientIp,
|
||||
serverIp: options.serverIp,
|
||||
path: options.path,
|
||||
query: options.query,
|
||||
headers: options.headers,
|
||||
|
||||
// TLS information
|
||||
isTls: options.isTls,
|
||||
tlsVersion: options.tlsVersion,
|
||||
|
||||
// Route information
|
||||
routeName: options.routeName,
|
||||
routeId: options.routeId,
|
||||
|
||||
// Additional properties
|
||||
timestamp: Date.now(),
|
||||
connectionId: options.connectionId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new incoming connection
|
||||
*/
|
||||
@ -325,7 +370,7 @@ export class RouteConnectionHandler {
|
||||
): void {
|
||||
const connectionId = record.id;
|
||||
const action = route.action;
|
||||
|
||||
|
||||
// We should have a target configuration for forwarding
|
||||
if (!action.target) {
|
||||
console.log(`[${connectionId}] Forward action missing target configuration`);
|
||||
@ -333,24 +378,82 @@ export class RouteConnectionHandler {
|
||||
this.connectionManager.cleanupConnection(record, 'missing_target');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create the routing context for this connection
|
||||
const routeContext = this.createRouteContext({
|
||||
connectionId: record.id,
|
||||
port: record.localPort,
|
||||
domain: record.lockedDomain,
|
||||
clientIp: record.remoteIP,
|
||||
serverIp: socket.localAddress || '',
|
||||
isTls: record.isTLS || false,
|
||||
tlsVersion: record.tlsVersion,
|
||||
routeName: route.name,
|
||||
routeId: route.id
|
||||
});
|
||||
|
||||
// Cache the context for potential reuse
|
||||
this.routeContextCache.set(connectionId, routeContext);
|
||||
|
||||
// Determine host using function or static value
|
||||
let targetHost: string | string[];
|
||||
if (typeof action.target.host === 'function') {
|
||||
try {
|
||||
targetHost = action.target.host(routeContext);
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(`[${connectionId}] Dynamic host resolved to: ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[${connectionId}] Error in host mapping function: ${err}`);
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'host_mapping_error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
targetHost = action.target.host;
|
||||
}
|
||||
|
||||
// If an array of hosts, select one randomly for load balancing
|
||||
const selectedHost = Array.isArray(targetHost)
|
||||
? targetHost[Math.floor(Math.random() * targetHost.length)]
|
||||
: targetHost;
|
||||
|
||||
// Determine port using function or static value
|
||||
let targetPort: number;
|
||||
if (typeof action.target.port === 'function') {
|
||||
try {
|
||||
targetPort = action.target.port(routeContext);
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(`[${connectionId}] Dynamic port mapping: ${record.localPort} -> ${targetPort}`);
|
||||
}
|
||||
// Store the resolved target port in the context for potential future use
|
||||
routeContext.targetPort = targetPort;
|
||||
} catch (err) {
|
||||
console.log(`[${connectionId}] Error in port mapping function: ${err}`);
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'port_mapping_error');
|
||||
return;
|
||||
}
|
||||
} else if (action.target.preservePort) {
|
||||
// Use incoming port if preservePort is true
|
||||
targetPort = record.localPort;
|
||||
} else {
|
||||
// Use static port from configuration
|
||||
targetPort = action.target.port;
|
||||
}
|
||||
|
||||
// Store the resolved host in the context
|
||||
routeContext.targetHost = selectedHost;
|
||||
|
||||
// Determine if this needs TLS handling
|
||||
if (action.tls) {
|
||||
switch (action.tls.mode) {
|
||||
case 'passthrough':
|
||||
// For TLS passthrough, just forward directly
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(`[${connectionId}] Using TLS passthrough to ${action.target.host}`);
|
||||
console.log(`[${connectionId}] Using TLS passthrough to ${selectedHost}:${targetPort}`);
|
||||
}
|
||||
|
||||
// Allow for array of hosts
|
||||
const targetHost = Array.isArray(action.target.host)
|
||||
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
|
||||
: action.target.host;
|
||||
|
||||
// Determine target port - either target port or preserve incoming port
|
||||
const targetPort = action.target.preservePort ? record.localPort : action.target.port;
|
||||
|
||||
return this.setupDirectConnection(
|
||||
socket,
|
||||
record,
|
||||
@ -358,7 +461,7 @@ export class RouteConnectionHandler {
|
||||
record.lockedDomain,
|
||||
initialChunk,
|
||||
undefined,
|
||||
targetHost,
|
||||
selectedHost,
|
||||
targetPort
|
||||
);
|
||||
|
||||
@ -402,14 +505,36 @@ export class RouteConnectionHandler {
|
||||
console.log(`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`);
|
||||
}
|
||||
|
||||
// Allow for array of hosts
|
||||
const targetHost = Array.isArray(action.target.host)
|
||||
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
|
||||
: action.target.host;
|
||||
|
||||
// Determine target port - either target port or preserve incoming port
|
||||
const targetPort = action.target.preservePort ? record.localPort : action.target.port;
|
||||
|
||||
// Get the appropriate host value
|
||||
let targetHost: string;
|
||||
|
||||
if (typeof action.target.host === 'function') {
|
||||
// For function-based host, use the same routeContext created earlier
|
||||
const hostResult = action.target.host(routeContext);
|
||||
targetHost = Array.isArray(hostResult)
|
||||
? hostResult[Math.floor(Math.random() * hostResult.length)]
|
||||
: hostResult;
|
||||
} else {
|
||||
// For static host value
|
||||
targetHost = Array.isArray(action.target.host)
|
||||
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
|
||||
: action.target.host;
|
||||
}
|
||||
|
||||
// Determine port - either function-based, static, or preserve incoming port
|
||||
let targetPort: number;
|
||||
if (typeof action.target.port === 'function') {
|
||||
targetPort = action.target.port(routeContext);
|
||||
} else if (action.target.preservePort) {
|
||||
targetPort = record.localPort;
|
||||
} else {
|
||||
targetPort = action.target.port;
|
||||
}
|
||||
|
||||
// Update the connection record and context with resolved values
|
||||
record.targetHost = targetHost;
|
||||
record.targetPort = targetPort;
|
||||
|
||||
return this.setupDirectConnection(
|
||||
socket,
|
||||
record,
|
||||
@ -552,13 +677,23 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Determine target host and port if not provided
|
||||
const finalTargetHost = targetHost ||
|
||||
record.targetHost ||
|
||||
(this.settings.defaults?.target?.host || 'localhost');
|
||||
|
||||
// Determine target port
|
||||
const finalTargetPort = targetPort ||
|
||||
record.targetPort ||
|
||||
(overridePort !== undefined ? overridePort :
|
||||
(this.settings.defaults?.target?.port || 443));
|
||||
|
||||
// Update record with final target information
|
||||
record.targetHost = finalTargetHost;
|
||||
record.targetPort = finalTargetPort;
|
||||
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(`[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}`);
|
||||
}
|
||||
|
||||
// Setup connection options
|
||||
const connectionOptions: plugins.net.NetConnectOpts = {
|
||||
host: finalTargetHost,
|
||||
|
@ -58,36 +58,88 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
|
||||
/**
|
||||
* Rebuild the port mapping for fast lookups
|
||||
* Also logs information about the ports being listened on
|
||||
*/
|
||||
private rebuildPortMap(): void {
|
||||
this.portMap.clear();
|
||||
|
||||
this.portRangeCache.clear(); // Clear cache when rebuilding
|
||||
|
||||
// Track ports for logging
|
||||
const portToRoutesMap = new Map<number, string[]>();
|
||||
|
||||
for (const route of this.routes) {
|
||||
const ports = this.expandPortRange(route.match.ports);
|
||||
|
||||
|
||||
// Skip if no ports were found
|
||||
if (ports.length === 0) {
|
||||
console.warn(`Route ${route.name || 'unnamed'} has no valid ports to listen on`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const port of ports) {
|
||||
// Add to portMap for routing
|
||||
if (!this.portMap.has(port)) {
|
||||
this.portMap.set(port, []);
|
||||
}
|
||||
this.portMap.get(port)!.push(route);
|
||||
|
||||
// Add to tracking for logging
|
||||
if (!portToRoutesMap.has(port)) {
|
||||
portToRoutesMap.set(port, []);
|
||||
}
|
||||
portToRoutesMap.get(port)!.push(route.name || 'unnamed');
|
||||
}
|
||||
}
|
||||
|
||||
// Log summary of ports and routes
|
||||
const totalPorts = this.portMap.size;
|
||||
const totalRoutes = this.routes.length;
|
||||
console.log(`Route manager configured with ${totalRoutes} routes across ${totalPorts} ports`);
|
||||
|
||||
// Log port details if detailed logging is enabled
|
||||
const enableDetailedLogging = this.options.enableDetailedLogging;
|
||||
if (enableDetailedLogging) {
|
||||
for (const [port, routes] of this.portMap.entries()) {
|
||||
console.log(`Port ${port}: ${routes.length} routes (${portToRoutesMap.get(port)!.join(', ')})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a port range specification into an array of individual ports
|
||||
* Uses caching to improve performance for frequently used port ranges
|
||||
*
|
||||
* @public - Made public to allow external code to interpret port ranges
|
||||
*/
|
||||
private expandPortRange(portRange: TPortRange): number[] {
|
||||
public expandPortRange(portRange: TPortRange): number[] {
|
||||
// For simple number, return immediately
|
||||
if (typeof portRange === 'number') {
|
||||
return [portRange];
|
||||
}
|
||||
|
||||
|
||||
// Create a cache key for this port range
|
||||
const cacheKey = JSON.stringify(portRange);
|
||||
|
||||
// Check if we have a cached result
|
||||
if (this.portRangeCache.has(cacheKey)) {
|
||||
return this.portRangeCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
// Process the port range
|
||||
let result: number[] = [];
|
||||
|
||||
if (Array.isArray(portRange)) {
|
||||
// Handle array of port objects or numbers
|
||||
return portRange.flatMap(item => {
|
||||
result = portRange.flatMap(item => {
|
||||
if (typeof item === 'number') {
|
||||
return [item];
|
||||
} else if (typeof item === 'object' && 'from' in item && 'to' in item) {
|
||||
// Handle port range object - check valid range
|
||||
if (item.from > item.to) {
|
||||
console.warn(`Invalid port range: from (${item.from}) > to (${item.to})`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle port range object
|
||||
const ports: number[] = [];
|
||||
for (let p = item.from; p <= item.to; p++) {
|
||||
@ -98,14 +150,24 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
// Cache the result
|
||||
this.portRangeCache.set(cacheKey, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoization cache for expanded port ranges
|
||||
*/
|
||||
private portRangeCache: Map<string, number[]> = new Map();
|
||||
|
||||
/**
|
||||
* Get all ports that should be listened on
|
||||
* This method automatically infers all required ports from route configurations
|
||||
*/
|
||||
public getListeningPorts(): number[] {
|
||||
// Return the unique set of ports from all routes
|
||||
return Array.from(this.portMap.keys());
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { SecurityManager } from './security-manager.js';
|
||||
import { TlsManager } from './tls-manager.js';
|
||||
import { NetworkProxyBridge } from './network-proxy-bridge.js';
|
||||
import { TimeoutManager } from './timeout-manager.js';
|
||||
// import { PortRangeManager } from './port-range-manager.js';
|
||||
import { PortManager } from './port-manager.js';
|
||||
import { RouteManager } from './route-manager.js';
|
||||
import { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
|
||||
@ -39,7 +39,8 @@ import type { IRouteConfig } from './models/route-types.js';
|
||||
* - Advanced options (timeout, headers, etc.)
|
||||
*/
|
||||
export class SmartProxy extends plugins.EventEmitter {
|
||||
private netServers: plugins.net.Server[] = [];
|
||||
// Port manager handles dynamic listener management
|
||||
private portManager: PortManager;
|
||||
private connectionLogger: NodeJS.Timeout | null = null;
|
||||
private isShuttingDown: boolean = false;
|
||||
|
||||
@ -49,8 +50,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
private tlsManager: TlsManager;
|
||||
private networkProxyBridge: NetworkProxyBridge;
|
||||
private timeoutManager: TimeoutManager;
|
||||
// private portRangeManager: PortRangeManager;
|
||||
private routeManager: RouteManager;
|
||||
public routeManager: RouteManager; // Made public for route management
|
||||
private routeConnectionHandler: RouteConnectionHandler;
|
||||
|
||||
// Port80Handler for ACME certificate management
|
||||
@ -151,8 +151,6 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Create the route manager
|
||||
this.routeManager = new RouteManager(this.settings);
|
||||
|
||||
// Create port range manager
|
||||
// this.portRangeManager = new PortRangeManager(this.settings);
|
||||
|
||||
// Create other required components
|
||||
this.tlsManager = new TlsManager(this.settings);
|
||||
@ -168,6 +166,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.timeoutManager,
|
||||
this.routeManager
|
||||
);
|
||||
|
||||
// Initialize port manager
|
||||
this.portManager = new PortManager(this.settings, this.routeConnectionHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -271,33 +272,8 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Get listening ports from RouteManager
|
||||
const listeningPorts = this.routeManager.getListeningPorts();
|
||||
|
||||
// Create servers for each port
|
||||
for (const port of listeningPorts) {
|
||||
const server = plugins.net.createServer((socket) => {
|
||||
// Check if shutting down
|
||||
if (this.isShuttingDown) {
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to route connection handler
|
||||
this.routeConnectionHandler.handleConnection(socket);
|
||||
}).on('error', (err: Error) => {
|
||||
console.log(`Server Error on port ${port}: ${err.message}`);
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
||||
console.log(
|
||||
`SmartProxy -> OK: Now listening on port ${port}${
|
||||
isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
||||
this.netServers.push(server);
|
||||
}
|
||||
// Start port listeners using the PortManager
|
||||
await this.portManager.addPorts(listeningPorts);
|
||||
|
||||
// Set up periodic connection logging and inactivity checks
|
||||
this.connectionLogger = setInterval(() => {
|
||||
@ -383,6 +359,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
public async stop() {
|
||||
console.log('SmartProxy shutting down...');
|
||||
this.isShuttingDown = true;
|
||||
this.portManager.setShuttingDown(true);
|
||||
|
||||
// Stop CertProvisioner if active
|
||||
if (this.certProvisioner) {
|
||||
@ -401,31 +378,14 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Stop accepting new connections
|
||||
const closeServerPromises: Promise<void>[] = this.netServers.map(
|
||||
(server) =>
|
||||
new Promise<void>((resolve) => {
|
||||
if (!server.listening) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
console.log(`Error closing server: ${err.message}`);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Stop the connection logger
|
||||
if (this.connectionLogger) {
|
||||
clearInterval(this.connectionLogger);
|
||||
this.connectionLogger = null;
|
||||
}
|
||||
|
||||
// Wait for servers to close
|
||||
await Promise.all(closeServerPromises);
|
||||
// Stop all port listeners
|
||||
await this.portManager.closeAll();
|
||||
console.log('All servers closed. Cleaning up active connections...');
|
||||
|
||||
// Clean up all active connections
|
||||
@ -434,8 +394,6 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Stop NetworkProxy
|
||||
await this.networkProxyBridge.stop();
|
||||
|
||||
// Clear all servers
|
||||
this.netServers = [];
|
||||
|
||||
console.log('SmartProxy shutdown complete.');
|
||||
}
|
||||
@ -479,6 +437,12 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// Update routes in RouteManager
|
||||
this.routeManager.updateRoutes(newRoutes);
|
||||
|
||||
// Get the new set of required ports
|
||||
const requiredPorts = this.routeManager.getListeningPorts();
|
||||
|
||||
// Update port listeners to match the new configuration
|
||||
await this.portManager.updatePorts(requiredPorts);
|
||||
|
||||
// If NetworkProxy is initialized, resync the configurations
|
||||
if (this.networkProxyBridge.getNetworkProxy()) {
|
||||
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
|
||||
@ -609,6 +573,41 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new listening port without changing the route configuration
|
||||
*
|
||||
* This allows you to add a port listener without updating routes.
|
||||
* Useful for preparing to listen on a port before adding routes for it.
|
||||
*
|
||||
* @param port The port to start listening on
|
||||
* @returns Promise that resolves when the port is listening
|
||||
*/
|
||||
public async addListeningPort(port: number): Promise<void> {
|
||||
return this.portManager.addPort(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening on a specific port without changing the route configuration
|
||||
*
|
||||
* This allows you to stop a port listener without updating routes.
|
||||
* Useful for temporary maintenance or port changes.
|
||||
*
|
||||
* @param port The port to stop listening on
|
||||
* @returns Promise that resolves when the port is closed
|
||||
*/
|
||||
public async removeListeningPort(port: number): Promise<void> {
|
||||
return this.portManager.removePort(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all ports currently being listened on
|
||||
*
|
||||
* @returns Array of port numbers
|
||||
*/
|
||||
public getListeningPorts(): number[] {
|
||||
return this.portManager.getListeningPorts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about current connections
|
||||
*/
|
||||
@ -638,7 +637,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
terminationStats,
|
||||
acmeEnabled: !!this.port80Handler,
|
||||
port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null,
|
||||
routes: this.routeManager.getListeningPorts().length
|
||||
routes: this.routeManager.getListeningPorts().length,
|
||||
listeningPorts: this.portManager.getListeningPorts(),
|
||||
activePorts: this.portManager.getListeningPorts().length
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,11 @@
|
||||
* - Static file server routes (createStaticFileRoute)
|
||||
* - API routes (createApiRoute)
|
||||
* - WebSocket routes (createWebSocketRoute)
|
||||
* - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute)
|
||||
* - Dynamic routing (createDynamicRoute, createSmartLoadBalancer)
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange } from '../models/route-types.js';
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
|
||||
|
||||
/**
|
||||
* Create an HTTP-only route configuration
|
||||
@ -452,4 +454,168 @@ export function createWebSocketRoute(
|
||||
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<string, string | string[]>;
|
||||
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
|
||||
};
|
||||
}
|
@ -9,14 +9,24 @@ import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange } from '../mod
|
||||
|
||||
/**
|
||||
* Validates a port range or port number
|
||||
* @param port Port number or port range
|
||||
* @param port Port number, port range, or port function
|
||||
* @returns True if valid, false otherwise
|
||||
*/
|
||||
export function isValidPort(port: TPortRange): boolean {
|
||||
export function isValidPort(port: any): boolean {
|
||||
if (typeof port === 'number') {
|
||||
return port > 0 && port < 65536; // Valid port range is 1-65535
|
||||
} else if (Array.isArray(port)) {
|
||||
return port.every(p => typeof p === 'number' && p > 0 && p < 65536);
|
||||
return port.every(p =>
|
||||
(typeof p === 'number' && p > 0 && p < 65536) ||
|
||||
(typeof p === 'object' && 'from' in p && 'to' in p &&
|
||||
p.from > 0 && p.from < 65536 && p.to > 0 && p.to < 65536)
|
||||
);
|
||||
} else if (typeof port === 'function') {
|
||||
// For function-based ports, we can't validate the result at config time
|
||||
// so we just check that it's a function
|
||||
return true;
|
||||
} else if (typeof port === 'object' && 'from' in port && 'to' in port) {
|
||||
return port.from > 0 && port.from < 65536 && port.to > 0 && port.to < 65536;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -100,11 +110,20 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
|
||||
// Validate target host
|
||||
if (!action.target.host) {
|
||||
errors.push('Target host is required');
|
||||
} else if (typeof action.target.host !== 'string' &&
|
||||
!Array.isArray(action.target.host) &&
|
||||
typeof action.target.host !== 'function') {
|
||||
errors.push('Target host must be a string, array of strings, or function');
|
||||
}
|
||||
|
||||
// Validate target port
|
||||
if (!action.target.port || !isValidPort(action.target.port)) {
|
||||
errors.push('Valid target port is required');
|
||||
if (action.target.port === undefined) {
|
||||
errors.push('Target port is required');
|
||||
} else if (typeof action.target.port !== 'number' &&
|
||||
typeof action.target.port !== 'function') {
|
||||
errors.push('Target port must be a number or a function');
|
||||
} else if (typeof action.target.port === 'number' && !isValidPort(action.target.port)) {
|
||||
errors.push('Target port must be between 1 and 65535');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user