Files
smartproxy/ts/detection/protocol-detector.ts

222 lines
6.7 KiB
TypeScript
Raw Normal View History

/**
* 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}`;
}
}