281 lines
8.1 KiB
TypeScript
281 lines
8.1 KiB
TypeScript
![]() |
/**
|
||
|
* HTTP protocol detector
|
||
|
*/
|
||
|
|
||
|
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';
|
||
|
|
||
|
/**
|
||
|
* HTTP detector implementation
|
||
|
*/
|
||
|
export class HttpDetector implements IProtocolDetector {
|
||
|
/**
|
||
|
* Minimum bytes needed to identify HTTP method
|
||
|
*/
|
||
|
private static readonly MIN_HTTP_METHOD_SIZE = 3; // GET
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get minimum bytes needed for detection
|
||
|
*/
|
||
|
getMinimumBytes(): number {
|
||
|
return HttpDetector.MIN_HTTP_METHOD_SIZE;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Quick check if buffer starts with HTTP method
|
||
|
*/
|
||
|
static quickCheck(buffer: Buffer): boolean {
|
||
|
if (buffer.length < 3) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// 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 ');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle fragmented HTTP detection with connection tracking
|
||
|
*/
|
||
|
static detectWithFragments(
|
||
|
buffer: Buffer,
|
||
|
connectionId: string,
|
||
|
options?: IDetectionOptions
|
||
|
): IDetectionResult | null {
|
||
|
const detector = new HttpDetector();
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
// 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);
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Try detection on accumulated buffer
|
||
|
const result = detector.detect(fullBuffer, options);
|
||
|
|
||
|
if (result && result.isComplete) {
|
||
|
// Success - clean up
|
||
|
this.fragmentedBuffers.delete(connectionId);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
}
|