/** * 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 { IRouteContext } from '../models/route-types.js'; import { createSocketTracker } from '../../../core/utils/socket-tracker.js'; /** * Minimal HTTP request parser for socket handlers. * Parses method, path, and optionally headers from a raw buffer. */ function parseHttpRequest(data: Buffer, extractHeaders: boolean = false): { method: string; path: string; headers: Record; isComplete: boolean; body?: string; } | null { const str = data.toString('utf8'); const headerEnd = str.indexOf('\r\n\r\n'); const isComplete = headerEnd !== -1; const headerSection = isComplete ? str.slice(0, headerEnd) : str; const lines = headerSection.split('\r\n'); const requestLine = lines[0]; if (!requestLine) return null; const parts = requestLine.split(' '); if (parts.length < 2) return null; const method = parts[0]; const path = parts[1]; // Quick check: valid HTTP method const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE']; if (!validMethods.includes(method)) return null; const headers: Record = {}; if (extractHeaders) { for (let i = 1; i < lines.length; i++) { const colonIdx = lines[i].indexOf(':'); if (colonIdx > 0) { const name = lines[i].slice(0, colonIdx).trim().toLowerCase(); const value = lines[i].slice(colonIdx + 1).trim(); headers[name] = value; } } } const body = isComplete ? str.slice(headerEnd + 4) : undefined; return { method, path, headers, isComplete, body }; } /** * 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 */ httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => { const tracker = createSocketTracker(socket); const handleData = (data: Buffer) => { const parsed = parseHttpRequest(data); if (parsed) { const path = parsed.path || '/'; const domain = context.domain || 'localhost'; const port = context.port; const 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 { socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'); } socket.end(); tracker.cleanup(); }; 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 */ 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 processData = (data: Buffer) => { if (requestParsed) return; const parsed = parseHttpRequest(data, true); if (!parsed || !parsed.isComplete) { return; // Not a complete HTTP request yet } requestParsed = true; socket.removeListener('data', processData); const req = { method: parsed.method, url: parsed.path, headers: parsed.headers, body: parsed.body || '' }; 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; 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); responseTimer = setTimeout(() => { if (!ended) { res.send(''); } responseTimer = null; }, 1000); tracker.addTimer(responseTimer); } catch (error) { if (!ended) { res.status(500); res.send('Internal Server Error'); } tracker.safeDestroy(error instanceof Error ? error : new Error('Handler error')); } }; tracker.addListener('data', processData); tracker.addListener('error', (err) => { if (!requestParsed) { tracker.safeDestroy(err); } }); tracker.addListener('close', () => { if (responseTimer) { clearTimeout(responseTimer); responseTimer = null; } tracker.cleanup(); }); } };