/** * Main protocol detector that orchestrates detection across different protocols */ import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from './models/detection-types.js'; import { TlsDetector } from './detectors/tls-detector.js'; import { HttpDetector } from './detectors/http-detector.js'; /** * Main protocol detector class */ export class ProtocolDetector { /** * Connection tracking for fragmented detection */ private static connectionTracking = new Map(); /** * Detect protocol from buffer data * * @param buffer The buffer to analyze * @param options Detection options * @returns Detection result with protocol information */ static async detect( buffer: Buffer, options?: IDetectionOptions ): Promise { // Quick sanity check if (!buffer || buffer.length === 0) { return { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: true }; } // Try TLS detection first (more specific) const tlsDetector = new TlsDetector(); if (tlsDetector.canHandle(buffer)) { const tlsResult = tlsDetector.detect(buffer, options); if (tlsResult) { return tlsResult; } } // Try HTTP detection const httpDetector = new HttpDetector(); if (httpDetector.canHandle(buffer)) { const httpResult = httpDetector.detect(buffer, options); if (httpResult) { return httpResult; } } // Neither TLS nor HTTP return { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: true }; } /** * Detect protocol with connection tracking for fragmented data * * @param buffer The buffer to analyze * @param connectionId Unique connection identifier * @param options Detection options * @returns Detection result with protocol information */ static async detectWithConnectionTracking( buffer: Buffer, connectionId: string, options?: IDetectionOptions ): Promise { // Initialize or get connection tracking let tracking = this.connectionTracking.get(connectionId); if (!tracking) { tracking = { startTime: Date.now() }; this.connectionTracking.set(connectionId, tracking); } // Check timeout if (options?.timeout) { const elapsed = Date.now() - tracking.startTime; if (elapsed > options.timeout) { // Timeout - clean up and return unknown this.connectionTracking.delete(connectionId); TlsDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup HttpDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup return { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: true }; } } // If we already know the protocol, use the appropriate detector if (tracking.protocol === 'tls') { const result = TlsDetector.detectWithFragments(buffer, connectionId, options); if (result && result.isComplete) { this.connectionTracking.delete(connectionId); } return result || { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: true }; } else if (tracking.protocol === 'http') { const result = HttpDetector.detectWithFragments(buffer, connectionId, options); if (result && result.isComplete) { this.connectionTracking.delete(connectionId); } return result || { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: true }; } // First time detection - try to determine protocol // Quick checks first if (buffer.length > 0) { // TLS always starts with specific byte values if (buffer[0] >= 0x14 && buffer[0] <= 0x18) { tracking.protocol = 'tls'; const result = TlsDetector.detectWithFragments(buffer, connectionId, options); if (result) { if (result.isComplete) { this.connectionTracking.delete(connectionId); } return result; } } // HTTP starts with ASCII text else if (HttpDetector.quickCheck(buffer)) { tracking.protocol = 'http'; const result = HttpDetector.detectWithFragments(buffer, connectionId, options); if (result) { if (result.isComplete) { this.connectionTracking.delete(connectionId); } return result; } } } // Can't determine protocol yet return { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: false, bytesNeeded: 10 // Need more data to determine protocol }; } /** * Clean up old connection tracking entries * * @param maxAge Maximum age in milliseconds (default: 30 seconds) */ static cleanupConnections(maxAge: number = 30000): void { const now = Date.now(); const toDelete: string[] = []; for (const [connectionId, tracking] of this.connectionTracking.entries()) { if (now - tracking.startTime > maxAge) { toDelete.push(connectionId); } } for (const connectionId of toDelete) { this.connectionTracking.delete(connectionId); // Also clean up detector-specific buffers TlsDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup HttpDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup } // Also trigger cleanup in detectors HttpDetector.cleanupFragments(maxAge); } /** * Extract domain from connection info * * @param connectionInfo Connection information from detection * @returns The domain/hostname if found */ static extractDomain(connectionInfo: IConnectionInfo): string | undefined { // For both TLS and HTTP, domain is stored in the domain field return connectionInfo.domain; } /** * Create a connection ID from connection parameters * * @param params Connection parameters * @returns A unique connection identifier */ static createConnectionId(params: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number; socketId?: string; }): string { // If socketId is provided, use it if (params.socketId) { return params.socketId; } // Otherwise create from connection tuple const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params; return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`; } }