fix(detection): fix SNI detection in TLS detector
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
| { | ||||
|   "expiryDate": "2025-10-19T22:36:33.093Z", | ||||
|   "issueDate": "2025-07-21T22:36:33.093Z", | ||||
|   "savedAt": "2025-07-21T22:36:33.094Z" | ||||
|   "expiryDate": "2025-10-19T23:55:27.838Z", | ||||
|   "issueDate": "2025-07-21T23:55:27.838Z", | ||||
|   "savedAt": "2025-07-21T23:55:27.838Z" | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								readme.plan.md
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,281 +1,114 @@ | ||||
| /** | ||||
|  * HTTP protocol detector | ||||
|  * HTTP Protocol Detector | ||||
|  *  | ||||
|  * Simplified HTTP detection using the new architecture | ||||
|  */ | ||||
|  | ||||
| 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'; | ||||
| import type { IDetectionResult, IDetectionOptions } from '../models/detection-types.js'; | ||||
| import type { IProtocolDetectionResult, IConnectionContext } from '../../protocols/common/types.js'; | ||||
| import type { THttpMethod } from '../../protocols/http/index.js'; | ||||
| import { QuickProtocolDetector } from './quick-detector.js'; | ||||
| import { RoutingExtractor } from './routing-extractor.js'; | ||||
| import { DetectionFragmentManager } from '../utils/fragment-manager.js'; | ||||
|  | ||||
| /** | ||||
|  * HTTP detector implementation | ||||
|  * Simplified HTTP detector | ||||
|  */ | ||||
| export class HttpDetector implements IProtocolDetector { | ||||
|   /** | ||||
|    * Minimum bytes needed to identify HTTP method | ||||
|    */ | ||||
|   private static readonly MIN_HTTP_METHOD_SIZE = 3; // GET | ||||
|   private quickDetector = new QuickProtocolDetector(); | ||||
|   private fragmentManager: DetectionFragmentManager; | ||||
|    | ||||
|   /** | ||||
|    * Maximum reasonable HTTP header size | ||||
|    */ | ||||
|   private static readonly MAX_HEADER_SIZE = 8192; | ||||
|    | ||||
|   /** | ||||
|    * Fragment tracking for incomplete headers | ||||
|    */ | ||||
|   private static fragmentedBuffers = new Map<string, BufferAccumulator>(); | ||||
|    | ||||
|   /** | ||||
|    * 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 | ||||
|       }; | ||||
|     } | ||||
|   constructor(fragmentManager?: DetectionFragmentManager) { | ||||
|     this.fragmentManager = fragmentManager || new DetectionFragmentManager(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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); | ||||
|     const result = this.quickDetector.quickDetect(buffer); | ||||
|     return result.protocol === 'http' && result.confidence > 50; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get minimum bytes needed for detection | ||||
|    */ | ||||
|   getMinimumBytes(): number { | ||||
|     return HttpDetector.MIN_HTTP_METHOD_SIZE; | ||||
|     return 4; // "GET " minimum | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Quick check if buffer starts with HTTP method | ||||
|    * Detect HTTP protocol from buffer | ||||
|    */ | ||||
|   static quickCheck(buffer: Buffer): boolean { | ||||
|     if (buffer.length < 3) { | ||||
|       return false; | ||||
|   detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null { | ||||
|     // Quick detection first | ||||
|     const quickResult = this.quickDetector.quickDetect(buffer); | ||||
|      | ||||
|     if (quickResult.protocol !== 'http' || quickResult.confidence < 50) { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     // 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 '); | ||||
|     // Extract routing information | ||||
|     const routing = RoutingExtractor.extract(buffer, 'http'); | ||||
|      | ||||
|     // If we don't need full headers, we can return early | ||||
|     if (quickResult.confidence >= 95 && !options?.extractFullHeaders) { | ||||
|       return { | ||||
|         protocol: 'http', | ||||
|         connectionInfo: {  | ||||
|           protocol: 'http', | ||||
|           method: quickResult.metadata?.method as THttpMethod, | ||||
|           domain: routing?.domain, | ||||
|           path: routing?.path | ||||
|         }, | ||||
|         isComplete: true | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Check if we have complete headers | ||||
|     const headersEnd = buffer.indexOf('\r\n\r\n'); | ||||
|     const isComplete = headersEnd !== -1; | ||||
|      | ||||
|     return { | ||||
|       protocol: 'http', | ||||
|       connectionInfo: { | ||||
|         protocol: 'http', | ||||
|         domain: routing?.domain, | ||||
|         path: routing?.path, | ||||
|         method: quickResult.metadata?.method as THttpMethod | ||||
|       }, | ||||
|       isComplete, | ||||
|       bytesNeeded: isComplete ? undefined : buffer.length + 512 // Need more for headers | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle fragmented HTTP detection with connection tracking | ||||
|    * Handle fragmented detection | ||||
|    */ | ||||
|   static detectWithFragments( | ||||
|   detectWithContext( | ||||
|     buffer: Buffer, | ||||
|     connectionId: string, | ||||
|     context: IConnectionContext, | ||||
|     options?: IDetectionOptions | ||||
|   ): IDetectionResult | null { | ||||
|     const detector = new HttpDetector(); | ||||
|     const handler = this.fragmentManager.getHandler('http'); | ||||
|     const connectionId = DetectionFragmentManager.createConnectionId(context); | ||||
|      | ||||
|     // 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; | ||||
|     } | ||||
|     // Add fragment | ||||
|     const result = handler.addFragment(connectionId, buffer); | ||||
|      | ||||
|     // 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); | ||||
|     if (result.error) { | ||||
|       handler.complete(connectionId); | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     // Try detection on accumulated buffer | ||||
|     const result = detector.detect(fullBuffer, options); | ||||
|     const detectResult = this.detect(result.buffer!, options); | ||||
|      | ||||
|     if (result && result.isComplete) { | ||||
|       // Success - clean up | ||||
|       this.fragmentedBuffers.delete(connectionId); | ||||
|       return result; | ||||
|     if (detectResult && detectResult.isComplete) { | ||||
|       handler.complete(connectionId); | ||||
|     } | ||||
|      | ||||
|     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(); | ||||
|     } | ||||
|     return detectResult; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										148
									
								
								ts/detection/detectors/quick-detector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								ts/detection/detectors/quick-detector.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| /** | ||||
|  * Quick Protocol Detector | ||||
|  *  | ||||
|  * Lightweight protocol identification based on minimal bytes | ||||
|  * No parsing, just identification | ||||
|  */ | ||||
|  | ||||
| import type { IProtocolDetector, IProtocolDetectionResult } from '../../protocols/common/types.js'; | ||||
| import { TlsRecordType } from '../../protocols/tls/index.js'; | ||||
| import { HttpParser } from '../../protocols/http/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Quick protocol detector for fast identification | ||||
|  */ | ||||
| export class QuickProtocolDetector implements IProtocolDetector { | ||||
|   /** | ||||
|    * Check if this detector can handle the data | ||||
|    */ | ||||
|   canHandle(data: Buffer): boolean { | ||||
|     return data.length >= 1; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Perform quick detection based on first few bytes | ||||
|    */ | ||||
|   quickDetect(data: Buffer): IProtocolDetectionResult { | ||||
|     if (data.length === 0) { | ||||
|       return { | ||||
|         protocol: 'unknown', | ||||
|         confidence: 0, | ||||
|         requiresMoreData: true | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Check for TLS | ||||
|     const tlsResult = this.checkTls(data); | ||||
|     if (tlsResult.confidence > 80) { | ||||
|       return tlsResult; | ||||
|     } | ||||
|      | ||||
|     // Check for HTTP | ||||
|     const httpResult = this.checkHttp(data); | ||||
|     if (httpResult.confidence > 80) { | ||||
|       return httpResult; | ||||
|     } | ||||
|      | ||||
|     // Need more data or unknown | ||||
|     return { | ||||
|       protocol: 'unknown', | ||||
|       confidence: 0, | ||||
|       requiresMoreData: data.length < 20 | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if data looks like TLS | ||||
|    */ | ||||
|   private checkTls(data: Buffer): IProtocolDetectionResult { | ||||
|     if (data.length < 3) { | ||||
|       return { | ||||
|         protocol: 'tls', | ||||
|         confidence: 0, | ||||
|         requiresMoreData: true | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     const firstByte = data[0]; | ||||
|     const secondByte = data[1]; | ||||
|      | ||||
|     // Check for valid TLS record type | ||||
|     const validRecordTypes = [ | ||||
|       TlsRecordType.CHANGE_CIPHER_SPEC, | ||||
|       TlsRecordType.ALERT, | ||||
|       TlsRecordType.HANDSHAKE, | ||||
|       TlsRecordType.APPLICATION_DATA, | ||||
|       TlsRecordType.HEARTBEAT | ||||
|     ]; | ||||
|      | ||||
|     if (!validRecordTypes.includes(firstByte)) { | ||||
|       return { | ||||
|         protocol: 'tls', | ||||
|         confidence: 0 | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Check TLS version byte (0x03 for all TLS/SSL versions) | ||||
|     if (secondByte !== 0x03) { | ||||
|       return { | ||||
|         protocol: 'tls', | ||||
|         confidence: 0 | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // High confidence it's TLS | ||||
|     return { | ||||
|       protocol: 'tls', | ||||
|       confidence: 95, | ||||
|       metadata: { | ||||
|         recordType: firstByte | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if data looks like HTTP | ||||
|    */ | ||||
|   private checkHttp(data: Buffer): IProtocolDetectionResult { | ||||
|     if (data.length < 3) { | ||||
|       return { | ||||
|         protocol: 'http', | ||||
|         confidence: 0, | ||||
|         requiresMoreData: true | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Quick check for HTTP methods | ||||
|     const start = data.subarray(0, Math.min(10, data.length)).toString('ascii'); | ||||
|      | ||||
|     // Check common HTTP methods | ||||
|     const httpMethods = ['GET ', 'POST ', 'PUT ', 'DELETE ', 'HEAD ', 'OPTIONS', 'PATCH ', 'CONNECT', 'TRACE ']; | ||||
|     for (const method of httpMethods) { | ||||
|       if (start.startsWith(method)) { | ||||
|         return { | ||||
|           protocol: 'http', | ||||
|           confidence: 95, | ||||
|           metadata: { | ||||
|             method: method.trim() | ||||
|           } | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check if it might be HTTP but need more data | ||||
|     if (HttpParser.isPrintableAscii(data, Math.min(20, data.length))) { | ||||
|       // Could be HTTP, but not sure | ||||
|       return { | ||||
|         protocol: 'http', | ||||
|         confidence: 30, | ||||
|         requiresMoreData: data.length < 20 | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     return { | ||||
|       protocol: 'http', | ||||
|       confidence: 0 | ||||
|     }; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										147
									
								
								ts/detection/detectors/routing-extractor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								ts/detection/detectors/routing-extractor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| /** | ||||
|  * Routing Information Extractor | ||||
|  *  | ||||
|  * Extracts minimal routing information from protocols | ||||
|  * without full parsing | ||||
|  */ | ||||
|  | ||||
| import type { IRoutingInfo, IConnectionContext, TProtocolType } from '../../protocols/common/types.js'; | ||||
| import { SniExtraction } from '../../protocols/tls/sni/sni-extraction.js'; | ||||
| import { HttpParser } from '../../protocols/http/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Extracts routing information from protocol data | ||||
|  */ | ||||
| export class RoutingExtractor { | ||||
|   /** | ||||
|    * Extract routing info based on protocol type | ||||
|    */ | ||||
|   static extract( | ||||
|     data: Buffer,  | ||||
|     protocol: TProtocolType, | ||||
|     context?: IConnectionContext | ||||
|   ): IRoutingInfo | null { | ||||
|     switch (protocol) { | ||||
|       case 'tls': | ||||
|       case 'https': | ||||
|         return this.extractTlsRouting(data, context); | ||||
|        | ||||
|       case 'http': | ||||
|         return this.extractHttpRouting(data); | ||||
|        | ||||
|       default: | ||||
|         return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Extract routing from TLS ClientHello (SNI) | ||||
|    */ | ||||
|   private static extractTlsRouting( | ||||
|     data: Buffer, | ||||
|     context?: IConnectionContext | ||||
|   ): IRoutingInfo | null { | ||||
|     try { | ||||
|       // Quick SNI extraction without full parsing | ||||
|       const sni = SniExtraction.extractSNI(data); | ||||
|        | ||||
|       if (sni) { | ||||
|         return { | ||||
|           domain: sni, | ||||
|           protocol: 'tls', | ||||
|           port: 443  // Default HTTPS port | ||||
|         }; | ||||
|       } | ||||
|        | ||||
|       return null; | ||||
|     } catch (error) { | ||||
|       // Extraction failed, return null | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Extract routing from HTTP headers (Host header) | ||||
|    */ | ||||
|   private static extractHttpRouting(data: Buffer): IRoutingInfo | null { | ||||
|     try { | ||||
|       // Look for first line | ||||
|       const firstLineEnd = data.indexOf('\n'); | ||||
|       if (firstLineEnd === -1) { | ||||
|         return null; | ||||
|       } | ||||
|        | ||||
|       // Parse request line | ||||
|       const firstLine = data.subarray(0, firstLineEnd).toString('ascii').trim(); | ||||
|       const requestLine = HttpParser.parseRequestLine(firstLine); | ||||
|        | ||||
|       if (!requestLine) { | ||||
|         return null; | ||||
|       } | ||||
|        | ||||
|       // Look for Host header | ||||
|       let pos = firstLineEnd + 1; | ||||
|       const maxSearch = Math.min(data.length, 4096); // Don't search too far | ||||
|        | ||||
|       while (pos < maxSearch) { | ||||
|         const lineEnd = data.indexOf('\n', pos); | ||||
|         if (lineEnd === -1) break; | ||||
|          | ||||
|         const line = data.subarray(pos, lineEnd).toString('ascii').trim(); | ||||
|          | ||||
|         // Empty line means end of headers | ||||
|         if (line.length === 0) break; | ||||
|          | ||||
|         // Check for Host header | ||||
|         if (line.toLowerCase().startsWith('host:')) { | ||||
|           const hostValue = line.substring(5).trim(); | ||||
|           const domain = HttpParser.extractDomainFromHost(hostValue); | ||||
|            | ||||
|           return { | ||||
|             domain, | ||||
|             path: requestLine.path, | ||||
|             protocol: 'http', | ||||
|             port: 80  // Default HTTP port | ||||
|           }; | ||||
|         } | ||||
|          | ||||
|         pos = lineEnd + 1; | ||||
|       } | ||||
|        | ||||
|       // No Host header found, but we have the path | ||||
|       return { | ||||
|         path: requestLine.path, | ||||
|         protocol: 'http', | ||||
|         port: 80 | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       // Extraction failed | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Try to extract domain from any protocol | ||||
|    */ | ||||
|   static extractDomain(data: Buffer, hint?: TProtocolType): string | null { | ||||
|     // If we have a hint, use it | ||||
|     if (hint) { | ||||
|       const routing = this.extract(data, hint); | ||||
|       return routing?.domain || null; | ||||
|     } | ||||
|      | ||||
|     // Try TLS first (more specific) | ||||
|     const tlsRouting = this.extractTlsRouting(data); | ||||
|     if (tlsRouting?.domain) { | ||||
|       return tlsRouting.domain; | ||||
|     } | ||||
|      | ||||
|     // Try HTTP | ||||
|     const httpRouting = this.extractHttpRouting(data); | ||||
|     if (httpRouting?.domain) { | ||||
|       return httpRouting.domain; | ||||
|     } | ||||
|      | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
| @@ -5,7 +5,7 @@ | ||||
| // TLS detector doesn't need plugins imports | ||||
| import type { IProtocolDetector } from '../models/interfaces.js'; | ||||
| import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../models/detection-types.js'; | ||||
| import { readUInt16BE, readUInt24BE, BufferAccumulator } from '../utils/buffer-utils.js'; | ||||
| import { readUInt16BE, BufferAccumulator } from '../utils/buffer-utils.js'; | ||||
| import { tlsVersionToString } from '../utils/parser-utils.js'; | ||||
|  | ||||
| // Import from protocols | ||||
| @@ -29,6 +29,13 @@ export class TlsDetector implements IProtocolDetector { | ||||
|    */ | ||||
|   private static fragmentedBuffers = new Map<string, BufferAccumulator>(); | ||||
|    | ||||
|   /** | ||||
|    * Create connection ID from context | ||||
|    */ | ||||
|   private createConnectionId(context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number }): string { | ||||
|     return `${context.sourceIp || 'unknown'}:${context.sourcePort || 0}->${context.destIp || 'unknown'}:${context.destPort || 0}`; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Detect TLS protocol from buffer | ||||
|    */ | ||||
| @@ -201,11 +208,11 @@ export class TlsDetector implements IProtocolDetector { | ||||
|   /** | ||||
|    * Parse cipher suites | ||||
|    */ | ||||
|   private parseCipherSuites(data: Buffer): number[] { | ||||
|   private parseCipherSuites(cipherData: Buffer): number[] { | ||||
|     const suites: number[] = []; | ||||
|      | ||||
|     for (let i = 0; i + 1 < data.length; i += 2) { | ||||
|       const suite = readUInt16BE(data, i); | ||||
|     for (let i = 0; i < cipherData.length - 1; i += 2) { | ||||
|       const suite = readUInt16BE(cipherData, i); | ||||
|       suites.push(suite); | ||||
|     } | ||||
|      | ||||
| @@ -213,45 +220,31 @@ export class TlsDetector implements IProtocolDetector { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle fragmented TLS detection with connection tracking | ||||
|    * Detect with context for fragmented data | ||||
|    */ | ||||
|   static detectWithFragments( | ||||
|   detectWithContext( | ||||
|     buffer: Buffer, | ||||
|     connectionId: string, | ||||
|     context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number }, | ||||
|     options?: IDetectionOptions | ||||
|   ): IDetectionResult | null { | ||||
|     const detector = new TlsDetector(); | ||||
|     const connectionId = this.createConnectionId(context); | ||||
|      | ||||
|     // 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); | ||||
|     // Get or create buffer accumulator for this connection | ||||
|     let accumulator = TlsDetector.fragmentedBuffers.get(connectionId); | ||||
|     if (!accumulator) { | ||||
|       accumulator = new BufferAccumulator(); | ||||
|       this.fragmentedBuffers.set(connectionId, accumulator); | ||||
|       TlsDetector.fragmentedBuffers.set(connectionId, accumulator); | ||||
|     } | ||||
|      | ||||
|     // Add new data | ||||
|     accumulator.append(buffer); | ||||
|     const fullBuffer = accumulator.getBuffer(); | ||||
|      | ||||
|     // Try detection on accumulated buffer | ||||
|     const result = detector.detect(fullBuffer, options); | ||||
|     // Try detection on accumulated data | ||||
|     const result = this.detect(accumulator.getBuffer(), options); | ||||
|      | ||||
|     if (result && result.isComplete) { | ||||
|       // Success - clean up | ||||
|       this.fragmentedBuffers.delete(connectionId); | ||||
|       return result; | ||||
|     } | ||||
|      | ||||
|     // Check timeout | ||||
|     if (options?.timeout) { | ||||
|       // TODO: Implement timeout handling | ||||
|     // If detection is complete or we have too much data, clean up | ||||
|     if (result?.isComplete || accumulator.length() > 65536) { | ||||
|       TlsDetector.fragmentedBuffers.delete(connectionId); | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   | ||||
| @@ -16,7 +16,10 @@ export * from './models/interfaces.js'; | ||||
| // Individual detectors | ||||
| export * from './detectors/tls-detector.js'; | ||||
| export * from './detectors/http-detector.js'; | ||||
| export * from './detectors/quick-detector.js'; | ||||
| export * from './detectors/routing-extractor.js'; | ||||
|  | ||||
| // Utilities | ||||
| export * from './utils/buffer-utils.js'; | ||||
| export * from './utils/parser-utils.js'; | ||||
| export * from './utils/fragment-manager.js'; | ||||
| @@ -1,34 +1,45 @@ | ||||
| /** | ||||
|  * Main protocol detector that orchestrates detection across different protocols | ||||
|  * Protocol Detector | ||||
|  *  | ||||
|  * Simplified protocol detection using the new architecture | ||||
|  */ | ||||
|  | ||||
| import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from './models/detection-types.js'; | ||||
| import type { IDetectionResult, IDetectionOptions } from './models/detection-types.js'; | ||||
| import type { IConnectionContext } from '../protocols/common/types.js'; | ||||
| import { TlsDetector } from './detectors/tls-detector.js'; | ||||
| import { HttpDetector } from './detectors/http-detector.js'; | ||||
| import { DetectionFragmentManager } from './utils/fragment-manager.js'; | ||||
|  | ||||
| /** | ||||
|  * Main protocol detector class | ||||
|  */ | ||||
| export class ProtocolDetector { | ||||
|   /** | ||||
|    * Connection tracking for fragmented detection | ||||
|    */ | ||||
|   private static connectionTracking = new Map<string, { | ||||
|     startTime: number; | ||||
|     protocol?: 'tls' | 'http' | 'unknown'; | ||||
|   }>(); | ||||
|   private static instance: ProtocolDetector; | ||||
|   private fragmentManager: DetectionFragmentManager; | ||||
|   private tlsDetector: TlsDetector; | ||||
|   private httpDetector: HttpDetector; | ||||
|    | ||||
|   constructor() { | ||||
|     this.fragmentManager = new DetectionFragmentManager(); | ||||
|     this.tlsDetector = new TlsDetector(); | ||||
|     this.httpDetector = new HttpDetector(this.fragmentManager); | ||||
|   } | ||||
|    | ||||
|   private static getInstance(): ProtocolDetector { | ||||
|     if (!this.instance) { | ||||
|       this.instance = new ProtocolDetector(); | ||||
|     } | ||||
|     return this.instance; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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<IDetectionResult> { | ||||
|   static async detect(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> { | ||||
|     return this.getInstance().detectInstance(buffer, options); | ||||
|   } | ||||
|    | ||||
|   private async detectInstance(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> { | ||||
|     // Quick sanity check | ||||
|     if (!buffer || buffer.length === 0) { | ||||
|       return { | ||||
| @@ -39,18 +50,16 @@ export class ProtocolDetector { | ||||
|     } | ||||
|      | ||||
|     // Try TLS detection first (more specific) | ||||
|     const tlsDetector = new TlsDetector(); | ||||
|     if (tlsDetector.canHandle(buffer)) { | ||||
|       const tlsResult = tlsDetector.detect(buffer, options); | ||||
|     if (this.tlsDetector.canHandle(buffer)) { | ||||
|       const tlsResult = this.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 (this.httpDetector.canHandle(buffer)) { | ||||
|       const httpResult = this.httpDetector.detect(buffer, options); | ||||
|       if (httpResult) { | ||||
|         return httpResult; | ||||
|       } | ||||
| @@ -66,142 +75,121 @@ export class ProtocolDetector { | ||||
|    | ||||
|   /** | ||||
|    * 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 | ||||
|    * @deprecated Use detectWithContext instead | ||||
|    */ | ||||
|   static async detectWithConnectionTracking( | ||||
|     buffer: Buffer, | ||||
|     connectionId: string, | ||||
|     options?: IDetectionOptions | ||||
|   ): Promise<IDetectionResult> { | ||||
|     // Initialize or get connection tracking | ||||
|     let tracking = this.connectionTracking.get(connectionId); | ||||
|     if (!tracking) { | ||||
|       tracking = { startTime: Date.now() }; | ||||
|       this.connectionTracking.set(connectionId, tracking); | ||||
|     // Convert connection ID to context | ||||
|     const context: IConnectionContext = { | ||||
|       id: connectionId, | ||||
|       sourceIp: 'unknown', | ||||
|       sourcePort: 0, | ||||
|       destIp: 'unknown', | ||||
|       destPort: 0, | ||||
|       timestamp: Date.now() | ||||
|     }; | ||||
|      | ||||
|     return this.getInstance().detectWithContextInstance(buffer, context, options); | ||||
|   } | ||||
|    | ||||
|     // 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 | ||||
|   /** | ||||
|    * Detect protocol with connection context for fragmented data | ||||
|    */ | ||||
|   static async detectWithContext( | ||||
|     buffer: Buffer, | ||||
|     context: IConnectionContext, | ||||
|     options?: IDetectionOptions | ||||
|   ): Promise<IDetectionResult> { | ||||
|     return this.getInstance().detectWithContextInstance(buffer, context, options); | ||||
|   } | ||||
|    | ||||
|   private async detectWithContextInstance( | ||||
|     buffer: Buffer, | ||||
|     context: IConnectionContext, | ||||
|     options?: IDetectionOptions | ||||
|   ): Promise<IDetectionResult> { | ||||
|     // Quick sanity check | ||||
|     if (!buffer || buffer.length === 0) { | ||||
|       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); | ||||
|     // First peek to determine protocol type | ||||
|     if (this.tlsDetector.canHandle(buffer)) { | ||||
|       const result = this.tlsDetector.detectWithContext(buffer, context, 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 (this.httpDetector.canHandle(buffer)) { | ||||
|       const result = this.httpDetector.detectWithContext(buffer, context, options); | ||||
|       if (result) { | ||||
|           if (result.isComplete) { | ||||
|             this.connectionTracking.delete(connectionId); | ||||
|           } | ||||
|         return result; | ||||
|       } | ||||
|     } | ||||
|     } | ||||
|      | ||||
|     // Can't determine protocol yet | ||||
|     // Can't determine protocol | ||||
|     return { | ||||
|       protocol: 'unknown', | ||||
|       connectionInfo: { protocol: 'unknown' }, | ||||
|       isComplete: false, | ||||
|       bytesNeeded: 10 // Need more data to determine protocol | ||||
|       bytesNeeded: Math.max( | ||||
|         this.tlsDetector.getMinimumBytes(), | ||||
|         this.httpDetector.getMinimumBytes() | ||||
|       ) | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clean up resources | ||||
|    */ | ||||
|   static cleanup(): void { | ||||
|     this.getInstance().cleanupInstance(); | ||||
|   } | ||||
|    | ||||
|   private cleanupInstance(): void { | ||||
|     this.fragmentManager.cleanup(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Destroy detector instance | ||||
|    */ | ||||
|   static destroy(): void { | ||||
|     this.getInstance().destroyInstance(); | ||||
|     this.instance = null as any; | ||||
|   } | ||||
|    | ||||
|   private destroyInstance(): void { | ||||
|     this.fragmentManager.destroy(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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); | ||||
|     // Cleanup is now handled internally by the fragment manager | ||||
|     this.getInstance().fragmentManager.cleanup(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * 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; | ||||
|   static extractDomain(connectionInfo: any): string | undefined { | ||||
|     return connectionInfo.domain || connectionInfo.sni || connectionInfo.host; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create a connection ID from connection parameters | ||||
|    *  | ||||
|    * @param params Connection parameters | ||||
|    * @returns A unique connection identifier | ||||
|    * @deprecated Use createConnectionContext instead | ||||
|    */ | ||||
|   static createConnectionId(params: { | ||||
|     sourceIp?: string; | ||||
| @@ -219,4 +207,24 @@ export class ProtocolDetector { | ||||
|     const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params; | ||||
|     return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create a connection context from parameters | ||||
|    */ | ||||
|   static createConnectionContext(params: { | ||||
|     sourceIp?: string; | ||||
|     sourcePort?: number; | ||||
|     destIp?: string; | ||||
|     destPort?: number; | ||||
|     socketId?: string; | ||||
|   }): IConnectionContext { | ||||
|     return { | ||||
|       id: params.socketId, | ||||
|       sourceIp: params.sourceIp || 'unknown', | ||||
|       sourcePort: params.sourcePort || 0, | ||||
|       destIp: params.destIp || 'unknown', | ||||
|       destPort: params.destPort || 0, | ||||
|       timestamp: Date.now() | ||||
|     }; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										64
									
								
								ts/detection/utils/fragment-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								ts/detection/utils/fragment-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| /** | ||||
|  * Fragment Manager for Detection Module | ||||
|  *  | ||||
|  * Manages fragmented protocol data using the shared fragment handler | ||||
|  */ | ||||
|  | ||||
| import { FragmentHandler, type IFragmentOptions } from '../../protocols/common/fragment-handler.js'; | ||||
| import type { IConnectionContext } from '../../protocols/common/types.js'; | ||||
|  | ||||
| /** | ||||
|  * Detection-specific fragment manager | ||||
|  */ | ||||
| export class DetectionFragmentManager { | ||||
|   private tlsFragments: FragmentHandler; | ||||
|   private httpFragments: FragmentHandler; | ||||
|    | ||||
|   constructor() { | ||||
|     // Configure fragment handlers with appropriate limits | ||||
|     const tlsOptions: IFragmentOptions = { | ||||
|       maxBufferSize: 16384,  // TLS record max size | ||||
|       timeout: 5000, | ||||
|       cleanupInterval: 30000 | ||||
|     }; | ||||
|      | ||||
|     const httpOptions: IFragmentOptions = { | ||||
|       maxBufferSize: 8192,   // HTTP header reasonable limit | ||||
|       timeout: 5000, | ||||
|       cleanupInterval: 30000 | ||||
|     }; | ||||
|      | ||||
|     this.tlsFragments = new FragmentHandler(tlsOptions); | ||||
|     this.httpFragments = new FragmentHandler(httpOptions); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get fragment handler for protocol type | ||||
|    */ | ||||
|   getHandler(protocol: 'tls' | 'http'): FragmentHandler { | ||||
|     return protocol === 'tls' ? this.tlsFragments : this.httpFragments; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create connection ID from context | ||||
|    */ | ||||
|   static createConnectionId(context: IConnectionContext): string { | ||||
|     return context.id || `${context.sourceIp}:${context.sourcePort}-${context.destIp}:${context.destPort}`; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clean up all handlers | ||||
|    */ | ||||
|   cleanup(): void { | ||||
|     this.tlsFragments.cleanup(); | ||||
|     this.httpFragments.cleanup(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Destroy all handlers | ||||
|    */ | ||||
|   destroy(): void { | ||||
|     this.tlsFragments.destroy(); | ||||
|     this.httpFragments.destroy(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										163
									
								
								ts/protocols/common/fragment-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								ts/protocols/common/fragment-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| /** | ||||
|  * Shared Fragment Handler for Protocol Detection | ||||
|  *  | ||||
|  * Provides unified fragment buffering and reassembly for protocols | ||||
|  * that may span multiple TCP packets. | ||||
|  */ | ||||
|  | ||||
| import { Buffer } from 'buffer'; | ||||
|  | ||||
| /** | ||||
|  * Fragment tracking information | ||||
|  */ | ||||
| export interface IFragmentInfo { | ||||
|   buffer: Buffer; | ||||
|   timestamp: number; | ||||
|   connectionId: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Options for fragment handling | ||||
|  */ | ||||
| export interface IFragmentOptions { | ||||
|   maxBufferSize?: number; | ||||
|   timeout?: number; | ||||
|   cleanupInterval?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Result of fragment processing | ||||
|  */ | ||||
| export interface IFragmentResult { | ||||
|   isComplete: boolean; | ||||
|   buffer?: Buffer; | ||||
|   needsMoreData: boolean; | ||||
|   error?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Shared fragment handler for protocol detection | ||||
|  */ | ||||
| export class FragmentHandler { | ||||
|   private fragments = new Map<string, IFragmentInfo>(); | ||||
|   private cleanupTimer?: NodeJS.Timeout; | ||||
|    | ||||
|   constructor(private options: IFragmentOptions = {}) { | ||||
|     // Start cleanup timer if not already running | ||||
|     if (options.cleanupInterval && !this.cleanupTimer) { | ||||
|       this.cleanupTimer = setInterval( | ||||
|         () => this.cleanup(), | ||||
|         options.cleanupInterval | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add a fragment for a connection | ||||
|    */ | ||||
|   addFragment(connectionId: string, fragment: Buffer): IFragmentResult { | ||||
|     const existing = this.fragments.get(connectionId); | ||||
|      | ||||
|     if (existing) { | ||||
|       // Append to existing buffer | ||||
|       const newBuffer = Buffer.concat([existing.buffer, fragment]); | ||||
|        | ||||
|       // Check size limit | ||||
|       const maxSize = this.options.maxBufferSize || 65536; | ||||
|       if (newBuffer.length > maxSize) { | ||||
|         this.fragments.delete(connectionId); | ||||
|         return { | ||||
|           isComplete: false, | ||||
|           needsMoreData: false, | ||||
|           error: 'Buffer size exceeded maximum allowed' | ||||
|         }; | ||||
|       } | ||||
|        | ||||
|       // Update fragment info | ||||
|       this.fragments.set(connectionId, { | ||||
|         buffer: newBuffer, | ||||
|         timestamp: Date.now(), | ||||
|         connectionId | ||||
|       }); | ||||
|        | ||||
|       return { | ||||
|         isComplete: false, | ||||
|         buffer: newBuffer, | ||||
|         needsMoreData: true | ||||
|       }; | ||||
|     } else { | ||||
|       // New fragment | ||||
|       this.fragments.set(connectionId, { | ||||
|         buffer: fragment, | ||||
|         timestamp: Date.now(), | ||||
|         connectionId | ||||
|       }); | ||||
|        | ||||
|       return { | ||||
|         isComplete: false, | ||||
|         buffer: fragment, | ||||
|         needsMoreData: true | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the current buffer for a connection | ||||
|    */ | ||||
|   getBuffer(connectionId: string): Buffer | undefined { | ||||
|     return this.fragments.get(connectionId)?.buffer; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Mark a connection as complete and clean up | ||||
|    */ | ||||
|   complete(connectionId: string): void { | ||||
|     this.fragments.delete(connectionId); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if we're tracking a connection | ||||
|    */ | ||||
|   hasConnection(connectionId: string): boolean { | ||||
|     return this.fragments.has(connectionId); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clean up expired fragments | ||||
|    */ | ||||
|   cleanup(): void { | ||||
|     const now = Date.now(); | ||||
|     const timeout = this.options.timeout || 5000; | ||||
|      | ||||
|     for (const [connectionId, info] of this.fragments.entries()) { | ||||
|       if (now - info.timestamp > timeout) { | ||||
|         this.fragments.delete(connectionId); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Clear all fragments | ||||
|    */ | ||||
|   clear(): void { | ||||
|     this.fragments.clear(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Destroy the handler and clean up resources | ||||
|    */ | ||||
|   destroy(): void { | ||||
|     if (this.cleanupTimer) { | ||||
|       clearInterval(this.cleanupTimer); | ||||
|       this.cleanupTimer = undefined; | ||||
|     } | ||||
|     this.clear(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the number of tracked connections | ||||
|    */ | ||||
|   get size(): number { | ||||
|     return this.fragments.size; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/protocols/common/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/protocols/common/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /** | ||||
|  * Common Protocol Infrastructure | ||||
|  *  | ||||
|  * Shared utilities and types for protocol handling | ||||
|  */ | ||||
|  | ||||
| export * from './fragment-handler.js'; | ||||
| export * from './types.js'; | ||||
							
								
								
									
										76
									
								
								ts/protocols/common/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								ts/protocols/common/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /** | ||||
|  * Common Protocol Types | ||||
|  *  | ||||
|  * Shared types used across different protocol implementations | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Supported protocol types | ||||
|  */ | ||||
| export type TProtocolType = 'tls' | 'http' | 'https' | 'websocket' | 'unknown'; | ||||
|  | ||||
| /** | ||||
|  * Protocol detection result | ||||
|  */ | ||||
| export interface IProtocolDetectionResult { | ||||
|   protocol: TProtocolType; | ||||
|   confidence: number; // 0-100 | ||||
|   requiresMoreData?: boolean; | ||||
|   metadata?: { | ||||
|     version?: string; | ||||
|     method?: string; | ||||
|     [key: string]: any; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Routing information extracted from protocols | ||||
|  */ | ||||
| export interface IRoutingInfo { | ||||
|   domain?: string; | ||||
|   port?: number; | ||||
|   path?: string; | ||||
|   protocol: TProtocolType; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Connection context for protocol operations | ||||
|  */ | ||||
| export interface IConnectionContext { | ||||
|   id: string; | ||||
|   sourceIp?: string; | ||||
|   sourcePort?: number; | ||||
|   destIp?: string; | ||||
|   destPort?: number; | ||||
|   timestamp?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Protocol detection options | ||||
|  */ | ||||
| export interface IProtocolDetectionOptions { | ||||
|   quickMode?: boolean;        // Only do minimal detection | ||||
|   extractRouting?: boolean;   // Extract routing information | ||||
|   maxWaitTime?: number;       // Max time to wait for complete data | ||||
|   maxBufferSize?: number;     // Max buffer size for fragmented data | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Base interface for protocol detectors | ||||
|  */ | ||||
| export interface IProtocolDetector { | ||||
|   /** | ||||
|    * Check if this detector can handle the data | ||||
|    */ | ||||
|   canHandle(data: Buffer): boolean; | ||||
|    | ||||
|   /** | ||||
|    * Perform quick detection (first few bytes only) | ||||
|    */ | ||||
|   quickDetect(data: Buffer): IProtocolDetectionResult; | ||||
|    | ||||
|   /** | ||||
|    * Extract routing information if possible | ||||
|    */ | ||||
|   extractRouting?(data: Buffer, context?: IConnectionContext): IRoutingInfo | null; | ||||
| } | ||||
| @@ -5,6 +5,7 @@ | ||||
|  * smartproxy-specific implementation details. | ||||
|  */ | ||||
|  | ||||
| export * as common from './common/index.js'; | ||||
| export * as tls from './tls/index.js'; | ||||
| export * as http from './http/index.js'; | ||||
| export * as proxy from './proxy/index.js'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user