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'; /** * 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 { // 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(); } } }