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