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,5 +1,14 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-07-22 - 21.0.0 - BREAKING_CHANGE(forwarding) | ||||||
|  | Remove legacy forwarding module | ||||||
|  |  | ||||||
|  | - 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 | ||||||
|  | - MIGRATION: Replace `import { forwarding } from '@push.rocks/smartproxy'` with direct imports of route helpers | ||||||
|  |  | ||||||
| ## 2025-07-21 - 20.0.2 - fix(docs) | ## 2025-07-21 - 20.0.2 - fix(docs) | ||||||
| Update documentation to improve clarity | Update documentation to improve clarity | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@push.rocks/smartproxy", |   "name": "@push.rocks/smartproxy", | ||||||
|   "version": "20.0.2", |   "version": "21.0.0", | ||||||
|   "private": false, |   "private": false, | ||||||
|   "description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.", |   "description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.", | ||||||
|   "main": "dist_ts/index.js", |   "main": "dist_ts/index.js", | ||||||
|   | |||||||
							
								
								
									
										2749
									
								
								test-output.log
									
									
									
									
									
								
							
							
						
						
									
										2749
									
								
								test-output.log
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,9 +1,6 @@ | |||||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||||
| import * as plugins from '../ts/plugins.js'; | import * as plugins from '../ts/plugins.js'; | ||||||
| import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js'; |  | ||||||
|  |  | ||||||
| // First, import the components directly to avoid issues with compiled modules |  | ||||||
| import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js'; |  | ||||||
| // Import route-based helpers | // Import route-based helpers | ||||||
| import { | import { | ||||||
|   createHttpRoute, |   createHttpRoute, | ||||||
|   | |||||||
| @@ -1,53 +0,0 @@ | |||||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; |  | ||||||
| import * as plugins from '../ts/plugins.js'; |  | ||||||
|  |  | ||||||
| // First, import the components directly to avoid issues with compiled modules |  | ||||||
| import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js'; |  | ||||||
| // Import route-based helpers from the correct location |  | ||||||
| import { |  | ||||||
|   createHttpRoute, |  | ||||||
|   createHttpsTerminateRoute, |  | ||||||
|   createHttpsPassthroughRoute, |  | ||||||
|   createHttpToHttpsRedirect, |  | ||||||
|   createCompleteHttpsServer, |  | ||||||
|   createLoadBalancerRoute |  | ||||||
| } from '../ts/proxies/smart-proxy/utils/route-patterns.js'; |  | ||||||
|  |  | ||||||
| // Create helper functions for building forwarding configs |  | ||||||
| const helpers = { |  | ||||||
|   httpOnly: () => ({ type: 'http-only' as const }), |  | ||||||
|   tlsTerminateToHttp: () => ({ type: 'https-terminate-to-http' as const }), |  | ||||||
|   tlsTerminateToHttps: () => ({ type: 'https-terminate-to-https' as const }), |  | ||||||
|   httpsPassthrough: () => ({ type: 'https-passthrough' as const }) |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => { |  | ||||||
|   // HTTP-only defaults |  | ||||||
|   const httpConfig = { |  | ||||||
|     type: 'http-only' as const, |  | ||||||
|     target: { host: 'localhost', port: 3000 } |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   const httpWithDefaults = ForwardingHandlerFactory['applyDefaults'](httpConfig); |  | ||||||
|    |  | ||||||
|   expect(httpWithDefaults.port).toEqual(80); |  | ||||||
|   expect(httpWithDefaults.socket).toEqual('/tmp/forwarding-http-only-80.sock'); |  | ||||||
|    |  | ||||||
|   // HTTPS passthrough defaults |  | ||||||
|   const httpsPassthroughConfig = { |  | ||||||
|     type: 'https-passthrough' as const, |  | ||||||
|     target: { host: 'localhost', port: 443 } |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   const httpsPassthroughWithDefaults = ForwardingHandlerFactory['applyDefaults'](httpsPassthroughConfig); |  | ||||||
|    |  | ||||||
|   expect(httpsPassthroughWithDefaults.port).toEqual(443); |  | ||||||
|   expect(httpsPassthroughWithDefaults.socket).toEqual('/tmp/forwarding-https-passthrough-443.sock'); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('ForwardingHandlerFactory - factory function for handlers', async () => { |  | ||||||
|   // @todo Implement unit tests for ForwardingHandlerFactory |  | ||||||
|   // These tests would need proper mocking of the handlers |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export default tap.start(); |  | ||||||
| @@ -47,7 +47,7 @@ import { | |||||||
|   addRateLimiting, |   addRateLimiting, | ||||||
|   addBasicAuth, |   addBasicAuth, | ||||||
|   addJwtAuth |   addJwtAuth | ||||||
| } from '../ts/proxies/smart-proxy/utils/route-patterns.js'; | } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; | ||||||
|  |  | ||||||
| import type { | import type { | ||||||
|   IRouteConfig, |   IRouteConfig, | ||||||
|   | |||||||
| @@ -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'; | export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js'; | ||||||
|  |  | ||||||
| // Modular exports for new architecture | // Modular exports for new architecture | ||||||
| export * as forwarding from './forwarding/index.js'; |  | ||||||
| // Certificate module has been removed - use SmartCertManager instead | // Certificate module has been removed - use SmartCertManager instead | ||||||
| export * as tls from './tls/index.js'; | export * as tls from './tls/index.js'; | ||||||
| export * as routing from './routing/index.js'; | export * as routing from './routing/index.js'; | ||||||
| @@ -16,7 +16,6 @@ export interface IAcmeOptions { | |||||||
|   routeForwards?: any[]; |   routeForwards?: any[]; | ||||||
| } | } | ||||||
| import type { IRouteConfig } from './route-types.js'; | import type { IRouteConfig } from './route-types.js'; | ||||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Provision object for static or HTTP-01 certificate |  * Provision object for static or HTTP-01 certificate | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import * as plugins from '../../../plugins.js'; | import * as plugins from '../../../plugins.js'; | ||||||
| // Certificate types removed - use local definition | // 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 { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; | ||||||
| import type { IRouteContext } from '../../../core/models/route-context.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 route utilities for route operations | ||||||
| export * from './route-utils.js'; | export * from './route-utils.js'; | ||||||
|  |  | ||||||
| // Export route patterns with renamed exports to avoid conflicts | // Export additional functions from route-helpers that weren't already exported | ||||||
| import { |  | ||||||
|   createWebSocketRoute as createWebSocketPatternRoute, |  | ||||||
|   createLoadBalancerRoute as createLoadBalancerPatternRoute, |  | ||||||
|   createApiGatewayRoute, |  | ||||||
|   addRateLimiting, |  | ||||||
|   addBasicAuth, |  | ||||||
|   addJwtAuth |  | ||||||
| } from './route-patterns.js'; |  | ||||||
|  |  | ||||||
| export { | export { | ||||||
|   createWebSocketPatternRoute, |  | ||||||
|   createLoadBalancerPatternRoute, |  | ||||||
|   createApiGatewayRoute, |   createApiGatewayRoute, | ||||||
|   addRateLimiting, |   addRateLimiting, | ||||||
|   addBasicAuth, |   addBasicAuth, | ||||||
|   addJwtAuth |   addJwtAuth | ||||||
| }; | } from './route-helpers.js'; | ||||||
|  |  | ||||||
| // Migration utilities have been removed as they are no longer needed | // Migration utilities have been removed as they are no longer needed | ||||||
| @@ -20,6 +20,7 @@ | |||||||
|  |  | ||||||
| import * as plugins from '../../../plugins.js'; | import * as plugins from '../../../plugins.js'; | ||||||
| import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.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 |  * Create an HTTP-only route configuration | ||||||
| @@ -211,26 +212,62 @@ export function createCompleteHttpsServer( | |||||||
| /** | /** | ||||||
|  * Create a load balancer route (round-robin between multiple backend hosts) |  * Create a load balancer route (round-robin between multiple backend hosts) | ||||||
|  * @param domains Domain(s) to match |  * @param domains Domain(s) to match | ||||||
|  * @param hosts Array of backend hosts to load balance between |  * @param backendsOrHosts Array of backend servers OR array of host strings (legacy) | ||||||
|  * @param port Backend port |  * @param portOrOptions Port number (legacy) OR options object | ||||||
|  * @param options Additional route options |  * @param options Additional route options (legacy) | ||||||
|  * @returns Route configuration object |  * @returns Route configuration object | ||||||
|  */ |  */ | ||||||
| export function createLoadBalancerRoute( | export function createLoadBalancerRoute( | ||||||
|   domains: string | string[], |   domains: string | string[], | ||||||
|   hosts: string[], |   backendsOrHosts: Array<{ host: string; port: number }> | string[], | ||||||
|   port: number, |   portOrOptions?: number | { | ||||||
|   options: { |     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?: { |     tls?: { | ||||||
|       mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; |       mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||||
|       certificate?: 'auto' | { key: string; cert: string }; |       certificate?: 'auto' | { key: string; cert: string }; | ||||||
|     }; |     }; | ||||||
|     [key: string]: any; |     [key: string]: any; | ||||||
|   } = {} |   } | ||||||
| ): IRouteConfig { | ): 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 |   // Create route match | ||||||
|   const match: IRouteMatch = { |   const match: IRouteMatch = { | ||||||
|     ports: options.match?.ports || (options.tls ? 443 : 80), |     ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80), | ||||||
|     domains |     domains | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -247,10 +284,18 @@ export function createLoadBalancerRoute( | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // Add TLS configuration if provided |   // Add TLS configuration if provided | ||||||
|   if (options.tls) { |   if (finalOptions.tls || finalOptions.useTls) { | ||||||
|     action.tls = { |     action.tls = { | ||||||
|       mode: options.tls.mode, |       mode: finalOptions.tls?.mode || 'terminate', | ||||||
|       certificate: options.tls.certificate || 'auto' |       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 { |   return { | ||||||
|     match, |     match, | ||||||
|     action, |     action, | ||||||
|     name: options.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`, |     name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||||
|     ...options |     ...finalOptions | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -339,16 +384,26 @@ export function createApiRoute( | |||||||
| /** | /** | ||||||
|  * Create a WebSocket route configuration |  * Create a WebSocket route configuration | ||||||
|  * @param domains Domain(s) to match |  * @param domains Domain(s) to match | ||||||
|  * @param wsPath WebSocket path (e.g., "/ws") |  * @param targetOrPath Target server OR WebSocket path (legacy) | ||||||
|  * @param target Target WebSocket server host and port |  * @param targetOrOptions Target server (legacy) OR options | ||||||
|  * @param options Additional route options |  * @param options Additional route options (legacy) | ||||||
|  * @returns Route configuration object |  * @returns Route configuration object | ||||||
|  */ |  */ | ||||||
| export function createWebSocketRoute( | export function createWebSocketRoute( | ||||||
|   domains: string | string[], |   domains: string | string[], | ||||||
|   wsPath: string, |   targetOrPath: { host: string | string[]; port: number } | string, | ||||||
|   target: { host: string | string[]; port: number }, |   targetOrOptions?: { host: string | string[]; port: number } | { | ||||||
|   options: { |     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; |     useTls?: boolean; | ||||||
|     certificate?: 'auto' | { key: string; cert: string }; |     certificate?: 'auto' | { key: string; cert: string }; | ||||||
|     httpPort?: number | number[]; |     httpPort?: number | number[]; | ||||||
| @@ -357,16 +412,33 @@ export function createWebSocketRoute( | |||||||
|     pingTimeout?: number; |     pingTimeout?: number; | ||||||
|     name?: string; |     name?: string; | ||||||
|     [key: string]: any; |     [key: string]: any; | ||||||
|   } = {} |   } | ||||||
| ): IRouteConfig { | ): 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 |   // Normalize WebSocket path | ||||||
|   const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`; |   const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`; | ||||||
|  |  | ||||||
|   // Create route match |   // Create route match | ||||||
|   const match: IRouteMatch = { |   const match: IRouteMatch = { | ||||||
|     ports: options.useTls |     ports: finalOptions.useTls | ||||||
|       ? (options.httpsPort || 443) |       ? (finalOptions.httpsPort || 443) | ||||||
|       : (options.httpPort || 80), |       : (finalOptions.httpPort || 80), | ||||||
|     domains, |     domains, | ||||||
|     path: normalizedPath |     path: normalizedPath | ||||||
|   }; |   }; | ||||||
| @@ -377,16 +449,16 @@ export function createWebSocketRoute( | |||||||
|     targets: [target], |     targets: [target], | ||||||
|     websocket: { |     websocket: { | ||||||
|       enabled: true, |       enabled: true, | ||||||
|       pingInterval: options.pingInterval || 30000, // 30 seconds |       pingInterval: finalOptions.pingInterval || 30000, // 30 seconds | ||||||
|       pingTimeout: options.pingTimeout || 5000    // 5 seconds |       pingTimeout: finalOptions.pingTimeout || 5000    // 5 seconds | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // Add TLS configuration if using HTTPS |   // Add TLS configuration if using HTTPS | ||||||
|   if (options.useTls) { |   if (finalOptions.useTls) { | ||||||
|     action.tls = { |     action.tls = { | ||||||
|       mode: 'terminate', |       mode: 'terminate', | ||||||
|       certificate: options.certificate || 'auto' |       certificate: finalOptions.certificate || 'auto' | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -394,9 +466,9 @@ export function createWebSocketRoute( | |||||||
|   return { |   return { | ||||||
|     match, |     match, | ||||||
|     action, |     action, | ||||||
|     name: options.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, |     name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, | ||||||
|     priority: options.priority || 100, // Higher priority for WebSocket routes |     priority: finalOptions.priority || 100, // Higher priority for WebSocket routes | ||||||
|     ...options |     ...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