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 { // 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) | null = null; let cleanupServer: ((reason: string) => Promise) | 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 }); } }