This commit addresses critical issues where unhandled socket connection errors (ECONNREFUSED) would crash the server and cause memory leaks with rising connection counts. Changes: - Add createSocketWithErrorHandler() utility that attaches error handlers immediately upon socket creation - Update https-passthrough-handler to use safe socket creation and clean up client sockets on server connection failure - Update https-terminate-to-http-handler to use safe socket creation - Ensure proper connection cleanup when server connections fail - Document the fix in readme.hints.md and create implementation plan in readme.plan.md The fix prevents race conditions where sockets could emit errors before handlers were attached, and ensures failed connections are properly cleaned up to prevent memory leaks.
185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
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
|
|
});
|
|
}
|
|
} |