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