change to route based approach
This commit is contained in:
		| @@ -1,5 +1,7 @@ | ||||
| /** | ||||
|  * SmartProxy implementation | ||||
|  * | ||||
|  * Version 14.0.0: Unified Route-Based Configuration API | ||||
|  */ | ||||
| // Re-export models | ||||
| export * from './models/index.js'; | ||||
| @@ -7,12 +9,26 @@ export * from './models/index.js'; | ||||
| // Export the main SmartProxy class | ||||
| export { SmartProxy } from './smart-proxy.js'; | ||||
|  | ||||
| // Export supporting classes | ||||
| // Export core supporting classes | ||||
| export { ConnectionManager } from './connection-manager.js'; | ||||
| export { SecurityManager } from './security-manager.js'; | ||||
| export { DomainConfigManager } from './domain-config-manager.js'; | ||||
| export { TimeoutManager } from './timeout-manager.js'; | ||||
| export { TlsManager } from './tls-manager.js'; | ||||
| export { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| export { PortRangeManager } from './port-range-manager.js'; | ||||
| export { ConnectionHandler } from './connection-handler.js'; | ||||
|  | ||||
| // Export route-based components | ||||
| 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'; | ||||
|   | ||||
| @@ -2,3 +2,7 @@ | ||||
|  * SmartProxy models | ||||
|  */ | ||||
| export * from './interfaces.js'; | ||||
| export * from './route-types.js'; | ||||
|  | ||||
| // Re-export IRoutedSmartProxyOptions explicitly to avoid ambiguity | ||||
| export type { ISmartProxyOptions as IRoutedSmartProxyOptions } from './interfaces.js'; | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import type { IForwardConfig } from '../../../forwarding/config/forwarding-types.js'; | ||||
| import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||
| import type { IRouteConfig } from './route-types.js'; | ||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Provision object for static or HTTP-01 certificate | ||||
| @@ -7,27 +9,102 @@ import type { IForwardConfig } from '../../../forwarding/config/forwarding-types | ||||
| export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | ||||
|  | ||||
| /** | ||||
|  * Domain configuration with forwarding configuration | ||||
|  * Alias for backward compatibility with code that uses IRoutedSmartProxyOptions | ||||
|  */ | ||||
| export type IRoutedSmartProxyOptions = ISmartProxyOptions; | ||||
|  | ||||
| /** | ||||
|  * Legacy domain configuration interface for backward compatibility | ||||
|  */ | ||||
| export interface IDomainConfig { | ||||
|   domains: string[]; // Glob patterns for domain(s) | ||||
|   forwarding: IForwardConfig; // Unified forwarding configuration | ||||
|   domains: string[]; | ||||
|   forwarding: { | ||||
|     type: TForwardingType; | ||||
|     target: { | ||||
|       host: string | string[]; | ||||
|       port: number; | ||||
|     }; | ||||
|     acme?: { | ||||
|       enabled?: boolean; | ||||
|       maintenance?: boolean; | ||||
|       production?: boolean; | ||||
|       forwardChallenges?: { | ||||
|         host: string; | ||||
|         port: number; | ||||
|         useTls?: boolean; | ||||
|       }; | ||||
|     }; | ||||
|     http?: { | ||||
|       enabled?: boolean; | ||||
|       redirectToHttps?: boolean; | ||||
|       headers?: Record<string, string>; | ||||
|     }; | ||||
|     https?: { | ||||
|       customCert?: { | ||||
|         key: string; | ||||
|         cert: string; | ||||
|       }; | ||||
|       forwardSni?: boolean; | ||||
|     }; | ||||
|     security?: { | ||||
|       allowedIps?: string[]; | ||||
|       blockedIps?: string[]; | ||||
|       maxConnections?: number; | ||||
|     }; | ||||
|     advanced?: { | ||||
|       portRanges?: Array<{ from: number; to: number }>; | ||||
|       networkProxyPort?: number; | ||||
|       keepAlive?: boolean; | ||||
|       timeout?: number; | ||||
|       headers?: Record<string, string>; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration options for the SmartProxy | ||||
|  * Helper functions for type checking - now always assume route-based config | ||||
|  */ | ||||
| export function isLegacyOptions(options: any): boolean { | ||||
|   return false; // No longer supporting legacy options | ||||
| } | ||||
|  | ||||
| export function isRoutedOptions(options: any): boolean { | ||||
|   return true; // Always assume routed options | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * SmartProxy configuration options | ||||
|  */ | ||||
| import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||
| export interface ISmartProxyOptions { | ||||
|   fromPort: number; | ||||
|   toPort: number; | ||||
|   targetIP?: string; // Global target host to proxy to, defaults to 'localhost' | ||||
|   domainConfigs: IDomainConfig[]; | ||||
|   // The unified configuration array (required) | ||||
|   routes: IRouteConfig[]; | ||||
|  | ||||
|   // Legacy options for backward compatibility | ||||
|   fromPort?: number; | ||||
|   toPort?: number; | ||||
|   sniEnabled?: boolean; | ||||
|   domainConfigs?: IDomainConfig[]; | ||||
|   targetIP?: string; | ||||
|   defaultAllowedIPs?: string[]; | ||||
|   defaultBlockedIPs?: string[]; | ||||
|   globalPortRanges?: Array<{ from: number; to: number }>; | ||||
|   forwardAllGlobalRanges?: boolean; | ||||
|   preserveSourceIP?: boolean; | ||||
|  | ||||
|   // Global/default settings | ||||
|   defaults?: { | ||||
|     target?: { | ||||
|       host: string; // Default host to use when not specified in routes | ||||
|       port: number; // Default port to use when not specified in routes | ||||
|     }; | ||||
|     security?: { | ||||
|       allowedIPs?: string[]; // Default allowed IPs | ||||
|       blockedIPs?: string[]; // Default blocked IPs | ||||
|       maxConnections?: number; // Default max connections | ||||
|     }; | ||||
|     preserveSourceIP?: boolean; // Default source IP preservation | ||||
|   }; | ||||
|  | ||||
|   // TLS options | ||||
|   pfx?: Buffer; | ||||
|   key?: string | Buffer | Array<Buffer | string>; | ||||
| @@ -50,8 +127,6 @@ export interface ISmartProxyOptions { | ||||
|   inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) | ||||
|  | ||||
|   gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown | ||||
|   globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges | ||||
|   forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP | ||||
|  | ||||
|   // Socket optimization settings | ||||
|   noDelay?: boolean; // Disable Nagle's algorithm (default: true) | ||||
| @@ -108,6 +183,9 @@ export interface IConnectionRecord { | ||||
|   pendingData: Buffer[]; // Buffer to hold data during connection setup | ||||
|   pendingDataSize: number; // Track total size of pending data | ||||
|  | ||||
|   // Legacy property for backward compatibility | ||||
|   domainConfig?: IDomainConfig; | ||||
|  | ||||
|   // Enhanced tracking fields | ||||
|   bytesReceived: number; // Total bytes received | ||||
|   bytesSent: number; // Total bytes sent | ||||
| @@ -116,7 +194,7 @@ export interface IConnectionRecord { | ||||
|   isTLS: boolean; // Whether this connection is a TLS connection | ||||
|   tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete | ||||
|   hasReceivedInitialData: boolean; // Whether initial data has been received | ||||
|   domainConfig?: IDomainConfig; // Associated domain config for this connection | ||||
|   routeConfig?: IRouteConfig; // Associated route config for this connection | ||||
|  | ||||
|   // Keep-alive tracking | ||||
|   hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection | ||||
|   | ||||
							
								
								
									
										184
									
								
								ts/proxies/smart-proxy/models/route-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								ts/proxies/smart-proxy/models/route-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Supported action types for route configurations | ||||
|  */ | ||||
| export type TRouteActionType = 'forward' | 'redirect' | 'block'; | ||||
|  | ||||
| /** | ||||
|  * TLS handling modes for route configurations | ||||
|  */ | ||||
| export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||
|  | ||||
| /** | ||||
|  * Port range specification format | ||||
|  */ | ||||
| export type TPortRange = number | number[] | Array<{ from: number; to: number }>; | ||||
|  | ||||
| /** | ||||
|  * Route match criteria for incoming requests | ||||
|  */ | ||||
| export interface IRouteMatch { | ||||
|   // Listen on these ports (required) | ||||
|   ports: TPortRange; | ||||
|    | ||||
|   // Optional domain patterns to match (default: all domains) | ||||
|   domains?: string | string[]; | ||||
|    | ||||
|   // Advanced matching criteria | ||||
|   path?: string;           // Match specific paths | ||||
|   clientIp?: string[];     // Match specific client IPs | ||||
|   tlsVersion?: string[];   // Match specific TLS versions | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * TLS configuration for route actions | ||||
|  */ | ||||
| export interface IRouteTls { | ||||
|   mode: TTlsMode; | ||||
|   certificate?: 'auto' | {   // Auto = use ACME | ||||
|     key: string; | ||||
|     cert: string; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Redirect configuration for route actions | ||||
|  */ | ||||
| export interface IRouteRedirect { | ||||
|   to: string;            // URL or template with {domain}, {port}, etc. | ||||
|   status: 301 | 302 | 307 | 308; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Security options for route actions | ||||
|  */ | ||||
| export interface IRouteSecurity { | ||||
|   allowedIps?: string[]; | ||||
|   blockedIps?: string[]; | ||||
|   maxConnections?: number; | ||||
|   authentication?: { | ||||
|     type: 'basic' | 'digest' | 'oauth'; | ||||
|     // Auth-specific options would go here | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Advanced options for route actions | ||||
|  */ | ||||
| export interface IRouteAdvanced { | ||||
|   timeout?: number; | ||||
|   headers?: Record<string, string>; | ||||
|   keepAlive?: boolean; | ||||
|   // Additional advanced options would go here | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Action configuration for route handling | ||||
|  */ | ||||
| export interface IRouteAction { | ||||
|   // Basic routing | ||||
|   type: TRouteActionType; | ||||
|    | ||||
|   // Target for forwarding | ||||
|   target?: IRouteTarget; | ||||
|    | ||||
|   // TLS handling | ||||
|   tls?: IRouteTls; | ||||
|    | ||||
|   // For redirects | ||||
|   redirect?: IRouteRedirect; | ||||
|    | ||||
|   // Security options | ||||
|   security?: IRouteSecurity; | ||||
|    | ||||
|   // Advanced options | ||||
|   advanced?: IRouteAdvanced; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * The core unified configuration interface | ||||
|  */ | ||||
| export interface IRouteConfig { | ||||
|   // What to match | ||||
|   match: IRouteMatch; | ||||
|    | ||||
|   // What to do with matched traffic | ||||
|   action: IRouteAction; | ||||
|    | ||||
|   // Optional metadata | ||||
|   name?: string;             // Human-readable name for this route | ||||
|   description?: string;      // Description of the route's purpose | ||||
|   priority?: number;         // Controls matching order (higher = matched first) | ||||
|   tags?: string[];           // Arbitrary tags for categorization | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Unified SmartProxy options with routes-based configuration | ||||
|  */ | ||||
| export interface IRoutedSmartProxyOptions { | ||||
|   // The unified configuration array (required) | ||||
|   routes: IRouteConfig[]; | ||||
|    | ||||
|   // Global/default settings | ||||
|   defaults?: { | ||||
|     target?: { | ||||
|       host: string; | ||||
|       port: number; | ||||
|     }; | ||||
|     security?: IRouteSecurity; | ||||
|     tls?: IRouteTls; | ||||
|     // ...other defaults | ||||
|   }; | ||||
|    | ||||
|   // Other global settings remain (acme, etc.) | ||||
|   acme?: IAcmeOptions; | ||||
|    | ||||
|   // Connection timeouts and other global settings | ||||
|   initialDataTimeout?: number; | ||||
|   socketTimeout?: number; | ||||
|   inactivityCheckInterval?: number; | ||||
|   maxConnectionLifetime?: number; | ||||
|   inactivityTimeout?: number; | ||||
|   gracefulShutdownTimeout?: number; | ||||
|    | ||||
|   // Socket optimization settings | ||||
|   noDelay?: boolean; | ||||
|   keepAlive?: boolean; | ||||
|   keepAliveInitialDelay?: number; | ||||
|   maxPendingDataSize?: number; | ||||
|    | ||||
|   // Enhanced features | ||||
|   disableInactivityCheck?: boolean; | ||||
|   enableKeepAliveProbes?: boolean; | ||||
|   enableDetailedLogging?: boolean; | ||||
|   enableTlsDebugLogging?: boolean; | ||||
|   enableRandomizedTimeouts?: boolean; | ||||
|   allowSessionTicket?: boolean; | ||||
|    | ||||
|   // Rate limiting and security | ||||
|   maxConnectionsPerIP?: number; | ||||
|   connectionRateLimitPerMinute?: number; | ||||
|    | ||||
|   // Enhanced keep-alive settings | ||||
|   keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; | ||||
|   keepAliveInactivityMultiplier?: number; | ||||
|   extendedKeepAliveLifetime?: number; | ||||
|    | ||||
|   /** | ||||
|    * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges, | ||||
|    * or a static certificate object for immediate provisioning. | ||||
|    */ | ||||
|   certProvisionFunction?: (domain: string) => Promise<any>; | ||||
| } | ||||
							
								
								
									
										1117
									
								
								ts/proxies/smart-proxy/route-connection-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1117
									
								
								ts/proxies/smart-proxy/route-connection-handler.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										344
									
								
								ts/proxies/smart-proxy/route-helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								ts/proxies/smart-proxy/route-helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,344 @@ | ||||
| import type {  | ||||
|   IRouteConfig,  | ||||
|   IRouteMatch,  | ||||
|   IRouteAction,  | ||||
|   IRouteTarget, | ||||
|   IRouteTls, | ||||
|   IRouteRedirect, | ||||
|   IRouteSecurity, | ||||
|   IRouteAdvanced | ||||
| } from './models/route-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Basic helper function to create a route configuration | ||||
|  */ | ||||
| export function createRoute( | ||||
|   match: IRouteMatch, | ||||
|   action: IRouteAction, | ||||
|   metadata?: { | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return { | ||||
|     match, | ||||
|     action, | ||||
|     ...metadata | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a basic HTTP route configuration  | ||||
|  */ | ||||
| export function createHttpRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 80 | ||||
|     domains?: string | string[]; | ||||
|     path?: string; | ||||
|     target: IRouteTarget; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 80, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: options.target, | ||||
|       ...(options.headers || options.security ? { | ||||
|         advanced: { | ||||
|           ...(options.headers ? { headers: options.headers } : {}) | ||||
|         }, | ||||
|         ...(options.security ? { security: options.security } : {}) | ||||
|       } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'HTTP Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTPS route configuration with TLS termination | ||||
|  */ | ||||
| export function createHttpsRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 443 | ||||
|     domains: string | string[]; | ||||
|     path?: string; | ||||
|     target: IRouteTarget; | ||||
|     tlsMode?: 'terminate' | 'terminate-and-reencrypt'; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 443, | ||||
|       domains: options.domains, | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: options.target, | ||||
|       tls: { | ||||
|         mode: options.tlsMode || 'terminate', | ||||
|         certificate: options.certificate || 'auto' | ||||
|       }, | ||||
|       ...(options.headers || options.security ? { | ||||
|         advanced: { | ||||
|           ...(options.headers ? { headers: options.headers } : {}) | ||||
|         }, | ||||
|         ...(options.security ? { security: options.security } : {}) | ||||
|       } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'HTTPS Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTPS passthrough route configuration | ||||
|  */ | ||||
| export function createPassthroughRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 443 | ||||
|     domains?: string | string[]; | ||||
|     target: IRouteTarget; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 443, | ||||
|       ...(options.domains ? { domains: options.domains } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: options.target, | ||||
|       tls: { | ||||
|         mode: 'passthrough' | ||||
|       }, | ||||
|       ...(options.security ? { security: options.security } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'HTTPS Passthrough Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a redirect route configuration | ||||
|  */ | ||||
| export function createRedirectRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 80 | ||||
|     domains?: string | string[]; | ||||
|     path?: string; | ||||
|     redirectTo: string; | ||||
|     statusCode?: 301 | 302 | 307 | 308; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 80, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'redirect', | ||||
|       redirect: { | ||||
|         to: options.redirectTo, | ||||
|         status: options.statusCode || 301 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Redirect Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTP to HTTPS redirect route configuration | ||||
|  */ | ||||
| export function createHttpToHttpsRedirect( | ||||
|   options: { | ||||
|     domains: string | string[]; | ||||
|     statusCode?: 301 | 302 | 307 | 308; | ||||
|     name?: string; | ||||
|     priority?: number; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains]; | ||||
|    | ||||
|   return createRedirectRoute({ | ||||
|     ports: 80, | ||||
|     domains: options.domains, | ||||
|     redirectTo: 'https://{domain}{path}', | ||||
|     statusCode: options.statusCode || 301, | ||||
|     name: options.name || `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`, | ||||
|     priority: options.priority || 100 // High priority for redirects | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a block route configuration | ||||
|  */ | ||||
| export function createBlockRoute( | ||||
|   options: { | ||||
|     ports: number | number[]; | ||||
|     domains?: string | string[]; | ||||
|     clientIp?: string[]; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.clientIp ? { clientIp: options.clientIp } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'block' | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Block Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority || 1000, // Very high priority for blocks | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a load balancer route configuration | ||||
|  */ | ||||
| export function createLoadBalancerRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 443 | ||||
|     domains: string | string[]; | ||||
|     path?: string; | ||||
|     targets: string[]; // Array of host names/IPs for load balancing | ||||
|     targetPort: number; | ||||
|     tlsMode?: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   const useTls = options.tlsMode !== undefined; | ||||
|   const defaultPort = useTls ? 443 : 80; | ||||
|    | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || defaultPort, | ||||
|       domains: options.domains, | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: options.targets, | ||||
|         port: options.targetPort | ||||
|       }, | ||||
|       ...(useTls ? { | ||||
|         tls: { | ||||
|           mode: options.tlsMode!, | ||||
|           ...(options.tlsMode !== 'passthrough' && options.certificate ? { | ||||
|             certificate: options.certificate | ||||
|           } : {}) | ||||
|         } | ||||
|       } : {}), | ||||
|       ...(options.headers || options.security ? { | ||||
|         advanced: { | ||||
|           ...(options.headers ? { headers: options.headers } : {}) | ||||
|         }, | ||||
|         ...(options.security ? { security: options.security } : {}) | ||||
|       } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Load Balanced Route', | ||||
|       description: options.description || `Load balancing across ${options.targets.length} backends`, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a complete HTTPS server configuration with HTTP redirect | ||||
|  */ | ||||
| export function createHttpsServer( | ||||
|   options: { | ||||
|     domains: string | string[]; | ||||
|     target: IRouteTarget; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     security?: IRouteSecurity; | ||||
|     addHttpRedirect?: boolean; | ||||
|     name?: string; | ||||
|   } | ||||
| ): IRouteConfig[] { | ||||
|   const routes: IRouteConfig[] = []; | ||||
|   const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains]; | ||||
|    | ||||
|   // Add HTTPS route | ||||
|   routes.push(createHttpsRoute({ | ||||
|     domains: options.domains, | ||||
|     target: options.target, | ||||
|     certificate: options.certificate || 'auto', | ||||
|     security: options.security, | ||||
|     name: options.name || `HTTPS Server for ${domainArray.join(', ')}` | ||||
|   })); | ||||
|    | ||||
|   // Add HTTP to HTTPS redirect if requested | ||||
|   if (options.addHttpRedirect !== false) { | ||||
|     routes.push(createHttpToHttpsRedirect({ | ||||
|       domains: options.domains, | ||||
|       name: `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`, | ||||
|       priority: 100 | ||||
|     })); | ||||
|   } | ||||
|    | ||||
|   return routes; | ||||
| } | ||||
							
								
								
									
										587
									
								
								ts/proxies/smart-proxy/route-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										587
									
								
								ts/proxies/smart-proxy/route-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,587 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   IRouteConfig, | ||||
|   IRouteMatch, | ||||
|   IRouteAction, | ||||
|   TPortRange | ||||
| } from './models/route-types.js'; | ||||
| import type { | ||||
|   ISmartProxyOptions, | ||||
|   IRoutedSmartProxyOptions, | ||||
|   IDomainConfig | ||||
| } from './models/interfaces.js'; | ||||
| import { | ||||
|   isRoutedOptions, | ||||
|   isLegacyOptions | ||||
| } from './models/interfaces.js'; | ||||
|  | ||||
| /** | ||||
|  * Result of route matching | ||||
|  */ | ||||
| export interface IRouteMatchResult { | ||||
|   route: IRouteConfig; | ||||
|   // Additional match parameters (path, query, etc.) | ||||
|   params?: Record<string, string>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * The RouteManager handles all routing decisions based on connections and attributes | ||||
|  */ | ||||
| export class RouteManager extends plugins.EventEmitter { | ||||
|   private routes: IRouteConfig[] = []; | ||||
|   private portMap: Map<number, IRouteConfig[]> = new Map(); | ||||
|   private options: IRoutedSmartProxyOptions; | ||||
|    | ||||
|   constructor(options: ISmartProxyOptions) { | ||||
|     super(); | ||||
|      | ||||
|     // We no longer support legacy options, always use provided options | ||||
|     this.options = options; | ||||
|      | ||||
|     // Initialize routes from either source | ||||
|     this.updateRoutes(this.options.routes); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update routes with new 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; | ||||
|     }); | ||||
|      | ||||
|     // Rebuild port mapping for fast lookups | ||||
|     this.rebuildPortMap(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Rebuild the port mapping for fast lookups | ||||
|    */ | ||||
|   private rebuildPortMap(): void { | ||||
|     this.portMap.clear(); | ||||
|      | ||||
|     for (const route of this.routes) { | ||||
|       const ports = this.expandPortRange(route.match.ports); | ||||
|        | ||||
|       for (const port of ports) { | ||||
|         if (!this.portMap.has(port)) { | ||||
|           this.portMap.set(port, []); | ||||
|         } | ||||
|         this.portMap.get(port)!.push(route); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Expand a port range specification into an array of individual ports | ||||
|    */ | ||||
|   private expandPortRange(portRange: TPortRange): number[] { | ||||
|     if (typeof portRange === 'number') { | ||||
|       return [portRange]; | ||||
|     } | ||||
|      | ||||
|     if (Array.isArray(portRange)) { | ||||
|       // Handle array of port objects or numbers | ||||
|       return portRange.flatMap(item => { | ||||
|         if (typeof item === 'number') { | ||||
|           return [item]; | ||||
|         } else if (typeof item === 'object' && 'from' in item && 'to' in item) { | ||||
|           // Handle port range object | ||||
|           const ports: number[] = []; | ||||
|           for (let p = item.from; p <= item.to; p++) { | ||||
|             ports.push(p); | ||||
|           } | ||||
|           return ports; | ||||
|         } | ||||
|         return []; | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     return []; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all ports that should be listened on | ||||
|    */ | ||||
|   public getListeningPorts(): number[] { | ||||
|     return Array.from(this.portMap.keys()); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all routes for a given port | ||||
|    */ | ||||
|   public getRoutesForPort(port: number): IRouteConfig[] { | ||||
|     return this.portMap.get(port) || []; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Test if a pattern matches a domain using glob matching | ||||
|    */ | ||||
|   private matchDomain(pattern: string, domain: string): boolean { | ||||
|     // Convert glob pattern to regex | ||||
|     const regexPattern = pattern | ||||
|       .replace(/\./g, '\\.')    // Escape dots | ||||
|       .replace(/\*/g, '.*');    // Convert * to .* | ||||
|      | ||||
|     const regex = new RegExp(`^${regexPattern}$`, 'i'); | ||||
|     return regex.test(domain); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Match a domain against all patterns in a route | ||||
|    */ | ||||
|   private matchRouteDomain(route: IRouteConfig, domain: string): boolean { | ||||
|     if (!route.match.domains) { | ||||
|       // If no domains specified, match all domains | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     const patterns = Array.isArray(route.match.domains)  | ||||
|       ? route.match.domains  | ||||
|       : [route.match.domains]; | ||||
|      | ||||
|     return patterns.some(pattern => this.matchDomain(pattern, domain)); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a client IP is allowed by a route's security settings | ||||
|    */ | ||||
|   private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean { | ||||
|     const security = route.action.security; | ||||
|      | ||||
|     if (!security) { | ||||
|       return true; // No security settings means allowed | ||||
|     } | ||||
|      | ||||
|     // Check blocked IPs first | ||||
|     if (security.blockedIps && security.blockedIps.length > 0) { | ||||
|       for (const pattern of security.blockedIps) { | ||||
|         if (this.matchIpPattern(pattern, clientIp)) { | ||||
|           return false; // IP is blocked | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // If there are allowed IPs, check them | ||||
|     if (security.allowedIps && security.allowedIps.length > 0) { | ||||
|       for (const pattern of security.allowedIps) { | ||||
|         if (this.matchIpPattern(pattern, clientIp)) { | ||||
|           return true; // IP is allowed | ||||
|         } | ||||
|       } | ||||
|       return false; // IP not in allowed list | ||||
|     } | ||||
|      | ||||
|     // No allowed IPs specified, so IP is allowed | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Match an IP against a pattern | ||||
|    */ | ||||
|   private matchIpPattern(pattern: string, ip: string): boolean { | ||||
|     // Handle exact match | ||||
|     if (pattern === ip) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Handle CIDR notation (e.g., 192.168.1.0/24) | ||||
|     if (pattern.includes('/')) { | ||||
|       return this.matchIpCidr(pattern, ip); | ||||
|     } | ||||
|      | ||||
|     // Handle glob pattern (e.g., 192.168.1.*) | ||||
|     if (pattern.includes('*')) { | ||||
|       const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||
|       const regex = new RegExp(`^${regexPattern}$`); | ||||
|       return regex.test(ip); | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Match an IP against a CIDR pattern | ||||
|    */ | ||||
|   private matchIpCidr(cidr: string, ip: string): boolean { | ||||
|     try { | ||||
|       // In a real implementation, you'd use a proper IP library | ||||
|       // This is a simplified implementation | ||||
|       const [subnet, bits] = cidr.split('/'); | ||||
|       const mask = parseInt(bits, 10); | ||||
|        | ||||
|       // Convert IP addresses to numeric values | ||||
|       const ipNum = this.ipToNumber(ip); | ||||
|       const subnetNum = this.ipToNumber(subnet); | ||||
|        | ||||
|       // Calculate subnet mask | ||||
|       const maskNum = ~(2 ** (32 - mask) - 1); | ||||
|        | ||||
|       // Check if IP is in subnet | ||||
|       return (ipNum & maskNum) === (subnetNum & maskNum); | ||||
|     } catch (e) { | ||||
|       console.error(`Error matching IP ${ip} against CIDR ${cidr}:`, e); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Convert an IP address to a numeric value | ||||
|    */ | ||||
|   private ipToNumber(ip: string): number { | ||||
|     const parts = ip.split('.').map(part => parseInt(part, 10)); | ||||
|     return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Find the matching route for a connection | ||||
|    */ | ||||
|   public findMatchingRoute(options: { | ||||
|     port: number; | ||||
|     domain?: string; | ||||
|     clientIp: string; | ||||
|     path?: string; | ||||
|     tlsVersion?: string; | ||||
|   }): IRouteMatchResult | null { | ||||
|     const { port, domain, clientIp, path, tlsVersion } = options; | ||||
|      | ||||
|     // Get all routes for this port | ||||
|     const routesForPort = this.getRoutesForPort(port); | ||||
|      | ||||
|     // Find the first matching route based on priority order | ||||
|     for (const route of routesForPort) { | ||||
|       // Check domain match if specified | ||||
|       if (domain && !this.matchRouteDomain(route, domain)) { | ||||
|         continue; | ||||
|       } | ||||
|        | ||||
|       // Check path match if specified in both route and request | ||||
|       if (path && route.match.path) { | ||||
|         if (!this.matchPath(route.match.path, path)) { | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Check client IP match | ||||
|       if (route.match.clientIp && !route.match.clientIp.some(pattern =>  | ||||
|         this.matchIpPattern(pattern, clientIp))) { | ||||
|         continue; | ||||
|       } | ||||
|        | ||||
|       // Check TLS version match | ||||
|       if (tlsVersion && route.match.tlsVersion &&  | ||||
|           !route.match.tlsVersion.includes(tlsVersion)) { | ||||
|         continue; | ||||
|       } | ||||
|        | ||||
|       // Check security settings | ||||
|       if (!this.isClientIpAllowed(route, clientIp)) { | ||||
|         continue; | ||||
|       } | ||||
|        | ||||
|       // All checks passed, this route matches | ||||
|       return { route }; | ||||
|     } | ||||
|      | ||||
|     return null; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Match a path against a pattern | ||||
|    */ | ||||
|   private matchPath(pattern: string, path: string): boolean { | ||||
|     // Convert the glob pattern to a regex | ||||
|     const regexPattern = pattern | ||||
|       .replace(/\./g, '\\.')    // Escape dots | ||||
|       .replace(/\*/g, '.*')     // Convert * to .* | ||||
|       .replace(/\//g, '\\/');   // Escape slashes | ||||
|      | ||||
|     const regex = new RegExp(`^${regexPattern}$`); | ||||
|     return regex.test(path); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Convert a domain config to routes | ||||
|    * (For backward compatibility with code that still uses domainConfigs) | ||||
|    */ | ||||
|   public domainConfigToRoutes(domainConfig: IDomainConfig): IRouteConfig[] { | ||||
|     const routes: IRouteConfig[] = []; | ||||
|     const { domains, forwarding } = domainConfig; | ||||
|      | ||||
|     // Determine the action based on forwarding type | ||||
|     let action: IRouteAction = { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: forwarding.target.host, | ||||
|         port: forwarding.target.port | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     // Set TLS mode based on forwarding type | ||||
|     switch (forwarding.type) { | ||||
|       case 'http-only': | ||||
|         // No TLS settings needed | ||||
|         break; | ||||
|       case 'https-passthrough': | ||||
|         action.tls = { mode: 'passthrough' }; | ||||
|         break; | ||||
|       case 'https-terminate-to-http': | ||||
|         action.tls = {  | ||||
|           mode: 'terminate', | ||||
|           certificate: forwarding.https?.customCert ? { | ||||
|             key: forwarding.https.customCert.key, | ||||
|             cert: forwarding.https.customCert.cert | ||||
|           } : 'auto' | ||||
|         }; | ||||
|         break; | ||||
|       case 'https-terminate-to-https': | ||||
|         action.tls = {  | ||||
|           mode: 'terminate-and-reencrypt', | ||||
|           certificate: forwarding.https?.customCert ? { | ||||
|             key: forwarding.https.customCert.key, | ||||
|             cert: forwarding.https.customCert.cert | ||||
|           } : 'auto' | ||||
|         }; | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|     // Add security settings if present | ||||
|     if (forwarding.security) { | ||||
|       action.security = { | ||||
|         allowedIps: forwarding.security.allowedIps, | ||||
|         blockedIps: forwarding.security.blockedIps, | ||||
|         maxConnections: forwarding.security.maxConnections | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Add advanced settings if present | ||||
|     if (forwarding.advanced) { | ||||
|       action.advanced = { | ||||
|         timeout: forwarding.advanced.timeout, | ||||
|         headers: forwarding.advanced.headers, | ||||
|         keepAlive: forwarding.advanced.keepAlive | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Determine which port to use based on forwarding type | ||||
|     const defaultPort = forwarding.type.startsWith('https') ? 443 : 80; | ||||
|      | ||||
|     // Add the main route | ||||
|     routes.push({ | ||||
|       match: { | ||||
|         ports: defaultPort, | ||||
|         domains | ||||
|       }, | ||||
|       action, | ||||
|       name: `Route for ${domains.join(', ')}` | ||||
|     }); | ||||
|      | ||||
|     // Add HTTP redirect if needed | ||||
|     if (forwarding.http?.redirectToHttps) { | ||||
|       routes.push({ | ||||
|         match: { | ||||
|           ports: 80, | ||||
|           domains | ||||
|         }, | ||||
|         action: { | ||||
|           type: 'redirect', | ||||
|           redirect: { | ||||
|             to: 'https://{domain}{path}', | ||||
|             status: 301 | ||||
|           } | ||||
|         }, | ||||
|         name: `HTTP Redirect for ${domains.join(', ')}`, | ||||
|         priority: 100 // Higher priority for redirects | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     // Add port ranges if specified | ||||
|     if (forwarding.advanced?.portRanges) { | ||||
|       for (const range of forwarding.advanced.portRanges) { | ||||
|         routes.push({ | ||||
|           match: { | ||||
|             ports: [{ from: range.from, to: range.to }], | ||||
|             domains | ||||
|           }, | ||||
|           action, | ||||
|           name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')}` | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return routes; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update routes based on domain configs | ||||
|    * (For backward compatibility with code that still uses domainConfigs) | ||||
|    */ | ||||
|   public updateFromDomainConfigs(domainConfigs: IDomainConfig[]): void { | ||||
|     const routes: IRouteConfig[] = []; | ||||
|      | ||||
|     // Convert each domain config to routes | ||||
|     for (const config of domainConfigs) { | ||||
|       routes.push(...this.domainConfigToRoutes(config)); | ||||
|     } | ||||
|      | ||||
|     // Merge with existing routes that aren't derived from domain configs | ||||
|     const nonDomainRoutes = this.routes.filter(r =>  | ||||
|       !r.name || !r.name.includes('for ')); | ||||
|      | ||||
|     this.updateRoutes([...nonDomainRoutes, ...routes]); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate the route configuration and return any warnings | ||||
|    */ | ||||
|   public validateConfiguration(): string[] { | ||||
|     const warnings: string[] = []; | ||||
|     const duplicatePorts = new Map<number, number>(); | ||||
|      | ||||
|     // Check for routes with the same exact match criteria | ||||
|     for (let i = 0; i < this.routes.length; i++) { | ||||
|       for (let j = i + 1; j < this.routes.length; j++) { | ||||
|         const route1 = this.routes[i]; | ||||
|         const route2 = this.routes[j]; | ||||
|          | ||||
|         // Check if route match criteria are the same | ||||
|         if (this.areMatchesSimilar(route1.match, route2.match)) { | ||||
|           warnings.push( | ||||
|             `Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` + | ||||
|             `The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.` | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check for routes that may never be matched due to priority | ||||
|     for (let i = 0; i < this.routes.length; i++) { | ||||
|       const route = this.routes[i]; | ||||
|       const higherPriorityRoutes = this.routes.filter(r =>  | ||||
|         (r.priority || 0) > (route.priority || 0)); | ||||
|        | ||||
|       for (const higherRoute of higherPriorityRoutes) { | ||||
|         if (this.isRouteShadowed(route, higherRoute)) { | ||||
|           warnings.push( | ||||
|             `Route "${route.name || i}" may never be matched because it is shadowed by ` + | ||||
|             `higher priority route "${higherRoute.name || 'unnamed'}"` | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return warnings; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if two route matches are similar (potential conflict) | ||||
|    */ | ||||
|   private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean { | ||||
|     // Check port overlap | ||||
|     const ports1 = new Set(this.expandPortRange(match1.ports)); | ||||
|     const ports2 = new Set(this.expandPortRange(match2.ports)); | ||||
|      | ||||
|     let havePortOverlap = false; | ||||
|     for (const port of ports1) { | ||||
|       if (ports2.has(port)) { | ||||
|         havePortOverlap = true; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     if (!havePortOverlap) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Check domain overlap | ||||
|     if (match1.domains && match2.domains) { | ||||
|       const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains]; | ||||
|       const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains]; | ||||
|        | ||||
|       // Check if any domain pattern from match1 could match any from match2 | ||||
|       let haveDomainOverlap = false; | ||||
|       for (const domain1 of domains1) { | ||||
|         for (const domain2 of domains2) { | ||||
|           if (domain1 === domain2 ||  | ||||
|               (domain1.includes('*') || domain2.includes('*'))) { | ||||
|             haveDomainOverlap = true; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|         if (haveDomainOverlap) break; | ||||
|       } | ||||
|        | ||||
|       if (!haveDomainOverlap) { | ||||
|         return false; | ||||
|       } | ||||
|     } else if (match1.domains || match2.domains) { | ||||
|       // One has domains, the other doesn't - they could overlap | ||||
|       // The one with domains is more specific, so it's not exactly a conflict | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Check path overlap | ||||
|     if (match1.path && match2.path) { | ||||
|       // This is a simplified check - in a real implementation, | ||||
|       // you'd need to check if the path patterns could match the same paths | ||||
|       return match1.path === match2.path ||  | ||||
|              match1.path.includes('*') ||  | ||||
|              match2.path.includes('*'); | ||||
|     } else if (match1.path || match2.path) { | ||||
|       // One has a path, the other doesn't | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // If we get here, the matches have significant overlap | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a route is completely shadowed by a higher priority route | ||||
|    */ | ||||
|   private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean { | ||||
|     // If they don't have similar match criteria, no shadowing occurs | ||||
|     if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // If higher priority route has more specific criteria, no shadowing | ||||
|     if (this.isRouteMoreSpecific(higherPriorityRoute.match, route.match)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // If higher priority route is equally or less specific but has higher priority, | ||||
|     // it shadows the lower priority route | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if route1 is more specific than route2 | ||||
|    */ | ||||
|   private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean { | ||||
|     // Check if match1 has more specific criteria | ||||
|     let match1Points = 0; | ||||
|     let match2Points = 0; | ||||
|      | ||||
|     // Path is the most specific | ||||
|     if (match1.path) match1Points += 3; | ||||
|     if (match2.path) match2Points += 3; | ||||
|      | ||||
|     // Domain is next most specific | ||||
|     if (match1.domains) match1Points += 2; | ||||
|     if (match2.domains) match2Points += 2; | ||||
|      | ||||
|     // Client IP and TLS version are least specific | ||||
|     if (match1.clientIp) match1Points += 1; | ||||
|     if (match2.clientIp) match2Points += 1; | ||||
|      | ||||
|     if (match1.tlsVersion) match1Points += 1; | ||||
|     if (match2.tlsVersion) match2Points += 1; | ||||
|      | ||||
|     return match1Points > match2Points; | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
|  | ||||
| // Importing from the new structure | ||||
| // Importing required components | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { DomainConfigManager } from './domain-config-manager.js'; | ||||
| @@ -8,23 +8,27 @@ 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 { ConnectionHandler } from './connection-handler.js'; | ||||
| import { RouteManager } from './route-manager.js'; | ||||
| import { RouteConnectionHandler } from './route-connection-handler.js'; | ||||
|  | ||||
| // External dependencies from migrated modules | ||||
| // External dependencies | ||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||
| import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; | ||||
| import type { ICertificateData } from '../../certificate/models/certificate-types.js'; | ||||
| import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; | ||||
| import type { TForwardingType } from '../../forwarding/config/forwarding-types.js'; | ||||
| import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; | ||||
|  | ||||
| // Import types from models | ||||
| import type { ISmartProxyOptions, IDomainConfig } from './models/interfaces.js'; | ||||
| // Provide backward compatibility types | ||||
| export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig }; | ||||
| // Import types and utilities | ||||
| import type {  | ||||
|   ISmartProxyOptions,  | ||||
|   IRoutedSmartProxyOptions, | ||||
|   IDomainConfig | ||||
| } from './models/interfaces.js'; | ||||
| import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js'; | ||||
| import type { IRouteConfig } from './models/route-types.js'; | ||||
|  | ||||
| /** | ||||
|  * SmartProxy - Main class that coordinates all components | ||||
|  * SmartProxy - Unified route-based API | ||||
|  */ | ||||
| export class SmartProxy extends plugins.EventEmitter { | ||||
|   private netServers: plugins.net.Server[] = []; | ||||
| @@ -34,24 +38,28 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   // Component managers | ||||
|   private connectionManager: ConnectionManager; | ||||
|   private securityManager: SecurityManager; | ||||
|   public domainConfigManager: DomainConfigManager; | ||||
|   private domainConfigManager: DomainConfigManager; | ||||
|   private tlsManager: TlsManager; | ||||
|   private networkProxyBridge: NetworkProxyBridge; | ||||
|   private timeoutManager: TimeoutManager; | ||||
|   private portRangeManager: PortRangeManager; | ||||
|   private connectionHandler: ConnectionHandler; | ||||
|   private routeManager: RouteManager; | ||||
|   private routeConnectionHandler: RouteConnectionHandler; | ||||
|    | ||||
|   // Port80Handler for ACME certificate management | ||||
|   private port80Handler: Port80Handler | null = null; | ||||
|   // CertProvisioner for unified certificate workflows | ||||
|   private certProvisioner?: CertProvisioner; | ||||
|    | ||||
|   /** | ||||
|    * Constructor that supports both legacy and route-based configuration | ||||
|    */ | ||||
|   constructor(settingsArg: ISmartProxyOptions) { | ||||
|     super(); | ||||
|      | ||||
|     // Set reasonable defaults for all settings | ||||
|     this.settings = { | ||||
|       ...settingsArg, | ||||
|       targetIP: settingsArg.targetIP || 'localhost', | ||||
|       initialDataTimeout: settingsArg.initialDataTimeout || 120000, | ||||
|       socketTimeout: settingsArg.socketTimeout || 3600000, | ||||
|       inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, | ||||
| @@ -76,12 +84,11 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, | ||||
|       extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, | ||||
|       networkProxyPort: settingsArg.networkProxyPort || 8443, | ||||
|       acme: settingsArg.acme || {}, | ||||
|       globalPortRanges: settingsArg.globalPortRanges || [], | ||||
|     }; | ||||
|      | ||||
|     // Set default ACME options if not provided | ||||
|     if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) { | ||||
|     this.settings.acme = this.settings.acme || {}; | ||||
|     if (Object.keys(this.settings.acme).length === 0) { | ||||
|       this.settings.acme = { | ||||
|         enabled: false, | ||||
|         port: 80, | ||||
| @@ -91,7 +98,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|         autoRenew: true, | ||||
|         certificateStore: './certs', | ||||
|         skipConfiguredCerts: false, | ||||
|         httpsRedirectPort: this.settings.fromPort, | ||||
|         httpsRedirectPort: this.settings.fromPort || 443, | ||||
|         renewCheckIntervalHours: 24, | ||||
|         domainForwards: [] | ||||
|       }; | ||||
| @@ -105,13 +112,20 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       this.securityManager,  | ||||
|       this.timeoutManager | ||||
|     ); | ||||
|      | ||||
|     // Create domain config manager and port range manager (for backward compatibility) | ||||
|     this.domainConfigManager = new DomainConfigManager(this.settings); | ||||
|     this.tlsManager = new TlsManager(this.settings); | ||||
|     this.networkProxyBridge = new NetworkProxyBridge(this.settings); | ||||
|     this.portRangeManager = new PortRangeManager(this.settings); | ||||
|      | ||||
|     // Initialize connection handler | ||||
|     this.connectionHandler = new ConnectionHandler( | ||||
|     // Create the new route manager | ||||
|     this.routeManager = new RouteManager(this.settings); | ||||
|      | ||||
|     // Create other required components | ||||
|     this.tlsManager = new TlsManager(this.settings); | ||||
|     this.networkProxyBridge = new NetworkProxyBridge(this.settings); | ||||
|      | ||||
|     // Initialize connection handler with route support | ||||
|     this.routeConnectionHandler = new RouteConnectionHandler( | ||||
|       this.settings, | ||||
|       this.connectionManager, | ||||
|       this.securityManager, | ||||
| @@ -119,12 +133,12 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       this.tlsManager, | ||||
|       this.networkProxyBridge, | ||||
|       this.timeoutManager, | ||||
|       this.portRangeManager | ||||
|       this.routeManager | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * The settings for the port proxy | ||||
|    * The settings for the SmartProxy | ||||
|    */ | ||||
|   public settings: ISmartProxyOptions; | ||||
|    | ||||
| @@ -142,8 +156,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       // Build and start the Port80Handler | ||||
|       this.port80Handler = buildPort80Handler({ | ||||
|         ...config, | ||||
|         httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort | ||||
|         httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443) | ||||
|       }); | ||||
|        | ||||
|       // Share Port80Handler with NetworkProxyBridge before start | ||||
|       this.networkProxyBridge.setPort80Handler(this.port80Handler); | ||||
|       await this.port80Handler.start(); | ||||
| @@ -154,7 +169,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start the proxy server | ||||
|    * Start the proxy server with support for both configuration types | ||||
|    */ | ||||
|   public async start() { | ||||
|     // Don't start if already shutting down | ||||
| @@ -163,11 +178,11 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Process domain configs | ||||
|     // Note: ensureForwardingConfig is no longer needed since forwarding is now required | ||||
|  | ||||
|     // Initialize domain config manager with the processed configs | ||||
|     this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); | ||||
|     // If using legacy format, make sure domainConfigs are initialized | ||||
|     if (isLegacyOptions(this.settings)) { | ||||
|       // Initialize domain config manager with the processed configs | ||||
|       this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); | ||||
|     } | ||||
|  | ||||
|     // Initialize Port80Handler if enabled | ||||
|     await this.initializePort80Handler(); | ||||
| @@ -176,20 +191,39 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     if (this.port80Handler) { | ||||
|       const acme = this.settings.acme!; | ||||
|  | ||||
|       // Convert domain forwards to use the new forwarding system if possible | ||||
|       // Setup domain forwards based on configuration type | ||||
|       const domainForwards = acme.domainForwards?.map(f => { | ||||
|         // If the domain has a forwarding config in domainConfigs, use that | ||||
|         const domainConfig = this.settings.domainConfigs.find( | ||||
|           dc => dc.domains.some(d => d === f.domain) | ||||
|         ); | ||||
|         if (isLegacyOptions(this.settings)) { | ||||
|           // If using legacy mode, check if domain config exists | ||||
|           const domainConfig = this.settings.domainConfigs.find( | ||||
|             dc => dc.domains.some(d => d === f.domain) | ||||
|           ); | ||||
|  | ||||
|         if (domainConfig?.forwarding) { | ||||
|           return { | ||||
|           if (domainConfig?.forwarding) { | ||||
|             return { | ||||
|               domain: f.domain, | ||||
|               forwardConfig: f.forwardConfig, | ||||
|               acmeForwardConfig: f.acmeForwardConfig, | ||||
|               sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false | ||||
|             }; | ||||
|           } | ||||
|         } else { | ||||
|           // In route mode, look for matching route | ||||
|           const route = this.routeManager.findMatchingRoute({ | ||||
|             port: 443, | ||||
|             domain: f.domain, | ||||
|             forwardConfig: f.forwardConfig, | ||||
|             acmeForwardConfig: f.acmeForwardConfig, | ||||
|             sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false | ||||
|           }; | ||||
|             clientIp: '127.0.0.1' // Dummy IP for finding routes | ||||
|           })?.route; | ||||
|  | ||||
|           if (route && route.action.type === 'forward' && route.action.tls) { | ||||
|             // If we found a matching route with TLS settings | ||||
|             return { | ||||
|               domain: f.domain, | ||||
|               forwardConfig: f.forwardConfig, | ||||
|               acmeForwardConfig: f.acmeForwardConfig, | ||||
|               sslRedirect: f.sslRedirect || false | ||||
|             }; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // Otherwise use the existing configuration | ||||
| @@ -201,17 +235,38 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|         }; | ||||
|       }) || []; | ||||
|  | ||||
|       this.certProvisioner = new CertProvisioner( | ||||
|         this.settings.domainConfigs, | ||||
|         this.port80Handler, | ||||
|         this.networkProxyBridge, | ||||
|         this.settings.certProvisionFunction, | ||||
|         acme.renewThresholdDays!, | ||||
|         acme.renewCheckIntervalHours!, | ||||
|         acme.autoRenew!, | ||||
|         domainForwards | ||||
|       ); | ||||
|       // Create CertProvisioner with appropriate parameters | ||||
|       if (isLegacyOptions(this.settings)) { | ||||
|         this.certProvisioner = new CertProvisioner( | ||||
|           this.settings.domainConfigs, | ||||
|           this.port80Handler, | ||||
|           this.networkProxyBridge, | ||||
|           this.settings.certProvisionFunction, | ||||
|           acme.renewThresholdDays!, | ||||
|           acme.renewCheckIntervalHours!, | ||||
|           acme.autoRenew!, | ||||
|           domainForwards | ||||
|         ); | ||||
|       } else { | ||||
|         // For route-based configuration, we need to adapt the interface | ||||
|         // Convert routes to domain configs for CertProvisioner | ||||
|         const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes( | ||||
|           (this.settings as IRoutedSmartProxyOptions).routes | ||||
|         ); | ||||
|  | ||||
|         this.certProvisioner = new CertProvisioner( | ||||
|           domainConfigs, | ||||
|           this.port80Handler, | ||||
|           this.networkProxyBridge, | ||||
|           this.settings.certProvisionFunction, | ||||
|           acme.renewThresholdDays!, | ||||
|           acme.renewCheckIntervalHours!, | ||||
|           acme.autoRenew!, | ||||
|           domainForwards | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       // Register certificate event handler | ||||
|       this.certProvisioner.on('certificate', (certData) => { | ||||
|         this.emit('certificate', { | ||||
|           domain: certData.domain, | ||||
| @@ -228,25 +283,22 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     } | ||||
|  | ||||
|     // Initialize and start NetworkProxy if needed | ||||
|     if ( | ||||
|       this.settings.useNetworkProxy && | ||||
|       this.settings.useNetworkProxy.length > 0 | ||||
|     ) { | ||||
|     if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { | ||||
|       await this.networkProxyBridge.initialize(); | ||||
|       await this.networkProxyBridge.start(); | ||||
|     } | ||||
|  | ||||
|     // Validate port configuration | ||||
|     const configWarnings = this.portRangeManager.validateConfiguration(); | ||||
|     // Validate the route configuration | ||||
|     const configWarnings = this.routeManager.validateConfiguration(); | ||||
|     if (configWarnings.length > 0) { | ||||
|       console.log("Port configuration warnings:"); | ||||
|       console.log("Route configuration warnings:"); | ||||
|       for (const warning of configWarnings) { | ||||
|         console.log(` - ${warning}`); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Get listening ports from PortRangeManager | ||||
|     const listeningPorts = this.portRangeManager.getListeningPorts(); | ||||
|     // Get listening ports from RouteManager | ||||
|     const listeningPorts = this.routeManager.getListeningPorts(); | ||||
|  | ||||
|     // Create servers for each port | ||||
|     for (const port of listeningPorts) { | ||||
| @@ -258,8 +310,8 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|           return; | ||||
|         } | ||||
|          | ||||
|         // Delegate to connection handler | ||||
|         this.connectionHandler.handleConnection(socket); | ||||
|         // Delegate to route connection handler | ||||
|         this.routeConnectionHandler.handleConnection(socket); | ||||
|       }).on('error', (err: Error) => { | ||||
|         console.log(`Server Error on port ${port}: ${err.message}`); | ||||
|       }); | ||||
| @@ -268,7 +320,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|         const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); | ||||
|         console.log( | ||||
|           `SmartProxy -> OK: Now listening on port ${port}${ | ||||
|             this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' | ||||
|             isLegacyOptions(this.settings) && this.settings.sniEnabled && !isNetworkProxyPort ?  | ||||
|               ' (SNI passthrough enabled)' :  | ||||
|               '' | ||||
|           }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` | ||||
|         ); | ||||
|       }); | ||||
| @@ -348,12 +402,70 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Extract domain configurations from routes for certificate provisioning | ||||
|    */ | ||||
|   private extractDomainConfigsFromRoutes(routes: IRouteConfig[]): IDomainConfig[] { | ||||
|     const domainConfigs: IDomainConfig[] = []; | ||||
|      | ||||
|     for (const route of routes) { | ||||
|       // Skip routes without domain specs | ||||
|       if (!route.match.domains) continue; | ||||
|        | ||||
|       // Skip non-forward routes | ||||
|       if (route.action.type !== 'forward') continue; | ||||
|        | ||||
|       // Only process routes that need TLS termination (those with certificates) | ||||
|       if (!route.action.tls ||  | ||||
|           route.action.tls.mode === 'passthrough' ||  | ||||
|           !route.action.target) continue; | ||||
|        | ||||
|       const domains = Array.isArray(route.match.domains)  | ||||
|         ? route.match.domains  | ||||
|         : [route.match.domains]; | ||||
|        | ||||
|       // Determine forwarding type based on TLS mode | ||||
|       const forwardingType = route.action.tls.mode === 'terminate'  | ||||
|         ? 'https-terminate-to-http'  | ||||
|         : 'https-terminate-to-https'; | ||||
|        | ||||
|       // Create a forwarding config | ||||
|       const forwarding = { | ||||
|         type: forwardingType as any, | ||||
|         target: { | ||||
|           host: Array.isArray(route.action.target.host)  | ||||
|             ? route.action.target.host[0]  | ||||
|             : route.action.target.host, | ||||
|           port: route.action.target.port | ||||
|         }, | ||||
|         // Add TLS settings | ||||
|         https: { | ||||
|           customCert: route.action.tls.certificate !== 'auto'  | ||||
|             ? route.action.tls.certificate  | ||||
|             : undefined | ||||
|         }, | ||||
|         // Add security settings if present | ||||
|         security: route.action.security, | ||||
|         // Add advanced settings if present | ||||
|         advanced: route.action.advanced | ||||
|       }; | ||||
|        | ||||
|       domainConfigs.push({ | ||||
|         domains, | ||||
|         forwarding | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     return domainConfigs; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop the proxy server | ||||
|    */ | ||||
|   public async stop() { | ||||
|     console.log('SmartProxy shutting down...'); | ||||
|     this.isShuttingDown = true; | ||||
|      | ||||
|     // Stop CertProvisioner if active | ||||
|     if (this.certProvisioner) { | ||||
|       await this.certProvisioner.stop(); | ||||
| @@ -411,14 +523,17 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Updates the domain configurations for the proxy | ||||
|    * Updates the domain configurations for the proxy (legacy support) | ||||
|    */ | ||||
|   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { | ||||
|     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); | ||||
|  | ||||
|     // Update domain configs in DomainConfigManager | ||||
|     // Update domain configs in DomainConfigManager (legacy) | ||||
|     this.domainConfigManager.updateDomainConfigs(newDomainConfigs); | ||||
|  | ||||
|     // Also update the RouteManager with these domain configs | ||||
|     this.routeManager.updateFromDomainConfigs(newDomainConfigs); | ||||
|  | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); | ||||
| @@ -428,7 +543,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     if (this.port80Handler && this.settings.acme?.enabled) { | ||||
|       for (const domainConfig of newDomainConfigs) { | ||||
|         // Skip certificate provisioning for http-only or passthrough configs that don't need certs | ||||
|         const forwardingType = domainConfig.forwarding.type; | ||||
|         const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); | ||||
|         const needsCertificate = | ||||
|           forwardingType === 'https-terminate-to-http' || | ||||
|           forwardingType === 'https-terminate-to-https'; | ||||
| @@ -490,6 +605,95 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update routes with new configuration (new API) | ||||
|    */ | ||||
|   public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> { | ||||
|     console.log(`Updating routes (${newRoutes.length} routes)`); | ||||
|      | ||||
|     // Update routes in RouteManager | ||||
|     this.routeManager.updateRoutes(newRoutes); | ||||
|      | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|       // Create equivalent domain configs for NetworkProxy | ||||
|       const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes); | ||||
|        | ||||
|       // Update domain configs in DomainConfigManager for sync | ||||
|       this.domainConfigManager.updateDomainConfigs(domainConfigs); | ||||
|        | ||||
|       // Sync with NetworkProxy | ||||
|       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); | ||||
|     } | ||||
|      | ||||
|     // If Port80Handler is running, provision certificates based on routes | ||||
|     if (this.port80Handler && this.settings.acme?.enabled) { | ||||
|       for (const route of newRoutes) { | ||||
|         // Skip routes without domains | ||||
|         if (!route.match.domains) continue; | ||||
|          | ||||
|         // Skip non-forward routes | ||||
|         if (route.action.type !== 'forward') continue; | ||||
|          | ||||
|         // Skip routes without TLS termination | ||||
|         if (!route.action.tls ||  | ||||
|             route.action.tls.mode === 'passthrough' ||  | ||||
|             !route.action.target) continue; | ||||
|          | ||||
|         // Skip certificate provisioning if certificate is not auto | ||||
|         if (route.action.tls.certificate !== 'auto') continue; | ||||
|          | ||||
|         const domains = Array.isArray(route.match.domains)  | ||||
|           ? route.match.domains  | ||||
|           : [route.match.domains]; | ||||
|          | ||||
|         for (const domain of domains) { | ||||
|           const isWildcard = domain.includes('*'); | ||||
|           let provision: string | plugins.tsclass.network.ICert = 'http01'; | ||||
|            | ||||
|           if (this.settings.certProvisionFunction) { | ||||
|             try { | ||||
|               provision = await this.settings.certProvisionFunction(domain); | ||||
|             } catch (err) { | ||||
|               console.log(`certProvider error for ${domain}: ${err}`); | ||||
|             } | ||||
|           } else if (isWildcard) { | ||||
|             console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); | ||||
|             continue; | ||||
|           } | ||||
|            | ||||
|           if (provision === 'http01') { | ||||
|             if (isWildcard) { | ||||
|               console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); | ||||
|               continue; | ||||
|             } | ||||
|              | ||||
|             // Register domain with Port80Handler | ||||
|             this.port80Handler.addDomain({ | ||||
|               domainName: domain, | ||||
|               sslRedirect: true, | ||||
|               acmeMaintenance: true | ||||
|             }); | ||||
|              | ||||
|             console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); | ||||
|           } else { | ||||
|             // Handle static certificate (e.g., DNS-01 provisioned) | ||||
|             const certObj = provision as plugins.tsclass.network.ICert; | ||||
|             const certData: ICertificateData = { | ||||
|               domain: certObj.domainName, | ||||
|               certificate: certObj.publicKey, | ||||
|               privateKey: certObj.privateKey, | ||||
|               expiryDate: new Date(certObj.validUntil) | ||||
|             }; | ||||
|             this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|             console.log(`Applied static certificate for ${domain} from certProvider`); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       console.log('Provisioned certificates for new routes'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Request a certificate for a specific domain | ||||
| @@ -583,7 +787,8 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       networkProxyConnections, | ||||
|       terminationStats, | ||||
|       acmeEnabled: !!this.port80Handler, | ||||
|       port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null | ||||
|       port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null, | ||||
|       routes: this.routeManager.getListeningPorts().length | ||||
|     }; | ||||
|   } | ||||
|    | ||||
| @@ -591,18 +796,44 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|    * Get a list of eligible domains for ACME certificates | ||||
|    */ | ||||
|   public getEligibleDomainsForCertificates(): string[] { | ||||
|     // Collect all non-wildcard domains from domain configs | ||||
|     const domains: string[] = []; | ||||
|      | ||||
|     for (const config of this.settings.domainConfigs) { | ||||
|     // Get domains from routes | ||||
|     const routes = isRoutedOptions(this.settings) ? this.settings.routes : []; | ||||
|      | ||||
|     for (const route of routes) { | ||||
|       if (!route.match.domains) continue; | ||||
|        | ||||
|       // Skip routes without TLS termination or auto certificates | ||||
|       if (route.action.type !== 'forward' ||  | ||||
|           !route.action.tls ||  | ||||
|           route.action.tls.mode === 'passthrough' ||  | ||||
|           route.action.tls.certificate !== 'auto') continue; | ||||
|        | ||||
|       const routeDomains = Array.isArray(route.match.domains)  | ||||
|         ? route.match.domains  | ||||
|         : [route.match.domains]; | ||||
|        | ||||
|       // Skip domains that can't be used with ACME | ||||
|       const eligibleDomains = config.domains.filter(domain =>  | ||||
|       const eligibleDomains = routeDomains.filter(domain =>  | ||||
|         !domain.includes('*') && this.isValidDomain(domain) | ||||
|       ); | ||||
|        | ||||
|       domains.push(...eligibleDomains); | ||||
|     } | ||||
|      | ||||
|     // For legacy mode, also get domains from domain configs | ||||
|     if (isLegacyOptions(this.settings)) { | ||||
|       for (const config of this.settings.domainConfigs) { | ||||
|         // Skip domains that can't be used with ACME | ||||
|         const eligibleDomains = config.domains.filter(domain =>  | ||||
|           !domain.includes('*') && this.isValidDomain(domain) | ||||
|         ); | ||||
|          | ||||
|         domains.push(...eligibleDomains); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return domains; | ||||
|   } | ||||
|    | ||||
|   | ||||
		Reference in New Issue
	
	Block a user