/** * Socket Handler Functions * * This module provides pre-built socket handlers for common use cases * like echoing, proxying, HTTP responses, and redirects. */ import * as plugins from '../../../../plugins.js'; import type { IRouteConfig, TPortRange, IRouteContext } from '../../models/route-types.js'; import { ProtocolDetector } from '../../../../detection/index.js'; import { createSocketTracker } from '../../../../core/utils/socket-tracker.js'; /** * Pre-built socket handlers for common use cases */ export const SocketHandlers = { /** * Simple echo server handler */ echo: (socket: plugins.net.Socket, context: IRouteContext) => { socket.write('ECHO SERVER READY\n'); socket.on('data', data => socket.write(data)); }, /** * TCP proxy handler */ proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => { const target = plugins.net.connect(targetPort, targetHost); socket.pipe(target); target.pipe(socket); socket.on('close', () => target.destroy()); target.on('close', () => socket.destroy()); target.on('error', (err) => { console.error('Proxy target error:', err); socket.destroy(); }); }, /** * Line-based protocol handler */ lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => { let buffer = ''; socket.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; lines.forEach(line => { if (line.trim()) { handler(line.trim(), socket); } }); }); }, /** * Simple HTTP response handler (for testing) */ httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => { const response = [ `HTTP/1.1 ${statusCode} ${statusCode === 200 ? 'OK' : 'Error'}`, 'Content-Type: text/plain', `Content-Length: ${body.length}`, 'Connection: close', '', body ].join('\r\n'); socket.write(response); socket.end(); }, /** * Block connection immediately */ block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => { const finalMessage = message || `Connection blocked from ${context.clientIp}`; if (finalMessage) { socket.write(finalMessage); } socket.end(); }, /** * HTTP block response */ httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => { const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`; const finalMessage = message || defaultMessage; const response = [ `HTTP/1.1 ${statusCode} ${finalMessage}`, 'Content-Type: text/plain', `Content-Length: ${finalMessage.length}`, 'Connection: close', '', finalMessage ].join('\r\n'); socket.write(response); socket.end(); }, /** * HTTP redirect handler * Uses the centralized detection module for HTTP parsing */ httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => { const tracker = createSocketTracker(socket); const connectionId = ProtocolDetector.createConnectionId({ socketId: context.connectionId || `${Date.now()}-${Math.random()}` }); const handleData = async (data: Buffer) => { // Use detection module for parsing const detectionResult = await ProtocolDetector.detectWithConnectionTracking( data, connectionId, { extractFullHeaders: false } // We only need method and path ); if (detectionResult.protocol === 'http' && detectionResult.connectionInfo.path) { const method = detectionResult.connectionInfo.method || 'GET'; const path = detectionResult.connectionInfo.path || '/'; const domain = context.domain || 'localhost'; const port = context.port; let finalLocation = locationTemplate .replace('{domain}', domain) .replace('{port}', String(port)) .replace('{path}', path) .replace('{clientIp}', context.clientIp); const message = `Redirecting to ${finalLocation}`; const response = [ `HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`, `Location: ${finalLocation}`, 'Content-Type: text/plain', `Content-Length: ${message.length}`, 'Connection: close', '', message ].join('\r\n'); socket.write(response); } else { // Not a valid HTTP request, close connection socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'); } socket.end(); // Clean up detection state ProtocolDetector.cleanupConnections(); // Clean up all tracked resources tracker.cleanup(); }; // Use tracker to manage the listener socket.once('data', handleData); tracker.addListener('error', (err) => { tracker.safeDestroy(err); }); tracker.addListener('close', () => { tracker.cleanup(); }); }, /** * HTTP server handler for ACME challenges and other HTTP needs * Uses the centralized detection module for HTTP parsing */ httpServer: (handler: (req: { method: string; url: string; headers: Record; body?: string }, res: { status: (code: number) => void; header: (name: string, value: string) => void; send: (data: string) => void; end: () => void }) => void) => (socket: plugins.net.Socket, context: IRouteContext) => { const tracker = createSocketTracker(socket); let requestParsed = false; let responseTimer: NodeJS.Timeout | null = null; const connectionId = ProtocolDetector.createConnectionId({ socketId: context.connectionId || `${Date.now()}-${Math.random()}` }); const processData = async (data: Buffer) => { if (requestParsed) return; // Only handle the first request // Use HttpDetector for parsing const detectionResult = await ProtocolDetector.detectWithConnectionTracking( data, connectionId, { extractFullHeaders: true } ); if (detectionResult.protocol !== 'http' || !detectionResult.isComplete) { // Not a complete HTTP request yet return; } requestParsed = true; // Remove data listener after parsing request socket.removeListener('data', processData); const connInfo = detectionResult.connectionInfo; // Create request object from detection result const req = { method: connInfo.method || 'GET', url: connInfo.path || '/', headers: connInfo.headers || {}, body: detectionResult.remainingBuffer?.toString() || '' }; // Create response object let statusCode = 200; const responseHeaders: Record = {}; let ended = false; const res = { status: (code: number) => { statusCode = code; }, header: (name: string, value: string) => { responseHeaders[name] = value; }, send: (data: string) => { if (ended) return; ended = true; // Clear response timer since we're sending now if (responseTimer) { clearTimeout(responseTimer); responseTimer = null; } if (!responseHeaders['content-type']) { responseHeaders['content-type'] = 'text/plain'; } responseHeaders['content-length'] = String(data.length); responseHeaders['connection'] = 'close'; const statusText = statusCode === 200 ? 'OK' : statusCode === 404 ? 'Not Found' : statusCode === 500 ? 'Internal Server Error' : 'Response'; let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`; for (const [name, value] of Object.entries(responseHeaders)) { response += `${name}: ${value}\r\n`; } response += '\r\n'; response += data; socket.write(response); socket.end(); }, end: () => { if (ended) return; ended = true; socket.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'); socket.end(); } }; try { handler(req, res); // Ensure response is sent even if handler doesn't call send() responseTimer = setTimeout(() => { if (!ended) { res.send(''); } responseTimer = null; }, 1000); // Track and unref the timer tracker.addTimer(responseTimer); } catch (error) { if (!ended) { res.status(500); res.send('Internal Server Error'); } // Use safeDestroy for error cases tracker.safeDestroy(error instanceof Error ? error : new Error('Handler error')); } }; // Use tracker to manage listeners tracker.addListener('data', processData); tracker.addListener('error', (err) => { if (!requestParsed) { tracker.safeDestroy(err); } }); tracker.addListener('close', () => { // Clear any pending response timer if (responseTimer) { clearTimeout(responseTimer); responseTimer = null; } // Clean up detection state ProtocolDetector.cleanupConnections(); // Clean up all tracked resources tracker.cleanup(); }); } }; /** * Create a socket handler route configuration * @param domains Domain(s) to match * @param ports Port(s) to listen on * @param handler Socket handler function * @param options Additional route options * @returns Route configuration object */ export function createSocketHandlerRoute( domains: string | string[], ports: TPortRange, handler: (socket: plugins.net.Socket) => void | Promise, options: { name?: string; priority?: number; path?: string; } = {} ): IRouteConfig { return { name: options.name || 'socket-handler-route', priority: options.priority !== undefined ? options.priority : 50, match: { domains, ports, ...(options.path && { path: options.path }) }, action: { type: 'socket-handler', socketHandler: handler } }; }