fix(detection): fix SNI detection in TLS detector
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiryDate": "2025-10-19T22:36:33.093Z",
|
"expiryDate": "2025-10-19T23:55:27.838Z",
|
||||||
"issueDate": "2025-07-21T22:36:33.093Z",
|
"issueDate": "2025-07-21T23:55:27.838Z",
|
||||||
"savedAt": "2025-07-21T22:36:33.094Z"
|
"savedAt": "2025-07-21T23:55:27.838Z"
|
||||||
}
|
}
|
BIN
readme.plan.md
BIN
readme.plan.md
Binary file not shown.
@@ -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 { IProtocolDetector } from '../models/interfaces.js';
|
||||||
import type { IDetectionResult, IDetectionOptions, IConnectionInfo, THttpMethod } from '../models/detection-types.js';
|
import type { IDetectionResult, IDetectionOptions } from '../models/detection-types.js';
|
||||||
import { extractLine, isPrintableAscii, BufferAccumulator } from '../utils/buffer-utils.js';
|
import type { IProtocolDetectionResult, IConnectionContext } from '../../protocols/common/types.js';
|
||||||
import { parseHttpRequestLine, parseHttpHeaders, extractDomainFromHost, isHttpMethod } from '../utils/parser-utils.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 {
|
export class HttpDetector implements IProtocolDetector {
|
||||||
/**
|
private quickDetector = new QuickProtocolDetector();
|
||||||
* Minimum bytes needed to identify HTTP method
|
private fragmentManager: DetectionFragmentManager;
|
||||||
*/
|
|
||||||
private static readonly MIN_HTTP_METHOD_SIZE = 3; // GET
|
|
||||||
|
|
||||||
/**
|
constructor(fragmentManager?: DetectionFragmentManager) {
|
||||||
* Maximum reasonable HTTP header size
|
this.fragmentManager = fragmentManager || new DetectionFragmentManager();
|
||||||
*/
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if buffer can be handled by this detector
|
* Check if buffer can be handled by this detector
|
||||||
*/
|
*/
|
||||||
canHandle(buffer: Buffer): boolean {
|
canHandle(buffer: Buffer): boolean {
|
||||||
if (buffer.length < HttpDetector.MIN_HTTP_METHOD_SIZE) {
|
const result = this.quickDetector.quickDetect(buffer);
|
||||||
return false;
|
return result.protocol === 'http' && result.confidence > 50;
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get minimum bytes needed for detection
|
* Get minimum bytes needed for detection
|
||||||
*/
|
*/
|
||||||
getMinimumBytes(): number {
|
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 {
|
detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null {
|
||||||
if (buffer.length < 3) {
|
// Quick detection first
|
||||||
return false;
|
const quickResult = this.quickDetector.quickDetect(buffer);
|
||||||
|
|
||||||
|
if (quickResult.protocol !== 'http' || quickResult.confidence < 50) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check common HTTP methods
|
// Extract routing information
|
||||||
const start = buffer.slice(0, 7).toString('ascii');
|
const routing = RoutingExtractor.extract(buffer, 'http');
|
||||||
return start.startsWith('GET ') ||
|
|
||||||
start.startsWith('POST ') ||
|
// If we don't need full headers, we can return early
|
||||||
start.startsWith('PUT ') ||
|
if (quickResult.confidence >= 95 && !options?.extractFullHeaders) {
|
||||||
start.startsWith('DELETE ') ||
|
return {
|
||||||
start.startsWith('HEAD ') ||
|
protocol: 'http',
|
||||||
start.startsWith('OPTIONS') ||
|
connectionInfo: {
|
||||||
start.startsWith('PATCH ') ||
|
protocol: 'http',
|
||||||
start.startsWith('CONNECT') ||
|
method: quickResult.metadata?.method as THttpMethod,
|
||||||
start.startsWith('TRACE ');
|
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,
|
buffer: Buffer,
|
||||||
connectionId: string,
|
context: IConnectionContext,
|
||||||
options?: IDetectionOptions
|
options?: IDetectionOptions
|
||||||
): IDetectionResult | null {
|
): IDetectionResult | null {
|
||||||
const detector = new HttpDetector();
|
const handler = this.fragmentManager.getHandler('http');
|
||||||
|
const connectionId = DetectionFragmentManager.createConnectionId(context);
|
||||||
|
|
||||||
// Try direct detection first
|
// Add fragment
|
||||||
const directResult = detector.detect(buffer, options);
|
const result = handler.addFragment(connectionId, buffer);
|
||||||
if (directResult && directResult.isComplete) {
|
|
||||||
// Clean up any tracked fragments for this connection
|
|
||||||
this.fragmentedBuffers.delete(connectionId);
|
|
||||||
return directResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle fragmentation
|
if (result.error) {
|
||||||
let accumulator = this.fragmentedBuffers.get(connectionId);
|
handler.complete(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);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try detection on accumulated buffer
|
// Try detection on accumulated buffer
|
||||||
const result = detector.detect(fullBuffer, options);
|
const detectResult = this.detect(result.buffer!, options);
|
||||||
|
|
||||||
if (result && result.isComplete) {
|
if (detectResult && detectResult.isComplete) {
|
||||||
// Success - clean up
|
handler.complete(connectionId);
|
||||||
this.fragmentedBuffers.delete(connectionId);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return detectResult;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
148
ts/detection/detectors/quick-detector.ts
Normal file
148
ts/detection/detectors/quick-detector.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
147
ts/detection/detectors/routing-extractor.ts
Normal file
147
ts/detection/detectors/routing-extractor.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@
|
|||||||
// TLS detector doesn't need plugins imports
|
// TLS detector doesn't need plugins imports
|
||||||
import type { IProtocolDetector } from '../models/interfaces.js';
|
import type { IProtocolDetector } from '../models/interfaces.js';
|
||||||
import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../models/detection-types.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 { tlsVersionToString } from '../utils/parser-utils.js';
|
||||||
|
|
||||||
// Import from protocols
|
// Import from protocols
|
||||||
@@ -29,6 +29,13 @@ export class TlsDetector implements IProtocolDetector {
|
|||||||
*/
|
*/
|
||||||
private static fragmentedBuffers = new Map<string, BufferAccumulator>();
|
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
|
* Detect TLS protocol from buffer
|
||||||
*/
|
*/
|
||||||
@@ -201,11 +208,11 @@ export class TlsDetector implements IProtocolDetector {
|
|||||||
/**
|
/**
|
||||||
* Parse cipher suites
|
* Parse cipher suites
|
||||||
*/
|
*/
|
||||||
private parseCipherSuites(data: Buffer): number[] {
|
private parseCipherSuites(cipherData: Buffer): number[] {
|
||||||
const suites: number[] = [];
|
const suites: number[] = [];
|
||||||
|
|
||||||
for (let i = 0; i + 1 < data.length; i += 2) {
|
for (let i = 0; i < cipherData.length - 1; i += 2) {
|
||||||
const suite = readUInt16BE(data, i);
|
const suite = readUInt16BE(cipherData, i);
|
||||||
suites.push(suite);
|
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(
|
detectWithContext(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
connectionId: string,
|
context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number },
|
||||||
options?: IDetectionOptions
|
options?: IDetectionOptions
|
||||||
): IDetectionResult | null {
|
): IDetectionResult | null {
|
||||||
const detector = new TlsDetector();
|
const connectionId = this.createConnectionId(context);
|
||||||
|
|
||||||
// Try direct detection first
|
// Get or create buffer accumulator for this connection
|
||||||
const directResult = detector.detect(buffer, options);
|
let accumulator = TlsDetector.fragmentedBuffers.get(connectionId);
|
||||||
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);
|
|
||||||
if (!accumulator) {
|
if (!accumulator) {
|
||||||
accumulator = new BufferAccumulator();
|
accumulator = new BufferAccumulator();
|
||||||
this.fragmentedBuffers.set(connectionId, accumulator);
|
TlsDetector.fragmentedBuffers.set(connectionId, accumulator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add new data
|
||||||
accumulator.append(buffer);
|
accumulator.append(buffer);
|
||||||
const fullBuffer = accumulator.getBuffer();
|
|
||||||
|
|
||||||
// Try detection on accumulated buffer
|
// Try detection on accumulated data
|
||||||
const result = detector.detect(fullBuffer, options);
|
const result = this.detect(accumulator.getBuffer(), options);
|
||||||
|
|
||||||
if (result && result.isComplete) {
|
// If detection is complete or we have too much data, clean up
|
||||||
// Success - clean up
|
if (result?.isComplete || accumulator.length() > 65536) {
|
||||||
this.fragmentedBuffers.delete(connectionId);
|
TlsDetector.fragmentedBuffers.delete(connectionId);
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check timeout
|
|
||||||
if (options?.timeout) {
|
|
||||||
// TODO: Implement timeout handling
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@@ -16,7 +16,10 @@ export * from './models/interfaces.js';
|
|||||||
// Individual detectors
|
// Individual detectors
|
||||||
export * from './detectors/tls-detector.js';
|
export * from './detectors/tls-detector.js';
|
||||||
export * from './detectors/http-detector.js';
|
export * from './detectors/http-detector.js';
|
||||||
|
export * from './detectors/quick-detector.js';
|
||||||
|
export * from './detectors/routing-extractor.js';
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
export * from './utils/buffer-utils.js';
|
export * from './utils/buffer-utils.js';
|
||||||
export * from './utils/parser-utils.js';
|
export * from './utils/parser-utils.js';
|
||||||
|
export * from './utils/fragment-manager.js';
|
@@ -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 { TlsDetector } from './detectors/tls-detector.js';
|
||||||
import { HttpDetector } from './detectors/http-detector.js';
|
import { HttpDetector } from './detectors/http-detector.js';
|
||||||
|
import { DetectionFragmentManager } from './utils/fragment-manager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main protocol detector class
|
* Main protocol detector class
|
||||||
*/
|
*/
|
||||||
export class ProtocolDetector {
|
export class ProtocolDetector {
|
||||||
/**
|
private static instance: ProtocolDetector;
|
||||||
* Connection tracking for fragmented detection
|
private fragmentManager: DetectionFragmentManager;
|
||||||
*/
|
private tlsDetector: TlsDetector;
|
||||||
private static connectionTracking = new Map<string, {
|
private httpDetector: HttpDetector;
|
||||||
startTime: number;
|
|
||||||
protocol?: 'tls' | 'http' | 'unknown';
|
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
|
* Detect protocol from buffer data
|
||||||
*
|
|
||||||
* @param buffer The buffer to analyze
|
|
||||||
* @param options Detection options
|
|
||||||
* @returns Detection result with protocol information
|
|
||||||
*/
|
*/
|
||||||
static async detect(
|
static async detect(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> {
|
||||||
buffer: Buffer,
|
return this.getInstance().detectInstance(buffer, options);
|
||||||
options?: IDetectionOptions
|
}
|
||||||
): Promise<IDetectionResult> {
|
|
||||||
|
private async detectInstance(buffer: Buffer, options?: IDetectionOptions): Promise<IDetectionResult> {
|
||||||
// Quick sanity check
|
// Quick sanity check
|
||||||
if (!buffer || buffer.length === 0) {
|
if (!buffer || buffer.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -39,18 +50,16 @@ export class ProtocolDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try TLS detection first (more specific)
|
// Try TLS detection first (more specific)
|
||||||
const tlsDetector = new TlsDetector();
|
if (this.tlsDetector.canHandle(buffer)) {
|
||||||
if (tlsDetector.canHandle(buffer)) {
|
const tlsResult = this.tlsDetector.detect(buffer, options);
|
||||||
const tlsResult = tlsDetector.detect(buffer, options);
|
|
||||||
if (tlsResult) {
|
if (tlsResult) {
|
||||||
return tlsResult;
|
return tlsResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try HTTP detection
|
// Try HTTP detection
|
||||||
const httpDetector = new HttpDetector();
|
if (this.httpDetector.canHandle(buffer)) {
|
||||||
if (httpDetector.canHandle(buffer)) {
|
const httpResult = this.httpDetector.detect(buffer, options);
|
||||||
const httpResult = httpDetector.detect(buffer, options);
|
|
||||||
if (httpResult) {
|
if (httpResult) {
|
||||||
return httpResult;
|
return httpResult;
|
||||||
}
|
}
|
||||||
@@ -66,142 +75,121 @@ export class ProtocolDetector {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect protocol with connection tracking for fragmented data
|
* Detect protocol with connection tracking for fragmented data
|
||||||
*
|
* @deprecated Use detectWithContext instead
|
||||||
* @param buffer The buffer to analyze
|
|
||||||
* @param connectionId Unique connection identifier
|
|
||||||
* @param options Detection options
|
|
||||||
* @returns Detection result with protocol information
|
|
||||||
*/
|
*/
|
||||||
static async detectWithConnectionTracking(
|
static async detectWithConnectionTracking(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
options?: IDetectionOptions
|
options?: IDetectionOptions
|
||||||
): Promise<IDetectionResult> {
|
): Promise<IDetectionResult> {
|
||||||
// Initialize or get connection tracking
|
// Convert connection ID to context
|
||||||
let tracking = this.connectionTracking.get(connectionId);
|
const context: IConnectionContext = {
|
||||||
if (!tracking) {
|
id: connectionId,
|
||||||
tracking = { startTime: Date.now() };
|
sourceIp: 'unknown',
|
||||||
this.connectionTracking.set(connectionId, tracking);
|
sourcePort: 0,
|
||||||
}
|
destIp: 'unknown',
|
||||||
|
destPort: 0,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
// Check timeout
|
return this.getInstance().detectWithContextInstance(buffer, context, options);
|
||||||
if (options?.timeout) {
|
}
|
||||||
const elapsed = Date.now() - tracking.startTime;
|
|
||||||
if (elapsed > options.timeout) {
|
/**
|
||||||
// Timeout - clean up and return unknown
|
* Detect protocol with connection context for fragmented data
|
||||||
this.connectionTracking.delete(connectionId);
|
*/
|
||||||
TlsDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup
|
static async detectWithContext(
|
||||||
HttpDetector.detectWithFragments(Buffer.alloc(0), connectionId); // Force cleanup
|
buffer: Buffer,
|
||||||
|
context: IConnectionContext,
|
||||||
return {
|
options?: IDetectionOptions
|
||||||
protocol: 'unknown',
|
): Promise<IDetectionResult> {
|
||||||
connectionInfo: { protocol: 'unknown' },
|
return this.getInstance().detectWithContextInstance(buffer, context, options);
|
||||||
isComplete: true
|
}
|
||||||
};
|
|
||||||
}
|
private async detectWithContextInstance(
|
||||||
}
|
buffer: Buffer,
|
||||||
|
context: IConnectionContext,
|
||||||
// If we already know the protocol, use the appropriate detector
|
options?: IDetectionOptions
|
||||||
if (tracking.protocol === 'tls') {
|
): Promise<IDetectionResult> {
|
||||||
const result = TlsDetector.detectWithFragments(buffer, connectionId, options);
|
// Quick sanity check
|
||||||
if (result && result.isComplete) {
|
if (!buffer || buffer.length === 0) {
|
||||||
this.connectionTracking.delete(connectionId);
|
return {
|
||||||
}
|
|
||||||
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',
|
protocol: 'unknown',
|
||||||
connectionInfo: { protocol: 'unknown' },
|
connectionInfo: { protocol: 'unknown' },
|
||||||
isComplete: true
|
isComplete: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// First time detection - try to determine protocol
|
// First peek to determine protocol type
|
||||||
// Quick checks first
|
if (this.tlsDetector.canHandle(buffer)) {
|
||||||
if (buffer.length > 0) {
|
const result = this.tlsDetector.detectWithContext(buffer, context, options);
|
||||||
// TLS always starts with specific byte values
|
if (result) {
|
||||||
if (buffer[0] >= 0x14 && buffer[0] <= 0x18) {
|
return result;
|
||||||
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
|
if (this.httpDetector.canHandle(buffer)) {
|
||||||
|
const result = this.httpDetector.detectWithContext(buffer, context, options);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't determine protocol
|
||||||
return {
|
return {
|
||||||
protocol: 'unknown',
|
protocol: 'unknown',
|
||||||
connectionInfo: { protocol: 'unknown' },
|
connectionInfo: { protocol: 'unknown' },
|
||||||
isComplete: false,
|
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
|
* Clean up old connection tracking entries
|
||||||
*
|
*
|
||||||
* @param maxAge Maximum age in milliseconds (default: 30 seconds)
|
* @param maxAge Maximum age in milliseconds (default: 30 seconds)
|
||||||
*/
|
*/
|
||||||
static cleanupConnections(maxAge: number = 30000): void {
|
static cleanupConnections(maxAge: number = 30000): void {
|
||||||
const now = Date.now();
|
// Cleanup is now handled internally by the fragment manager
|
||||||
const toDelete: string[] = [];
|
this.getInstance().fragmentManager.cleanup();
|
||||||
|
|
||||||
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
|
* Extract domain from connection info
|
||||||
*
|
|
||||||
* @param connectionInfo Connection information from detection
|
|
||||||
* @returns The domain/hostname if found
|
|
||||||
*/
|
*/
|
||||||
static extractDomain(connectionInfo: IConnectionInfo): string | undefined {
|
static extractDomain(connectionInfo: any): string | undefined {
|
||||||
// For both TLS and HTTP, domain is stored in the domain field
|
return connectionInfo.domain || connectionInfo.sni || connectionInfo.host;
|
||||||
return connectionInfo.domain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a connection ID from connection parameters
|
* Create a connection ID from connection parameters
|
||||||
*
|
* @deprecated Use createConnectionContext instead
|
||||||
* @param params Connection parameters
|
|
||||||
* @returns A unique connection identifier
|
|
||||||
*/
|
*/
|
||||||
static createConnectionId(params: {
|
static createConnectionId(params: {
|
||||||
sourceIp?: string;
|
sourceIp?: string;
|
||||||
@@ -219,4 +207,24 @@ export class ProtocolDetector {
|
|||||||
const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params;
|
const { sourceIp = 'unknown', sourcePort = 0, destIp = 'unknown', destPort = 0 } = params;
|
||||||
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
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()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
64
ts/detection/utils/fragment-manager.ts
Normal file
64
ts/detection/utils/fragment-manager.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
163
ts/protocols/common/fragment-handler.ts
Normal file
163
ts/protocols/common/fragment-handler.ts
Normal file
@@ -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<string, IFragmentInfo>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
8
ts/protocols/common/index.ts
Normal file
8
ts/protocols/common/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Common Protocol Infrastructure
|
||||||
|
*
|
||||||
|
* Shared utilities and types for protocol handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './fragment-handler.js';
|
||||||
|
export * from './types.js';
|
76
ts/protocols/common/types.ts
Normal file
76
ts/protocols/common/types.ts
Normal file
@@ -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;
|
||||||
|
}
|
@@ -5,6 +5,7 @@
|
|||||||
* smartproxy-specific implementation details.
|
* smartproxy-specific implementation details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * as common from './common/index.js';
|
||||||
export * as tls from './tls/index.js';
|
export * as tls from './tls/index.js';
|
||||||
export * as http from './http/index.js';
|
export * as http from './http/index.js';
|
||||||
export * as proxy from './proxy/index.js';
|
export * as proxy from './proxy/index.js';
|
||||||
|
Reference in New Issue
Block a user