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