fix(detection): fix SNI detection in TLS detector
This commit is contained in:
		| @@ -1,5 +1,5 @@ | |||||||
| { | { | ||||||
|   "expiryDate": "2025-10-19T22:36:33.093Z", |   "expiryDate": "2025-10-19T23:55:27.838Z", | ||||||
|   "issueDate": "2025-07-21T22:36:33.093Z", |   "issueDate": "2025-07-21T23:55:27.838Z", | ||||||
|   "savedAt": "2025-07-21T22:36:33.094Z" |   "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 { IProtocolDetector } from '../models/interfaces.js'; | ||||||
| import type { IDetectionResult, IDetectionOptions, IConnectionInfo, THttpMethod } from '../models/detection-types.js'; | import type { IDetectionResult, IDetectionOptions } from '../models/detection-types.js'; | ||||||
| import { extractLine, isPrintableAscii, BufferAccumulator } from '../utils/buffer-utils.js'; | import type { IProtocolDetectionResult, IConnectionContext } from '../../protocols/common/types.js'; | ||||||
| import { parseHttpRequestLine, parseHttpHeaders, extractDomainFromHost, isHttpMethod } from '../utils/parser-utils.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 { | export class HttpDetector implements IProtocolDetector { | ||||||
|   /** |   private quickDetector = new QuickProtocolDetector(); | ||||||
|    * Minimum bytes needed to identify HTTP method |   private fragmentManager: DetectionFragmentManager; | ||||||
|    */ |  | ||||||
|   private static readonly MIN_HTTP_METHOD_SIZE = 3; // GET |  | ||||||
|    |    | ||||||
|   /** |   constructor(fragmentManager?: DetectionFragmentManager) { | ||||||
|    * Maximum reasonable HTTP header size |     this.fragmentManager = fragmentManager || new DetectionFragmentManager(); | ||||||
|    */ |  | ||||||
|   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 |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Check if buffer can be handled by this detector |    * Check if buffer can be handled by this detector | ||||||
|    */ |    */ | ||||||
|   canHandle(buffer: Buffer): boolean { |   canHandle(buffer: Buffer): boolean { | ||||||
|     if (buffer.length < HttpDetector.MIN_HTTP_METHOD_SIZE) { |     const result = this.quickDetector.quickDetect(buffer); | ||||||
|       return false; |     return result.protocol === 'http' && result.confidence > 50; | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // 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 |    * Get minimum bytes needed for detection | ||||||
|    */ |    */ | ||||||
|   getMinimumBytes(): number { |   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 { |   detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null { | ||||||
|     if (buffer.length < 3) { |     // Quick detection first | ||||||
|       return false; |     const quickResult = this.quickDetector.quickDetect(buffer); | ||||||
|  |      | ||||||
|  |     if (quickResult.protocol !== 'http' || quickResult.confidence < 50) { | ||||||
|  |       return null; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Check common HTTP methods |     // Extract routing information | ||||||
|     const start = buffer.slice(0, 7).toString('ascii'); |     const routing = RoutingExtractor.extract(buffer, 'http'); | ||||||
|     return start.startsWith('GET ') || |      | ||||||
|            start.startsWith('POST ') || |     // If we don't need full headers, we can return early | ||||||
|            start.startsWith('PUT ') || |     if (quickResult.confidence >= 95 && !options?.extractFullHeaders) { | ||||||
|            start.startsWith('DELETE ') || |       return { | ||||||
|            start.startsWith('HEAD ') || |         protocol: 'http', | ||||||
|            start.startsWith('OPTIONS') || |         connectionInfo: {  | ||||||
|            start.startsWith('PATCH ') || |           protocol: 'http', | ||||||
|            start.startsWith('CONNECT') || |           method: quickResult.metadata?.method as THttpMethod, | ||||||
|            start.startsWith('TRACE '); |           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, |     buffer: Buffer, | ||||||
|     connectionId: string, |     context: IConnectionContext, | ||||||
|     options?: IDetectionOptions |     options?: IDetectionOptions | ||||||
|   ): IDetectionResult | null { |   ): IDetectionResult | null { | ||||||
|     const detector = new HttpDetector(); |     const handler = this.fragmentManager.getHandler('http'); | ||||||
|  |     const connectionId = DetectionFragmentManager.createConnectionId(context); | ||||||
|      |      | ||||||
|     // Try direct detection first |     // Add fragment | ||||||
|     const directResult = detector.detect(buffer, options); |     const result = handler.addFragment(connectionId, buffer); | ||||||
|     if (directResult && directResult.isComplete) { |  | ||||||
|       // Clean up any tracked fragments for this connection |  | ||||||
|       this.fragmentedBuffers.delete(connectionId); |  | ||||||
|       return directResult; |  | ||||||
|     } |  | ||||||
|      |      | ||||||
|     // Handle fragmentation |     if (result.error) { | ||||||
|     let accumulator = this.fragmentedBuffers.get(connectionId); |       handler.complete(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; |       return null; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Try detection on accumulated buffer |     // Try detection on accumulated buffer | ||||||
|     const result = detector.detect(fullBuffer, options); |     const detectResult = this.detect(result.buffer!, options); | ||||||
|      |      | ||||||
|     if (result && result.isComplete) { |     if (detectResult && detectResult.isComplete) { | ||||||
|       // Success - clean up |       handler.complete(connectionId); | ||||||
|       this.fragmentedBuffers.delete(connectionId); |  | ||||||
|       return result; |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     return result; |     return detectResult; | ||||||
|   } |  | ||||||
|    |  | ||||||
|   /** |  | ||||||
|    * 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(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										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 | // TLS detector doesn't need plugins imports | ||||||
| import type { IProtocolDetector } from '../models/interfaces.js'; | import type { IProtocolDetector } from '../models/interfaces.js'; | ||||||
| import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../models/detection-types.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 { tlsVersionToString } from '../utils/parser-utils.js'; | ||||||
|  |  | ||||||
| // Import from protocols | // Import from protocols | ||||||
| @@ -29,6 +29,13 @@ export class TlsDetector implements IProtocolDetector { | |||||||
|    */ |    */ | ||||||
|   private static fragmentedBuffers = new Map<string, BufferAccumulator>(); |   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 |    * Detect TLS protocol from buffer | ||||||
|    */ |    */ | ||||||
| @@ -201,11 +208,11 @@ export class TlsDetector implements IProtocolDetector { | |||||||
|   /** |   /** | ||||||
|    * Parse cipher suites |    * Parse cipher suites | ||||||
|    */ |    */ | ||||||
|   private parseCipherSuites(data: Buffer): number[] { |   private parseCipherSuites(cipherData: Buffer): number[] { | ||||||
|     const suites: number[] = []; |     const suites: number[] = []; | ||||||
|      |      | ||||||
|     for (let i = 0; i + 1 < data.length; i += 2) { |     for (let i = 0; i < cipherData.length - 1; i += 2) { | ||||||
|       const suite = readUInt16BE(data, i); |       const suite = readUInt16BE(cipherData, i); | ||||||
|       suites.push(suite); |       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, |     buffer: Buffer, | ||||||
|     connectionId: string, |     context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number }, | ||||||
|     options?: IDetectionOptions |     options?: IDetectionOptions | ||||||
|   ): IDetectionResult | null { |   ): IDetectionResult | null { | ||||||
|     const detector = new TlsDetector(); |     const connectionId = this.createConnectionId(context); | ||||||
|      |      | ||||||
|     // Try direct detection first |     // Get or create buffer accumulator for this connection | ||||||
|     const directResult = detector.detect(buffer, options); |     let accumulator = TlsDetector.fragmentedBuffers.get(connectionId); | ||||||
|     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) { |     if (!accumulator) { | ||||||
|       accumulator = new BufferAccumulator(); |       accumulator = new BufferAccumulator(); | ||||||
|       this.fragmentedBuffers.set(connectionId, accumulator); |       TlsDetector.fragmentedBuffers.set(connectionId, accumulator); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     // Add new data | ||||||
|     accumulator.append(buffer); |     accumulator.append(buffer); | ||||||
|     const fullBuffer = accumulator.getBuffer(); |  | ||||||
|      |      | ||||||
|     // Try detection on accumulated buffer |     // Try detection on accumulated data | ||||||
|     const result = detector.detect(fullBuffer, options); |     const result = this.detect(accumulator.getBuffer(), options); | ||||||
|      |      | ||||||
|     if (result && result.isComplete) { |     // If detection is complete or we have too much data, clean up | ||||||
|       // Success - clean up |     if (result?.isComplete || accumulator.length() > 65536) { | ||||||
|       this.fragmentedBuffers.delete(connectionId); |       TlsDetector.fragmentedBuffers.delete(connectionId); | ||||||
|       return result; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Check timeout |  | ||||||
|     if (options?.timeout) { |  | ||||||
|       // TODO: Implement timeout handling |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     return result; |     return result; | ||||||
|   | |||||||
| @@ -16,7 +16,10 @@ export * from './models/interfaces.js'; | |||||||
| // Individual detectors | // Individual detectors | ||||||
| export * from './detectors/tls-detector.js'; | export * from './detectors/tls-detector.js'; | ||||||
| export * from './detectors/http-detector.js'; | export * from './detectors/http-detector.js'; | ||||||
|  | export * from './detectors/quick-detector.js'; | ||||||
|  | export * from './detectors/routing-extractor.js'; | ||||||
|  |  | ||||||
| // Utilities | // Utilities | ||||||
| export * from './utils/buffer-utils.js'; | export * from './utils/buffer-utils.js'; | ||||||
| export * from './utils/parser-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 { TlsDetector } from './detectors/tls-detector.js'; | ||||||
| import { HttpDetector } from './detectors/http-detector.js'; | import { HttpDetector } from './detectors/http-detector.js'; | ||||||
|  | import { DetectionFragmentManager } from './utils/fragment-manager.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Main protocol detector class |  * Main protocol detector class | ||||||
|  */ |  */ | ||||||
| export class ProtocolDetector { | export class ProtocolDetector { | ||||||
|   /** |   private static instance: ProtocolDetector; | ||||||
|    * Connection tracking for fragmented detection |   private fragmentManager: DetectionFragmentManager; | ||||||
|    */ |   private tlsDetector: TlsDetector; | ||||||
|   private static connectionTracking = new Map<string, { |   private httpDetector: HttpDetector; | ||||||
|     startTime: number; |    | ||||||
|     protocol?: 'tls' | 'http' | 'unknown'; |   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 |    * Detect protocol from buffer data | ||||||
|    *  |  | ||||||
|    * @param buffer The buffer to analyze |  | ||||||
|    * @param options Detection options |  | ||||||
|    * @returns Detection result with protocol information |  | ||||||
|    */ |    */ | ||||||
|   static async detect( |   static async detect(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> { | ||||||
|     buffer: Buffer,  |     return this.getInstance().detectInstance(buffer, options); | ||||||
|     options?: IDetectionOptions |   } | ||||||
|   ): Promise<IDetectionResult> { |    | ||||||
|  |   private async detectInstance(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> { | ||||||
|     // Quick sanity check |     // Quick sanity check | ||||||
|     if (!buffer || buffer.length === 0) { |     if (!buffer || buffer.length === 0) { | ||||||
|       return { |       return { | ||||||
| @@ -39,18 +50,16 @@ export class ProtocolDetector { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Try TLS detection first (more specific) |     // Try TLS detection first (more specific) | ||||||
|     const tlsDetector = new TlsDetector(); |     if (this.tlsDetector.canHandle(buffer)) { | ||||||
|     if (tlsDetector.canHandle(buffer)) { |       const tlsResult = this.tlsDetector.detect(buffer, options); | ||||||
|       const tlsResult = tlsDetector.detect(buffer, options); |  | ||||||
|       if (tlsResult) { |       if (tlsResult) { | ||||||
|         return tlsResult; |         return tlsResult; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Try HTTP detection |     // Try HTTP detection | ||||||
|     const httpDetector = new HttpDetector(); |     if (this.httpDetector.canHandle(buffer)) { | ||||||
|     if (httpDetector.canHandle(buffer)) { |       const httpResult = this.httpDetector.detect(buffer, options); | ||||||
|       const httpResult = httpDetector.detect(buffer, options); |  | ||||||
|       if (httpResult) { |       if (httpResult) { | ||||||
|         return httpResult; |         return httpResult; | ||||||
|       } |       } | ||||||
| @@ -66,142 +75,121 @@ export class ProtocolDetector { | |||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Detect protocol with connection tracking for fragmented data |    * Detect protocol with connection tracking for fragmented data | ||||||
|    *  |    * @deprecated Use detectWithContext instead | ||||||
|    * @param buffer The buffer to analyze |  | ||||||
|    * @param connectionId Unique connection identifier |  | ||||||
|    * @param options Detection options |  | ||||||
|    * @returns Detection result with protocol information |  | ||||||
|    */ |    */ | ||||||
|   static async detectWithConnectionTracking( |   static async detectWithConnectionTracking( | ||||||
|     buffer: Buffer, |     buffer: Buffer, | ||||||
|     connectionId: string, |     connectionId: string, | ||||||
|     options?: IDetectionOptions |     options?: IDetectionOptions | ||||||
|   ): Promise<IDetectionResult> { |   ): Promise<IDetectionResult> { | ||||||
|     // Initialize or get connection tracking |     // Convert connection ID to context | ||||||
|     let tracking = this.connectionTracking.get(connectionId); |     const context: IConnectionContext = { | ||||||
|     if (!tracking) { |       id: connectionId, | ||||||
|       tracking = { startTime: Date.now() }; |       sourceIp: 'unknown', | ||||||
|       this.connectionTracking.set(connectionId, tracking); |       sourcePort: 0, | ||||||
|  |       destIp: 'unknown', | ||||||
|  |       destPort: 0, | ||||||
|  |       timestamp: Date.now() | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     return this.getInstance().detectWithContextInstance(buffer, context, options); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|     // Check timeout |   /** | ||||||
|     if (options?.timeout) { |    * Detect protocol with connection context for fragmented data | ||||||
|       const elapsed = Date.now() - tracking.startTime; |    */ | ||||||
|       if (elapsed > options.timeout) { |   static async detectWithContext( | ||||||
|         // Timeout - clean up and return unknown |     buffer: Buffer, | ||||||
|         this.connectionTracking.delete(connectionId); |     context: IConnectionContext, | ||||||
|         TlsDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup |     options?: IDetectionOptions | ||||||
|         HttpDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup |   ): 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 { |       return { | ||||||
|         protocol: 'unknown', |         protocol: 'unknown', | ||||||
|         connectionInfo: { protocol: 'unknown' }, |         connectionInfo: { protocol: 'unknown' }, | ||||||
|         isComplete: true |         isComplete: true | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|      |      | ||||||
|     // If we already know the protocol, use the appropriate detector |     // First peek to determine protocol type | ||||||
|     if (tracking.protocol === 'tls') { |     if (this.tlsDetector.canHandle(buffer)) { | ||||||
|       const result = TlsDetector.detectWithFragments(buffer, connectionId, options); |       const result = this.tlsDetector.detectWithContext(buffer, context, 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) { | ||||||
|           if (result.isComplete) { |  | ||||||
|             this.connectionTracking.delete(connectionId); |  | ||||||
|           } |  | ||||||
|         return result; |         return result; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|       // HTTP starts with ASCII text |      | ||||||
|       else if (HttpDetector.quickCheck(buffer)) { |     if (this.httpDetector.canHandle(buffer)) { | ||||||
|         tracking.protocol = 'http'; |       const result = this.httpDetector.detectWithContext(buffer, context, options); | ||||||
|         const result = HttpDetector.detectWithFragments(buffer, connectionId, options); |  | ||||||
|       if (result) { |       if (result) { | ||||||
|           if (result.isComplete) { |  | ||||||
|             this.connectionTracking.delete(connectionId); |  | ||||||
|           } |  | ||||||
|         return result; |         return result; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|      |      | ||||||
|     // Can't determine protocol yet |     // Can't determine protocol | ||||||
|     return { |     return { | ||||||
|       protocol: 'unknown', |       protocol: 'unknown', | ||||||
|       connectionInfo: { protocol: 'unknown' }, |       connectionInfo: { protocol: 'unknown' }, | ||||||
|       isComplete: false, |       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 |    * Clean up old connection tracking entries | ||||||
|    *  |    *  | ||||||
|    * @param maxAge Maximum age in milliseconds (default: 30 seconds) |    * @param maxAge Maximum age in milliseconds (default: 30 seconds) | ||||||
|    */ |    */ | ||||||
|   static cleanupConnections(maxAge: number = 30000): void { |   static cleanupConnections(maxAge: number = 30000): void { | ||||||
|     const now = Date.now(); |     // Cleanup is now handled internally by the fragment manager | ||||||
|     const toDelete: string[] = []; |     this.getInstance().fragmentManager.cleanup(); | ||||||
|      |  | ||||||
|     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 |    * Extract domain from connection info | ||||||
|    *  |  | ||||||
|    * @param connectionInfo Connection information from detection |  | ||||||
|    * @returns The domain/hostname if found |  | ||||||
|    */ |    */ | ||||||
|   static extractDomain(connectionInfo: IConnectionInfo): string | undefined { |   static extractDomain(connectionInfo: any): string | undefined { | ||||||
|     // For both TLS and HTTP, domain is stored in the domain field |     return connectionInfo.domain || connectionInfo.sni || connectionInfo.host; | ||||||
|     return connectionInfo.domain; |  | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Create a connection ID from connection parameters |    * Create a connection ID from connection parameters | ||||||
|    *  |    * @deprecated Use createConnectionContext instead | ||||||
|    * @param params Connection parameters |  | ||||||
|    * @returns A unique connection identifier |  | ||||||
|    */ |    */ | ||||||
|   static createConnectionId(params: { |   static createConnectionId(params: { | ||||||
|     sourceIp?: string; |     sourceIp?: string; | ||||||
| @@ -219,4 +207,24 @@ export class ProtocolDetector { | |||||||
|     const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params; |     const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params; | ||||||
|     return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`; |     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. |  * smartproxy-specific implementation details. | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | export * as common from './common/index.js'; | ||||||
| export * as tls from './tls/index.js'; | export * as tls from './tls/index.js'; | ||||||
| export * as http from './http/index.js'; | export * as http from './http/index.js'; | ||||||
| export * as proxy from './proxy/index.js'; | export * as proxy from './proxy/index.js'; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user