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

310 lines
8.9 KiB
TypeScript
Raw Normal View History

/**
* Protocol Detector
*
* Simplified protocol detection using the new architecture
*/
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 {
private static instance: ProtocolDetector;
private fragmentManager: DetectionFragmentManager;
private tlsDetector: TlsDetector;
private httpDetector: HttpDetector;
2025-07-22 11:28:06 +00:00
private connectionProtocols: Map<string, 'tls' | 'http'> = new Map();
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
*/
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 {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
// Try TLS detection first (more specific)
if (this.tlsDetector.canHandle(buffer)) {
const tlsResult = this.tlsDetector.detect(buffer, options);
if (tlsResult) {
return tlsResult;
}
}
// Try HTTP detection
if (this.httpDetector.canHandle(buffer)) {
const httpResult = this.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
* @deprecated Use detectWithContext instead
*/
static async detectWithConnectionTracking(
buffer: Buffer,
connectionId: string,
options?: IDetectionOptions
): Promise<IDetectionResult> {
// Convert connection ID to context
const context: IConnectionContext = {
id: connectionId,
sourceIp: 'unknown',
sourcePort: 0,
destIp: 'unknown',
destPort: 0,
timestamp: Date.now()
};
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
};
}
2025-07-22 10:35:39 +00:00
const connectionId = DetectionFragmentManager.createConnectionId(context);
2025-07-22 11:28:06 +00:00
// Check if we already know the protocol for this connection
const knownProtocol = this.connectionProtocols.get(connectionId);
if (knownProtocol === 'http') {
const result = this.httpDetector.detectWithContext(buffer, context, options);
if (result) {
if (result.isComplete) {
this.connectionProtocols.delete(connectionId);
}
return result;
}
} else if (knownProtocol === 'tls') {
2025-07-22 10:35:39 +00:00
// Handle TLS with fragment accumulation
const handler = this.fragmentManager.getHandler('tls');
const fragmentResult = handler.addFragment(connectionId, buffer);
if (fragmentResult.error) {
handler.complete(connectionId);
2025-07-22 11:28:06 +00:00
this.connectionProtocols.delete(connectionId);
2025-07-22 10:35:39 +00:00
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
const result = this.tlsDetector.detect(fragmentResult.buffer!, options);
if (result) {
2025-07-22 10:35:39 +00:00
if (result.isComplete) {
handler.complete(connectionId);
2025-07-22 11:28:06 +00:00
this.connectionProtocols.delete(connectionId);
2025-07-22 10:35:39 +00:00
}
return result;
}
}
2025-07-22 11:28:06 +00:00
// If we don't know the protocol yet, try to detect it
if (!knownProtocol) {
// First peek to determine protocol type
if (this.tlsDetector.canHandle(buffer)) {
this.connectionProtocols.set(connectionId, 'tls');
// Handle TLS with fragment accumulation
const handler = this.fragmentManager.getHandler('tls');
const fragmentResult = handler.addFragment(connectionId, buffer);
if (fragmentResult.error) {
handler.complete(connectionId);
this.connectionProtocols.delete(connectionId);
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: true
};
}
const result = this.tlsDetector.detect(fragmentResult.buffer!, options);
if (result) {
if (result.isComplete) {
handler.complete(connectionId);
this.connectionProtocols.delete(connectionId);
}
return result;
}
}
if (this.httpDetector.canHandle(buffer)) {
this.connectionProtocols.set(connectionId, 'http');
const result = this.httpDetector.detectWithContext(buffer, context, options);
if (result) {
if (result.isComplete) {
this.connectionProtocols.delete(connectionId);
}
return result;
}
}
}
// Can't determine protocol
return {
protocol: 'unknown',
connectionInfo: { protocol: 'unknown' },
isComplete: false,
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
*
2025-07-22 10:35:39 +00:00
* @param _maxAge Maximum age in milliseconds (default: 30 seconds)
*/
2025-07-22 10:35:39 +00:00
static cleanupConnections(_maxAge: number = 30000): void {
// Cleanup is now handled internally by the fragment manager
this.getInstance().fragmentManager.cleanup();
}
2025-07-22 10:35:39 +00:00
/**
* Clean up fragments for a specific connection
*/
static cleanupConnection(context: IConnectionContext): void {
const instance = this.getInstance();
const connectionId = DetectionFragmentManager.createConnectionId(context);
// Clean up both TLS and HTTP fragments for this connection
instance.fragmentManager.getHandler('tls').complete(connectionId);
instance.fragmentManager.getHandler('http').complete(connectionId);
2025-07-22 11:28:06 +00:00
// Remove from connection protocols tracking
instance.connectionProtocols.delete(connectionId);
2025-07-22 10:35:39 +00:00
}
/**
* Extract domain from connection info
*/
static extractDomain(connectionInfo: any): string | undefined {
return connectionInfo.domain || connectionInfo.sni || connectionInfo.host;
}
/**
* Create a connection ID from connection parameters
* @deprecated Use createConnectionContext instead
*/
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}`;
}
/**
* 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()
};
}
}