fix(detection): fix SNI detection in TLS detector
Some checks failed
Default (tags) / security (push) Successful in 53s
Default (tags) / test (push) Failing after 43m34s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped

This commit is contained in:
Juergen Kunz
2025-07-22 00:19:59 +00:00
parent 36068a6d92
commit 8936f4ad46
13 changed files with 837 additions and 393 deletions

View File

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

View 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
};
}
}

View 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;
}
}

View File

@@ -5,7 +5,7 @@
// TLS detector doesn't need plugins imports
import type { IProtocolDetector } from '../models/interfaces.js';
import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../models/detection-types.js';
import { readUInt16BE, readUInt24BE, BufferAccumulator } from '../utils/buffer-utils.js';
import { readUInt16BE, BufferAccumulator } from '../utils/buffer-utils.js';
import { tlsVersionToString } from '../utils/parser-utils.js';
// Import from protocols
@@ -29,6 +29,13 @@ export class TlsDetector implements IProtocolDetector {
*/
private static fragmentedBuffers = new Map<string, BufferAccumulator>();
/**
* Create connection ID from context
*/
private createConnectionId(context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number }): string {
return `${context.sourceIp || 'unknown'}:${context.sourcePort || 0}->${context.destIp || 'unknown'}:${context.destPort || 0}`;
}
/**
* Detect TLS protocol from buffer
*/
@@ -201,11 +208,11 @@ export class TlsDetector implements IProtocolDetector {
/**
* Parse cipher suites
*/
private parseCipherSuites(data: Buffer): number[] {
private parseCipherSuites(cipherData: Buffer): number[] {
const suites: number[] = [];
for (let i = 0; i + 1 < data.length; i += 2) {
const suite = readUInt16BE(data, i);
for (let i = 0; i < cipherData.length - 1; i += 2) {
const suite = readUInt16BE(cipherData, i);
suites.push(suite);
}
@@ -213,45 +220,31 @@ export class TlsDetector implements IProtocolDetector {
}
/**
* Handle fragmented TLS detection with connection tracking
* Detect with context for fragmented data
*/
static detectWithFragments(
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;

View File

@@ -16,7 +16,10 @@ export * from './models/interfaces.js';
// Individual detectors
export * from './detectors/tls-detector.js';
export * from './detectors/http-detector.js';
export * from './detectors/quick-detector.js';
export * from './detectors/routing-extractor.js';
// Utilities
export * from './utils/buffer-utils.js';
export * from './utils/parser-utils.js';
export * from './utils/parser-utils.js';
export * from './utils/fragment-manager.js';

View File

@@ -1,34 +1,45 @@
/**
* Main protocol detector that orchestrates detection across different protocols
* Protocol Detector
*
* Simplified protocol detection using the new architecture
*/
import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from './models/detection-types.js';
import type { IDetectionResult, IDetectionOptions } from './models/detection-types.js';
import type { IConnectionContext } from '../protocols/common/types.js';
import { TlsDetector } from './detectors/tls-detector.js';
import { HttpDetector } from './detectors/http-detector.js';
import { DetectionFragmentManager } from './utils/fragment-manager.js';
/**
* Main protocol detector class
*/
export class ProtocolDetector {
/**
* Connection tracking for fragmented detection
*/
private static connectionTracking = new Map<string, {
startTime: number;
protocol?: 'tls' | 'http' | 'unknown';
}>();
private static instance: ProtocolDetector;
private fragmentManager: DetectionFragmentManager;
private tlsDetector: TlsDetector;
private httpDetector: HttpDetector;
constructor() {
this.fragmentManager = new DetectionFragmentManager();
this.tlsDetector = new TlsDetector();
this.httpDetector = new HttpDetector(this.fragmentManager);
}
private static getInstance(): ProtocolDetector {
if (!this.instance) {
this.instance = new ProtocolDetector();
}
return this.instance;
}
/**
* Detect protocol from buffer data
*
* @param buffer The buffer to analyze
* @param options Detection options
* @returns Detection result with protocol information
*/
static async detect(
buffer: Buffer,
options?: IDetectionOptions
): Promise<IDetectionResult> {
static async detect(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> {
return this.getInstance().detectInstance(buffer, options);
}
private async detectInstance(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> {
// Quick sanity check
if (!buffer || buffer.length === 0) {
return {
@@ -39,18 +50,16 @@ export class ProtocolDetector {
}
// Try TLS detection first (more specific)
const tlsDetector = new TlsDetector();
if (tlsDetector.canHandle(buffer)) {
const tlsResult = tlsDetector.detect(buffer, options);
if (this.tlsDetector.canHandle(buffer)) {
const tlsResult = this.tlsDetector.detect(buffer, options);
if (tlsResult) {
return tlsResult;
}
}
// Try HTTP detection
const httpDetector = new HttpDetector();
if (httpDetector.canHandle(buffer)) {
const httpResult = httpDetector.detect(buffer, options);
if (this.httpDetector.canHandle(buffer)) {
const httpResult = this.httpDetector.detect(buffer, options);
if (httpResult) {
return httpResult;
}
@@ -66,142 +75,121 @@ export class ProtocolDetector {
/**
* Detect protocol with connection tracking for fragmented data
*
* @param buffer The buffer to analyze
* @param connectionId Unique connection identifier
* @param options Detection options
* @returns Detection result with protocol information
* @deprecated Use detectWithContext instead
*/
static async detectWithConnectionTracking(
buffer: Buffer,
connectionId: string,
options?: IDetectionOptions
): Promise<IDetectionResult> {
// Initialize or get connection tracking
let tracking = this.connectionTracking.get(connectionId);
if (!tracking) {
tracking = { startTime: Date.now() };
this.connectionTracking.set(connectionId, tracking);
}
// Convert connection ID to context
const context: IConnectionContext = {
id: connectionId,
sourceIp: 'unknown',
sourcePort: 0,
destIp: 'unknown',
destPort: 0,
timestamp: Date.now()
};
// 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<IDetectionResult> {
return this.getInstance().detectWithContextInstance(buffer, context, options);
}
private async detectWithContextInstance(
buffer: Buffer,
context: IConnectionContext,
options?: IDetectionOptions
): Promise<IDetectionResult> {
// Quick sanity check
if (!buffer || buffer.length === 0) {
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
// 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()
};
}
}

View 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();
}
}