import * as plugins from '../../plugins.js'; import { logger } from '../../core/utils/logger.js'; import type { IRouteConfig, IRouteContext } from './models/route-types.js'; import type { RoutePreprocessor } from './route-preprocessor.js'; /** * Unix domain socket server that receives relayed connections from the Rust proxy. * * When Rust encounters a route of type `socket-handler`, it connects to this * Unix socket, sends a JSON metadata line, then proxies the raw TCP bytes. * This server reads the metadata, finds the original JS handler, builds an * IRouteContext, and hands the socket to the handler. */ export class SocketHandlerServer { private server: plugins.net.Server | null = null; private socketPath: string; private preprocessor: RoutePreprocessor; constructor(preprocessor: RoutePreprocessor) { this.preprocessor = preprocessor; this.socketPath = `/tmp/smartproxy-relay-${process.pid}.sock`; } /** * The Unix socket path this server listens on. */ public getSocketPath(): string { return this.socketPath; } /** * Start listening for relayed connections from Rust. */ public async start(): Promise { // Clean up stale socket file try { await plugins.fs.promises.unlink(this.socketPath); } catch { // Ignore if doesn't exist } return new Promise((resolve, reject) => { this.server = plugins.net.createServer((socket) => { this.handleConnection(socket); }); this.server.on('error', (err) => { logger.log('error', `SocketHandlerServer error: ${err.message}`, { component: 'socket-handler-server' }); }); this.server.listen(this.socketPath, () => { logger.log('info', `SocketHandlerServer listening on ${this.socketPath}`, { component: 'socket-handler-server' }); resolve(); }); this.server.on('error', reject); }); } /** * Stop the server and clean up. */ public async stop(): Promise { if (this.server) { return new Promise((resolve) => { this.server!.close(() => { this.server = null; // Clean up socket file plugins.fs.unlink(this.socketPath, () => resolve()); }); }); } } /** * Handle an incoming relayed connection from Rust. * * Protocol: Rust sends a single JSON line with metadata, then raw bytes follow. * JSON format: { "routeKey": "my-route", "remoteIP": "1.2.3.4", "remotePort": 12345, * "localPort": 443, "isTLS": true, "domain": "example.com" } */ private handleConnection(socket: plugins.net.Socket): void { let metadataBuffer = ''; let metadataParsed = false; const onData = (chunk: Buffer) => { if (metadataParsed) return; metadataBuffer += chunk.toString('utf8'); const newlineIndex = metadataBuffer.indexOf('\n'); if (newlineIndex === -1) { // Haven't received full metadata line yet if (metadataBuffer.length > 8192) { logger.log('error', 'Socket handler metadata too large, closing', { component: 'socket-handler-server' }); socket.destroy(); } return; } metadataParsed = true; socket.removeListener('data', onData); const metadataJson = metadataBuffer.slice(0, newlineIndex); const remainingData = metadataBuffer.slice(newlineIndex + 1); let metadata: any; try { metadata = JSON.parse(metadataJson); } catch { logger.log('error', `Invalid socket handler metadata JSON: ${metadataJson.slice(0, 200)}`, { component: 'socket-handler-server' }); socket.destroy(); return; } this.dispatchToHandler(socket, metadata, remainingData); }; socket.on('data', onData); socket.on('error', (err) => { logger.log('error', `Socket handler relay error: ${err.message}`, { component: 'socket-handler-server' }); }); } /** * Dispatch a relayed connection to the appropriate JS handler. */ private dispatchToHandler(socket: plugins.net.Socket, metadata: any, remainingData: string): void { const routeKey = metadata.routeKey as string; if (!routeKey) { logger.log('error', 'Socket handler relay missing routeKey', { component: 'socket-handler-server' }); socket.destroy(); return; } const originalRoute = this.preprocessor.getOriginalRoute(routeKey); if (!originalRoute) { logger.log('error', `No handler found for route: ${routeKey}`, { component: 'socket-handler-server' }); socket.destroy(); return; } const handler = originalRoute.action.socketHandler; if (!handler) { logger.log('error', `Route ${routeKey} has no socketHandler`, { component: 'socket-handler-server' }); socket.destroy(); return; } // Build route context const context: IRouteContext = { port: metadata.localPort || 0, domain: metadata.domain, clientIp: metadata.remoteIP || 'unknown', serverIp: '0.0.0.0', path: metadata.path, isTls: metadata.isTLS || false, tlsVersion: metadata.tlsVersion, routeName: originalRoute.name, routeId: originalRoute.id, timestamp: Date.now(), connectionId: metadata.connectionId || `relay-${Date.now()}`, }; // If there was remaining data after the metadata line, push it back if (remainingData.length > 0) { socket.unshift(Buffer.from(remainingData, 'utf8')); } // Call the handler try { handler(socket, context); } catch (err: any) { logger.log('error', `Socket handler threw for route ${routeKey}: ${err.message}`, { component: 'socket-handler-server' }); socket.destroy(); } } }