diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index 99eaa7e..ad004c2 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -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" } \ No newline at end of file diff --git a/readme.plan.md b/readme.plan.md index 4dc0553..de69fca 100644 Binary files a/readme.plan.md and b/readme.plan.md differ diff --git a/ts/detection/detectors/http-detector.ts b/ts/detection/detectors/http-detector.ts index 110ff5c..00c0cc6 100644 --- a/ts/detection/detectors/http-detector.ts +++ b/ts/detection/detectors/http-detector.ts @@ -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(); - - /** - * 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; } } \ No newline at end of file diff --git a/ts/detection/detectors/quick-detector.ts b/ts/detection/detectors/quick-detector.ts new file mode 100644 index 0000000..f9dedab --- /dev/null +++ b/ts/detection/detectors/quick-detector.ts @@ -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 + }; + } +} \ No newline at end of file diff --git a/ts/detection/detectors/routing-extractor.ts b/ts/detection/detectors/routing-extractor.ts new file mode 100644 index 0000000..a05ca26 --- /dev/null +++ b/ts/detection/detectors/routing-extractor.ts @@ -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; + } +} \ No newline at end of file diff --git a/ts/detection/detectors/tls-detector.ts b/ts/detection/detectors/tls-detector.ts index 1ddfe39..0f60641 100644 --- a/ts/detection/detectors/tls-detector.ts +++ b/ts/detection/detectors/tls-detector.ts @@ -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(); + /** + * 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( - buffer: Buffer, - connectionId: string, + detectWithContext( + buffer: Buffer, + 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; diff --git a/ts/detection/index.ts b/ts/detection/index.ts index 21c384a..a16a0a4 100644 --- a/ts/detection/index.ts +++ b/ts/detection/index.ts @@ -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'; \ No newline at end of file +export * from './utils/parser-utils.js'; +export * from './utils/fragment-manager.js'; \ No newline at end of file diff --git a/ts/detection/protocol-detector.ts b/ts/detection/protocol-detector.ts index 0995718..2caa52b 100644 --- a/ts/detection/protocol-detector.ts +++ b/ts/detection/protocol-detector.ts @@ -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(); + 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 { + static async detect(buffer: Buffer, options?: IDetectionOptions): Promise { + return this.getInstance().detectInstance(buffer, options); + } + + private async detectInstance(buffer: Buffer, options?: IDetectionOptions): Promise { // 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 { - // 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() + }; - // Check timeout - if (options?.timeout) { - const elapsed = Date.now() - tracking.startTime; - if (elapsed > options.timeout) { - // Timeout - clean up and return unknown - this.connectionTracking.delete(connectionId); - TlsDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup - HttpDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup - - return { - protocol: 'unknown', - connectionInfo: { protocol: 'unknown' }, - isComplete: true - }; - } - } - - // If we already know the protocol, use the appropriate detector - if (tracking.protocol === 'tls') { - const result = TlsDetector.detectWithFragments(buffer, connectionId, options); - if (result && result.isComplete) { - this.connectionTracking.delete(connectionId); - } - return result || { - protocol: 'unknown', - connectionInfo: { protocol: 'unknown' }, - isComplete: true - }; - } else if (tracking.protocol === 'http') { - const result = HttpDetector.detectWithFragments(buffer, connectionId, options); - if (result && result.isComplete) { - this.connectionTracking.delete(connectionId); - } - return result || { + return this.getInstance().detectWithContextInstance(buffer, context, options); + } + + /** + * Detect protocol with connection context for fragmented data + */ + static async detectWithContext( + buffer: Buffer, + context: IConnectionContext, + options?: IDetectionOptions + ): Promise { + return this.getInstance().detectWithContextInstance(buffer, context, options); + } + + private async detectWithContextInstance( + buffer: Buffer, + context: IConnectionContext, + options?: IDetectionOptions + ): Promise { + // Quick sanity check + if (!buffer || buffer.length === 0) { + return { protocol: 'unknown', connectionInfo: { protocol: 'unknown' }, isComplete: true }; } - // First time detection - try to determine protocol - // Quick checks first - if (buffer.length > 0) { - // TLS always starts with specific byte values - if (buffer[0] >= 0x14 && buffer[0] <= 0x18) { - tracking.protocol = 'tls'; - const result = TlsDetector.detectWithFragments(buffer, connectionId, options); - if (result) { - if (result.isComplete) { - this.connectionTracking.delete(connectionId); - } - return result; - } - } - // HTTP starts with ASCII text - else if (HttpDetector.quickCheck(buffer)) { - tracking.protocol = 'http'; - const result = HttpDetector.detectWithFragments(buffer, connectionId, options); - if (result) { - if (result.isComplete) { - this.connectionTracking.delete(connectionId); - } - return result; - } + // First peek to determine protocol type + if (this.tlsDetector.canHandle(buffer)) { + const result = this.tlsDetector.detectWithContext(buffer, context, options); + if (result) { + return result; } } - // Can't determine protocol yet + if (this.httpDetector.canHandle(buffer)) { + const result = this.httpDetector.detectWithContext(buffer, context, options); + if (result) { + return result; + } + } + + // 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() + }; + } } \ No newline at end of file diff --git a/ts/detection/utils/fragment-manager.ts b/ts/detection/utils/fragment-manager.ts new file mode 100644 index 0000000..bab3f78 --- /dev/null +++ b/ts/detection/utils/fragment-manager.ts @@ -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(); + } +} \ No newline at end of file diff --git a/ts/protocols/common/fragment-handler.ts b/ts/protocols/common/fragment-handler.ts new file mode 100644 index 0000000..b3f75f5 --- /dev/null +++ b/ts/protocols/common/fragment-handler.ts @@ -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(); + 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; + } +} \ No newline at end of file diff --git a/ts/protocols/common/index.ts b/ts/protocols/common/index.ts new file mode 100644 index 0000000..3586bd9 --- /dev/null +++ b/ts/protocols/common/index.ts @@ -0,0 +1,8 @@ +/** + * Common Protocol Infrastructure + * + * Shared utilities and types for protocol handling + */ + +export * from './fragment-handler.js'; +export * from './types.js'; \ No newline at end of file diff --git a/ts/protocols/common/types.ts b/ts/protocols/common/types.ts new file mode 100644 index 0000000..1e50273 --- /dev/null +++ b/ts/protocols/common/types.ts @@ -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; +} \ No newline at end of file diff --git a/ts/protocols/index.ts b/ts/protocols/index.ts index 80125c3..fbbec38 100644 --- a/ts/protocols/index.ts +++ b/ts/protocols/index.ts @@ -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';