import * as plugins from '../../../plugins.js'; import type { IRouteConfig } from '../../smart-proxy/models/route-types.js'; import type { IConnectionRecord } from '../../smart-proxy/models/interfaces.js'; import type { ILogger } from '../models/types.js'; import { createLogger } from '../models/types.js'; import type { IRouteContext } from '../../../core/models/route-context.js'; import { HttpStatus, getStatusText } from '../models/http-types.js'; export interface IStaticHandlerContext { connectionId: string; connectionManager: any; // Avoid circular deps settings: any; logger?: ILogger; } /** * Handles static routes including ACME challenges */ export class StaticHandler { /** * Handle static routes */ public static async handleStatic( socket: plugins.net.Socket, route: IRouteConfig, context: IStaticHandlerContext, record: IConnectionRecord, initialChunk?: Buffer ): Promise { const { connectionId, connectionManager, settings } = context; const logger = context.logger || createLogger(settings.logLevel || 'info'); if (!route.action.handler) { logger.error(`[${connectionId}] Static route '${route.name}' has no handler`); socket.end(); connectionManager.cleanupConnection(record, 'no_handler'); return; } let buffer = Buffer.alloc(0); let processingData = false; const handleHttpData = async (chunk: Buffer) => { // Accumulate the data buffer = Buffer.concat([buffer, chunk]); // Prevent concurrent processing of the same buffer if (processingData) return; processingData = true; try { // Process data until we have a complete request or need more data await processBuffer(); } finally { processingData = false; } }; const processBuffer = async () => { // Look for end of HTTP headers const headerEndIndex = buffer.indexOf('\r\n\r\n'); if (headerEndIndex === -1) { // Need more data if (buffer.length > 8192) { // Prevent excessive buffering logger.error(`[${connectionId}] HTTP headers too large`); socket.end(); connectionManager.cleanupConnection(record, 'headers_too_large'); } return; // Wait for more data to arrive } // Parse the HTTP request const headerBuffer = buffer.slice(0, headerEndIndex); const headers = headerBuffer.toString(); const lines = headers.split('\r\n'); if (lines.length === 0) { logger.error(`[${connectionId}] Invalid HTTP request`); socket.end(); connectionManager.cleanupConnection(record, 'invalid_request'); return; } // Parse request line const requestLine = lines[0]; const requestParts = requestLine.split(' '); if (requestParts.length < 3) { logger.error(`[${connectionId}] Invalid HTTP request line`); socket.end(); connectionManager.cleanupConnection(record, 'invalid_request_line'); return; } const [method, path, httpVersion] = requestParts; // Parse headers const headersMap: Record = {}; for (let i = 1; i < lines.length; i++) { const colonIndex = lines[i].indexOf(':'); if (colonIndex > 0) { const key = lines[i].slice(0, colonIndex).trim().toLowerCase(); const value = lines[i].slice(colonIndex + 1).trim(); headersMap[key] = value; } } // Check for Content-Length to handle request body const requestBodyLength = parseInt(headersMap['content-length'] || '0', 10); const bodyStartIndex = headerEndIndex + 4; // Skip the \r\n\r\n // If there's a body, ensure we have the full body if (requestBodyLength > 0) { const totalExpectedLength = bodyStartIndex + requestBodyLength; // If we don't have the complete body yet, wait for more data if (buffer.length < totalExpectedLength) { // Implement a reasonable body size limit to prevent memory issues if (requestBodyLength > 1024 * 1024) { // 1MB limit logger.error(`[${connectionId}] Request body too large`); socket.end(); connectionManager.cleanupConnection(record, 'body_too_large'); return; } return; // Wait for more data } } // Extract query string if present let pathname = path; let query: string | undefined; const queryIndex = path.indexOf('?'); if (queryIndex !== -1) { pathname = path.slice(0, queryIndex); query = path.slice(queryIndex + 1); } try { // Get request body if present let requestBody: Buffer | undefined; if (requestBodyLength > 0) { requestBody = buffer.slice(bodyStartIndex, bodyStartIndex + requestBodyLength); } // Pause socket to prevent data loss during async processing socket.pause(); // Remove the data listener since we're handling the request socket.removeListener('data', handleHttpData); // Build route context with parsed HTTP information const context: IRouteContext = { port: record.localPort, domain: record.lockedDomain || headersMap['host']?.split(':')[0], clientIp: record.remoteIP, serverIp: socket.localAddress!, path: pathname, query: query, headers: headersMap, isTls: record.isTLS, tlsVersion: record.tlsVersion, routeName: route.name, routeId: route.id, timestamp: Date.now(), connectionId, }; // Since IRouteContext doesn't have a body property, // we need an alternative approach to handle the body let response; if (requestBody) { if (settings.enableDetailedLogging) { logger.info( `[${connectionId}] Processing request with body (${requestBody.length} bytes)` ); } // Pass the body as an additional parameter by extending the context object // This is not type-safe, but it allows handlers that expect a body to work const extendedContext = { ...context, // Provide both raw buffer and string representation requestBody: requestBody, requestBodyText: requestBody.toString(), method: method, }; // Call the handler with the extended context // The handler needs to know to look for the non-standard properties response = await route.action.handler(extendedContext as any); } else { // Call the handler with the standard context const extendedContext = { ...context, method: method, }; response = await route.action.handler(extendedContext as any); } // Prepare the HTTP response const responseHeaders = response.headers || {}; const contentLength = Buffer.byteLength(response.body || ''); responseHeaders['Content-Length'] = contentLength.toString(); if (!responseHeaders['Content-Type']) { responseHeaders['Content-Type'] = 'text/plain'; } // Build the response let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`; for (const [key, value] of Object.entries(responseHeaders)) { httpResponse += `${key}: ${value}\r\n`; } httpResponse += '\r\n'; // Send response socket.write(httpResponse); if (response.body) { socket.write(response.body); } socket.end(); connectionManager.cleanupConnection(record, 'completed'); } catch (error) { logger.error(`[${connectionId}] Error in static handler: ${error}`); // Send error response const errorResponse = 'HTTP/1.1 500 Internal Server Error\r\n' + 'Content-Type: text/plain\r\n' + 'Content-Length: 21\r\n' + '\r\n' + 'Internal Server Error'; socket.write(errorResponse); socket.end(); connectionManager.cleanupConnection(record, 'handler_error'); } }; // Process initial chunk if provided if (initialChunk && initialChunk.length > 0) { if (settings.enableDetailedLogging) { logger.info(`[${connectionId}] Processing initial data chunk (${initialChunk.length} bytes)`); } // Process the initial chunk immediately handleHttpData(initialChunk); } // Listen for additional data socket.on('data', handleHttpData); // Ensure cleanup on socket close socket.once('close', () => { socket.removeListener('data', handleHttpData); }); } }