update
This commit is contained in:
		| @@ -6,8 +6,8 @@ import type { | ||||
| } from './types.js'; | ||||
|  | ||||
| import type { | ||||
|   IForwardConfig | ||||
| } from '../smartproxy/types/forwarding.types.js'; | ||||
|   ForwardConfig as IForwardConfig | ||||
| } from '../forwarding/config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Converts a forwarding configuration target to the legacy format | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| /** | ||||
|  * HTTP routing | ||||
|  */ | ||||
|  | ||||
| export * from './proxy-router.js'; | ||||
|   | ||||
| @@ -1,22 +1,29 @@ | ||||
| import * as plugins from './plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { ReverseProxyConfig } from '../../proxies/network-proxy/models/types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Optional path pattern configuration that can be added to proxy configs | ||||
|  */ | ||||
| export interface IPathPatternConfig { | ||||
| export interface PathPatternConfig { | ||||
|   pathPattern?: string; | ||||
| } | ||||
| 
 | ||||
| // Backward compatibility
 | ||||
| export type IPathPatternConfig = PathPatternConfig; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for router result with additional metadata | ||||
|  */ | ||||
| export interface IRouterResult { | ||||
|   config: plugins.tsclass.network.IReverseProxyConfig; | ||||
| export interface RouterResult { | ||||
|   config: ReverseProxyConfig; | ||||
|   pathMatch?: string; | ||||
|   pathParams?: Record<string, string>; | ||||
|   pathRemainder?: string; | ||||
| } | ||||
| 
 | ||||
| // Backward compatibility
 | ||||
| export type IRouterResult = RouterResult; | ||||
| 
 | ||||
| /** | ||||
|  * Router for HTTP reverse proxy requests | ||||
|  *  | ||||
| @@ -34,13 +41,13 @@ export interface IRouterResult { | ||||
|  */ | ||||
| export class ProxyRouter { | ||||
|   // Store original configs for reference
 | ||||
|   private reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; | ||||
|   private reverseProxyConfigs: ReverseProxyConfig[] = []; | ||||
|   // Default config to use when no match is found (optional)
 | ||||
|   private defaultConfig?: plugins.tsclass.network.IReverseProxyConfig; | ||||
|   private defaultConfig?: ReverseProxyConfig; | ||||
|   // Store path patterns separately since they're not in the original interface
 | ||||
|   private pathPatterns: Map<plugins.tsclass.network.IReverseProxyConfig, string> = new Map(); | ||||
|   private pathPatterns: Map<ReverseProxyConfig, string> = new Map(); | ||||
|   // Logger interface
 | ||||
|   private logger: {  | ||||
|   private logger: { | ||||
|     error: (message: string, data?: any) => void; | ||||
|     warn: (message: string, data?: any) => void; | ||||
|     info: (message: string, data?: any) => void; | ||||
| @@ -48,8 +55,8 @@ export class ProxyRouter { | ||||
|   }; | ||||
| 
 | ||||
|   constructor( | ||||
|     configs?: plugins.tsclass.network.IReverseProxyConfig[], | ||||
|     logger?: {  | ||||
|     configs?: ReverseProxyConfig[], | ||||
|     logger?: { | ||||
|       error: (message: string, data?: any) => void; | ||||
|       warn: (message: string, data?: any) => void; | ||||
|       info: (message: string, data?: any) => void; | ||||
| @@ -66,12 +73,12 @@ export class ProxyRouter { | ||||
|    * Sets a new set of reverse configs to be routed to | ||||
|    * @param reverseCandidatesArg Array of reverse proxy configurations | ||||
|    */ | ||||
|   public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]): void { | ||||
|   public setNewProxyConfigs(reverseCandidatesArg: ReverseProxyConfig[]): void { | ||||
|     this.reverseProxyConfigs = [...reverseCandidatesArg]; | ||||
|      | ||||
| 
 | ||||
|     // Find default config if any (config with "*" as hostname)
 | ||||
|     this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*'); | ||||
|      | ||||
| 
 | ||||
|     this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`); | ||||
|   } | ||||
| 
 | ||||
| @@ -80,17 +87,17 @@ export class ProxyRouter { | ||||
|    * @param req The incoming HTTP request | ||||
|    * @returns The matching proxy config or undefined if no match found | ||||
|    */ | ||||
|   public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig { | ||||
|   public routeReq(req: plugins.http.IncomingMessage): ReverseProxyConfig { | ||||
|     const result = this.routeReqWithDetails(req); | ||||
|     return result ? result.config : undefined; | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   /** | ||||
|    * Routes a request with detailed matching information | ||||
|    * @param req The incoming HTTP request | ||||
|    * @returns Detailed routing result including matched config and path information | ||||
|    */ | ||||
|   public routeReqWithDetails(req: plugins.http.IncomingMessage): IRouterResult | undefined { | ||||
|   public routeReqWithDetails(req: plugins.http.IncomingMessage): RouterResult | undefined { | ||||
|     // Extract and validate host header
 | ||||
|     const originalHost = req.headers.host; | ||||
|     if (!originalHost) { | ||||
| @@ -202,7 +209,7 @@ export class ProxyRouter { | ||||
|   /** | ||||
|    * Find a config for a specific host and path | ||||
|    */ | ||||
|   private findConfigForHost(hostname: string, path: string): IRouterResult | undefined { | ||||
|   private findConfigForHost(hostname: string, path: string): RouterResult | undefined { | ||||
|     // Find all configs for this hostname
 | ||||
|     const configs = this.reverseProxyConfigs.filter( | ||||
|       config => config.hostName.toLowerCase() === hostname.toLowerCase() | ||||
| @@ -349,7 +356,7 @@ export class ProxyRouter { | ||||
|    * Gets all currently active proxy configurations | ||||
|    * @returns Array of all active configurations | ||||
|    */ | ||||
|   public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] { | ||||
|   public getProxyConfigs(): ReverseProxyConfig[] { | ||||
|     return [...this.reverseProxyConfigs]; | ||||
|   } | ||||
|    | ||||
| @@ -373,7 +380,7 @@ export class ProxyRouter { | ||||
|    * @param pathPattern Optional path pattern for route matching | ||||
|    */ | ||||
|   public addProxyConfig( | ||||
|     config: plugins.tsclass.network.IReverseProxyConfig,  | ||||
|     config: ReverseProxyConfig, | ||||
|     pathPattern?: string | ||||
|   ): void { | ||||
|     this.reverseProxyConfigs.push(config); | ||||
| @@ -391,7 +398,7 @@ export class ProxyRouter { | ||||
|    * @returns Boolean indicating if the config was found and updated | ||||
|    */ | ||||
|   public setPathPattern( | ||||
|     config: plugins.tsclass.network.IReverseProxyConfig,  | ||||
|     config: ReverseProxyConfig, | ||||
|     pathPattern: string | ||||
|   ): boolean { | ||||
|     const exists = this.reverseProxyConfigs.includes(config); | ||||
| @@ -3,7 +3,8 @@ | ||||
|  */ | ||||
|  | ||||
| // Legacy exports (to maintain backward compatibility) | ||||
| export * from './nfttablesproxy/classes.nftablesproxy.js'; | ||||
| // Migrated to the new proxies structure | ||||
| export * from './proxies/nftables-proxy/index.js'; | ||||
| export * from './networkproxy/index.js'; | ||||
| // Export port80handler elements selectively to avoid conflicts | ||||
| export { | ||||
| @@ -17,11 +18,13 @@ export { | ||||
| export { Port80HandlerEvents } from './certificate/events/certificate-events.js'; | ||||
|  | ||||
| export * from './redirect/classes.redirect.js'; | ||||
| export * from './smartproxy/classes.smartproxy.js'; | ||||
| export * from './proxies/smart-proxy/index.js'; | ||||
| // Original: export * from './smartproxy/classes.pp.snihandler.js' | ||||
| // Now we export from the new module | ||||
| export { SniHandler } from './tls/sni/sni-handler.js'; | ||||
| export * from './smartproxy/classes.pp.interfaces.js'; | ||||
| // Original: export * from './smartproxy/classes.pp.interfaces.js' | ||||
| // Now we export from the new module | ||||
| export * from './proxies/smart-proxy/models/interfaces.js'; | ||||
|  | ||||
| // Core types and utilities | ||||
| export * from './core/models/common-types.js'; | ||||
|   | ||||
| @@ -1,7 +1,3 @@ | ||||
| // Re-export all components for easier imports | ||||
| export * from './classes.np.types.js'; | ||||
| export * from './classes.np.certificatemanager.js'; | ||||
| export * from './classes.np.connectionpool.js'; | ||||
| export * from './classes.np.requesthandler.js'; | ||||
| export * from './classes.np.websockethandler.js'; | ||||
| export * from './classes.np.networkproxy.js'; | ||||
| // Re-export all components from the new location | ||||
| // This file is for backward compatibility only | ||||
| export * from '../proxies/network-proxy/index.js'; | ||||
|   | ||||
| @@ -1,27 +1,27 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import { fileURLToPath } from 'url'; | ||||
| import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { Port80HandlerEvents } from '../common/types.js'; | ||||
| import { buildPort80Handler } from '../certificate/acme/acme-factory.js'; | ||||
| import { subscribeToPort80Handler } from '../common/eventUtils.js'; | ||||
| import type { IDomainOptions } from '../common/types.js'; | ||||
| import { type NetworkProxyOptions, type CertificateEntry, type Logger, createLogger } from './models/types.js'; | ||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||
| import { CertificateEvents } from '../../certificate/events/certificate-events.js'; | ||||
| import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; | ||||
| import { subscribeToPort80Handler } from '../../core/utils/event-utils.js'; | ||||
| import type { DomainOptions } from '../../certificate/models/certificate-types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages SSL certificates for NetworkProxy including ACME integration | ||||
|  */ | ||||
| export class CertificateManager { | ||||
|   private defaultCertificates: { key: string; cert: string }; | ||||
|   private certificateCache: Map<string, ICertificateEntry> = new Map(); | ||||
|   private certificateCache: Map<string, CertificateEntry> = new Map(); | ||||
|   private port80Handler: Port80Handler | null = null; | ||||
|   private externalPort80Handler: boolean = false; | ||||
|   private certificateStoreDir: string; | ||||
|   private logger: ILogger; | ||||
|   private logger: Logger; | ||||
|   private httpsServer: plugins.https.Server | null = null; | ||||
|    | ||||
|   constructor(private options: INetworkProxyOptions) { | ||||
| 
 | ||||
|   constructor(private options: NetworkProxyOptions) { | ||||
|     this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs'); | ||||
|     this.logger = createLogger(options.logLevel || 'info'); | ||||
|      | ||||
| @@ -94,10 +94,10 @@ export class CertificateManager { | ||||
|       // Clean up existing handler if needed
 | ||||
|       if (this.port80Handler !== handler) { | ||||
|         // Unregister event handlers to avoid memory leaks
 | ||||
|         this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_ISSUED); | ||||
|         this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_RENEWED); | ||||
|         this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_FAILED); | ||||
|         this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_EXPIRING); | ||||
|         this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_ISSUED); | ||||
|         this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_RENEWED); | ||||
|         this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_FAILED); | ||||
|         this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_EXPIRING); | ||||
|       } | ||||
|     } | ||||
|      | ||||
| @@ -220,7 +220,7 @@ export class CertificateManager { | ||||
|         this.logger.info(`No certificate found for ${domain}, registering for issuance`); | ||||
|          | ||||
|         // Register with new domain options format
 | ||||
|         const domainOptions: IDomainOptions = { | ||||
|         const domainOptions: DomainOptions = { | ||||
|           domainName: domain, | ||||
|           sslRedirect: true, | ||||
|           acmeMaintenance: true | ||||
| @@ -273,7 +273,7 @@ export class CertificateManager { | ||||
|   /** | ||||
|    * Gets a certificate for a domain | ||||
|    */ | ||||
|   public getCertificate(domain: string): ICertificateEntry | undefined { | ||||
|   public getCertificate(domain: string): CertificateEntry | undefined { | ||||
|     return this.certificateCache.get(domain); | ||||
|   } | ||||
|    | ||||
| @@ -299,7 +299,7 @@ export class CertificateManager { | ||||
|      | ||||
|     try { | ||||
|       // Use the new domain options format
 | ||||
|       const domainOptions: IDomainOptions = { | ||||
|       const domainOptions: DomainOptions = { | ||||
|         domainName: domain, | ||||
|         sslRedirect: true, | ||||
|         acmeMaintenance: true | ||||
| @@ -340,7 +340,7 @@ export class CertificateManager { | ||||
|       } | ||||
|        | ||||
|       // Register the domain for certificate issuance with new domain options format
 | ||||
|       const domainOptions: IDomainOptions = { | ||||
|       const domainOptions: DomainOptions = { | ||||
|         domainName: domain, | ||||
|         sslRedirect: true, | ||||
|         acmeMaintenance: true | ||||
| @@ -1,15 +1,15 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { type INetworkProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './classes.np.types.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { type NetworkProxyOptions, type ConnectionEntry, type Logger, createLogger } from './models/types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages a pool of backend connections for efficient reuse | ||||
|  */ | ||||
| export class ConnectionPool { | ||||
|   private connectionPool: Map<string, Array<IConnectionEntry>> = new Map(); | ||||
|   private connectionPool: Map<string, Array<ConnectionEntry>> = new Map(); | ||||
|   private roundRobinPositions: Map<string, number> = new Map(); | ||||
|   private logger: ILogger; | ||||
|    | ||||
|   constructor(private options: INetworkProxyOptions) { | ||||
|   private logger: Logger; | ||||
| 
 | ||||
|   constructor(private options: NetworkProxyOptions) { | ||||
|     this.logger = createLogger(options.logLevel || 'info'); | ||||
|   } | ||||
|    | ||||
| @@ -4,5 +4,10 @@ | ||||
| // Re-export models | ||||
| export * from './models/index.js'; | ||||
|  | ||||
| // Core NetworkProxy will be added later: | ||||
| // export { NetworkProxy } from './network-proxy.js'; | ||||
| // Export NetworkProxy and supporting classes | ||||
| export { NetworkProxy } from './network-proxy.js'; | ||||
| export { CertificateManager } from './certificate-manager.js'; | ||||
| export { ConnectionPool } from './connection-pool.js'; | ||||
| export { RequestHandler } from './request-handler.js'; | ||||
| export type { IMetricsTracker, MetricsTracker } from './request-handler.js'; | ||||
| export { WebSocketHandler } from './websocket-handler.js'; | ||||
|   | ||||
| @@ -1,11 +1,18 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js'; | ||||
| import { CertificateManager } from './classes.np.certificatemanager.js'; | ||||
| import { ConnectionPool } from './classes.np.connectionpool.js'; | ||||
| import { RequestHandler, type IMetricsTracker } from './classes.np.requesthandler.js'; | ||||
| import { WebSocketHandler } from './classes.np.websockethandler.js'; | ||||
| import { ProxyRouter } from '../classes.router.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { | ||||
|   createLogger | ||||
| } from './models/types.js'; | ||||
| import type { | ||||
|   NetworkProxyOptions, | ||||
|   Logger, | ||||
|   ReverseProxyConfig | ||||
| } from './models/types.js'; | ||||
| import { CertificateManager } from './certificate-manager.js'; | ||||
| import { ConnectionPool } from './connection-pool.js'; | ||||
| import { RequestHandler, type IMetricsTracker } from './request-handler.js'; | ||||
| import { WebSocketHandler } from './websocket-handler.js'; | ||||
| import { ProxyRouter } from '../../http/router/index.js'; | ||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||
| 
 | ||||
| /** | ||||
|  * NetworkProxy provides a reverse proxy with TLS termination, WebSocket support, | ||||
| @@ -17,8 +24,8 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|     return {}; | ||||
|   } | ||||
|   // Configuration
 | ||||
|   public options: INetworkProxyOptions; | ||||
|   public proxyConfigs: IReverseProxyConfig[] = []; | ||||
|   public options: NetworkProxyOptions; | ||||
|   public proxyConfigs: ReverseProxyConfig[] = []; | ||||
|    | ||||
|   // Server instances (HTTP/2 with HTTP/1 fallback)
 | ||||
|   public httpsServer: any; | ||||
| @@ -38,7 +45,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   public requestsServed: number = 0; | ||||
|   public failedRequests: number = 0; | ||||
|    | ||||
|   // Tracking for PortProxy integration
 | ||||
|   // Tracking for SmartProxy integration
 | ||||
|   private portProxyConnections: number = 0; | ||||
|   private tlsTerminatedConnections: number = 0; | ||||
|    | ||||
| @@ -47,12 +54,12 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   private connectionPoolCleanupInterval: NodeJS.Timeout; | ||||
|    | ||||
|   // Logger
 | ||||
|   private logger: ILogger; | ||||
|   private logger: Logger; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates a new NetworkProxy instance | ||||
|    */ | ||||
|   constructor(optionsArg: INetworkProxyOptions) { | ||||
|   constructor(optionsArg: NetworkProxyOptions) { | ||||
|     // Set default options
 | ||||
|     this.options = { | ||||
|       port: optionsArg.port, | ||||
| @@ -66,7 +73,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|         allowHeaders: 'Content-Type, Authorization', | ||||
|         maxAge: 86400 | ||||
|       }, | ||||
|       // Defaults for PortProxy integration
 | ||||
|       // Defaults for SmartProxy integration
 | ||||
|       connectionPoolSize: optionsArg.connectionPoolSize || 50, | ||||
|       portProxyIntegration: optionsArg.portProxyIntegration || false, | ||||
|       useExternalPort80Handler: optionsArg.useExternalPort80Handler || false, | ||||
| @@ -114,7 +121,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the port number this NetworkProxy is listening on | ||||
|    * Useful for PortProxy to determine where to forward connections | ||||
|    * Useful for SmartProxy to determine where to forward connections | ||||
|    */ | ||||
|   public getListeningPort(): number { | ||||
|     return this.options.port; | ||||
| @@ -152,7 +159,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
| 
 | ||||
|   /** | ||||
|    * Returns current server metrics | ||||
|    * Useful for PortProxy to determine which NetworkProxy to use for load balancing | ||||
|    * Useful for SmartProxy to determine which NetworkProxy to use for load balancing | ||||
|    */ | ||||
|   public getMetrics(): any { | ||||
|     return { | ||||
| @@ -247,14 +254,14 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|       this.socketMap.add(connection); | ||||
|       this.connectedClients = this.socketMap.getArray().length; | ||||
|        | ||||
|       // Check for connection from PortProxy by inspecting the source port
 | ||||
|       // Check for connection from SmartProxy by inspecting the source port
 | ||||
|       const localPort = connection.localPort || 0; | ||||
|       const remotePort = connection.remotePort || 0; | ||||
|        | ||||
|       // If this connection is from a PortProxy (usually indicated by it coming from localhost)
 | ||||
|       // If this connection is from a SmartProxy (usually indicated by it coming from localhost)
 | ||||
|       if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) { | ||||
|         this.portProxyConnections++; | ||||
|         this.logger.debug(`New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`); | ||||
|         this.logger.debug(`New connection from SmartProxy (local: ${localPort}, remote: ${remotePort})`); | ||||
|       } else { | ||||
|         this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`); | ||||
|       } | ||||
| @@ -265,7 +272,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|           this.socketMap.remove(connection); | ||||
|           this.connectedClients = this.socketMap.getArray().length; | ||||
|            | ||||
|           // If this was a PortProxy connection, decrement the counter
 | ||||
|           // If this was a SmartProxy connection, decrement the counter
 | ||||
|           if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) { | ||||
|             this.portProxyConnections--; | ||||
|           } | ||||
| @@ -321,7 +328,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|    * Updates proxy configurations | ||||
|    */ | ||||
|   public async updateProxyConfigs( | ||||
|     proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[] | ||||
|     proxyConfigsArg: ReverseProxyConfig[] | ||||
|   ): Promise<void> { | ||||
|     this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`); | ||||
|      | ||||
| @@ -366,20 +373,20 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Converts PortProxy domain configurations to NetworkProxy configs | ||||
|    * @param domainConfigs PortProxy domain configs | ||||
|    * Converts SmartProxy domain configurations to NetworkProxy configs | ||||
|    * @param domainConfigs SmartProxy domain configs | ||||
|    * @param sslKeyPair Default SSL key pair to use if not specified | ||||
|    * @returns Array of NetworkProxy configs | ||||
|    */ | ||||
|   public convertPortProxyConfigs( | ||||
|   public convertSmartProxyConfigs( | ||||
|     domainConfigs: Array<{ | ||||
|       domains: string[]; | ||||
|       targetIPs?: string[]; | ||||
|       allowedIPs?: string[]; | ||||
|     }>, | ||||
|     sslKeyPair?: { key: string; cert: string } | ||||
|   ): plugins.tsclass.network.IReverseProxyConfig[] { | ||||
|     const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; | ||||
|   ): ReverseProxyConfig[] { | ||||
|     const proxyConfigs: ReverseProxyConfig[] = []; | ||||
|      | ||||
|     // Use default certificates if not provided
 | ||||
|     const defaultCerts = this.certificateManager.getDefaultCertificates(); | ||||
| @@ -404,7 +411,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     this.logger.info(`Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`); | ||||
|     this.logger.info(`Converted ${domainConfigs.length} SmartProxy configs to ${proxyConfigs.length} NetworkProxy configs`); | ||||
|     return proxyConfigs; | ||||
|   } | ||||
| 
 | ||||
| @@ -471,7 +478,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   /** | ||||
|    * Gets all proxy configurations currently in use | ||||
|    */ | ||||
|   public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] { | ||||
|   public getProxyConfigs(): ReverseProxyConfig[] { | ||||
|     return [...this.proxyConfigs]; | ||||
|   } | ||||
| } | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js'; | ||||
| import { ConnectionPool } from './classes.np.connectionpool.js'; | ||||
| import { ProxyRouter } from '../classes.router.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { type NetworkProxyOptions, type Logger, createLogger, type ReverseProxyConfig } from './models/types.js'; | ||||
| import { ConnectionPool } from './connection-pool.js'; | ||||
| import { ProxyRouter } from '../../http/router/index.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for tracking metrics | ||||
| @@ -11,18 +11,21 @@ export interface IMetricsTracker { | ||||
|   incrementFailedRequests(): void; | ||||
| } | ||||
| 
 | ||||
| // Backward compatibility
 | ||||
| export type MetricsTracker = IMetricsTracker; | ||||
| 
 | ||||
| /** | ||||
|  * Handles HTTP request processing and proxying | ||||
|  */ | ||||
| export class RequestHandler { | ||||
|   private defaultHeaders: { [key: string]: string } = {}; | ||||
|   private logger: ILogger; | ||||
|   private logger: Logger; | ||||
|   private metricsTracker: IMetricsTracker | null = null; | ||||
|   // HTTP/2 client sessions for backend proxying
 | ||||
|   private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map(); | ||||
|    | ||||
| 
 | ||||
|   constructor( | ||||
|     private options: INetworkProxyOptions, | ||||
|     private options: NetworkProxyOptions, | ||||
|     private connectionPool: ConnectionPool, | ||||
|     private router: ProxyRouter | ||||
|   ) { | ||||
| @@ -134,7 +137,7 @@ export class RequestHandler { | ||||
|     this.applyDefaultHeaders(res); | ||||
|      | ||||
|     // Determine routing configuration
 | ||||
|     let proxyConfig: IReverseProxyConfig | undefined; | ||||
|     let proxyConfig: ReverseProxyConfig | undefined; | ||||
|     try { | ||||
|       proxyConfig = this.router.routeReq(req); | ||||
|     } catch (err) { | ||||
| @@ -232,7 +235,7 @@ export class RequestHandler { | ||||
|       // Remove host header to avoid issues with virtual hosts on target server
 | ||||
|       // The host header should match the target server's expected hostname
 | ||||
|       if (options.headers && options.headers.host) { | ||||
|         if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) { | ||||
|         if ((proxyConfig as ReverseProxyConfig).rewriteHostHeader) { | ||||
|           options.headers.host = `${destination.host}:${destination.port}`; | ||||
|         } | ||||
|       } | ||||
| @@ -433,7 +436,9 @@ export class RequestHandler { | ||||
|           // Map status and headers back to HTTP/2
 | ||||
|           const responseHeaders: Record<string, number|string|string[]> = {}; | ||||
|           for (const [k, v] of Object.entries(proxyRes.headers)) { | ||||
|             if (v !== undefined) responseHeaders[k] = v; | ||||
|             if (v !== undefined) { | ||||
|               responseHeaders[k] = v as string | string[]; | ||||
|             } | ||||
|           } | ||||
|           stream.respond({ ':status': proxyRes.statusCode || 500, ...responseHeaders }); | ||||
|           proxyRes.pipe(stream); | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { type INetworkProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js'; | ||||
| import { ConnectionPool } from './classes.np.connectionpool.js'; | ||||
| import { ProxyRouter } from '../classes.router.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { type NetworkProxyOptions, type WebSocketWithHeartbeat, type Logger, createLogger, type ReverseProxyConfig } from './models/types.js'; | ||||
| import { ConnectionPool } from './connection-pool.js'; | ||||
| import { ProxyRouter } from '../../http/router/index.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Handles WebSocket connections and proxying | ||||
| @@ -9,10 +9,10 @@ import { ProxyRouter } from '../classes.router.js'; | ||||
| export class WebSocketHandler { | ||||
|   private heartbeatInterval: NodeJS.Timeout | null = null; | ||||
|   private wsServer: plugins.ws.WebSocketServer | null = null; | ||||
|   private logger: ILogger; | ||||
|    | ||||
|   private logger: Logger; | ||||
| 
 | ||||
|   constructor( | ||||
|     private options: INetworkProxyOptions, | ||||
|     private options: NetworkProxyOptions, | ||||
|     private connectionPool: ConnectionPool, | ||||
|     private router: ProxyRouter | ||||
|   ) { | ||||
| @@ -30,7 +30,7 @@ export class WebSocketHandler { | ||||
|     }); | ||||
| 
 | ||||
|     // Handle WebSocket connections
 | ||||
|     this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage) => { | ||||
|     this.wsServer.on('connection', (wsIncoming: WebSocketWithHeartbeat, req: plugins.http.IncomingMessage) => { | ||||
|       this.handleWebSocketConnection(wsIncoming, req); | ||||
|     }); | ||||
|      | ||||
| @@ -58,7 +58,7 @@ export class WebSocketHandler { | ||||
|       this.logger.debug(`WebSocket heartbeat check for ${this.wsServer.clients.size} clients`); | ||||
|        | ||||
|       this.wsServer.clients.forEach((ws: plugins.wsDefault) => { | ||||
|         const wsWithHeartbeat = ws as IWebSocketWithHeartbeat; | ||||
|         const wsWithHeartbeat = ws as WebSocketWithHeartbeat; | ||||
|          | ||||
|         if (wsWithHeartbeat.isAlive === false) { | ||||
|           this.logger.debug('Terminating inactive WebSocket connection'); | ||||
| @@ -79,7 +79,7 @@ export class WebSocketHandler { | ||||
|   /** | ||||
|    * Handle a new WebSocket connection | ||||
|    */ | ||||
|   private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage): void { | ||||
|   private handleWebSocketConnection(wsIncoming: WebSocketWithHeartbeat, req: plugins.http.IncomingMessage): void { | ||||
|     try { | ||||
|       // Initialize heartbeat tracking
 | ||||
|       wsIncoming.isAlive = true; | ||||
| @@ -127,7 +127,7 @@ export class WebSocketHandler { | ||||
|       } | ||||
|        | ||||
|       // Override host header if needed
 | ||||
|       if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) { | ||||
|       if ((proxyConfig as ReverseProxyConfig).rewriteHostHeader) { | ||||
|         headers['host'] = `${destination.host}:${destination.port}`; | ||||
|       } | ||||
|        | ||||
| @@ -1,5 +1,5 @@ | ||||
| /** | ||||
|  * NfTablesProxy implementation | ||||
|  */ | ||||
| // Core NfTablesProxy will be added later: | ||||
| // export { NfTablesProxy } from './nftables-proxy.js'; | ||||
| export * from './nftables-proxy.js'; | ||||
| export * from './models/index.js'; | ||||
|   | ||||
							
								
								
									
										30
									
								
								ts/proxies/nftables-proxy/models/errors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								ts/proxies/nftables-proxy/models/errors.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| /** | ||||
|  * Custom error classes for better error handling | ||||
|  */ | ||||
| export class NftBaseError extends Error { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftBaseError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class NftValidationError extends NftBaseError { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftValidationError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class NftExecutionError extends NftBaseError { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftExecutionError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class NftResourceError extends NftBaseError { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftResourceError'; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								ts/proxies/nftables-proxy/models/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ts/proxies/nftables-proxy/models/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| /** | ||||
|  * Export all models | ||||
|  */ | ||||
| export * from './interfaces.js'; | ||||
| export * from './errors.js'; | ||||
							
								
								
									
										94
									
								
								ts/proxies/nftables-proxy/models/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								ts/proxies/nftables-proxy/models/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /** | ||||
|  * Interfaces for NfTablesProxy | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Represents a port range for forwarding | ||||
|  */ | ||||
| export interface PortRange { | ||||
|   from: number; | ||||
|   to: number; | ||||
| } | ||||
|  | ||||
| // Legacy interface name for backward compatibility | ||||
| export type IPortRange = PortRange; | ||||
|  | ||||
| /** | ||||
|  * Settings for NfTablesProxy. | ||||
|  */ | ||||
| export interface NfTableProxyOptions { | ||||
|   // Basic settings | ||||
|   fromPort: number | PortRange | Array<number | PortRange>; // Support single port, port range, or multiple ports/ranges | ||||
|   toPort: number | PortRange | Array<number | PortRange>; | ||||
|   toHost?: string; // Target host for proxying; defaults to 'localhost' | ||||
|    | ||||
|   // Advanced settings | ||||
|   preserveSourceIP?: boolean; // If true, the original source IP is preserved | ||||
|   deleteOnExit?: boolean;     // If true, clean up rules before process exit | ||||
|   protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp' | ||||
|   enableLogging?: boolean;    // Enable detailed logging | ||||
|   ipv6Support?: boolean;      // Enable IPv6 support | ||||
|   logFormat?: 'plain' | 'json'; // Format for logs | ||||
|    | ||||
|   // Source filtering | ||||
|   allowedSourceIPs?: string[]; // If provided, only these IPs are allowed | ||||
|   bannedSourceIPs?: string[];  // If provided, these IPs are blocked | ||||
|   useIPSets?: boolean;        // Use nftables sets for efficient IP management | ||||
|    | ||||
|   // Rule management | ||||
|   forceCleanSlate?: boolean;   // Clear all NfTablesProxy rules before starting | ||||
|   tableName?: string;          // Custom table name (defaults to 'portproxy') | ||||
|    | ||||
|   // Connection management | ||||
|   maxRetries?: number;        // Maximum number of retries for failed commands | ||||
|   retryDelayMs?: number;      // Delay between retries in milliseconds | ||||
|   useAdvancedNAT?: boolean;   // Use connection tracking for stateful NAT | ||||
|    | ||||
|   // Quality of Service | ||||
|   qos?: { | ||||
|     enabled: boolean; | ||||
|     maxRate?: string;         // e.g. "10mbps" | ||||
|     priority?: number;        // 1 (highest) to 10 (lowest) | ||||
|     markConnections?: boolean; // Mark connections for easier management | ||||
|   }; | ||||
|    | ||||
|   // Integration with PortProxy/NetworkProxy | ||||
|   netProxyIntegration?: { | ||||
|     enabled: boolean; | ||||
|     redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy | ||||
|     sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // Legacy interface name for backward compatibility | ||||
| export type INfTableProxySettings = NfTableProxyOptions; | ||||
|  | ||||
| /** | ||||
|  * Interface for status reporting | ||||
|  */ | ||||
| export interface NfTablesStatus { | ||||
|   active: boolean; | ||||
|   ruleCount: { | ||||
|     total: number; | ||||
|     added: number; | ||||
|     verified: number; | ||||
|   }; | ||||
|   tablesConfigured: { family: string; tableName: string }[]; | ||||
|   metrics: { | ||||
|     forwardedConnections?: number; | ||||
|     activeConnections?: number; | ||||
|     bytesForwarded?: { | ||||
|       sent: number; | ||||
|       received: number; | ||||
|     }; | ||||
|   }; | ||||
|   qosEnabled?: boolean; | ||||
|   ipSetsConfigured?: { | ||||
|     name: string; | ||||
|     elementCount: number; | ||||
|     type: string; | ||||
|   }[]; | ||||
| } | ||||
|  | ||||
| // Legacy interface name for backward compatibility | ||||
| export type INfTablesStatus = NfTablesStatus; | ||||
| @@ -3,95 +3,20 @@ import { promisify } from 'util'; | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import * as os from 'os'; | ||||
| import { | ||||
|   NftBaseError, | ||||
|   NftValidationError, | ||||
|   NftExecutionError, | ||||
|   NftResourceError | ||||
| } from './models/index.js'; | ||||
| import type { | ||||
|   PortRange, | ||||
|   NfTableProxyOptions, | ||||
|   NfTablesStatus | ||||
| } from './models/index.js'; | ||||
| 
 | ||||
| const execAsync = promisify(exec); | ||||
| 
 | ||||
| /** | ||||
|  * Custom error classes for better error handling | ||||
|  */ | ||||
| export class NftBaseError extends Error { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftBaseError'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class NftValidationError extends NftBaseError { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftValidationError'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class NftExecutionError extends NftBaseError { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftExecutionError'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class NftResourceError extends NftBaseError { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'NftResourceError'; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Represents a port range for forwarding | ||||
|  */ | ||||
| export interface IPortRange { | ||||
|   from: number; | ||||
|   to: number; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Settings for NfTablesProxy. | ||||
|  */ | ||||
| export interface INfTableProxySettings { | ||||
|   // Basic settings
 | ||||
|   fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges
 | ||||
|   toPort: number | IPortRange | Array<number | IPortRange>; | ||||
|   toHost?: string; // Target host for proxying; defaults to 'localhost'
 | ||||
|    | ||||
|   // Advanced settings
 | ||||
|   preserveSourceIP?: boolean; // If true, the original source IP is preserved
 | ||||
|   deleteOnExit?: boolean;     // If true, clean up rules before process exit
 | ||||
|   protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp'
 | ||||
|   enableLogging?: boolean;    // Enable detailed logging
 | ||||
|   ipv6Support?: boolean;      // Enable IPv6 support
 | ||||
|   logFormat?: 'plain' | 'json'; // Format for logs
 | ||||
|    | ||||
|   // Source filtering
 | ||||
|   allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
 | ||||
|   bannedSourceIPs?: string[];  // If provided, these IPs are blocked
 | ||||
|   useIPSets?: boolean;        // Use nftables sets for efficient IP management
 | ||||
|    | ||||
|   // Rule management
 | ||||
|   forceCleanSlate?: boolean;   // Clear all NfTablesProxy rules before starting
 | ||||
|   tableName?: string;          // Custom table name (defaults to 'portproxy')
 | ||||
|    | ||||
|   // Connection management
 | ||||
|   maxRetries?: number;        // Maximum number of retries for failed commands
 | ||||
|   retryDelayMs?: number;      // Delay between retries in milliseconds
 | ||||
|   useAdvancedNAT?: boolean;   // Use connection tracking for stateful NAT
 | ||||
|    | ||||
|   // Quality of Service
 | ||||
|   qos?: { | ||||
|     enabled: boolean; | ||||
|     maxRate?: string;         // e.g. "10mbps"
 | ||||
|     priority?: number;        // 1 (highest) to 10 (lowest)
 | ||||
|     markConnections?: boolean; // Mark connections for easier management
 | ||||
|   }; | ||||
|    | ||||
|   // Integration with PortProxy/NetworkProxy
 | ||||
|   netProxyIntegration?: { | ||||
|     enabled: boolean; | ||||
|     redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy
 | ||||
|     sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination
 | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Represents a rule added to nftables | ||||
|  */ | ||||
| @@ -105,40 +30,13 @@ interface NfTablesRule { | ||||
|   verified?: boolean;    // Whether the rule has been verified as applied
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Interface for status reporting | ||||
|  */ | ||||
| export interface INfTablesStatus { | ||||
|   active: boolean; | ||||
|   ruleCount: { | ||||
|     total: number; | ||||
|     added: number; | ||||
|     verified: number; | ||||
|   }; | ||||
|   tablesConfigured: { family: string; tableName: string }[]; | ||||
|   metrics: { | ||||
|     forwardedConnections?: number; | ||||
|     activeConnections?: number; | ||||
|     bytesForwarded?: { | ||||
|       sent: number; | ||||
|       received: number; | ||||
|     }; | ||||
|   }; | ||||
|   qosEnabled?: boolean; | ||||
|   ipSetsConfigured?: { | ||||
|     name: string; | ||||
|     elementCount: number; | ||||
|     type: string; | ||||
|   }[]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * NfTablesProxy sets up nftables NAT rules to forward TCP traffic. | ||||
|  * Enhanced with multi-port support, IPv6, connection tracking, metrics, | ||||
|  * and more advanced features. | ||||
|  */ | ||||
| export class NfTablesProxy { | ||||
|   public settings: INfTableProxySettings; | ||||
|   public settings: NfTableProxyOptions; | ||||
|   private rules: NfTablesRule[] = []; | ||||
|   private ipSets: Map<string, string[]> = new Map(); // Store IP sets for tracking
 | ||||
|   private ruleTag: string; | ||||
| @@ -146,7 +44,7 @@ export class NfTablesProxy { | ||||
|   private tempFilePath: string; | ||||
|   private static NFT_CMD = 'nft'; | ||||
| 
 | ||||
|   constructor(settings: INfTableProxySettings) { | ||||
|   constructor(settings: NfTableProxyOptions) { | ||||
|     // Validate inputs to prevent command injection
 | ||||
|     this.validateSettings(settings); | ||||
|      | ||||
| @@ -199,9 +97,9 @@ export class NfTablesProxy { | ||||
|   /** | ||||
|    * Validates settings to prevent command injection and ensure valid values | ||||
|    */ | ||||
|   private validateSettings(settings: INfTableProxySettings): void { | ||||
|   private validateSettings(settings: NfTableProxyOptions): void { | ||||
|     // Validate port numbers
 | ||||
|     const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => { | ||||
|     const validatePorts = (port: number | PortRange | Array<number | PortRange>) => { | ||||
|       if (Array.isArray(port)) { | ||||
|         port.forEach(p => validatePorts(p)); | ||||
|         return; | ||||
| @@ -275,8 +173,8 @@ export class NfTablesProxy { | ||||
|   /** | ||||
|    * Normalizes port specifications into an array of port ranges | ||||
|    */ | ||||
|   private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] { | ||||
|     const result: IPortRange[] = []; | ||||
|   private normalizePortSpec(portSpec: number | PortRange | Array<number | PortRange>): PortRange[] { | ||||
|     const result: PortRange[] = []; | ||||
|      | ||||
|     if (Array.isArray(portSpec)) { | ||||
|       // If it's an array, process each element
 | ||||
| @@ -687,7 +585,7 @@ export class NfTablesProxy { | ||||
|   /** | ||||
|    * Gets a comma-separated list of all ports from a port specification | ||||
|    */ | ||||
|   private getAllPorts(portSpec: number | IPortRange | Array<number | IPortRange>): string { | ||||
|   private getAllPorts(portSpec: number | PortRange | Array<number | PortRange>): string { | ||||
|     const portRanges = this.normalizePortSpec(portSpec); | ||||
|     const ports: string[] = []; | ||||
|      | ||||
| @@ -842,8 +740,8 @@ export class NfTablesProxy { | ||||
|     family: string, | ||||
|     preroutingChain: string, | ||||
|     postroutingChain: string, | ||||
|     fromPortRanges: IPortRange[], | ||||
|     toPortRange: IPortRange | ||||
|     fromPortRanges: PortRange[], | ||||
|     toPortRange: PortRange | ||||
|   ): Promise<boolean> { | ||||
|     try { | ||||
|       let rulesetContent = ''; | ||||
| @@ -958,8 +856,8 @@ export class NfTablesProxy { | ||||
|     family: string, | ||||
|     preroutingChain: string, | ||||
|     postroutingChain: string, | ||||
|     fromPortRanges: IPortRange[], | ||||
|     toPortRanges: IPortRange[] | ||||
|     fromPortRanges: PortRange[], | ||||
|     toPortRanges: PortRange[] | ||||
|   ): Promise<boolean> { | ||||
|     try { | ||||
|       let rulesetContent = ''; | ||||
| @@ -1410,8 +1308,8 @@ export class NfTablesProxy { | ||||
|   /** | ||||
|    * Get detailed status about the current state of the proxy | ||||
|    */ | ||||
|   public async getStatus(): Promise<INfTablesStatus> { | ||||
|     const result: INfTablesStatus = { | ||||
|   public async getStatus(): Promise<NfTablesStatus> { | ||||
|     const result: NfTablesStatus = { | ||||
|       active: this.rules.some(r => r.added), | ||||
|       ruleCount: { | ||||
|         total: this.rules.length, | ||||
| @@ -1,25 +1,25 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   IConnectionRecord, | ||||
|   IDomainConfig, | ||||
|   ISmartProxyOptions, | ||||
| } from './classes.pp.interfaces.js'; | ||||
| import { ConnectionManager } from './classes.pp.connectionmanager.js'; | ||||
| import { SecurityManager } from './classes.pp.securitymanager.js'; | ||||
| import { DomainConfigManager } from './classes.pp.domainconfigmanager.js'; | ||||
| import { TlsManager } from './classes.pp.tlsmanager.js'; | ||||
| import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; | ||||
| import { TimeoutManager } from './classes.pp.timeoutmanager.js'; | ||||
| import { PortRangeManager } from './classes.pp.portrangemanager.js'; | ||||
| import type { IForwardingHandler } from './types/forwarding.types.js'; | ||||
| import type { ForwardingType } from './types/forwarding.types.js'; | ||||
|   ConnectionRecord, | ||||
|   DomainConfig, | ||||
|   SmartProxyOptions, | ||||
| } from './models/interfaces.js'; | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { DomainConfigManager } from './domain-config-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 type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; | ||||
| import type { ForwardingType } from '../../forwarding/config/forwarding-types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Handles new connection processing and setup logic | ||||
|  */ | ||||
| export class ConnectionHandler { | ||||
|   constructor( | ||||
|     private settings: ISmartProxyOptions, | ||||
|     private settings: SmartProxyOptions, | ||||
|     private connectionManager: ConnectionManager, | ||||
|     private securityManager: SecurityManager, | ||||
|     private domainConfigManager: DomainConfigManager, | ||||
| @@ -102,7 +102,7 @@ export class ConnectionHandler { | ||||
|    */ | ||||
|   private handleNetworkProxyConnection( | ||||
|     socket: plugins.net.Socket, | ||||
|     record: IConnectionRecord | ||||
|     record: ConnectionRecord | ||||
|   ): void { | ||||
|     const connectionId = record.id; | ||||
|     let initialDataReceived = false; | ||||
| @@ -307,7 +307,7 @@ export class ConnectionHandler { | ||||
|   /** | ||||
|    * Handle a standard (non-NetworkProxy) connection | ||||
|    */ | ||||
|   private handleStandardConnection(socket: plugins.net.Socket, record: IConnectionRecord): void { | ||||
|   private handleStandardConnection(socket: plugins.net.Socket, record: ConnectionRecord): void { | ||||
|     const connectionId = record.id; | ||||
|     const localPort = record.localPort; | ||||
| 
 | ||||
| @@ -382,7 +382,7 @@ export class ConnectionHandler { | ||||
|     const setupConnection = ( | ||||
|       serverName: string, | ||||
|       initialChunk?: Buffer, | ||||
|       forcedDomain?: IDomainConfig, | ||||
|       forcedDomain?: DomainConfig, | ||||
|       overridePort?: number | ||||
|     ) => { | ||||
|       // Clear the initial timeout since we've received data
 | ||||
| @@ -730,8 +730,8 @@ export class ConnectionHandler { | ||||
|    */ | ||||
|   private setupDirectConnection( | ||||
|     socket: plugins.net.Socket, | ||||
|     record: IConnectionRecord, | ||||
|     domainConfig?: IDomainConfig, | ||||
|     record: ConnectionRecord, | ||||
|     domainConfig?: DomainConfig, | ||||
|     serverName?: string, | ||||
|     initialChunk?: Buffer, | ||||
|     overridePort?: number | ||||
| @@ -1,20 +1,20 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IConnectionRecord, ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import { SecurityManager } from './classes.pp.securitymanager.js'; | ||||
| import { TimeoutManager } from './classes.pp.timeoutmanager.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { ConnectionRecord, SmartProxyOptions } from './models/interfaces.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { TimeoutManager } from './timeout-manager.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages connection lifecycle, tracking, and cleanup | ||||
|  */ | ||||
| export class ConnectionManager { | ||||
|   private connectionRecords: Map<string, IConnectionRecord> = new Map(); | ||||
|   private connectionRecords: Map<string, ConnectionRecord> = new Map(); | ||||
|   private terminationStats: { | ||||
|     incoming: Record<string, number>; | ||||
|     outgoing: Record<string, number>; | ||||
|   } = { incoming: {}, outgoing: {} }; | ||||
|    | ||||
| 
 | ||||
|   constructor( | ||||
|     private settings: ISmartProxyOptions, | ||||
|     private settings: SmartProxyOptions, | ||||
|     private securityManager: SecurityManager, | ||||
|     private timeoutManager: TimeoutManager | ||||
|   ) {} | ||||
| @@ -30,12 +30,12 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Create and track a new connection | ||||
|    */ | ||||
|   public createConnection(socket: plugins.net.Socket): IConnectionRecord { | ||||
|   public createConnection(socket: plugins.net.Socket): ConnectionRecord { | ||||
|     const connectionId = this.generateConnectionId(); | ||||
|     const remoteIP = socket.remoteAddress || ''; | ||||
|     const localPort = socket.localPort || 0; | ||||
| 
 | ||||
|     const record: IConnectionRecord = { | ||||
|     const record: ConnectionRecord = { | ||||
|       id: connectionId, | ||||
|       incoming: socket, | ||||
|       outgoing: null, | ||||
| @@ -66,22 +66,22 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Track an existing connection | ||||
|    */ | ||||
|   public trackConnection(connectionId: string, record: IConnectionRecord): void { | ||||
|   public trackConnection(connectionId: string, record: ConnectionRecord): void { | ||||
|     this.connectionRecords.set(connectionId, record); | ||||
|     this.securityManager.trackConnectionByIP(record.remoteIP, connectionId); | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   /** | ||||
|    * Get a connection by ID | ||||
|    */ | ||||
|   public getConnection(connectionId: string): IConnectionRecord | undefined { | ||||
|   public getConnection(connectionId: string): ConnectionRecord | undefined { | ||||
|     return this.connectionRecords.get(connectionId); | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   /** | ||||
|    * Get all active connections | ||||
|    */ | ||||
|   public getConnections(): Map<string, IConnectionRecord> { | ||||
|   public getConnections(): Map<string, ConnectionRecord> { | ||||
|     return this.connectionRecords; | ||||
|   } | ||||
|    | ||||
| @@ -95,7 +95,7 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Initiates cleanup once for a connection | ||||
|    */ | ||||
|   public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void { | ||||
|   public initiateCleanupOnce(record: ConnectionRecord, reason: string = 'normal'): void { | ||||
|     if (this.settings.enableDetailedLogging) { | ||||
|       console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`); | ||||
|     } | ||||
| @@ -114,7 +114,7 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Clean up a connection record | ||||
|    */ | ||||
|   public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void { | ||||
|   public cleanupConnection(record: ConnectionRecord, reason: string = 'normal'): void { | ||||
|     if (!record.connectionClosed) { | ||||
|       record.connectionClosed = true; | ||||
| 
 | ||||
| @@ -178,7 +178,7 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Helper method to clean up a socket | ||||
|    */ | ||||
|   private cleanupSocket(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void { | ||||
|   private cleanupSocket(record: ConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void { | ||||
|     try { | ||||
|       if (!socket.destroyed) { | ||||
|         // Try graceful shutdown first, then force destroy after a short timeout
 | ||||
| @@ -213,7 +213,7 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Creates a generic error handler for incoming or outgoing sockets | ||||
|    */ | ||||
|   public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) { | ||||
|   public handleError(side: 'incoming' | 'outgoing', record: ConnectionRecord) { | ||||
|     return (err: Error) => { | ||||
|       const code = (err as any).code; | ||||
|       let reason = 'error'; | ||||
| @@ -256,7 +256,7 @@ export class ConnectionManager { | ||||
|   /** | ||||
|    * Creates a generic close handler for incoming or outgoing sockets | ||||
|    */ | ||||
|   public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) { | ||||
|   public handleClose(side: 'incoming' | 'outgoing', record: ConnectionRecord) { | ||||
|     return () => { | ||||
|       if (this.settings.enableDetailedLogging) { | ||||
|         console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`); | ||||
| @@ -1,24 +1,25 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import type { ForwardingType, IForwardConfig, IForwardingHandler } from './types/forwarding.types.js'; | ||||
| import { ForwardingHandlerFactory } from './forwarding/forwarding.factory.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { DomainConfig, SmartProxyOptions } from './models/interfaces.js'; | ||||
| import type { ForwardingType, ForwardConfig } from '../../forwarding/config/forwarding-types.js'; | ||||
| import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; | ||||
| import { ForwardingHandlerFactory } from '../../forwarding/factory/forwarding-factory.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages domain configurations and target selection | ||||
|  */ | ||||
| export class DomainConfigManager { | ||||
|   // Track round-robin indices for domain configs
 | ||||
|   private domainTargetIndices: Map<IDomainConfig, number> = new Map(); | ||||
|   private domainTargetIndices: Map<DomainConfig, number> = new Map(); | ||||
| 
 | ||||
|   // Cache forwarding handlers for each domain config
 | ||||
|   private forwardingHandlers: Map<IDomainConfig, IForwardingHandler> = new Map(); | ||||
|   private forwardingHandlers: Map<DomainConfig, ForwardingHandler> = new Map(); | ||||
| 
 | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
|   constructor(private settings: SmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Updates the domain configurations | ||||
|    */ | ||||
|   public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void { | ||||
|   public updateDomainConfigs(newDomainConfigs: DomainConfig[]): void { | ||||
|     this.settings.domainConfigs = newDomainConfigs; | ||||
| 
 | ||||
|     // Reset target indices for removed configs
 | ||||
| @@ -30,7 +31,7 @@ export class DomainConfigManager { | ||||
|     } | ||||
| 
 | ||||
|     // Clear handlers for removed configs and create handlers for new configs
 | ||||
|     const handlersToRemove: IDomainConfig[] = []; | ||||
|     const handlersToRemove: DomainConfig[] = []; | ||||
|     for (const [config] of this.forwardingHandlers) { | ||||
|       if (!currentConfigSet.has(config)) { | ||||
|         handlersToRemove.push(config); | ||||
| @@ -58,14 +59,14 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Get all domain configurations | ||||
|    */ | ||||
|   public getDomainConfigs(): IDomainConfig[] { | ||||
|   public getDomainConfigs(): DomainConfig[] { | ||||
|     return this.settings.domainConfigs; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Find domain config matching a server name | ||||
|    */ | ||||
|   public findDomainConfig(serverName: string): IDomainConfig | undefined { | ||||
|   public findDomainConfig(serverName: string): DomainConfig | undefined { | ||||
|     if (!serverName) return undefined; | ||||
|      | ||||
|     return this.settings.domainConfigs.find((config) => | ||||
| @@ -76,7 +77,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Find domain config for a specific port | ||||
|    */ | ||||
|   public findDomainConfigForPort(port: number): IDomainConfig | undefined { | ||||
|   public findDomainConfigForPort(port: number): DomainConfig | undefined { | ||||
|     return this.settings.domainConfigs.find( | ||||
|       (domain) => { | ||||
|         const portRanges = domain.forwarding?.advanced?.portRanges; | ||||
| @@ -97,7 +98,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Get target IP with round-robin support | ||||
|    */ | ||||
|   public getTargetIP(domainConfig: IDomainConfig): string { | ||||
|   public getTargetIP(domainConfig: DomainConfig): string { | ||||
|     const targetHosts = Array.isArray(domainConfig.forwarding.target.host) | ||||
|       ? domainConfig.forwarding.target.host | ||||
|       : [domainConfig.forwarding.target.host]; | ||||
| @@ -116,21 +117,21 @@ export class DomainConfigManager { | ||||
|    * 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 { | ||||
|   public getTargetHost(domainConfig: DomainConfig): string { | ||||
|     return this.getTargetIP(domainConfig); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get target port from domain config | ||||
|    */ | ||||
|   public getTargetPort(domainConfig: IDomainConfig, defaultPort: number): number { | ||||
|   public getTargetPort(domainConfig: DomainConfig, defaultPort: number): number { | ||||
|     return domainConfig.forwarding.target.port || defaultPort; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Checks if a domain should use NetworkProxy | ||||
|    */ | ||||
|   public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean { | ||||
|   public shouldUseNetworkProxy(domainConfig: DomainConfig): boolean { | ||||
|     const forwardingType = this.getForwardingType(domainConfig); | ||||
|     return forwardingType === 'https-terminate-to-http' || | ||||
|            forwardingType === 'https-terminate-to-https'; | ||||
| @@ -139,7 +140,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Gets the NetworkProxy port for a domain | ||||
|    */ | ||||
|   public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined { | ||||
|   public getNetworkProxyPort(domainConfig: DomainConfig): number | undefined { | ||||
|     // First check if we should use NetworkProxy at all
 | ||||
|     if (!this.shouldUseNetworkProxy(domainConfig)) { | ||||
|       return undefined; | ||||
| @@ -154,7 +155,7 @@ export class DomainConfigManager { | ||||
|    * This method combines domain-specific security rules from the forwarding configuration | ||||
|    * with global security defaults when necessary. | ||||
|    */ | ||||
|   public getEffectiveIPRules(domainConfig: IDomainConfig): { | ||||
|   public getEffectiveIPRules(domainConfig: DomainConfig): { | ||||
|     allowedIPs: string[], | ||||
|     blockedIPs: string[] | ||||
|   } { | ||||
| @@ -200,7 +201,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Get connection timeout for a domain | ||||
|    */ | ||||
|   public getConnectionTimeout(domainConfig?: IDomainConfig): number { | ||||
|   public getConnectionTimeout(domainConfig?: DomainConfig): number { | ||||
|     if (domainConfig?.forwarding.advanced?.timeout) { | ||||
|       return domainConfig.forwarding.advanced.timeout; | ||||
|     } | ||||
| @@ -211,7 +212,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Creates a forwarding handler for a domain configuration | ||||
|    */ | ||||
|   private createForwardingHandler(domainConfig: IDomainConfig): IForwardingHandler { | ||||
|   private createForwardingHandler(domainConfig: DomainConfig): ForwardingHandler { | ||||
|     // Create a new handler using the factory
 | ||||
|     const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding); | ||||
| 
 | ||||
| @@ -227,7 +228,7 @@ export class DomainConfigManager { | ||||
|    * Gets a forwarding handler for a domain config | ||||
|    * If no handler exists, creates one | ||||
|    */ | ||||
|   public getForwardingHandler(domainConfig: IDomainConfig): IForwardingHandler { | ||||
|   public getForwardingHandler(domainConfig: DomainConfig): ForwardingHandler { | ||||
|     // If we already have a handler, return it
 | ||||
|     if (this.forwardingHandlers.has(domainConfig)) { | ||||
|       return this.forwardingHandlers.get(domainConfig)!; | ||||
| @@ -243,7 +244,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Gets the forwarding type for a domain config | ||||
|    */ | ||||
|   public getForwardingType(domainConfig?: IDomainConfig): ForwardingType | undefined { | ||||
|   public getForwardingType(domainConfig?: DomainConfig): ForwardingType | undefined { | ||||
|     if (!domainConfig?.forwarding) return undefined; | ||||
|     return domainConfig.forwarding.type; | ||||
|   } | ||||
| @@ -251,7 +252,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Checks if the forwarding type requires TLS termination | ||||
|    */ | ||||
|   public requiresTlsTermination(domainConfig?: IDomainConfig): boolean { | ||||
|   public requiresTlsTermination(domainConfig?: DomainConfig): boolean { | ||||
|     if (!domainConfig) return false; | ||||
| 
 | ||||
|     const forwardingType = this.getForwardingType(domainConfig); | ||||
| @@ -262,7 +263,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Checks if the forwarding type supports HTTP | ||||
|    */ | ||||
|   public supportsHttp(domainConfig?: IDomainConfig): boolean { | ||||
|   public supportsHttp(domainConfig?: DomainConfig): boolean { | ||||
|     if (!domainConfig) return false; | ||||
| 
 | ||||
|     const forwardingType = this.getForwardingType(domainConfig); | ||||
| @@ -284,7 +285,7 @@ export class DomainConfigManager { | ||||
|   /** | ||||
|    * Checks if HTTP requests should be redirected to HTTPS | ||||
|    */ | ||||
|   public shouldRedirectToHttps(domainConfig?: IDomainConfig): boolean { | ||||
|   public shouldRedirectToHttps(domainConfig?: DomainConfig): boolean { | ||||
|     if (!domainConfig?.forwarding) return false; | ||||
| 
 | ||||
|     // Only check for redirect if HTTP is enabled
 | ||||
| @@ -4,5 +4,15 @@ | ||||
| // Re-export models | ||||
| export * from './models/index.js'; | ||||
|  | ||||
| // Core SmartProxy will be added later: | ||||
| // export { SmartProxy } from './smart-proxy.js'; | ||||
| // Export the main SmartProxy class | ||||
| export { SmartProxy } from './smart-proxy.js'; | ||||
|  | ||||
| // Export 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'; | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { NetworkProxy } from '../networkproxy/classes.np.networkproxy.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { Port80HandlerEvents } from '../common/types.js'; | ||||
| import { subscribeToPort80Handler } from '../common/eventUtils.js'; | ||||
| import type { CertificateData } from '../certificate/models/certificate-types.js'; | ||||
| import type { IConnectionRecord, ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js'; | ||||
| 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 { CertificateData } from '../../certificate/models/certificate-types.js'; | ||||
| import type { ConnectionRecord, SmartProxyOptions, DomainConfig } from './models/interfaces.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages NetworkProxy integration for TLS termination | ||||
| @@ -12,8 +12,8 @@ import type { IConnectionRecord, ISmartProxyOptions, IDomainConfig } from './cla | ||||
| export class NetworkProxyBridge { | ||||
|   private networkProxy: NetworkProxy | null = null; | ||||
|   private port80Handler: Port80Handler | null = null; | ||||
|    | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
| 
 | ||||
|   constructor(private settings: SmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Set the Port80Handler to use for certificate management | ||||
| @@ -183,7 +183,7 @@ export class NetworkProxyBridge { | ||||
|   public forwardToNetworkProxy( | ||||
|     connectionId: string, | ||||
|     socket: plugins.net.Socket, | ||||
|     record: IConnectionRecord, | ||||
|     record: ConnectionRecord, | ||||
|     initialData: Buffer, | ||||
|     customProxyPort?: number, | ||||
|     onError?: (reason: string) => void | ||||
| @@ -283,7 +283,7 @@ export class NetworkProxyBridge { | ||||
|       } | ||||
| 
 | ||||
|       // Convert domain configs to NetworkProxy configs
 | ||||
|       const proxyConfigs = this.networkProxy.convertPortProxyConfigs( | ||||
|       const proxyConfigs = this.networkProxy.convertSmartProxyConfigs( | ||||
|         this.settings.domainConfigs, | ||||
|         certPair | ||||
|       ); | ||||
| @@ -1,10 +1,10 @@ | ||||
| import type{ ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import type { SmartProxyOptions } from './models/interfaces.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages port ranges and port-based configuration | ||||
|  */ | ||||
| export class PortRangeManager { | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
|   constructor(private settings: SmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Get all ports that should be listened on | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { SmartProxyOptions } from './models/interfaces.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Handles security aspects like IP tracking, rate limiting, and authorization | ||||
| @@ -7,8 +7,8 @@ import type { ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| export class SecurityManager { | ||||
|   private connectionsByIP: Map<string, Set<string>> = new Map(); | ||||
|   private connectionRateByIP: Map<string, number[]> = new Map(); | ||||
|    | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
| 
 | ||||
|   constructor(private settings: SmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Get connections count by IP | ||||
| @@ -1,22 +1,27 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| 
 | ||||
| import { ConnectionManager } from './classes.pp.connectionmanager.js'; | ||||
| import { SecurityManager } from './classes.pp.securitymanager.js'; | ||||
| import { DomainConfigManager } from './classes.pp.domainconfigmanager.js'; | ||||
| import { TlsManager } from './classes.pp.tlsmanager.js'; | ||||
| import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; | ||||
| import { TimeoutManager } from './classes.pp.timeoutmanager.js'; | ||||
| import { PortRangeManager } from './classes.pp.portrangemanager.js'; | ||||
| import { ConnectionHandler } from './classes.pp.connectionhandler.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { CertProvisioner } from '../certificate/providers/cert-provisioner.js'; | ||||
| import type { CertificateData } from '../certificate/models/certificate-types.js'; | ||||
| import { buildPort80Handler } from '../certificate/acme/acme-factory.js'; | ||||
| import type { ForwardingType } from './types/forwarding.types.js'; | ||||
| import { createPort80HandlerOptions } from '../common/port80-adapter.js'; | ||||
| // Importing from the new structure
 | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { DomainConfigManager } from './domain-config-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 { ConnectionHandler } from './connection-handler.js'; | ||||
| 
 | ||||
| import type { ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js'; | ||||
| export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig }; | ||||
| // External dependencies from migrated modules
 | ||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||
| import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; | ||||
| import type { CertificateData } from '../../certificate/models/certificate-types.js'; | ||||
| import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; | ||||
| import type { ForwardingType } from '../../forwarding/config/forwarding-types.js'; | ||||
| import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; | ||||
| 
 | ||||
| // Import types from models
 | ||||
| import type { SmartProxyOptions, DomainConfig } from './models/interfaces.js'; | ||||
| // Provide backward compatibility types
 | ||||
| export type { SmartProxyOptions as IPortProxySettings, DomainConfig as IDomainConfig }; | ||||
| 
 | ||||
| /** | ||||
|  * SmartProxy - Main class that coordinates all components | ||||
| @@ -41,7 +46,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   // CertProvisioner for unified certificate workflows
 | ||||
|   private certProvisioner?: CertProvisioner; | ||||
|    | ||||
|   constructor(settingsArg: ISmartProxyOptions) { | ||||
|   constructor(settingsArg: SmartProxyOptions) { | ||||
|     super(); | ||||
|     // Set reasonable defaults for all settings
 | ||||
|     this.settings = { | ||||
| @@ -121,7 +126,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   /** | ||||
|    * The settings for the port proxy | ||||
|    */ | ||||
|   public settings: ISmartProxyOptions; | ||||
|   public settings: SmartProxyOptions; | ||||
|    | ||||
|   /** | ||||
|    * Initialize the Port80Handler for ACME certificate management | ||||
| @@ -154,7 +159,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   public async start() { | ||||
|     // Don't start if already shutting down
 | ||||
|     if (this.isShuttingDown) { | ||||
|       console.log("Cannot start PortProxy while it's shutting down"); | ||||
|       console.log("Cannot start SmartProxy while it's shutting down"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
| @@ -262,7 +267,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       server.listen(port, () => { | ||||
|         const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); | ||||
|         console.log( | ||||
|           `PortProxy -> OK: Now listening on port ${port}${ | ||||
|           `SmartProxy -> OK: Now listening on port ${port}${ | ||||
|             this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' | ||||
|           }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` | ||||
|         ); | ||||
| @@ -347,7 +352,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|    * Stop the proxy server | ||||
|    */ | ||||
|   public async stop() { | ||||
|     console.log('PortProxy shutting down...'); | ||||
|     console.log('SmartProxy shutting down...'); | ||||
|     this.isShuttingDown = true; | ||||
|     // Stop CertProvisioner if active
 | ||||
|     if (this.certProvisioner) { | ||||
| @@ -402,13 +407,13 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     // Clear all servers
 | ||||
|     this.netServers = []; | ||||
| 
 | ||||
|     console.log('PortProxy shutdown complete.'); | ||||
|     console.log('SmartProxy shutdown complete.'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Updates the domain configurations for the proxy | ||||
|    */ | ||||
|   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { | ||||
|   public async updateDomainConfigs(newDomainConfigs: DomainConfig[]): Promise<void> { | ||||
|     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); | ||||
| 
 | ||||
|     // Update domain configs in DomainConfigManager
 | ||||
| @@ -1,10 +1,10 @@ | ||||
| import type { IConnectionRecord, ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import type { ConnectionRecord, SmartProxyOptions } from './models/interfaces.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages timeouts and inactivity tracking for connections | ||||
|  */ | ||||
| export class TimeoutManager { | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
|   constructor(private settings: SmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Ensure timeout values don't exceed Node.js max safe integer | ||||
| @@ -28,7 +28,7 @@ export class TimeoutManager { | ||||
|   /** | ||||
|    * Update connection activity timestamp | ||||
|    */ | ||||
|   public updateActivity(record: IConnectionRecord): void { | ||||
|   public updateActivity(record: ConnectionRecord): void { | ||||
|     record.lastActivity = Date.now(); | ||||
| 
 | ||||
|     // Clear any inactivity warning
 | ||||
| @@ -40,7 +40,7 @@ export class TimeoutManager { | ||||
|   /** | ||||
|    * Calculate effective inactivity timeout based on connection type | ||||
|    */ | ||||
|   public getEffectiveInactivityTimeout(record: IConnectionRecord): number { | ||||
|   public getEffectiveInactivityTimeout(record: ConnectionRecord): number { | ||||
|     let effectiveTimeout = this.settings.inactivityTimeout || 14400000; // 4 hours default
 | ||||
|      | ||||
|     // For immortal keep-alive connections, use an extremely long timeout
 | ||||
| @@ -60,7 +60,7 @@ export class TimeoutManager { | ||||
|   /** | ||||
|    * Calculate effective max lifetime based on connection type | ||||
|    */ | ||||
|   public getEffectiveMaxLifetime(record: IConnectionRecord): number { | ||||
|   public getEffectiveMaxLifetime(record: ConnectionRecord): number { | ||||
|     // Use domain-specific timeout from forwarding.advanced if available
 | ||||
|     const baseTimeout = record.domainConfig?.forwarding?.advanced?.timeout || | ||||
|                         this.settings.maxConnectionLifetime || | ||||
| @@ -91,8 +91,8 @@ export class TimeoutManager { | ||||
|    * @returns The cleanup timer | ||||
|    */ | ||||
|   public setupConnectionTimeout( | ||||
|     record: IConnectionRecord,  | ||||
|     onTimeout: (record: IConnectionRecord, reason: string) => void | ||||
|     record: ConnectionRecord, | ||||
|     onTimeout: (record: ConnectionRecord, reason: string) => void | ||||
|   ): NodeJS.Timeout { | ||||
|     // Clear any existing timer
 | ||||
|     if (record.cleanupTimer) { | ||||
| @@ -120,7 +120,7 @@ export class TimeoutManager { | ||||
|    * Check for inactivity on a connection | ||||
|    * @returns Object with check results | ||||
|    */ | ||||
|   public checkInactivity(record: IConnectionRecord): { | ||||
|   public checkInactivity(record: ConnectionRecord): { | ||||
|     isInactive: boolean; | ||||
|     shouldWarn: boolean; | ||||
|     inactivityTime: number; | ||||
| @@ -169,7 +169,7 @@ export class TimeoutManager { | ||||
|   /** | ||||
|    * Apply socket timeout settings | ||||
|    */ | ||||
|   public applySocketTimeouts(record: IConnectionRecord): void { | ||||
|   public applySocketTimeouts(record: ConnectionRecord): void { | ||||
|     // Skip for immortal keep-alive connections
 | ||||
|     if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { | ||||
|       // Disable timeouts completely for immortal connections
 | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import { SniHandler } from '../tls/sni/sni-handler.js'; | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { SmartProxyOptions } from './models/interfaces.js'; | ||||
| import { SniHandler } from '../../tls/sni/sni-handler.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Interface for connection information used for SNI extraction | ||||
| @@ -16,7 +16,7 @@ interface IConnectionInfo { | ||||
|  * Manages TLS-related operations including SNI extraction and validation | ||||
|  */ | ||||
| export class TlsManager { | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
|   constructor(private settings: SmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Check if a data chunk appears to be a TLS handshake | ||||
							
								
								
									
										15
									
								
								ts/smartproxy/LEGACY_NOTICE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								ts/smartproxy/LEGACY_NOTICE.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # Legacy Notice | ||||
|  | ||||
| These files have been migrated to the new directory structure as part of the project restructuring. The new locations are as follows: | ||||
|  | ||||
| - `classes.smartproxy.ts` -> `/ts/proxies/smart-proxy/smart-proxy.ts` | ||||
| - `classes.pp.connectionmanager.ts` -> `/ts/proxies/smart-proxy/connection-manager.ts` | ||||
| - `classes.pp.securitymanager.ts` -> `/ts/proxies/smart-proxy/security-manager.ts` | ||||
| - `classes.pp.domainconfigmanager.ts` -> `/ts/proxies/smart-proxy/domain-config-manager.ts` | ||||
| - `classes.pp.tlsmanager.ts` -> `/ts/proxies/smart-proxy/tls-manager.ts` | ||||
| - `classes.pp.networkproxybridge.ts` -> `/ts/proxies/smart-proxy/network-proxy-bridge.ts` | ||||
| - `classes.pp.timeoutmanager.ts` -> `/ts/proxies/smart-proxy/timeout-manager.ts` | ||||
| - `classes.pp.portrangemanager.ts` -> `/ts/proxies/smart-proxy/port-range-manager.ts` | ||||
| - `classes.pp.connectionhandler.ts` -> `/ts/proxies/smart-proxy/connection-handler.ts` | ||||
|  | ||||
| This directory is now considered legacy and should not be used for new code. | ||||
| @@ -1,132 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IForwardConfig } from './forwarding/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Provision object for static or HTTP-01 certificate | ||||
|  */ | ||||
| export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | ||||
|  | ||||
| /** Domain configuration with forwarding configuration */ | ||||
| export interface IDomainConfig { | ||||
|   domains: string[]; // Glob patterns for domain(s) | ||||
|   forwarding: IForwardConfig; // Unified forwarding configuration | ||||
| } | ||||
|  | ||||
| /** Port proxy settings including global allowed port ranges */ | ||||
| import type { IAcmeOptions } from '../common/types.js'; | ||||
| export interface ISmartProxyOptions { | ||||
|   fromPort: number; | ||||
|   toPort: number; | ||||
|   targetIP?: string; // Global target host to proxy to, defaults to 'localhost' | ||||
|   domainConfigs: IDomainConfig[]; | ||||
|   sniEnabled?: boolean; | ||||
|   defaultAllowedIPs?: string[]; | ||||
|   defaultBlockedIPs?: string[]; | ||||
|   preserveSourceIP?: boolean; | ||||
|  | ||||
|   // TLS options | ||||
|   pfx?: Buffer; | ||||
|   key?: string | Buffer | Array<Buffer | string>; | ||||
|   passphrase?: string; | ||||
|   cert?: string | Buffer | Array<string | Buffer>; | ||||
|   ca?: string | Buffer | Array<string | Buffer>; | ||||
|   ciphers?: string; | ||||
|   honorCipherOrder?: boolean; | ||||
|   rejectUnauthorized?: boolean; | ||||
|   secureProtocol?: string; | ||||
|   servername?: string; | ||||
|   minVersion?: string; | ||||
|   maxVersion?: string; | ||||
|  | ||||
|   // Timeout settings | ||||
|   initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) | ||||
|   socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) | ||||
|   inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) | ||||
|   maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h) | ||||
|   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) | ||||
|   keepAlive?: boolean; // Enable TCP keepalive (default: true) | ||||
|   keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms) | ||||
|   maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup | ||||
|  | ||||
|   // Enhanced features | ||||
|   disableInactivityCheck?: boolean; // Disable inactivity checking entirely | ||||
|   enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes | ||||
|   enableDetailedLogging?: boolean; // Enable detailed connection logging | ||||
|   enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging | ||||
|   enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd | ||||
|   allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true) | ||||
|  | ||||
|   // Rate limiting and security | ||||
|   maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP | ||||
|   connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP | ||||
|  | ||||
|   // Enhanced keep-alive settings | ||||
|   keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections | ||||
|   keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections | ||||
|   extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) | ||||
|  | ||||
|   // NetworkProxy integration | ||||
|   useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy | ||||
|   networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443) | ||||
|  | ||||
|   // ACME configuration options for SmartProxy | ||||
|   acme?: IAcmeOptions; | ||||
|    | ||||
|   /** | ||||
|    * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges, | ||||
|    * or a static certificate object for immediate provisioning. | ||||
|    */ | ||||
|   certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Enhanced connection record | ||||
|  */ | ||||
| export interface IConnectionRecord { | ||||
|   id: string; // Unique connection identifier | ||||
|   incoming: plugins.net.Socket; | ||||
|   outgoing: plugins.net.Socket | null; | ||||
|   incomingStartTime: number; | ||||
|   outgoingStartTime?: number; | ||||
|   outgoingClosedTime?: number; | ||||
|   lockedDomain?: string; // Used to lock this connection to the initial SNI | ||||
|   connectionClosed: boolean; // Flag to prevent multiple cleanup attempts | ||||
|   cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity | ||||
|   alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert | ||||
|   lastActivity: number; // Last activity timestamp for inactivity detection | ||||
|   pendingData: Buffer[]; // Buffer to hold data during connection setup | ||||
|   pendingDataSize: number; // Track total size of pending data | ||||
|  | ||||
|   // Enhanced tracking fields | ||||
|   bytesReceived: number; // Total bytes received | ||||
|   bytesSent: number; // Total bytes sent | ||||
|   remoteIP: string; // Remote IP (cached for logging after socket close) | ||||
|   localPort: number; // Local port (cached for logging) | ||||
|   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 | ||||
|  | ||||
|   // Keep-alive tracking | ||||
|   hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection | ||||
|   inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued | ||||
|   incomingTerminationReason?: string | null; // Reason for incoming termination | ||||
|   outgoingTerminationReason?: string | null; // Reason for outgoing termination | ||||
|  | ||||
|   // NetworkProxy tracking | ||||
|   usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy | ||||
|  | ||||
|   // Renegotiation handler | ||||
|   renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection | ||||
|  | ||||
|   // Browser connection tracking | ||||
|   isBrowserConnection?: boolean; // Whether this connection appears to be from a browser | ||||
|   domainSwitches?: number; // Number of times the domain has been switched on this connection | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Domain configuration with unified forwarding configuration | ||||
|  */ | ||||
| export interface IDomainConfig { | ||||
|   // Core properties - domain patterns | ||||
|   domains: string[]; | ||||
|  | ||||
|   // Unified forwarding configuration | ||||
|   forwarding: IForwardConfig; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Helper function to create a domain configuration | ||||
|  */ | ||||
| export function createDomainConfig( | ||||
|   domains: string | string[], | ||||
|   forwarding: IForwardConfig | ||||
| ): IDomainConfig { | ||||
|   // Normalize domains to an array | ||||
|   const domainArray = Array.isArray(domains) ? domains : [domains]; | ||||
|  | ||||
|   return { | ||||
|     domains: domainArray, | ||||
|     forwarding | ||||
|   }; | ||||
| } | ||||
| @@ -1,283 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { IDomainConfig } from './domain-config.js'; | ||||
| import type { IForwardingHandler } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerFactory } from './forwarding.factory.js'; | ||||
|  | ||||
| /** | ||||
|  * Events emitted by the DomainManager | ||||
|  */ | ||||
| export enum DomainManagerEvents { | ||||
|   DOMAIN_ADDED = 'domain-added', | ||||
|   DOMAIN_REMOVED = 'domain-removed', | ||||
|   DOMAIN_MATCHED = 'domain-matched', | ||||
|   DOMAIN_MATCH_FAILED = 'domain-match-failed', | ||||
|   CERTIFICATE_NEEDED = 'certificate-needed', | ||||
|   CERTIFICATE_LOADED = 'certificate-loaded', | ||||
|   ERROR = 'error' | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Manages domains and their forwarding handlers | ||||
|  */ | ||||
| export class DomainManager extends plugins.EventEmitter { | ||||
|   private domainConfigs: IDomainConfig[] = []; | ||||
|   private domainHandlers: Map<string, IForwardingHandler> = new Map(); | ||||
|    | ||||
|   /** | ||||
|    * Create a new DomainManager | ||||
|    * @param initialDomains Optional initial domain configurations | ||||
|    */ | ||||
|   constructor(initialDomains?: IDomainConfig[]) { | ||||
|     super(); | ||||
|      | ||||
|     if (initialDomains) { | ||||
|       this.setDomainConfigs(initialDomains); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set or replace all domain configurations | ||||
|    * @param configs Array of domain configurations | ||||
|    */ | ||||
|   public async setDomainConfigs(configs: IDomainConfig[]): Promise<void> { | ||||
|     // Clear existing handlers | ||||
|     this.domainHandlers.clear(); | ||||
|      | ||||
|     // Store new configurations | ||||
|     this.domainConfigs = [...configs]; | ||||
|      | ||||
|     // Initialize handlers for each domain | ||||
|     for (const config of this.domainConfigs) { | ||||
|       await this.createHandlersForDomain(config); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add a new domain configuration | ||||
|    * @param config The domain configuration to add | ||||
|    */ | ||||
|   public async addDomainConfig(config: IDomainConfig): Promise<void> { | ||||
|     // Check if any of these domains already exist | ||||
|     for (const domain of config.domains) { | ||||
|       if (this.domainHandlers.has(domain)) { | ||||
|         // Remove existing handler for this domain | ||||
|         this.domainHandlers.delete(domain); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add the new configuration | ||||
|     this.domainConfigs.push(config); | ||||
|      | ||||
|     // Create handlers for the new domain | ||||
|     await this.createHandlersForDomain(config); | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_ADDED, { | ||||
|       domains: config.domains, | ||||
|       forwardingType: config.forwarding.type | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove a domain configuration | ||||
|    * @param domain The domain to remove | ||||
|    * @returns True if the domain was found and removed | ||||
|    */ | ||||
|   public removeDomainConfig(domain: string): boolean { | ||||
|     // Find the config that includes this domain | ||||
|     const index = this.domainConfigs.findIndex(config =>  | ||||
|       config.domains.includes(domain) | ||||
|     ); | ||||
|      | ||||
|     if (index === -1) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Get the config | ||||
|     const config = this.domainConfigs[index]; | ||||
|      | ||||
|     // Remove all handlers for this config | ||||
|     for (const domainName of config.domains) { | ||||
|       this.domainHandlers.delete(domainName); | ||||
|     } | ||||
|      | ||||
|     // Remove the config | ||||
|     this.domainConfigs.splice(index, 1); | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_REMOVED, { | ||||
|       domains: config.domains | ||||
|     }); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Find the handler for a domain | ||||
|    * @param domain The domain to find a handler for | ||||
|    * @returns The handler or undefined if no match | ||||
|    */ | ||||
|   public findHandlerForDomain(domain: string): IForwardingHandler | undefined { | ||||
|     // Try exact match | ||||
|     if (this.domainHandlers.has(domain)) { | ||||
|       return this.domainHandlers.get(domain); | ||||
|     } | ||||
|      | ||||
|     // Try wildcard matches | ||||
|     const wildcardHandler = this.findWildcardHandler(domain); | ||||
|     if (wildcardHandler) { | ||||
|       return wildcardHandler; | ||||
|     } | ||||
|      | ||||
|     // No match found | ||||
|     return undefined; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a connection for a domain | ||||
|    * @param domain The domain | ||||
|    * @param socket The client socket | ||||
|    * @returns True if the connection was handled | ||||
|    */ | ||||
|   public handleConnection(domain: string, socket: plugins.net.Socket): boolean { | ||||
|     const handler = this.findHandlerForDomain(domain); | ||||
|      | ||||
|     if (!handler) { | ||||
|       this.emit(DomainManagerEvents.DOMAIN_MATCH_FAILED, { | ||||
|         domain, | ||||
|         remoteAddress: socket.remoteAddress | ||||
|       }); | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_MATCHED, { | ||||
|       domain, | ||||
|       handlerType: handler.constructor.name, | ||||
|       remoteAddress: socket.remoteAddress | ||||
|     }); | ||||
|      | ||||
|     // Handle the connection | ||||
|     handler.handleConnection(socket); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request for a domain | ||||
|    * @param domain The domain | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    * @returns True if the request was handled | ||||
|    */ | ||||
|   public handleHttpRequest(domain: string, req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): boolean { | ||||
|     const handler = this.findHandlerForDomain(domain); | ||||
|      | ||||
|     if (!handler) { | ||||
|       this.emit(DomainManagerEvents.DOMAIN_MATCH_FAILED, { | ||||
|         domain, | ||||
|         remoteAddress: req.socket.remoteAddress | ||||
|       }); | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_MATCHED, { | ||||
|       domain, | ||||
|       handlerType: handler.constructor.name, | ||||
|       remoteAddress: req.socket.remoteAddress | ||||
|     }); | ||||
|      | ||||
|     // Handle the request | ||||
|     handler.handleHttpRequest(req, res); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create handlers for a domain configuration | ||||
|    * @param config The domain configuration | ||||
|    */ | ||||
|   private async createHandlersForDomain(config: IDomainConfig): Promise<void> { | ||||
|     try { | ||||
|       // Create a handler for this forwarding configuration | ||||
|       const handler = ForwardingHandlerFactory.createHandler(config.forwarding); | ||||
|        | ||||
|       // Initialize the handler | ||||
|       await handler.initialize(); | ||||
|        | ||||
|       // Set up event forwarding | ||||
|       this.setupHandlerEvents(handler, config); | ||||
|        | ||||
|       // Store the handler for each domain in the config | ||||
|       for (const domain of config.domains) { | ||||
|         this.domainHandlers.set(domain, handler); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       this.emit(DomainManagerEvents.ERROR, { | ||||
|         domains: config.domains, | ||||
|         error: error instanceof Error ? error.message : String(error) | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set up event forwarding from a handler | ||||
|    * @param handler The handler | ||||
|    * @param config The domain configuration for this handler | ||||
|    */ | ||||
|   private setupHandlerEvents(handler: IForwardingHandler, config: IDomainConfig): void { | ||||
|     // Forward relevant events | ||||
|     handler.on(ForwardingHandlerEvents.CERTIFICATE_NEEDED, (data) => { | ||||
|       this.emit(DomainManagerEvents.CERTIFICATE_NEEDED, { | ||||
|         ...data, | ||||
|         domains: config.domains | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     handler.on(ForwardingHandlerEvents.CERTIFICATE_LOADED, (data) => { | ||||
|       this.emit(DomainManagerEvents.CERTIFICATE_LOADED, { | ||||
|         ...data, | ||||
|         domains: config.domains | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     handler.on(ForwardingHandlerEvents.ERROR, (data) => { | ||||
|       this.emit(DomainManagerEvents.ERROR, { | ||||
|         ...data, | ||||
|         domains: config.domains | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Find a handler for a domain using wildcard matching | ||||
|    * @param domain The domain to find a handler for | ||||
|    * @returns The handler or undefined if no match | ||||
|    */ | ||||
|   private findWildcardHandler(domain: string): IForwardingHandler | undefined { | ||||
|     // Exact match already checked in findHandlerForDomain | ||||
|      | ||||
|     // Try subdomain wildcard (*.example.com) | ||||
|     if (domain.includes('.')) { | ||||
|       const parts = domain.split('.'); | ||||
|       if (parts.length > 2) { | ||||
|         const wildcardDomain = `*.${parts.slice(1).join('.')}`; | ||||
|         if (this.domainHandlers.has(wildcardDomain)) { | ||||
|           return this.domainHandlers.get(wildcardDomain); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Try full wildcard | ||||
|     if (this.domainHandlers.has('*')) { | ||||
|       return this.domainHandlers.get('*'); | ||||
|     } | ||||
|      | ||||
|     // No match found | ||||
|     return undefined; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all domain configurations | ||||
|    * @returns Array of domain configurations | ||||
|    */ | ||||
|   public getDomainConfigs(): IDomainConfig[] { | ||||
|     return [...this.domainConfigs]; | ||||
|   } | ||||
| } | ||||
| @@ -1,155 +0,0 @@ | ||||
| import type { IForwardConfig, IForwardingHandler } from '../types/forwarding.types.js'; | ||||
| import { HttpForwardingHandler } from './http.handler.js'; | ||||
| import { HttpsPassthroughHandler } from './https-passthrough.handler.js'; | ||||
| import { HttpsTerminateToHttpHandler } from './https-terminate-to-http.handler.js'; | ||||
| import { HttpsTerminateToHttpsHandler } from './https-terminate-to-https.handler.js'; | ||||
|  | ||||
| /** | ||||
|  * Factory for creating forwarding handlers based on the configuration type | ||||
|  */ | ||||
| export class ForwardingHandlerFactory { | ||||
|   /** | ||||
|    * Create a forwarding handler based on the configuration | ||||
|    * @param config The forwarding configuration | ||||
|    * @returns The appropriate forwarding handler | ||||
|    */ | ||||
|   public static createHandler(config: IForwardConfig): IForwardingHandler { | ||||
|     // Create the appropriate handler based on the forwarding type | ||||
|     switch (config.type) { | ||||
|       case 'http-only': | ||||
|         return new HttpForwardingHandler(config); | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         return new HttpsPassthroughHandler(config); | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|         return new HttpsTerminateToHttpHandler(config); | ||||
|          | ||||
|       case 'https-terminate-to-https': | ||||
|         return new HttpsTerminateToHttpsHandler(config); | ||||
|          | ||||
|       default: | ||||
|         // Type system should prevent this, but just in case: | ||||
|         throw new Error(`Unknown forwarding type: ${(config as any).type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply default values to a forwarding configuration based on its type | ||||
|    * @param config The original forwarding configuration | ||||
|    * @returns A configuration with defaults applied | ||||
|    */ | ||||
|   public static applyDefaults(config: IForwardConfig): IForwardConfig { | ||||
|     // Create a deep copy of the configuration | ||||
|     const result: IForwardConfig = JSON.parse(JSON.stringify(config)); | ||||
|      | ||||
|     // Apply defaults based on forwarding type | ||||
|     switch (config.type) { | ||||
|       case 'http-only': | ||||
|         // Set defaults for HTTP-only mode | ||||
|         result.http = { | ||||
|           enabled: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         break; | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         // Set defaults for HTTPS passthrough | ||||
|         result.https = { | ||||
|           forwardSni: true, | ||||
|           ...config.https | ||||
|         }; | ||||
|         // SNI forwarding doesn't do HTTP | ||||
|         result.http = { | ||||
|           enabled: false, | ||||
|           ...config.http | ||||
|         }; | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|         // Set defaults for HTTPS termination to HTTP | ||||
|         result.https = { | ||||
|           ...config.https | ||||
|         }; | ||||
|         // Support HTTP access by default in this mode | ||||
|         result.http = { | ||||
|           enabled: true, | ||||
|           redirectToHttps: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         // Enable ACME by default | ||||
|         result.acme = { | ||||
|           enabled: true, | ||||
|           maintenance: true, | ||||
|           ...config.acme | ||||
|         }; | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-https': | ||||
|         // Similar to terminate-to-http but with different target handling | ||||
|         result.https = { | ||||
|           ...config.https | ||||
|         }; | ||||
|         result.http = { | ||||
|           enabled: true, | ||||
|           redirectToHttps: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         result.acme = { | ||||
|           enabled: true, | ||||
|           maintenance: true, | ||||
|           ...config.acme | ||||
|         }; | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate a forwarding configuration | ||||
|    * @param config The configuration to validate | ||||
|    * @throws Error if the configuration is invalid | ||||
|    */ | ||||
|   public static validateConfig(config: IForwardConfig): void { | ||||
|     // Validate common properties | ||||
|     if (!config.target) { | ||||
|       throw new Error('Forwarding configuration must include a target'); | ||||
|     } | ||||
|      | ||||
|     if (!config.target.host || (Array.isArray(config.target.host) && config.target.host.length === 0)) { | ||||
|       throw new Error('Target must include a host or array of hosts'); | ||||
|     } | ||||
|      | ||||
|     if (!config.target.port || config.target.port <= 0 || config.target.port > 65535) { | ||||
|       throw new Error('Target must include a valid port (1-65535)'); | ||||
|     } | ||||
|      | ||||
|     // Type-specific validation | ||||
|     switch (config.type) { | ||||
|       case 'http-only': | ||||
|         // HTTP-only needs http.enabled to be true | ||||
|         if (config.http?.enabled === false) { | ||||
|           throw new Error('HTTP-only forwarding must have HTTP enabled'); | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         // HTTPS passthrough doesn't support HTTP | ||||
|         if (config.http?.enabled === true) { | ||||
|           throw new Error('HTTPS passthrough does not support HTTP'); | ||||
|         } | ||||
|          | ||||
|         // HTTPS passthrough doesn't work with ACME | ||||
|         if (config.acme?.enabled === true) { | ||||
|           throw new Error('HTTPS passthrough does not support ACME'); | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|       case 'https-terminate-to-https': | ||||
|         // These modes support all options, nothing specific to validate | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,127 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler | ||||
| } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Base class for all forwarding handlers | ||||
|  */ | ||||
| export abstract class ForwardingHandler extends plugins.EventEmitter implements IForwardingHandler { | ||||
|   /** | ||||
|    * Create a new ForwardingHandler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(protected config: IForwardConfig) { | ||||
|     super(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler | ||||
|    * Base implementation does nothing, subclasses should override as needed | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // Base implementation - no initialization needed | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle a new socket connection | ||||
|    * @param socket The incoming socket connection | ||||
|    */ | ||||
|   public abstract handleConnection(socket: plugins.net.Socket): void; | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void; | ||||
|    | ||||
|   /** | ||||
|    * Get a target from the configuration, supporting round-robin selection | ||||
|    * @returns A resolved target object with host and port | ||||
|    */ | ||||
|   protected getTargetFromConfig(): { host: string, port: number } { | ||||
|     const { target } = this.config; | ||||
|      | ||||
|     // Handle round-robin host selection | ||||
|     if (Array.isArray(target.host)) { | ||||
|       if (target.host.length === 0) { | ||||
|         throw new Error('No target hosts specified'); | ||||
|       } | ||||
|        | ||||
|       // Simple round-robin selection | ||||
|       const randomIndex = Math.floor(Math.random() * target.host.length); | ||||
|       return { | ||||
|         host: target.host[randomIndex], | ||||
|         port: target.port | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Single host | ||||
|     return { | ||||
|       host: target.host, | ||||
|       port: target.port | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Redirect an HTTP request to HTTPS | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     const host = req.headers.host || ''; | ||||
|     const path = req.url || '/'; | ||||
|     const redirectUrl = `https://${host}${path}`; | ||||
|      | ||||
|     res.writeHead(301, { | ||||
|       'Location': redirectUrl, | ||||
|       'Cache-Control': 'no-cache' | ||||
|     }); | ||||
|     res.end(`Redirecting to ${redirectUrl}`); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|       statusCode: 301, | ||||
|       headers: { 'Location': redirectUrl }, | ||||
|       size: 0 | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply custom headers from configuration | ||||
|    * @param headers The original headers | ||||
|    * @param variables Variables to replace in the headers | ||||
|    * @returns The headers with custom values applied | ||||
|    */ | ||||
|   protected applyCustomHeaders( | ||||
|     headers: Record<string, string | string[] | undefined>, | ||||
|     variables: Record<string, string> | ||||
|   ): Record<string, string | string[] | undefined> { | ||||
|     const customHeaders = this.config.advanced?.headers || {}; | ||||
|     const result = { ...headers }; | ||||
|      | ||||
|     // Apply custom headers with variable substitution | ||||
|     for (const [key, value] of Object.entries(customHeaders)) { | ||||
|       let processedValue = value; | ||||
|        | ||||
|       // Replace variables in the header value | ||||
|       for (const [varName, varValue] of Object.entries(variables)) { | ||||
|         processedValue = processedValue.replace(`{${varName}}`, varValue); | ||||
|       } | ||||
|        | ||||
|       result[key] = processedValue; | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the timeout for this connection from configuration | ||||
|    * @returns Timeout in milliseconds | ||||
|    */ | ||||
|   protected getTimeout(): number { | ||||
|     return this.config.advanced?.timeout || 60000; // Default: 60 seconds | ||||
|   } | ||||
| } | ||||
| @@ -1,140 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTP-only forwarding | ||||
|  */ | ||||
| export class HttpForwardingHandler extends ForwardingHandler { | ||||
|   /** | ||||
|    * Create a new HTTP forwarding handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTP-only configuration | ||||
|     if (config.type !== 'http-only') { | ||||
|       throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a raw socket connection | ||||
|    * HTTP handler doesn't do much with raw sockets as it mainly processes | ||||
|    * parsed HTTP requests | ||||
|    */ | ||||
|   public handleConnection(socket: plugins.net.Socket): void { | ||||
|     // For HTTP, we mainly handle parsed requests, but we can still set up | ||||
|     // some basic connection tracking | ||||
|     const remoteAddress = socket.remoteAddress || 'unknown'; | ||||
|      | ||||
|     socket.on('close', (hadError) => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         hadError | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     socket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: error.message | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create a custom headers object with variables for substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track bytes for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,182 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS passthrough (SNI forwarding without termination) | ||||
|  */ | ||||
| export class HttpsPassthroughHandler extends ForwardingHandler { | ||||
|   /** | ||||
|    * Create a new HTTPS passthrough handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS passthrough configuration | ||||
|     if (config.type !== 'https-passthrough') { | ||||
|       throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by forwarding it without termination | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Log the connection | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Create a connection to the target server | ||||
|     const serverSocket = plugins.net.connect(target.port, target.host); | ||||
|      | ||||
|     // Handle errors on the server socket | ||||
|     serverSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `Target connection error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Close the client socket if it's still open | ||||
|       if (!clientSocket.destroyed) { | ||||
|         clientSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Handle errors on the client socket | ||||
|     clientSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `Client connection error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Close the server socket if it's still open | ||||
|       if (!serverSocket.destroyed) { | ||||
|         serverSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track data transfer for logging | ||||
|     let bytesSent = 0; | ||||
|     let bytesReceived = 0; | ||||
|      | ||||
|     // Forward data from client to server | ||||
|     clientSocket.on('data', (data) => { | ||||
|       bytesSent += data.length; | ||||
|        | ||||
|       // Check if server socket is writable | ||||
|       if (serverSocket.writable) { | ||||
|         const flushed = serverSocket.write(data); | ||||
|          | ||||
|         // Handle backpressure | ||||
|         if (!flushed) { | ||||
|           clientSocket.pause(); | ||||
|           serverSocket.once('drain', () => { | ||||
|             clientSocket.resume(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|         direction: 'outbound', | ||||
|         bytes: data.length, | ||||
|         total: bytesSent | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Forward data from server to client | ||||
|     serverSocket.on('data', (data) => { | ||||
|       bytesReceived += data.length; | ||||
|        | ||||
|       // Check if client socket is writable | ||||
|       if (clientSocket.writable) { | ||||
|         const flushed = clientSocket.write(data); | ||||
|          | ||||
|         // Handle backpressure | ||||
|         if (!flushed) { | ||||
|           serverSocket.pause(); | ||||
|           clientSocket.once('drain', () => { | ||||
|             serverSocket.resume(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|         direction: 'inbound', | ||||
|         bytes: data.length, | ||||
|         total: bytesReceived | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle connection close | ||||
|     const handleClose = () => { | ||||
|       if (!clientSocket.destroyed) { | ||||
|         clientSocket.destroy(); | ||||
|       } | ||||
|        | ||||
|       if (!serverSocket.destroyed) { | ||||
|         serverSocket.destroy(); | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         bytesSent, | ||||
|         bytesReceived | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Set up close handlers | ||||
|     clientSocket.on('close', handleClose); | ||||
|     serverSocket.on('close', handleClose); | ||||
|      | ||||
|     // Set timeouts | ||||
|     const timeout = this.getTimeout(); | ||||
|     clientSocket.setTimeout(timeout); | ||||
|     serverSocket.setTimeout(timeout); | ||||
|      | ||||
|     // Handle timeouts | ||||
|     clientSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'Client connection timeout' | ||||
|       }); | ||||
|       handleClose(); | ||||
|     }); | ||||
|      | ||||
|     serverSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'Server connection timeout' | ||||
|       }); | ||||
|       handleClose(); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request - HTTPS passthrough doesn't support HTTP | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // HTTPS passthrough doesn't support HTTP requests | ||||
|     res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||||
|     res.end('HTTP not supported for this domain'); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|       statusCode: 404, | ||||
|       headers: { 'Content-Type': 'text/plain' }, | ||||
|       size: 'HTTP not supported for this domain'.length | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -1,264 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS termination with HTTP backend | ||||
|  */ | ||||
| export class HttpsTerminateToHttpHandler extends ForwardingHandler { | ||||
|   private tlsServer: plugins.tls.Server | null = null; | ||||
|   private secureContext: plugins.tls.SecureContext | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new HTTPS termination with HTTP backend handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS terminate to HTTP configuration | ||||
|     if (config.type !== 'https-terminate-to-http') { | ||||
|       throw new Error(`Invalid configuration type for HttpsTerminateToHttpHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler, setting up TLS context | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // We need to load or create TLS certificates | ||||
|     if (this.config.https?.customCert) { | ||||
|       // Use custom certificate from configuration | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: this.config.https.customCert.key, | ||||
|         cert: this.config.https.customCert.cert | ||||
|       }); | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { | ||||
|         source: 'config', | ||||
|         domain: this.config.target.host | ||||
|       }); | ||||
|     } else if (this.config.acme?.enabled) { | ||||
|       // Request certificate through ACME if needed | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { | ||||
|         domain: Array.isArray(this.config.target.host)  | ||||
|           ? this.config.target.host[0]  | ||||
|           : this.config.target.host, | ||||
|         useProduction: this.config.acme.production || false | ||||
|       }); | ||||
|        | ||||
|       // In a real implementation, we would wait for the certificate to be issued | ||||
|       // For now, we'll use a dummy context | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', | ||||
|         cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set the secure context for TLS termination | ||||
|    * Called when a certificate is available | ||||
|    * @param context The secure context | ||||
|    */ | ||||
|   public setSecureContext(context: plugins.tls.SecureContext): void { | ||||
|     this.secureContext = context; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by terminating TLS and forwarding to HTTP backend | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Make sure we have a secure context | ||||
|     if (!this.secureContext) { | ||||
|       clientSocket.destroy(new Error('TLS secure context not initialized')); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     // Create a TLS socket using our secure context | ||||
|     const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { | ||||
|       secureContext: this.secureContext, | ||||
|       isServer: true, | ||||
|       server: this.tlsServer || undefined | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       tls: true | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS errors | ||||
|     tlsSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `TLS error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // The TLS socket will now emit HTTP traffic that can be processed | ||||
|     // In a real implementation, we would create an HTTP parser and handle | ||||
|     // the requests here, but for simplicity, we'll just log the data | ||||
|      | ||||
|     let dataBuffer = Buffer.alloc(0); | ||||
|      | ||||
|     tlsSocket.on('data', (data) => { | ||||
|       // Append to buffer | ||||
|       dataBuffer = Buffer.concat([dataBuffer, data]); | ||||
|        | ||||
|       // Very basic HTTP parsing - in a real implementation, use http-parser | ||||
|       if (dataBuffer.includes(Buffer.from('\r\n\r\n'))) { | ||||
|         const target = this.getTargetFromConfig(); | ||||
|          | ||||
|         // Simple example: forward the data to an HTTP server | ||||
|         const socket = plugins.net.connect(target.port, target.host, () => { | ||||
|           socket.write(dataBuffer); | ||||
|           dataBuffer = Buffer.alloc(0); | ||||
|            | ||||
|           // Set up bidirectional data flow | ||||
|           tlsSocket.pipe(socket); | ||||
|           socket.pipe(tlsSocket); | ||||
|         }); | ||||
|          | ||||
|         socket.on('error', (error) => { | ||||
|           this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|             remoteAddress, | ||||
|             error: `Target connection error: ${error.message}` | ||||
|           }); | ||||
|            | ||||
|           if (!tlsSocket.destroyed) { | ||||
|             tlsSocket.destroy(); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Handle close | ||||
|     tlsSocket.on('close', () => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request by forwarding to the HTTP backend | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Check if we should redirect to HTTPS | ||||
|     if (this.config.http?.redirectToHttps) { | ||||
|       this.redirectToHttps(req, res); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create custom headers with variable substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track response size for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,292 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS termination with HTTPS backend | ||||
|  */ | ||||
| export class HttpsTerminateToHttpsHandler extends ForwardingHandler { | ||||
|   private secureContext: plugins.tls.SecureContext | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new HTTPS termination with HTTPS backend handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS terminate to HTTPS configuration | ||||
|     if (config.type !== 'https-terminate-to-https') { | ||||
|       throw new Error(`Invalid configuration type for HttpsTerminateToHttpsHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler, setting up TLS context | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // We need to load or create TLS certificates for termination | ||||
|     if (this.config.https?.customCert) { | ||||
|       // Use custom certificate from configuration | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: this.config.https.customCert.key, | ||||
|         cert: this.config.https.customCert.cert | ||||
|       }); | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { | ||||
|         source: 'config', | ||||
|         domain: this.config.target.host | ||||
|       }); | ||||
|     } else if (this.config.acme?.enabled) { | ||||
|       // Request certificate through ACME if needed | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { | ||||
|         domain: Array.isArray(this.config.target.host)  | ||||
|           ? this.config.target.host[0]  | ||||
|           : this.config.target.host, | ||||
|         useProduction: this.config.acme.production || false | ||||
|       }); | ||||
|        | ||||
|       // In a real implementation, we would wait for the certificate to be issued | ||||
|       // For now, we'll use a dummy context | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', | ||||
|         cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set the secure context for TLS termination | ||||
|    * Called when a certificate is available | ||||
|    * @param context The secure context | ||||
|    */ | ||||
|   public setSecureContext(context: plugins.tls.SecureContext): void { | ||||
|     this.secureContext = context; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by terminating TLS and creating a new TLS connection to backend | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Make sure we have a secure context | ||||
|     if (!this.secureContext) { | ||||
|       clientSocket.destroy(new Error('TLS secure context not initialized')); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     // Create a TLS socket using our secure context | ||||
|     const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { | ||||
|       secureContext: this.secureContext, | ||||
|       isServer: true | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       tls: true | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS errors | ||||
|     tlsSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `TLS error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // The TLS socket will now emit HTTP traffic that can be processed | ||||
|     // In a real implementation, we would create an HTTP parser and handle | ||||
|     // the requests here, but for simplicity, we'll just forward the data | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Set up the connection to the HTTPS backend | ||||
|     const connectToBackend = () => { | ||||
|       const backendSocket = plugins.tls.connect({ | ||||
|         host: target.host, | ||||
|         port: target.port, | ||||
|         // In a real implementation, we would configure TLS options | ||||
|         rejectUnauthorized: false // For testing only, never use in production | ||||
|       }, () => { | ||||
|         this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|           direction: 'outbound', | ||||
|           target: `${target.host}:${target.port}`, | ||||
|           tls: true | ||||
|         }); | ||||
|          | ||||
|         // Set up bidirectional data flow | ||||
|         tlsSocket.pipe(backendSocket); | ||||
|         backendSocket.pipe(tlsSocket); | ||||
|       }); | ||||
|        | ||||
|       backendSocket.on('error', (error) => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: `Backend connection error: ${error.message}` | ||||
|         }); | ||||
|          | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Handle close | ||||
|       backendSocket.on('close', () => { | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Set timeout | ||||
|       const timeout = this.getTimeout(); | ||||
|       backendSocket.setTimeout(timeout); | ||||
|        | ||||
|       backendSocket.on('timeout', () => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: 'Backend connection timeout' | ||||
|         }); | ||||
|          | ||||
|         if (!backendSocket.destroyed) { | ||||
|           backendSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Wait for the TLS handshake to complete before connecting to backend | ||||
|     tlsSocket.on('secure', () => { | ||||
|       connectToBackend(); | ||||
|     }); | ||||
|      | ||||
|     // Handle close | ||||
|     tlsSocket.on('close', () => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request by forwarding to the HTTPS backend | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Check if we should redirect to HTTPS | ||||
|     if (this.config.http?.redirectToHttps) { | ||||
|       this.redirectToHttps(req, res); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create custom headers with variable substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers, | ||||
|       // In a real implementation, we would configure TLS options | ||||
|       rejectUnauthorized: false // For testing only, never use in production | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request using HTTPS | ||||
|     const proxyReq = plugins.https.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track response size for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| // Export types | ||||
| export type { | ||||
|   ForwardingType, | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler, | ||||
|   ITargetConfig, | ||||
|   IHttpOptions, | ||||
|   IHttpsOptions, | ||||
|   IAcmeForwardingOptions, | ||||
|   ISecurityOptions, | ||||
|   IAdvancedOptions | ||||
| } from '../types/forwarding.types.js'; | ||||
|  | ||||
| // Export values | ||||
| export { | ||||
|   ForwardingHandlerEvents, | ||||
|   httpOnly, | ||||
|   tlsTerminateToHttp, | ||||
|   tlsTerminateToHttps, | ||||
|   httpsPassthrough | ||||
| } from '../types/forwarding.types.js'; | ||||
|  | ||||
| // Export domain configuration | ||||
| export * from './domain-config.js'; | ||||
|  | ||||
| // Export handlers | ||||
| export { ForwardingHandler } from './forwarding.handler.js'; | ||||
| export { HttpForwardingHandler } from './http.handler.js'; | ||||
| export { HttpsPassthroughHandler } from './https-passthrough.handler.js'; | ||||
| export { HttpsTerminateToHttpHandler } from './https-terminate-to-http.handler.js'; | ||||
| export { HttpsTerminateToHttpsHandler } from './https-terminate-to-https.handler.js'; | ||||
|  | ||||
| // Export factory | ||||
| export { ForwardingHandlerFactory } from './forwarding.factory.js'; | ||||
|  | ||||
| // Export manager | ||||
| export { DomainManager, DomainManagerEvents } from './domain-manager.js'; | ||||
|  | ||||
| // Helper functions as a convenience object | ||||
| import { | ||||
|   httpOnly, | ||||
|   tlsTerminateToHttp, | ||||
|   tlsTerminateToHttps, | ||||
|   httpsPassthrough | ||||
| } from '../types/forwarding.types.js'; | ||||
|  | ||||
| export const helpers = { | ||||
|   httpOnly, | ||||
|   tlsTerminateToHttp, | ||||
|   tlsTerminateToHttps, | ||||
|   httpsPassthrough | ||||
| }; | ||||
| @@ -1,162 +0,0 @@ | ||||
| import type * as plugins from '../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * The primary forwarding types supported by SmartProxy | ||||
|  */ | ||||
| export type ForwardingType = | ||||
|   | 'http-only'                // HTTP forwarding only (no HTTPS) | ||||
|   | 'https-passthrough'        // Pass-through TLS traffic (SNI forwarding) | ||||
|   | 'https-terminate-to-http'  // Terminate TLS and forward to HTTP backend | ||||
|   | 'https-terminate-to-https'; // Terminate TLS and forward to HTTPS backend | ||||
|  | ||||
| /** | ||||
|  * Target configuration for forwarding | ||||
|  */ | ||||
| export interface ITargetConfig { | ||||
|   host: string | string[];  // Support single host or round-robin | ||||
|   port: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP-specific options for forwarding | ||||
|  */ | ||||
| export interface IHttpOptions { | ||||
|   enabled?: boolean;                 // Whether HTTP is enabled | ||||
|   redirectToHttps?: boolean;         // Redirect HTTP to HTTPS | ||||
|   headers?: Record<string, string>;  // Custom headers for HTTP responses | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTPS-specific options for forwarding | ||||
|  */ | ||||
| export interface IHttpsOptions { | ||||
|   customCert?: {                    // Use custom cert instead of auto-provisioned | ||||
|     key: string; | ||||
|     cert: string; | ||||
|   }; | ||||
|   forwardSni?: boolean;             // Forward SNI info in passthrough mode | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * ACME certificate handling options | ||||
|  */ | ||||
| export interface IAcmeForwardingOptions { | ||||
|   enabled?: boolean;                // Enable ACME certificate provisioning   | ||||
|   maintenance?: boolean;            // Auto-renew certificates | ||||
|   production?: boolean;             // Use production ACME servers | ||||
|   forwardChallenges?: {             // Forward ACME challenges | ||||
|     host: string; | ||||
|     port: number; | ||||
|     useTls?: boolean; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Security options for forwarding | ||||
|  */ | ||||
| export interface ISecurityOptions { | ||||
|   allowedIps?: string[];            // IPs allowed to connect | ||||
|   blockedIps?: string[];            // IPs blocked from connecting | ||||
|   maxConnections?: number;          // Max simultaneous connections | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Advanced options for forwarding | ||||
|  */ | ||||
| export interface IAdvancedOptions { | ||||
|   portRanges?: Array<{ from: number; to: number }>; // Allowed port ranges | ||||
|   networkProxyPort?: number;        // Custom NetworkProxy port if using terminate mode | ||||
|   keepAlive?: boolean;              // Enable TCP keepalive | ||||
|   timeout?: number;                 // Connection timeout in ms | ||||
|   headers?: Record<string, string>; // Custom headers with support for variables like {sni} | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Unified forwarding configuration interface | ||||
|  */ | ||||
| export interface IForwardConfig { | ||||
|   // Define the primary forwarding type - use-case driven approach | ||||
|   type: ForwardingType; | ||||
|    | ||||
|   // Target configuration | ||||
|   target: ITargetConfig; | ||||
|    | ||||
|   // Protocol options | ||||
|   http?: IHttpOptions; | ||||
|   https?: IHttpsOptions; | ||||
|   acme?: IAcmeForwardingOptions; | ||||
|    | ||||
|   // Security and advanced options | ||||
|   security?: ISecurityOptions; | ||||
|   advanced?: IAdvancedOptions; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Event types emitted by forwarding handlers | ||||
|  */ | ||||
| export enum ForwardingHandlerEvents { | ||||
|   CONNECTED = 'connected', | ||||
|   DISCONNECTED = 'disconnected', | ||||
|   ERROR = 'error', | ||||
|   DATA_FORWARDED = 'data-forwarded', | ||||
|   HTTP_REQUEST = 'http-request', | ||||
|   HTTP_RESPONSE = 'http-response', | ||||
|   CERTIFICATE_NEEDED = 'certificate-needed', | ||||
|   CERTIFICATE_LOADED = 'certificate-loaded' | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Base interface for forwarding handlers | ||||
|  */ | ||||
| export interface IForwardingHandler extends plugins.EventEmitter { | ||||
|   initialize(): Promise<void>; | ||||
|   handleConnection(socket: plugins.net.Socket): void; | ||||
|   handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Helper function types for common forwarding patterns | ||||
|  */ | ||||
| export const httpOnly = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'http-only', | ||||
|   target: partialConfig.target, | ||||
|   http: { enabled: true, ...(partialConfig.http || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
|  | ||||
| export const tlsTerminateToHttp = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'https-terminate-to-http', | ||||
|   target: partialConfig.target, | ||||
|   https: { ...(partialConfig.https || {}) }, | ||||
|   acme: { enabled: true, maintenance: true, ...(partialConfig.acme || {}) }, | ||||
|   http: { enabled: true, redirectToHttps: true, ...(partialConfig.http || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
|  | ||||
| export const tlsTerminateToHttps = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'https-terminate-to-https', | ||||
|   target: partialConfig.target, | ||||
|   https: { ...(partialConfig.https || {}) }, | ||||
|   acme: { enabled: true, maintenance: true, ...(partialConfig.acme || {}) }, | ||||
|   http: { enabled: true, redirectToHttps: true, ...(partialConfig.http || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
|  | ||||
| export const httpsPassthrough = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'https-passthrough', | ||||
|   target: partialConfig.target, | ||||
|   https: { forwardSni: true, ...(partialConfig.https || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user