update structure
This commit is contained in:
		
							
								
								
									
										127
									
								
								ts/forwarding/handlers/base-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								ts/forwarding/handlers/base-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   ForwardConfig, | ||||
|   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: ForwardConfig) { | ||||
|     super(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler | ||||
|    * Base implementation does nothing, subclasses should override as needed | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // Base implementation - no initialization needed | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle a new socket connection | ||||
|    * @param socket The incoming socket connection | ||||
|    */ | ||||
|   public abstract handleConnection(socket: plugins.net.Socket): void; | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void; | ||||
|    | ||||
|   /** | ||||
|    * Get a target from the configuration, supporting round-robin selection | ||||
|    * @returns A resolved target object with host and port | ||||
|    */ | ||||
|   protected getTargetFromConfig(): { host: string, port: number } { | ||||
|     const { target } = this.config; | ||||
|      | ||||
|     // Handle round-robin host selection | ||||
|     if (Array.isArray(target.host)) { | ||||
|       if (target.host.length === 0) { | ||||
|         throw new Error('No target hosts specified'); | ||||
|       } | ||||
|        | ||||
|       // Simple round-robin selection | ||||
|       const randomIndex = Math.floor(Math.random() * target.host.length); | ||||
|       return { | ||||
|         host: target.host[randomIndex], | ||||
|         port: target.port | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Single host | ||||
|     return { | ||||
|       host: target.host, | ||||
|       port: target.port | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Redirect an HTTP request to HTTPS | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     const host = req.headers.host || ''; | ||||
|     const path = req.url || '/'; | ||||
|     const redirectUrl = `https://${host}${path}`; | ||||
|      | ||||
|     res.writeHead(301, { | ||||
|       'Location': redirectUrl, | ||||
|       'Cache-Control': 'no-cache' | ||||
|     }); | ||||
|     res.end(`Redirecting to ${redirectUrl}`); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|       statusCode: 301, | ||||
|       headers: { 'Location': redirectUrl }, | ||||
|       size: 0 | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply custom headers from configuration | ||||
|    * @param headers The original headers | ||||
|    * @param variables Variables to replace in the headers | ||||
|    * @returns The headers with custom values applied | ||||
|    */ | ||||
|   protected applyCustomHeaders( | ||||
|     headers: Record<string, string | string[] | undefined>, | ||||
|     variables: Record<string, string> | ||||
|   ): Record<string, string | string[] | undefined> { | ||||
|     const customHeaders = this.config.advanced?.headers || {}; | ||||
|     const result = { ...headers }; | ||||
|      | ||||
|     // Apply custom headers with variable substitution | ||||
|     for (const [key, value] of Object.entries(customHeaders)) { | ||||
|       let processedValue = value; | ||||
|        | ||||
|       // Replace variables in the header value | ||||
|       for (const [varName, varValue] of Object.entries(variables)) { | ||||
|         processedValue = processedValue.replace(`{${varName}}`, varValue); | ||||
|       } | ||||
|        | ||||
|       result[key] = processedValue; | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the timeout for this connection from configuration | ||||
|    * @returns Timeout in milliseconds | ||||
|    */ | ||||
|   protected getTimeout(): number { | ||||
|     return this.config.advanced?.timeout || 60000; // Default: 60 seconds | ||||
|   } | ||||
| } | ||||
							
								
								
									
										140
									
								
								ts/forwarding/handlers/http-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								ts/forwarding/handlers/http-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { ForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTP-only forwarding | ||||
|  */ | ||||
| export class HttpForwardingHandler extends ForwardingHandler { | ||||
|   /** | ||||
|    * Create a new HTTP forwarding handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: ForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTP-only configuration | ||||
|     if (config.type !== 'http-only') { | ||||
|       throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a raw socket connection | ||||
|    * HTTP handler doesn't do much with raw sockets as it mainly processes | ||||
|    * parsed HTTP requests | ||||
|    */ | ||||
|   public handleConnection(socket: plugins.net.Socket): void { | ||||
|     // For HTTP, we mainly handle parsed requests, but we can still set up | ||||
|     // some basic connection tracking | ||||
|     const remoteAddress = socket.remoteAddress || 'unknown'; | ||||
|      | ||||
|     socket.on('close', (hadError) => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         hadError | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     socket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: error.message | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create a custom headers object with variables for substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track bytes for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										182
									
								
								ts/forwarding/handlers/https-passthrough-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								ts/forwarding/handlers/https-passthrough-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { ForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS passthrough (SNI forwarding without termination) | ||||
|  */ | ||||
| export class HttpsPassthroughHandler extends ForwardingHandler { | ||||
|   /** | ||||
|    * Create a new HTTPS passthrough handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: ForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS passthrough configuration | ||||
|     if (config.type !== 'https-passthrough') { | ||||
|       throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by forwarding it without termination | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Log the connection | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Create a connection to the target server | ||||
|     const serverSocket = plugins.net.connect(target.port, target.host); | ||||
|      | ||||
|     // Handle errors on the server socket | ||||
|     serverSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `Target connection error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Close the client socket if it's still open | ||||
|       if (!clientSocket.destroyed) { | ||||
|         clientSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Handle errors on the client socket | ||||
|     clientSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `Client connection error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Close the server socket if it's still open | ||||
|       if (!serverSocket.destroyed) { | ||||
|         serverSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track data transfer for logging | ||||
|     let bytesSent = 0; | ||||
|     let bytesReceived = 0; | ||||
|      | ||||
|     // Forward data from client to server | ||||
|     clientSocket.on('data', (data) => { | ||||
|       bytesSent += data.length; | ||||
|        | ||||
|       // Check if server socket is writable | ||||
|       if (serverSocket.writable) { | ||||
|         const flushed = serverSocket.write(data); | ||||
|          | ||||
|         // Handle backpressure | ||||
|         if (!flushed) { | ||||
|           clientSocket.pause(); | ||||
|           serverSocket.once('drain', () => { | ||||
|             clientSocket.resume(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|         direction: 'outbound', | ||||
|         bytes: data.length, | ||||
|         total: bytesSent | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Forward data from server to client | ||||
|     serverSocket.on('data', (data) => { | ||||
|       bytesReceived += data.length; | ||||
|        | ||||
|       // Check if client socket is writable | ||||
|       if (clientSocket.writable) { | ||||
|         const flushed = clientSocket.write(data); | ||||
|          | ||||
|         // Handle backpressure | ||||
|         if (!flushed) { | ||||
|           serverSocket.pause(); | ||||
|           clientSocket.once('drain', () => { | ||||
|             serverSocket.resume(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|         direction: 'inbound', | ||||
|         bytes: data.length, | ||||
|         total: bytesReceived | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle connection close | ||||
|     const handleClose = () => { | ||||
|       if (!clientSocket.destroyed) { | ||||
|         clientSocket.destroy(); | ||||
|       } | ||||
|        | ||||
|       if (!serverSocket.destroyed) { | ||||
|         serverSocket.destroy(); | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         bytesSent, | ||||
|         bytesReceived | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Set up close handlers | ||||
|     clientSocket.on('close', handleClose); | ||||
|     serverSocket.on('close', handleClose); | ||||
|      | ||||
|     // Set timeouts | ||||
|     const timeout = this.getTimeout(); | ||||
|     clientSocket.setTimeout(timeout); | ||||
|     serverSocket.setTimeout(timeout); | ||||
|      | ||||
|     // Handle timeouts | ||||
|     clientSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'Client connection timeout' | ||||
|       }); | ||||
|       handleClose(); | ||||
|     }); | ||||
|      | ||||
|     serverSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'Server connection timeout' | ||||
|       }); | ||||
|       handleClose(); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request - HTTPS passthrough doesn't support HTTP | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // HTTPS passthrough doesn't support HTTP requests | ||||
|     res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||||
|     res.end('HTTP not supported for this domain'); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|       statusCode: 404, | ||||
|       headers: { 'Content-Type': 'text/plain' }, | ||||
|       size: 'HTTP not supported for this domain'.length | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										264
									
								
								ts/forwarding/handlers/https-terminate-to-http-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								ts/forwarding/handlers/https-terminate-to-http-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { ForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS termination with HTTP backend | ||||
|  */ | ||||
| export class HttpsTerminateToHttpHandler extends ForwardingHandler { | ||||
|   private tlsServer: plugins.tls.Server | null = null; | ||||
|   private secureContext: plugins.tls.SecureContext | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new HTTPS termination with HTTP backend handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: ForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS terminate to HTTP configuration | ||||
|     if (config.type !== 'https-terminate-to-http') { | ||||
|       throw new Error(`Invalid configuration type for HttpsTerminateToHttpHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler, setting up TLS context | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // We need to load or create TLS certificates | ||||
|     if (this.config.https?.customCert) { | ||||
|       // Use custom certificate from configuration | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: this.config.https.customCert.key, | ||||
|         cert: this.config.https.customCert.cert | ||||
|       }); | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { | ||||
|         source: 'config', | ||||
|         domain: this.config.target.host | ||||
|       }); | ||||
|     } else if (this.config.acme?.enabled) { | ||||
|       // Request certificate through ACME if needed | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { | ||||
|         domain: Array.isArray(this.config.target.host)  | ||||
|           ? this.config.target.host[0]  | ||||
|           : this.config.target.host, | ||||
|         useProduction: this.config.acme.production || false | ||||
|       }); | ||||
|        | ||||
|       // In a real implementation, we would wait for the certificate to be issued | ||||
|       // For now, we'll use a dummy context | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', | ||||
|         cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set the secure context for TLS termination | ||||
|    * Called when a certificate is available | ||||
|    * @param context The secure context | ||||
|    */ | ||||
|   public setSecureContext(context: plugins.tls.SecureContext): void { | ||||
|     this.secureContext = context; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by terminating TLS and forwarding to HTTP backend | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Make sure we have a secure context | ||||
|     if (!this.secureContext) { | ||||
|       clientSocket.destroy(new Error('TLS secure context not initialized')); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     // Create a TLS socket using our secure context | ||||
|     const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { | ||||
|       secureContext: this.secureContext, | ||||
|       isServer: true, | ||||
|       server: this.tlsServer || undefined | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       tls: true | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS errors | ||||
|     tlsSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `TLS error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // The TLS socket will now emit HTTP traffic that can be processed | ||||
|     // In a real implementation, we would create an HTTP parser and handle | ||||
|     // the requests here, but for simplicity, we'll just log the data | ||||
|      | ||||
|     let dataBuffer = Buffer.alloc(0); | ||||
|      | ||||
|     tlsSocket.on('data', (data) => { | ||||
|       // Append to buffer | ||||
|       dataBuffer = Buffer.concat([dataBuffer, data]); | ||||
|        | ||||
|       // Very basic HTTP parsing - in a real implementation, use http-parser | ||||
|       if (dataBuffer.includes(Buffer.from('\r\n\r\n'))) { | ||||
|         const target = this.getTargetFromConfig(); | ||||
|          | ||||
|         // Simple example: forward the data to an HTTP server | ||||
|         const socket = plugins.net.connect(target.port, target.host, () => { | ||||
|           socket.write(dataBuffer); | ||||
|           dataBuffer = Buffer.alloc(0); | ||||
|            | ||||
|           // Set up bidirectional data flow | ||||
|           tlsSocket.pipe(socket); | ||||
|           socket.pipe(tlsSocket); | ||||
|         }); | ||||
|          | ||||
|         socket.on('error', (error) => { | ||||
|           this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|             remoteAddress, | ||||
|             error: `Target connection error: ${error.message}` | ||||
|           }); | ||||
|            | ||||
|           if (!tlsSocket.destroyed) { | ||||
|             tlsSocket.destroy(); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Handle close | ||||
|     tlsSocket.on('close', () => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request by forwarding to the HTTP backend | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Check if we should redirect to HTTPS | ||||
|     if (this.config.http?.redirectToHttps) { | ||||
|       this.redirectToHttps(req, res); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create custom headers with variable substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track response size for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										292
									
								
								ts/forwarding/handlers/https-terminate-to-https-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								ts/forwarding/handlers/https-terminate-to-https-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,292 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './base-handler.js'; | ||||
| import type { ForwardConfig } from '../config/forwarding-types.js'; | ||||
| import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS termination with HTTPS backend | ||||
|  */ | ||||
| export class HttpsTerminateToHttpsHandler extends ForwardingHandler { | ||||
|   private secureContext: plugins.tls.SecureContext | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new HTTPS termination with HTTPS backend handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: ForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS terminate to HTTPS configuration | ||||
|     if (config.type !== 'https-terminate-to-https') { | ||||
|       throw new Error(`Invalid configuration type for HttpsTerminateToHttpsHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler, setting up TLS context | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // We need to load or create TLS certificates for termination | ||||
|     if (this.config.https?.customCert) { | ||||
|       // Use custom certificate from configuration | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: this.config.https.customCert.key, | ||||
|         cert: this.config.https.customCert.cert | ||||
|       }); | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { | ||||
|         source: 'config', | ||||
|         domain: this.config.target.host | ||||
|       }); | ||||
|     } else if (this.config.acme?.enabled) { | ||||
|       // Request certificate through ACME if needed | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { | ||||
|         domain: Array.isArray(this.config.target.host)  | ||||
|           ? this.config.target.host[0]  | ||||
|           : this.config.target.host, | ||||
|         useProduction: this.config.acme.production || false | ||||
|       }); | ||||
|        | ||||
|       // In a real implementation, we would wait for the certificate to be issued | ||||
|       // For now, we'll use a dummy context | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', | ||||
|         cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set the secure context for TLS termination | ||||
|    * Called when a certificate is available | ||||
|    * @param context The secure context | ||||
|    */ | ||||
|   public setSecureContext(context: plugins.tls.SecureContext): void { | ||||
|     this.secureContext = context; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by terminating TLS and creating a new TLS connection to backend | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Make sure we have a secure context | ||||
|     if (!this.secureContext) { | ||||
|       clientSocket.destroy(new Error('TLS secure context not initialized')); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     // Create a TLS socket using our secure context | ||||
|     const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { | ||||
|       secureContext: this.secureContext, | ||||
|       isServer: true | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       tls: true | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS errors | ||||
|     tlsSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `TLS error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // The TLS socket will now emit HTTP traffic that can be processed | ||||
|     // In a real implementation, we would create an HTTP parser and handle | ||||
|     // the requests here, but for simplicity, we'll just forward the data | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Set up the connection to the HTTPS backend | ||||
|     const connectToBackend = () => { | ||||
|       const backendSocket = plugins.tls.connect({ | ||||
|         host: target.host, | ||||
|         port: target.port, | ||||
|         // In a real implementation, we would configure TLS options | ||||
|         rejectUnauthorized: false // For testing only, never use in production | ||||
|       }, () => { | ||||
|         this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|           direction: 'outbound', | ||||
|           target: `${target.host}:${target.port}`, | ||||
|           tls: true | ||||
|         }); | ||||
|          | ||||
|         // Set up bidirectional data flow | ||||
|         tlsSocket.pipe(backendSocket); | ||||
|         backendSocket.pipe(tlsSocket); | ||||
|       }); | ||||
|        | ||||
|       backendSocket.on('error', (error) => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: `Backend connection error: ${error.message}` | ||||
|         }); | ||||
|          | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Handle close | ||||
|       backendSocket.on('close', () => { | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Set timeout | ||||
|       const timeout = this.getTimeout(); | ||||
|       backendSocket.setTimeout(timeout); | ||||
|        | ||||
|       backendSocket.on('timeout', () => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: 'Backend connection timeout' | ||||
|         }); | ||||
|          | ||||
|         if (!backendSocket.destroyed) { | ||||
|           backendSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Wait for the TLS handshake to complete before connecting to backend | ||||
|     tlsSocket.on('secure', () => { | ||||
|       connectToBackend(); | ||||
|     }); | ||||
|      | ||||
|     // Handle close | ||||
|     tlsSocket.on('close', () => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request by forwarding to the HTTPS backend | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Check if we should redirect to HTTPS | ||||
|     if (this.config.http?.redirectToHttps) { | ||||
|       this.redirectToHttps(req, res); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create custom headers with variable substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers, | ||||
|       // In a real implementation, we would configure TLS options | ||||
|       rejectUnauthorized: false // For testing only, never use in production | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request using HTTPS | ||||
|     const proxyReq = plugins.https.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track response size for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								ts/forwarding/handlers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ts/forwarding/handlers/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| /** | ||||
|  * 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'; | ||||
		Reference in New Issue
	
	Block a user