/** * HTTP protocol detector */ import type { IProtocolDetector } from '../models/interfaces.js'; import type { IDetectionResult, IDetectionOptions, IConnectionInfo, THttpMethod } from '../models/detection-types.js'; import { extractLine, isPrintableAscii, BufferAccumulator } from '../utils/buffer-utils.js'; import { parseHttpRequestLine, parseHttpHeaders, extractDomainFromHost, isHttpMethod } from '../utils/parser-utils.js'; /** * HTTP detector implementation */ export class HttpDetector implements IProtocolDetector { /** * Minimum bytes needed to identify HTTP method */ private static readonly MIN_HTTP_METHOD_SIZE = 3; // GET /** * Maximum reasonable HTTP header size */ private static readonly MAX_HEADER_SIZE = 8192; /** * Fragment tracking for incomplete headers */ private static fragmentedBuffers = new Map(); /** * Detect HTTP protocol from buffer */ detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null { // Check if buffer is too small if (buffer.length < HttpDetector.MIN_HTTP_METHOD_SIZE) { return null; } // Quick check: first bytes should be printable ASCII if (!isPrintableAscii(buffer, Math.min(20, buffer.length))) { return null; } // Try to extract the first line const firstLineResult = extractLine(buffer, 0); if (!firstLineResult) { // No complete line yet return { protocol: 'http', connectionInfo: { protocol: 'http' }, isComplete: false, bytesNeeded: buffer.length + 100 // Estimate }; } // Parse the request line const requestLine = parseHttpRequestLine(firstLineResult.line); if (!requestLine) { // Not a valid HTTP request line return null; } // Initialize connection info const connectionInfo: IConnectionInfo = { protocol: 'http', method: requestLine.method, path: requestLine.path, httpVersion: requestLine.version }; // Check if we want to extract headers if (options?.extractFullHeaders !== false) { // Look for the end of headers (double CRLF) const headerEndSequence = Buffer.from('\r\n\r\n'); const headerEndIndex = buffer.indexOf(headerEndSequence); if (headerEndIndex === -1) { // Headers not complete yet const maxSize = options?.maxBufferSize || HttpDetector.MAX_HEADER_SIZE; if (buffer.length >= maxSize) { // Headers too large, reject return null; } return { protocol: 'http', connectionInfo, isComplete: false, bytesNeeded: buffer.length + 200 // Estimate }; } // Extract all header lines const headerLines: string[] = []; let currentOffset = firstLineResult.nextOffset; while (currentOffset < headerEndIndex) { const lineResult = extractLine(buffer, currentOffset); if (!lineResult) { break; } if (lineResult.line.length === 0) { // Empty line marks end of headers break; } headerLines.push(lineResult.line); currentOffset = lineResult.nextOffset; } // Parse headers const headers = parseHttpHeaders(headerLines); connectionInfo.headers = headers; // Extract domain from Host header const hostHeader = headers['host']; if (hostHeader) { connectionInfo.domain = extractDomainFromHost(hostHeader); } // Calculate remaining buffer const bodyStartIndex = headerEndIndex + 4; // After \r\n\r\n const remainingBuffer = buffer.length > bodyStartIndex ? buffer.slice(bodyStartIndex) : undefined; return { protocol: 'http', connectionInfo, remainingBuffer, isComplete: true }; } else { // Just extract Host header for domain let currentOffset = firstLineResult.nextOffset; const maxLines = 50; // Reasonable limit for (let i = 0; i < maxLines && currentOffset < buffer.length; i++) { const lineResult = extractLine(buffer, currentOffset); if (!lineResult) { // Need more data return { protocol: 'http', connectionInfo, isComplete: false, bytesNeeded: buffer.length + 50 }; } if (lineResult.line.length === 0) { // End of headers break; } // Quick check for Host header if (lineResult.line.toLowerCase().startsWith('host:')) { const colonIndex = lineResult.line.indexOf(':'); const hostValue = lineResult.line.slice(colonIndex + 1).trim(); connectionInfo.domain = extractDomainFromHost(hostValue); // If we only needed the domain, we can return early return { protocol: 'http', connectionInfo, isComplete: true }; } currentOffset = lineResult.nextOffset; } // If we reach here, no Host header found yet return { protocol: 'http', connectionInfo, isComplete: false, bytesNeeded: buffer.length + 100 }; } } /** * Check if buffer can be handled by this detector */ canHandle(buffer: Buffer): boolean { if (buffer.length < HttpDetector.MIN_HTTP_METHOD_SIZE) { return false; } // Check if first bytes could be an HTTP method const firstWord = buffer.slice(0, Math.min(10, buffer.length)).toString('ascii').split(' ')[0]; return isHttpMethod(firstWord); } /** * Get minimum bytes needed for detection */ getMinimumBytes(): number { return HttpDetector.MIN_HTTP_METHOD_SIZE; } /** * Quick check if buffer starts with HTTP method */ static quickCheck(buffer: Buffer): boolean { if (buffer.length < 3) { return false; } // Check common HTTP methods const start = buffer.slice(0, 7).toString('ascii'); return start.startsWith('GET ') || start.startsWith('POST ') || start.startsWith('PUT ') || start.startsWith('DELETE ') || start.startsWith('HEAD ') || start.startsWith('OPTIONS') || start.startsWith('PATCH ') || start.startsWith('CONNECT') || start.startsWith('TRACE '); } /** * Handle fragmented HTTP detection with connection tracking */ static detectWithFragments( buffer: Buffer, connectionId: string, options?: IDetectionOptions ): IDetectionResult | null { const detector = new HttpDetector(); // Try direct detection first const directResult = detector.detect(buffer, options); if (directResult && directResult.isComplete) { // Clean up any tracked fragments for this connection this.fragmentedBuffers.delete(connectionId); return directResult; } // Handle fragmentation let accumulator = this.fragmentedBuffers.get(connectionId); if (!accumulator) { accumulator = new BufferAccumulator(); this.fragmentedBuffers.set(connectionId, accumulator); } accumulator.append(buffer); const fullBuffer = accumulator.getBuffer(); // Check size limit const maxSize = options?.maxBufferSize || this.MAX_HEADER_SIZE; if (fullBuffer.length > maxSize) { // Too large, clean up and reject this.fragmentedBuffers.delete(connectionId); return null; } // Try detection on accumulated buffer const result = detector.detect(fullBuffer, options); if (result && result.isComplete) { // Success - clean up this.fragmentedBuffers.delete(connectionId); return result; } return result; } /** * Clean up old fragment buffers */ static cleanupFragments(maxAge: number = 5000): void { // TODO: Add timestamp tracking to BufferAccumulator for cleanup // For now, just clear if too many connections if (this.fragmentedBuffers.size > 1000) { this.fragmentedBuffers.clear(); } } }