BREAKING_CHANGE(core): remove legacy forwarding module in favor of route-based system
- Removed the forwarding namespace export from main index - Removed TForwardingType and all forwarding handlers - Consolidated route helper functions into route-helpers.ts - All functionality is now available through the route-based system - Users must migrate from forwarding.* imports to direct route helper imports
This commit is contained in:
		| @@ -1,76 +0,0 @@ | ||||
| import type * as plugins from '../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * The primary forwarding types supported by SmartProxy | ||||
|  * Used for configuration compatibility | ||||
|  */ | ||||
| export type TForwardingType = | ||||
|   | '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 | ||||
|  | ||||
| /** | ||||
|  * 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; | ||||
| } | ||||
|  | ||||
| // Route-based helpers are now available directly from route-patterns.ts | ||||
| import { | ||||
|   createHttpRoute, | ||||
|   createHttpsTerminateRoute, | ||||
|   createHttpsPassthroughRoute, | ||||
|   createHttpToHttpsRedirect, | ||||
|   createCompleteHttpsServer, | ||||
|   createLoadBalancerRoute | ||||
| } from '../../proxies/smart-proxy/utils/route-patterns.js'; | ||||
|  | ||||
| export { | ||||
|   createHttpRoute, | ||||
|   createHttpsTerminateRoute, | ||||
|   createHttpsPassthroughRoute, | ||||
|   createHttpToHttpsRedirect, | ||||
|   createCompleteHttpsServer, | ||||
|   createLoadBalancerRoute | ||||
| }; | ||||
|  | ||||
| // Note: Legacy helper functions have been removed | ||||
| // Please use the route-based helpers instead: | ||||
| // - createHttpRoute | ||||
| // - createHttpsTerminateRoute | ||||
| // - createHttpsPassthroughRoute | ||||
| // - createHttpToHttpsRedirect | ||||
| import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; | ||||
|  | ||||
| // For backward compatibility, kept only the basic configuration interface | ||||
| export interface IForwardConfig { | ||||
|   type: TForwardingType; | ||||
|   target: { | ||||
|     host: string | string[]; | ||||
|     port: number | 'preserve' | ((ctx: any) => number); | ||||
|   }; | ||||
|   http?: any; | ||||
|   https?: any; | ||||
|   acme?: any; | ||||
|   security?: any; | ||||
|   advanced?: any; | ||||
|   [key: string]: any; | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| /** | ||||
|  * Forwarding configuration exports | ||||
|  * | ||||
|  * Note: The legacy domain-based configuration has been replaced by route-based configuration. | ||||
|  * See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration. | ||||
|  */ | ||||
|  | ||||
| export type {  | ||||
|   TForwardingType, | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler | ||||
| } from './forwarding-types.js'; | ||||
|  | ||||
| export {  | ||||
|   ForwardingHandlerEvents | ||||
| } from './forwarding-types.js'; | ||||
|  | ||||
| // Import route helpers from route-patterns instead of deleted route-helpers | ||||
| export { | ||||
|   createHttpRoute, | ||||
|   createHttpsTerminateRoute, | ||||
|   createHttpsPassthroughRoute, | ||||
|   createHttpToHttpsRedirect, | ||||
|   createCompleteHttpsServer, | ||||
|   createLoadBalancerRoute | ||||
| } from '../../proxies/smart-proxy/utils/route-patterns.js'; | ||||
| @@ -1,189 +0,0 @@ | ||||
| import type { IForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandler } from '../handlers/base-handler.js'; | ||||
| import { HttpForwardingHandler } from '../handlers/http-handler.js'; | ||||
| import { HttpsPassthroughHandler } from '../handlers/https-passthrough-handler.js'; | ||||
| import { HttpsTerminateToHttpHandler } from '../handlers/https-terminate-to-http-handler.js'; | ||||
| import { HttpsTerminateToHttpsHandler } from '../handlers/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): ForwardingHandler { | ||||
|     // 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 | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 80; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         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 | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 443; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         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 | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 443; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         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 | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 443; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         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'); | ||||
|     } | ||||
|      | ||||
|     // Validate port if it's a number | ||||
|     if (typeof config.target.port === 'number') { | ||||
|       if (config.target.port <= 0 || config.target.port > 65535) { | ||||
|         throw new Error('Target must include a valid port (1-65535)'); | ||||
|       } | ||||
|     } else if (config.target.port !== 'preserve' && typeof config.target.port !== 'function') { | ||||
|       throw new Error('Target port must be a number, "preserve", or a function'); | ||||
|     } | ||||
|      | ||||
|     // 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,5 +0,0 @@ | ||||
| /** | ||||
|  * Forwarding factory implementations | ||||
|  */ | ||||
|  | ||||
| export { ForwardingHandlerFactory } from './forwarding-factory.js'; | ||||
| @@ -1,155 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler | ||||
| } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/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 | ||||
|    * @param incomingPort Optional incoming port for 'preserve' mode | ||||
|    * @returns A resolved target object with host and port | ||||
|    */ | ||||
|   protected getTargetFromConfig(incomingPort: number = 80): { 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: this.resolvePort(target.port, incomingPort) | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Single host | ||||
|     return { | ||||
|       host: target.host, | ||||
|       port: this.resolvePort(target.port, incomingPort) | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Resolves a port value, handling 'preserve' and function ports | ||||
|    * @param port The port value to resolve | ||||
|    * @param incomingPort Optional incoming port to use for 'preserve' mode | ||||
|    */ | ||||
|   protected resolvePort( | ||||
|     port: number | 'preserve' | ((ctx: any) => number),  | ||||
|     incomingPort: number = 80 | ||||
|   ): number { | ||||
|     if (typeof port === 'function') { | ||||
|       try { | ||||
|         // Create a minimal context for the function that includes the incoming port | ||||
|         const ctx = { port: incomingPort }; | ||||
|         return port(ctx); | ||||
|       } catch (err) { | ||||
|         console.error('Error resolving port function:', err); | ||||
|         return incomingPort; // Fall back to incoming port | ||||
|       } | ||||
|     } else if (port === 'preserve') { | ||||
|       return incomingPort; // Use the actual incoming port for 'preserve' | ||||
|     } else { | ||||
|       return 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)) { | ||||
|       if (typeof value !== 'string') continue; | ||||
|  | ||||
|       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,163 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { IForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
| import { setupSocketHandlers } from '../../core/utils/socket-utils.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}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Initialize the handler | ||||
|    * HTTP handler doesn't need special initialization | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // Basic initialization from parent class | ||||
|     await super.initialize(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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'; | ||||
|     const localPort = socket.localPort || 80; | ||||
|      | ||||
|     // Set up socket handlers with proper cleanup | ||||
|     const handleClose = (reason: string) => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         reason | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Use custom timeout handler that doesn't close the socket | ||||
|     setupSocketHandlers(socket, handleClose, () => { | ||||
|       // For HTTP, we can be more aggressive with timeouts since connections are shorter | ||||
|       // But still don't close immediately - let the connection finish naturally | ||||
|       console.warn(`HTTP socket timeout from ${remoteAddress}`); | ||||
|     }, 'http'); | ||||
|      | ||||
|     socket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: error.message | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       localPort | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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 local port from the request (for 'preserve' port handling) | ||||
|     const localPort = req.socket.localPort || 80; | ||||
|      | ||||
|     // Get the target from configuration, passing the incoming port | ||||
|     const target = this.getTargetFromConfig(localPort); | ||||
|      | ||||
|     // 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,185 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { IForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
| import { createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.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}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Initialize the handler | ||||
|    * HTTPS passthrough handler doesn't need special initialization | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // Basic initialization from parent class | ||||
|     await super.initialize(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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}` | ||||
|     }); | ||||
|      | ||||
|     // Track data transfer for logging | ||||
|     let bytesSent = 0; | ||||
|     let bytesReceived = 0; | ||||
|     let serverSocket: plugins.net.Socket | null = null; | ||||
|     let cleanupClient: ((reason: string) => Promise<void>) | null = null; | ||||
|     let cleanupServer: ((reason: string) => Promise<void>) | null = null; | ||||
|      | ||||
|     // Create a connection to the target server with immediate error handling | ||||
|     serverSocket = createSocketWithErrorHandler({ | ||||
|       port: target.port, | ||||
|       host: target.host, | ||||
|       onError: async (error) => { | ||||
|         // Server connection failed - clean up client socket immediately | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           error: error.message, | ||||
|           code: (error as any).code || 'UNKNOWN', | ||||
|           remoteAddress, | ||||
|           target: `${target.host}:${target.port}` | ||||
|         }); | ||||
|          | ||||
|         // Clean up the client socket since we can't forward | ||||
|         if (!clientSocket.destroyed) { | ||||
|           clientSocket.destroy(); | ||||
|         } | ||||
|          | ||||
|         this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|           remoteAddress, | ||||
|           bytesSent: 0, | ||||
|           bytesReceived: 0, | ||||
|           reason: `server_connection_failed: ${error.message}` | ||||
|         }); | ||||
|       }, | ||||
|       onConnect: () => { | ||||
|         // Connection successful - set up forwarding handlers | ||||
|         const handlers = createIndependentSocketHandlers( | ||||
|           clientSocket, | ||||
|           serverSocket!, | ||||
|           (reason) => { | ||||
|             this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|               remoteAddress, | ||||
|               bytesSent, | ||||
|               bytesReceived, | ||||
|               reason | ||||
|             }); | ||||
|           } | ||||
|         ); | ||||
|          | ||||
|         cleanupClient = handlers.cleanupClient; | ||||
|         cleanupServer = handlers.cleanupServer; | ||||
|          | ||||
|         // Setup handlers with custom timeout handling that doesn't close connections | ||||
|         const timeout = this.getTimeout(); | ||||
|          | ||||
|         setupSocketHandlers(clientSocket, cleanupClient, (socket) => { | ||||
|           // Just reset timeout, don't close | ||||
|           socket.setTimeout(timeout); | ||||
|         }, 'client'); | ||||
|          | ||||
|         setupSocketHandlers(serverSocket!, cleanupServer, (socket) => { | ||||
|           // Just reset timeout, don't close   | ||||
|           socket.setTimeout(timeout); | ||||
|         }, 'server'); | ||||
|          | ||||
|         // Forward data from client to server | ||||
|         clientSocket.on('data', (data) => { | ||||
|           bytesSent += data.length; | ||||
|            | ||||
|           // Check if server socket is writable | ||||
|           if (serverSocket && 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 | ||||
|           }); | ||||
|         }); | ||||
|          | ||||
|         // Set initial timeouts - they will be reset on each timeout event   | ||||
|         clientSocket.setTimeout(timeout); | ||||
|         serverSocket!.setTimeout(timeout); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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,312 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { IForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
| import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.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 | ||||
|     }); | ||||
|      | ||||
|     // Variables to track connections | ||||
|     let backendSocket: plugins.net.Socket | null = null; | ||||
|     let dataBuffer = Buffer.alloc(0); | ||||
|     let connectionEstablished = false; | ||||
|     let forwardingSetup = false; | ||||
|      | ||||
|     // Set up initial error handling for TLS socket | ||||
|     const tlsCleanupHandler = (reason: string) => { | ||||
|       if (!forwardingSetup) { | ||||
|         // If forwarding not set up yet, emit disconnected and cleanup | ||||
|         this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|           remoteAddress, | ||||
|           reason | ||||
|         }); | ||||
|         dataBuffer = Buffer.alloc(0); | ||||
|         connectionEstablished = false; | ||||
|          | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|         if (backendSocket && !backendSocket.destroyed) { | ||||
|           backendSocket.destroy(); | ||||
|         } | ||||
|       } | ||||
|       // If forwarding is setup, setupBidirectionalForwarding will handle cleanup | ||||
|     }; | ||||
|      | ||||
|     setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls'); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|       tlsCleanupHandler('timeout'); | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS data | ||||
|     tlsSocket.on('data', (data) => { | ||||
|       // If backend connection already established, just forward the data | ||||
|       if (connectionEstablished && backendSocket && !backendSocket.destroyed) { | ||||
|         backendSocket.write(data); | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // 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')) && !connectionEstablished) { | ||||
|         const target = this.getTargetFromConfig(); | ||||
|          | ||||
|         // Create backend connection with immediate error handling | ||||
|         backendSocket = createSocketWithErrorHandler({ | ||||
|           port: target.port, | ||||
|           host: target.host, | ||||
|           onError: (error) => { | ||||
|             this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|               error: error.message, | ||||
|               code: (error as any).code || 'UNKNOWN', | ||||
|               remoteAddress, | ||||
|               target: `${target.host}:${target.port}` | ||||
|             }); | ||||
|              | ||||
|             // Clean up the TLS socket since we can't forward | ||||
|             if (!tlsSocket.destroyed) { | ||||
|               tlsSocket.destroy(); | ||||
|             } | ||||
|              | ||||
|             this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|               remoteAddress, | ||||
|               reason: `backend_connection_failed: ${error.message}` | ||||
|             }); | ||||
|           }, | ||||
|           onConnect: () => { | ||||
|             connectionEstablished = true; | ||||
|              | ||||
|             // Send buffered data | ||||
|             if (dataBuffer.length > 0) { | ||||
|               backendSocket!.write(dataBuffer); | ||||
|               dataBuffer = Buffer.alloc(0); | ||||
|             } | ||||
|              | ||||
|             // Now set up bidirectional forwarding with proper cleanup | ||||
|             forwardingSetup = true; | ||||
|             setupBidirectionalForwarding(tlsSocket, backendSocket!, { | ||||
|               onCleanup: (reason) => { | ||||
|                 this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|                   remoteAddress, | ||||
|                   reason | ||||
|                 }); | ||||
|                 dataBuffer = Buffer.alloc(0); | ||||
|                 connectionEstablished = false; | ||||
|                 forwardingSetup = false; | ||||
|               }, | ||||
|               enableHalfOpen: false // Close both when one closes | ||||
|             }); | ||||
|           } | ||||
|         }); | ||||
|          | ||||
|         // Additional error logging for backend socket | ||||
|         backendSocket.on('error', (error) => { | ||||
|           if (!connectionEstablished) { | ||||
|             // Connection failed during setup | ||||
|             this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|               remoteAddress, | ||||
|               error: `Target connection error: ${error.message}` | ||||
|             }); | ||||
|           } | ||||
|           // If connected, setupBidirectionalForwarding handles cleanup | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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,297 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { IForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
| import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.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 | ||||
|     }); | ||||
|      | ||||
|     // Variable to track backend socket | ||||
|     let backendSocket: plugins.tls.TLSSocket | null = null; | ||||
|     let isConnectedToBackend = false; | ||||
|      | ||||
|     // Set up initial error handling for TLS socket | ||||
|     const tlsCleanupHandler = (reason: string) => { | ||||
|       if (!isConnectedToBackend) { | ||||
|         // If backend not connected yet, just emit disconnected event | ||||
|         this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|           remoteAddress, | ||||
|           reason | ||||
|         }); | ||||
|          | ||||
|         // Cleanup TLS socket if needed | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       } | ||||
|       // If connected to backend, setupBidirectionalForwarding will handle cleanup | ||||
|     }; | ||||
|      | ||||
|     setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls'); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|       tlsCleanupHandler('timeout'); | ||||
|     }); | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Set up the connection to the HTTPS backend | ||||
|     const connectToBackend = () => { | ||||
|       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 | ||||
|       }, () => { | ||||
|         isConnectedToBackend = true; | ||||
|          | ||||
|         this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|           direction: 'outbound', | ||||
|           target: `${target.host}:${target.port}`, | ||||
|           tls: true | ||||
|         }); | ||||
|          | ||||
|         // Set up bidirectional forwarding with proper cleanup | ||||
|         setupBidirectionalForwarding(tlsSocket, backendSocket!, { | ||||
|           onCleanup: (reason) => { | ||||
|             this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|               remoteAddress, | ||||
|               reason | ||||
|             }); | ||||
|           }, | ||||
|           enableHalfOpen: false // Close both when one closes | ||||
|         }); | ||||
|          | ||||
|         // Set timeout for backend socket | ||||
|         backendSocket!.setTimeout(timeout); | ||||
|          | ||||
|         backendSocket!.on('timeout', () => { | ||||
|           this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|             remoteAddress, | ||||
|             error: 'Backend connection timeout' | ||||
|           }); | ||||
|           // Let setupBidirectionalForwarding handle the cleanup | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       // Handle backend connection errors | ||||
|       backendSocket.on('error', (error) => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: `Backend connection error: ${error.message}` | ||||
|         }); | ||||
|          | ||||
|         if (!isConnectedToBackend) { | ||||
|           // Connection failed, clean up TLS socket | ||||
|           if (!tlsSocket.destroyed) { | ||||
|             tlsSocket.destroy(); | ||||
|           } | ||||
|           this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|             remoteAddress, | ||||
|             reason: `backend_connection_failed: ${error.message}` | ||||
|           }); | ||||
|         } | ||||
|         // If connected, let setupBidirectionalForwarding handle cleanup | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Wait for the TLS handshake to complete before connecting to backend | ||||
|     tlsSocket.on('secure', () => { | ||||
|       connectToBackend(); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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,9 +0,0 @@ | ||||
| /** | ||||
|  * Forwarding handler implementations | ||||
|  */ | ||||
|  | ||||
| export { ForwardingHandler } from './base-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'; | ||||
| @@ -1,35 +0,0 @@ | ||||
| /** | ||||
|  * Forwarding system module | ||||
|  * Provides a flexible and type-safe way to configure and manage various forwarding strategies | ||||
|  */ | ||||
|  | ||||
| // Export handlers | ||||
| export { ForwardingHandler } from './handlers/base-handler.js'; | ||||
| export * from './handlers/http-handler.js'; | ||||
| export * from './handlers/https-passthrough-handler.js'; | ||||
| export * from './handlers/https-terminate-to-http-handler.js'; | ||||
| export * from './handlers/https-terminate-to-https-handler.js'; | ||||
|  | ||||
| // Export factory | ||||
| export * from './factory/forwarding-factory.js'; | ||||
|  | ||||
| // Export types - these include TForwardingType and IForwardConfig | ||||
| export type {  | ||||
|   TForwardingType, | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler | ||||
| } from './config/forwarding-types.js'; | ||||
|  | ||||
| export {  | ||||
|   ForwardingHandlerEvents | ||||
| } from './config/forwarding-types.js'; | ||||
|  | ||||
| // Export route helpers directly from route-patterns | ||||
| export { | ||||
|   createHttpRoute, | ||||
|   createHttpsTerminateRoute, | ||||
|   createHttpsPassthroughRoute, | ||||
|   createHttpToHttpsRedirect, | ||||
|   createCompleteHttpsServer, | ||||
|   createLoadBalancerRoute | ||||
| } from '../proxies/smart-proxy/utils/route-patterns.js'; | ||||
| @@ -32,7 +32,6 @@ export * from './core/models/common-types.js'; | ||||
| export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js'; | ||||
|  | ||||
| // Modular exports for new architecture | ||||
| export * as forwarding from './forwarding/index.js'; | ||||
| // Certificate module has been removed - use SmartCertManager instead | ||||
| export * as tls from './tls/index.js'; | ||||
| export * as routing from './routing/index.js'; | ||||
| @@ -16,7 +16,6 @@ export interface IAcmeOptions { | ||||
|   routeForwards?: any[]; | ||||
| } | ||||
| import type { IRouteConfig } from './route-types.js'; | ||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Provision object for static or HTTP-01 certificate | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| // Certificate types removed - use local definition | ||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||
| import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; | ||||
| import type { IRouteContext } from '../../../core/models/route-context.js'; | ||||
|  | ||||
|   | ||||
| @@ -14,23 +14,12 @@ export * from './route-validators.js'; | ||||
| // Export route utilities for route operations | ||||
| export * from './route-utils.js'; | ||||
|  | ||||
| // Export route patterns with renamed exports to avoid conflicts | ||||
| import { | ||||
|   createWebSocketRoute as createWebSocketPatternRoute, | ||||
|   createLoadBalancerRoute as createLoadBalancerPatternRoute, | ||||
|   createApiGatewayRoute, | ||||
|   addRateLimiting, | ||||
|   addBasicAuth, | ||||
|   addJwtAuth | ||||
| } from './route-patterns.js'; | ||||
|  | ||||
| // Export additional functions from route-helpers that weren't already exported | ||||
| export { | ||||
|   createWebSocketPatternRoute, | ||||
|   createLoadBalancerPatternRoute, | ||||
|   createApiGatewayRoute, | ||||
|   addRateLimiting, | ||||
|   addBasicAuth, | ||||
|   addJwtAuth | ||||
| }; | ||||
| } from './route-helpers.js'; | ||||
|  | ||||
| // Migration utilities have been removed as they are no longer needed | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js'; | ||||
| import { mergeRouteConfigs } from './route-utils.js'; | ||||
|  | ||||
| /** | ||||
|  * Create an HTTP-only route configuration | ||||
| @@ -211,26 +212,62 @@ export function createCompleteHttpsServer( | ||||
| /** | ||||
|  * Create a load balancer route (round-robin between multiple backend hosts) | ||||
|  * @param domains Domain(s) to match | ||||
|  * @param hosts Array of backend hosts to load balance between | ||||
|  * @param port Backend port | ||||
|  * @param options Additional route options | ||||
|  * @param backendsOrHosts Array of backend servers OR array of host strings (legacy) | ||||
|  * @param portOrOptions Port number (legacy) OR options object | ||||
|  * @param options Additional route options (legacy) | ||||
|  * @returns Route configuration object | ||||
|  */ | ||||
| export function createLoadBalancerRoute( | ||||
|   domains: string | string[], | ||||
|   hosts: string[], | ||||
|   port: number, | ||||
|   options: { | ||||
|   backendsOrHosts: Array<{ host: string; port: number }> | string[], | ||||
|   portOrOptions?: number | { | ||||
|     tls?: { | ||||
|       mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||
|       certificate?: 'auto' | { key: string; cert: string }; | ||||
|     }; | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     algorithm?: 'round-robin' | 'least-connections' | 'ip-hash'; | ||||
|     healthCheck?: { | ||||
|       path: string; | ||||
|       interval: number; | ||||
|       timeout: number; | ||||
|       unhealthyThreshold: number; | ||||
|       healthyThreshold: number; | ||||
|     }; | ||||
|     [key: string]: any; | ||||
|   }, | ||||
|   options?: { | ||||
|     tls?: { | ||||
|       mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||
|       certificate?: 'auto' | { key: string; cert: string }; | ||||
|     }; | ||||
|     [key: string]: any; | ||||
|   } = {} | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   // Handle legacy signature: (domains, hosts[], port, options) | ||||
|   let backends: Array<{ host: string; port: number }>; | ||||
|   let finalOptions: any; | ||||
|    | ||||
|   if (Array.isArray(backendsOrHosts) && backendsOrHosts.length > 0 && typeof backendsOrHosts[0] === 'string') { | ||||
|     // Legacy signature | ||||
|     const hosts = backendsOrHosts as string[]; | ||||
|     const port = portOrOptions as number; | ||||
|     backends = hosts.map(host => ({ host, port })); | ||||
|     finalOptions = options || {}; | ||||
|   } else { | ||||
|     // New signature | ||||
|     backends = backendsOrHosts as Array<{ host: string; port: number }>; | ||||
|     finalOptions = (portOrOptions as any) || {}; | ||||
|   } | ||||
|    | ||||
|   // Extract hosts and ensure all backends use the same port | ||||
|   const port = backends[0].port; | ||||
|   const hosts = backends.map(backend => backend.host); | ||||
|    | ||||
|   // Create route match | ||||
|   const match: IRouteMatch = { | ||||
|     ports: options.match?.ports || (options.tls ? 443 : 80), | ||||
|     ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80), | ||||
|     domains | ||||
|   }; | ||||
|  | ||||
| @@ -247,10 +284,18 @@ export function createLoadBalancerRoute( | ||||
|   }; | ||||
|  | ||||
|   // Add TLS configuration if provided | ||||
|   if (options.tls) { | ||||
|   if (finalOptions.tls || finalOptions.useTls) { | ||||
|     action.tls = { | ||||
|       mode: options.tls.mode, | ||||
|       certificate: options.tls.certificate || 'auto' | ||||
|       mode: finalOptions.tls?.mode || 'terminate', | ||||
|       certificate: finalOptions.tls?.certificate || finalOptions.certificate || 'auto' | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // Add load balancing options | ||||
|   if (finalOptions.algorithm || finalOptions.healthCheck) { | ||||
|     action.loadBalancing = { | ||||
|       algorithm: finalOptions.algorithm || 'round-robin', | ||||
|       healthCheck: finalOptions.healthCheck | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -258,8 +303,8 @@ export function createLoadBalancerRoute( | ||||
|   return { | ||||
|     match, | ||||
|     action, | ||||
|     name: options.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||
|     ...options | ||||
|     name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||
|     ...finalOptions | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @@ -339,16 +384,26 @@ export function createApiRoute( | ||||
| /** | ||||
|  * Create a WebSocket route configuration | ||||
|  * @param domains Domain(s) to match | ||||
|  * @param wsPath WebSocket path (e.g., "/ws") | ||||
|  * @param target Target WebSocket server host and port | ||||
|  * @param options Additional route options | ||||
|  * @param targetOrPath Target server OR WebSocket path (legacy) | ||||
|  * @param targetOrOptions Target server (legacy) OR options | ||||
|  * @param options Additional route options (legacy) | ||||
|  * @returns Route configuration object | ||||
|  */ | ||||
| export function createWebSocketRoute( | ||||
|   domains: string | string[], | ||||
|   wsPath: string, | ||||
|   target: { host: string | string[]; port: number }, | ||||
|   options: { | ||||
|   targetOrPath: { host: string | string[]; port: number } | string, | ||||
|   targetOrOptions?: { host: string | string[]; port: number } | { | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     path?: string; | ||||
|     httpPort?: number | number[]; | ||||
|     httpsPort?: number | number[]; | ||||
|     pingInterval?: number; | ||||
|     pingTimeout?: number; | ||||
|     name?: string; | ||||
|     [key: string]: any; | ||||
|   }, | ||||
|   options?: { | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     httpPort?: number | number[]; | ||||
| @@ -357,16 +412,33 @@ export function createWebSocketRoute( | ||||
|     pingTimeout?: number; | ||||
|     name?: string; | ||||
|     [key: string]: any; | ||||
|   } = {} | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   // Handle different signatures | ||||
|   let target: { host: string | string[]; port: number }; | ||||
|   let wsPath: string; | ||||
|   let finalOptions: any; | ||||
|    | ||||
|   if (typeof targetOrPath === 'string') { | ||||
|     // Legacy signature: (domains, path, target, options) | ||||
|     wsPath = targetOrPath; | ||||
|     target = targetOrOptions as { host: string | string[]; port: number }; | ||||
|     finalOptions = options || {}; | ||||
|   } else { | ||||
|     // New signature: (domains, target, options) | ||||
|     target = targetOrPath; | ||||
|     finalOptions = (targetOrOptions as any) || {}; | ||||
|     wsPath = finalOptions.path || '/ws'; | ||||
|   } | ||||
|    | ||||
|   // Normalize WebSocket path | ||||
|   const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`; | ||||
|  | ||||
|   // Create route match | ||||
|   const match: IRouteMatch = { | ||||
|     ports: options.useTls | ||||
|       ? (options.httpsPort || 443) | ||||
|       : (options.httpPort || 80), | ||||
|     ports: finalOptions.useTls | ||||
|       ? (finalOptions.httpsPort || 443) | ||||
|       : (finalOptions.httpPort || 80), | ||||
|     domains, | ||||
|     path: normalizedPath | ||||
|   }; | ||||
| @@ -377,16 +449,16 @@ export function createWebSocketRoute( | ||||
|     targets: [target], | ||||
|     websocket: { | ||||
|       enabled: true, | ||||
|       pingInterval: options.pingInterval || 30000, // 30 seconds | ||||
|       pingTimeout: options.pingTimeout || 5000    // 5 seconds | ||||
|       pingInterval: finalOptions.pingInterval || 30000, // 30 seconds | ||||
|       pingTimeout: finalOptions.pingTimeout || 5000    // 5 seconds | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   // Add TLS configuration if using HTTPS | ||||
|   if (options.useTls) { | ||||
|   if (finalOptions.useTls) { | ||||
|     action.tls = { | ||||
|       mode: 'terminate', | ||||
|       certificate: options.certificate || 'auto' | ||||
|       certificate: finalOptions.certificate || 'auto' | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -394,9 +466,9 @@ export function createWebSocketRoute( | ||||
|   return { | ||||
|     match, | ||||
|     action, | ||||
|     name: options.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||
|     priority: options.priority || 100, // Higher priority for WebSocket routes | ||||
|     ...options | ||||
|     name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||
|     priority: finalOptions.priority || 100, // Higher priority for WebSocket routes | ||||
|     ...finalOptions | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @@ -1030,3 +1102,152 @@ export const SocketHandlers = { | ||||
|     }); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Create an API Gateway route pattern | ||||
|  * @param domains Domain(s) to match | ||||
|  * @param apiBasePath Base path for API endpoints (e.g., '/api') | ||||
|  * @param target Target host and port | ||||
|  * @param options Additional route options | ||||
|  * @returns API route configuration | ||||
|  */ | ||||
| export function createApiGatewayRoute( | ||||
|   domains: string | string[], | ||||
|   apiBasePath: string, | ||||
|   target: { host: string | string[]; port: number }, | ||||
|   options: { | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     addCorsHeaders?: boolean; | ||||
|     [key: string]: any; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   // Normalize apiBasePath to ensure it starts with / and doesn't end with / | ||||
|   const normalizedPath = apiBasePath.startsWith('/')  | ||||
|     ? apiBasePath  | ||||
|     : `/${apiBasePath}`; | ||||
|    | ||||
|   // Add wildcard to path to match all API endpoints | ||||
|   const apiPath = normalizedPath.endsWith('/')  | ||||
|     ? `${normalizedPath}*`  | ||||
|     : `${normalizedPath}/*`; | ||||
|    | ||||
|   // Create base route | ||||
|   const baseRoute = options.useTls | ||||
|     ? createHttpsTerminateRoute(domains, target, { | ||||
|         certificate: options.certificate || 'auto' | ||||
|       }) | ||||
|     : createHttpRoute(domains, target); | ||||
|    | ||||
|   // Add API-specific configurations | ||||
|   const apiRoute: Partial<IRouteConfig> = { | ||||
|     match: { | ||||
|       ...baseRoute.match, | ||||
|       path: apiPath | ||||
|     }, | ||||
|     name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`, | ||||
|     priority: options.priority || 100 // Higher priority for specific path matching | ||||
|   }; | ||||
|    | ||||
|   // Add CORS headers if requested | ||||
|   if (options.addCorsHeaders) { | ||||
|     apiRoute.headers = { | ||||
|       response: { | ||||
|         'Access-Control-Allow-Origin': '*', | ||||
|         'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||||
|         'Access-Control-Allow-Headers': 'Content-Type, Authorization', | ||||
|         'Access-Control-Max-Age': '86400' | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   return mergeRouteConfigs(baseRoute, apiRoute); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a rate limiting route pattern | ||||
|  * @param baseRoute Base route to add rate limiting to | ||||
|  * @param rateLimit Rate limiting configuration | ||||
|  * @returns Route with rate limiting | ||||
|  */ | ||||
| export function addRateLimiting( | ||||
|   baseRoute: IRouteConfig, | ||||
|   rateLimit: { | ||||
|     maxRequests: number; | ||||
|     window: number; // Time window in seconds | ||||
|     keyBy?: 'ip' | 'path' | 'header'; | ||||
|     headerName?: string; // Required if keyBy is 'header' | ||||
|     errorMessage?: string; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return mergeRouteConfigs(baseRoute, { | ||||
|     security: { | ||||
|       rateLimit: { | ||||
|         enabled: true, | ||||
|         maxRequests: rateLimit.maxRequests, | ||||
|         window: rateLimit.window, | ||||
|         keyBy: rateLimit.keyBy || 'ip', | ||||
|         headerName: rateLimit.headerName, | ||||
|         errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.' | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a basic authentication route pattern | ||||
|  * @param baseRoute Base route to add authentication to | ||||
|  * @param auth Authentication configuration | ||||
|  * @returns Route with basic authentication | ||||
|  */ | ||||
| export function addBasicAuth( | ||||
|   baseRoute: IRouteConfig, | ||||
|   auth: { | ||||
|     users: Array<{ username: string; password: string }>; | ||||
|     realm?: string; | ||||
|     excludePaths?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return mergeRouteConfigs(baseRoute, { | ||||
|     security: { | ||||
|       basicAuth: { | ||||
|         enabled: true, | ||||
|         users: auth.users, | ||||
|         realm: auth.realm || 'Restricted Area', | ||||
|         excludePaths: auth.excludePaths || [] | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a JWT authentication route pattern | ||||
|  * @param baseRoute Base route to add JWT authentication to | ||||
|  * @param jwt JWT authentication configuration | ||||
|  * @returns Route with JWT authentication | ||||
|  */ | ||||
| export function addJwtAuth( | ||||
|   baseRoute: IRouteConfig, | ||||
|   jwt: { | ||||
|     secret: string; | ||||
|     algorithm?: string; | ||||
|     issuer?: string; | ||||
|     audience?: string; | ||||
|     expiresIn?: number; // Time in seconds | ||||
|     excludePaths?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return mergeRouteConfigs(baseRoute, { | ||||
|     security: { | ||||
|       jwtAuth: { | ||||
|         enabled: true, | ||||
|         secret: jwt.secret, | ||||
|         algorithm: jwt.algorithm || 'HS256', | ||||
|         issuer: jwt.issuer, | ||||
|         audience: jwt.audience, | ||||
|         expiresIn: jwt.expiresIn, | ||||
|         excludePaths: jwt.excludePaths || [] | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,403 +0,0 @@ | ||||
| /** | ||||
|  * Route Patterns | ||||
|  *  | ||||
|  * This file provides pre-defined route patterns for common use cases. | ||||
|  * These patterns can be used as templates for creating route configurations. | ||||
|  */ | ||||
|  | ||||
| import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js'; | ||||
| import { mergeRouteConfigs } from './route-utils.js'; | ||||
| import { SocketHandlers } from './route-helpers.js'; | ||||
|  | ||||
| /** | ||||
|  * Create a basic HTTP route configuration | ||||
|  */ | ||||
| export function createHttpRoute( | ||||
|   domains: string | string[], | ||||
|   target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) }, | ||||
|   options: Partial<IRouteConfig> = {} | ||||
| ): IRouteConfig { | ||||
|   const route: IRouteConfig = { | ||||
|     match: { | ||||
|       domains, | ||||
|       ports: 80 | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       targets: [{ | ||||
|         host: target.host, | ||||
|         port: target.port | ||||
|       }] | ||||
|     }, | ||||
|     name: options.name || `HTTP: ${Array.isArray(domains) ? domains.join(', ') : domains}` | ||||
|   }; | ||||
|  | ||||
|   return mergeRouteConfigs(route, options); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTPS route with TLS termination | ||||
|  */ | ||||
| export function createHttpsTerminateRoute( | ||||
|   domains: string | string[], | ||||
|   target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) }, | ||||
|   options: Partial<IRouteConfig> & { | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     reencrypt?: boolean; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   const route: IRouteConfig = { | ||||
|     match: { | ||||
|       domains, | ||||
|       ports: 443 | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       targets: [{ | ||||
|         host: target.host, | ||||
|         port: target.port | ||||
|       }], | ||||
|       tls: { | ||||
|         mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate', | ||||
|         certificate: options.certificate || 'auto' | ||||
|       } | ||||
|     }, | ||||
|     name: options.name || `HTTPS (terminate): ${Array.isArray(domains) ? domains.join(', ') : domains}` | ||||
|   }; | ||||
|  | ||||
|   return mergeRouteConfigs(route, options); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTPS route with TLS passthrough | ||||
|  */ | ||||
| export function createHttpsPassthroughRoute( | ||||
|   domains: string | string[], | ||||
|   target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) }, | ||||
|   options: Partial<IRouteConfig> = {} | ||||
| ): IRouteConfig { | ||||
|   const route: IRouteConfig = { | ||||
|     match: { | ||||
|       domains, | ||||
|       ports: 443 | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       targets: [{ | ||||
|         host: target.host, | ||||
|         port: target.port | ||||
|       }], | ||||
|       tls: { | ||||
|         mode: 'passthrough' | ||||
|       } | ||||
|     }, | ||||
|     name: options.name || `HTTPS (passthrough): ${Array.isArray(domains) ? domains.join(', ') : domains}` | ||||
|   }; | ||||
|  | ||||
|   return mergeRouteConfigs(route, options); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTP to HTTPS redirect route | ||||
|  */ | ||||
| export function createHttpToHttpsRedirect( | ||||
|   domains: string | string[], | ||||
|   options: Partial<IRouteConfig> & { | ||||
|     redirectCode?: 301 | 302 | 307 | 308; | ||||
|     preservePath?: boolean; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   const route: IRouteConfig = { | ||||
|     match: { | ||||
|       domains, | ||||
|       ports: 80 | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'socket-handler', | ||||
|       socketHandler: SocketHandlers.httpRedirect( | ||||
|         options.preservePath ? 'https://{domain}{path}' : 'https://{domain}', | ||||
|         options.redirectCode || 301 | ||||
|       ) | ||||
|     }, | ||||
|     name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}` | ||||
|   }; | ||||
|  | ||||
|   return mergeRouteConfigs(route, options); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a complete HTTPS server with redirect from HTTP | ||||
|  */ | ||||
| export function createCompleteHttpsServer( | ||||
|   domains: string | string[], | ||||
|   target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) }, | ||||
|   options: Partial<IRouteConfig> & { | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     tlsMode?: 'terminate' | 'passthrough' | 'terminate-and-reencrypt'; | ||||
|     redirectCode?: 301 | 302 | 307 | 308; | ||||
|   } = {} | ||||
| ): IRouteConfig[] { | ||||
|   // Create the TLS route based on the selected mode | ||||
|   const tlsRoute = options.tlsMode === 'passthrough'  | ||||
|     ? createHttpsPassthroughRoute(domains, target, options) | ||||
|     : createHttpsTerminateRoute(domains, target, { | ||||
|         ...options, | ||||
|         reencrypt: options.tlsMode === 'terminate-and-reencrypt' | ||||
|       }); | ||||
|    | ||||
|   // Create the HTTP to HTTPS redirect route | ||||
|   const redirectRoute = createHttpToHttpsRedirect(domains, { | ||||
|     redirectCode: options.redirectCode, | ||||
|     preservePath: true | ||||
|   }); | ||||
|    | ||||
|   return [tlsRoute, redirectRoute]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an API Gateway route pattern | ||||
|  * @param domains Domain(s) to match | ||||
|  * @param apiBasePath Base path for API endpoints (e.g., '/api') | ||||
|  * @param target Target host and port | ||||
|  * @param options Additional route options | ||||
|  * @returns API route configuration | ||||
|  */ | ||||
| export function createApiGatewayRoute( | ||||
|   domains: string | string[], | ||||
|   apiBasePath: string, | ||||
|   target: { host: string | string[]; port: number }, | ||||
|   options: { | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     addCorsHeaders?: boolean; | ||||
|     [key: string]: any; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   // Normalize apiBasePath to ensure it starts with / and doesn't end with / | ||||
|   const normalizedPath = apiBasePath.startsWith('/')  | ||||
|     ? apiBasePath  | ||||
|     : `/${apiBasePath}`; | ||||
|    | ||||
|   // Add wildcard to path to match all API endpoints | ||||
|   const apiPath = normalizedPath.endsWith('/')  | ||||
|     ? `${normalizedPath}*`  | ||||
|     : `${normalizedPath}/*`; | ||||
|    | ||||
|   // Create base route | ||||
|   const baseRoute = options.useTls | ||||
|     ? createHttpsTerminateRoute(domains, target, { | ||||
|         certificate: options.certificate || 'auto' | ||||
|       }) | ||||
|     : createHttpRoute(domains, target); | ||||
|    | ||||
|   // Add API-specific configurations | ||||
|   const apiRoute: Partial<IRouteConfig> = { | ||||
|     match: { | ||||
|       ...baseRoute.match, | ||||
|       path: apiPath | ||||
|     }, | ||||
|     name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`, | ||||
|     priority: options.priority || 100 // Higher priority for specific path matching | ||||
|   }; | ||||
|    | ||||
|   // Add CORS headers if requested | ||||
|   if (options.addCorsHeaders) { | ||||
|     apiRoute.headers = { | ||||
|       response: { | ||||
|         'Access-Control-Allow-Origin': '*', | ||||
|         'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||||
|         'Access-Control-Allow-Headers': 'Content-Type, Authorization', | ||||
|         'Access-Control-Max-Age': '86400' | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   return mergeRouteConfigs(baseRoute, apiRoute); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a WebSocket route pattern | ||||
|  * @param domains Domain(s) to match | ||||
|  * @param target WebSocket server host and port | ||||
|  * @param options Additional route options | ||||
|  * @returns WebSocket route configuration | ||||
|  */ | ||||
| export function createWebSocketRoute( | ||||
|   domains: string | string[], | ||||
|   target: { host: string | string[]; port: number }, | ||||
|   options: { | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     path?: string; | ||||
|     [key: string]: any; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   // Create base route | ||||
|   const baseRoute = options.useTls | ||||
|     ? createHttpsTerminateRoute(domains, target, { | ||||
|         certificate: options.certificate || 'auto' | ||||
|       }) | ||||
|     : createHttpRoute(domains, target); | ||||
|    | ||||
|   // Add WebSocket-specific configurations | ||||
|   const wsRoute: Partial<IRouteConfig> = { | ||||
|     match: { | ||||
|       ...baseRoute.match, | ||||
|       path: options.path || '/ws', | ||||
|       headers: { | ||||
|         'Upgrade': 'websocket' | ||||
|       } | ||||
|     }, | ||||
|     action: { | ||||
|       ...baseRoute.action, | ||||
|       websocket: { | ||||
|         enabled: true, | ||||
|         pingInterval: options.pingInterval || 30000, // 30 seconds | ||||
|         pingTimeout: options.pingTimeout || 5000    // 5 seconds | ||||
|       } | ||||
|     }, | ||||
|     name: options.name || `WebSocket: ${Array.isArray(domains) ? domains.join(', ') : domains} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`, | ||||
|     priority: options.priority || 100 // Higher priority for WebSocket routes | ||||
|   }; | ||||
|    | ||||
|   return mergeRouteConfigs(baseRoute, wsRoute); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a load balancer route pattern | ||||
|  * @param domains Domain(s) to match | ||||
|  * @param backends Array of backend servers | ||||
|  * @param options Additional route options | ||||
|  * @returns Load balancer route configuration | ||||
|  */ | ||||
| export function createLoadBalancerRoute( | ||||
|   domains: string | string[], | ||||
|   backends: Array<{ host: string; port: number }>, | ||||
|   options: { | ||||
|     useTls?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     algorithm?: 'round-robin' | 'least-connections' | 'ip-hash'; | ||||
|     healthCheck?: { | ||||
|       path: string; | ||||
|       interval: number; | ||||
|       timeout: number; | ||||
|       unhealthyThreshold: number; | ||||
|       healthyThreshold: number; | ||||
|     }; | ||||
|     [key: string]: any; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   // Extract hosts and ensure all backends use the same port | ||||
|   const port = backends[0].port; | ||||
|   const hosts = backends.map(backend => backend.host); | ||||
|    | ||||
|   // Create route with multiple hosts for load balancing | ||||
|   const baseRoute = options.useTls | ||||
|     ? createHttpsTerminateRoute(domains, { host: hosts, port }, { | ||||
|         certificate: options.certificate || 'auto' | ||||
|       }) | ||||
|     : createHttpRoute(domains, { host: hosts, port }); | ||||
|    | ||||
|   // Add load balancing specific configurations | ||||
|   const lbRoute: Partial<IRouteConfig> = { | ||||
|     action: { | ||||
|       ...baseRoute.action, | ||||
|       loadBalancing: { | ||||
|         algorithm: options.algorithm || 'round-robin', | ||||
|         healthCheck: options.healthCheck | ||||
|       } | ||||
|     }, | ||||
|     name: options.name || `Load Balancer: ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||
|     priority: options.priority || 50 | ||||
|   }; | ||||
|    | ||||
|   return mergeRouteConfigs(baseRoute, lbRoute); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a rate limiting route pattern | ||||
|  * @param baseRoute Base route to add rate limiting to | ||||
|  * @param rateLimit Rate limiting configuration | ||||
|  * @returns Route with rate limiting | ||||
|  */ | ||||
| export function addRateLimiting( | ||||
|   baseRoute: IRouteConfig, | ||||
|   rateLimit: { | ||||
|     maxRequests: number; | ||||
|     window: number; // Time window in seconds | ||||
|     keyBy?: 'ip' | 'path' | 'header'; | ||||
|     headerName?: string; // Required if keyBy is 'header' | ||||
|     errorMessage?: string; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return mergeRouteConfigs(baseRoute, { | ||||
|     security: { | ||||
|       rateLimit: { | ||||
|         enabled: true, | ||||
|         maxRequests: rateLimit.maxRequests, | ||||
|         window: rateLimit.window, | ||||
|         keyBy: rateLimit.keyBy || 'ip', | ||||
|         headerName: rateLimit.headerName, | ||||
|         errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.' | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a basic authentication route pattern | ||||
|  * @param baseRoute Base route to add authentication to | ||||
|  * @param auth Authentication configuration | ||||
|  * @returns Route with basic authentication | ||||
|  */ | ||||
| export function addBasicAuth( | ||||
|   baseRoute: IRouteConfig, | ||||
|   auth: { | ||||
|     users: Array<{ username: string; password: string }>; | ||||
|     realm?: string; | ||||
|     excludePaths?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return mergeRouteConfigs(baseRoute, { | ||||
|     security: { | ||||
|       basicAuth: { | ||||
|         enabled: true, | ||||
|         users: auth.users, | ||||
|         realm: auth.realm || 'Restricted Area', | ||||
|         excludePaths: auth.excludePaths || [] | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a JWT authentication route pattern | ||||
|  * @param baseRoute Base route to add JWT authentication to | ||||
|  * @param jwt JWT authentication configuration | ||||
|  * @returns Route with JWT authentication | ||||
|  */ | ||||
| export function addJwtAuth( | ||||
|   baseRoute: IRouteConfig, | ||||
|   jwt: { | ||||
|     secret: string; | ||||
|     algorithm?: string; | ||||
|     issuer?: string; | ||||
|     audience?: string; | ||||
|     expiresIn?: number; // Time in seconds | ||||
|     excludePaths?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return mergeRouteConfigs(baseRoute, { | ||||
|     security: { | ||||
|       jwtAuth: { | ||||
|         enabled: true, | ||||
|         secret: jwt.secret, | ||||
|         algorithm: jwt.algorithm || 'HS256', | ||||
|         issuer: jwt.issuer, | ||||
|         audience: jwt.audience, | ||||
|         expiresIn: jwt.expiresIn, | ||||
|         excludePaths: jwt.excludePaths || [] | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user