feat(detection): add centralized protocol detection module
- Created ts/detection module for unified protocol detection - Implemented TLS and HTTP detectors with fragmentation support - Moved TLS detection logic from existing code to centralized module - Updated RouteConnectionHandler to use ProtocolDetector for both TLS and HTTP - Refactored ACME HTTP parsing to use detection module - Added comprehensive tests for detection functionality - Eliminated duplicate protocol detection code across codebase This centralizes all non-destructive protocol detection into a single module, improving code organization and reducing duplication between ACME and routing.
This commit is contained in:
174
ts/detection/utils/buffer-utils.ts
Normal file
174
ts/detection/utils/buffer-utils.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Buffer manipulation utilities for protocol detection
|
||||
*/
|
||||
|
||||
/**
|
||||
* BufferAccumulator class for handling fragmented data
|
||||
*/
|
||||
export class BufferAccumulator {
|
||||
private chunks: Buffer[] = [];
|
||||
private totalLength = 0;
|
||||
|
||||
/**
|
||||
* Append data to the accumulator
|
||||
*/
|
||||
append(data: Buffer): void {
|
||||
this.chunks.push(data);
|
||||
this.totalLength += data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the accumulated buffer
|
||||
*/
|
||||
getBuffer(): Buffer {
|
||||
if (this.chunks.length === 0) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
if (this.chunks.length === 1) {
|
||||
return this.chunks[0];
|
||||
}
|
||||
return Buffer.concat(this.chunks, this.totalLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current buffer length
|
||||
*/
|
||||
length(): number {
|
||||
return this.totalLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all accumulated data
|
||||
*/
|
||||
clear(): void {
|
||||
this.chunks = [];
|
||||
this.totalLength = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if accumulator has minimum bytes
|
||||
*/
|
||||
hasMinimumBytes(minBytes: number): boolean {
|
||||
return this.totalLength >= minBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a big-endian 16-bit integer from buffer
|
||||
*/
|
||||
export function readUInt16BE(buffer: Buffer, offset: number): number {
|
||||
if (offset + 2 > buffer.length) {
|
||||
throw new Error('Buffer too short for UInt16BE read');
|
||||
}
|
||||
return (buffer[offset] << 8) | buffer[offset + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a big-endian 24-bit integer from buffer
|
||||
*/
|
||||
export function readUInt24BE(buffer: Buffer, offset: number): number {
|
||||
if (offset + 3 > buffer.length) {
|
||||
throw new Error('Buffer too short for UInt24BE read');
|
||||
}
|
||||
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a byte sequence in a buffer
|
||||
*/
|
||||
export function findSequence(buffer: Buffer, sequence: Buffer, startOffset = 0): number {
|
||||
if (sequence.length === 0) {
|
||||
return startOffset;
|
||||
}
|
||||
|
||||
const searchLength = buffer.length - sequence.length + 1;
|
||||
for (let i = startOffset; i < searchLength; i++) {
|
||||
let found = true;
|
||||
for (let j = 0; j < sequence.length; j++) {
|
||||
if (buffer[i + j] !== sequence[j]) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a line from buffer (up to CRLF or LF)
|
||||
*/
|
||||
export function extractLine(buffer: Buffer, startOffset = 0): { line: string; nextOffset: number } | null {
|
||||
let lineEnd = -1;
|
||||
let skipBytes = 1;
|
||||
|
||||
// Look for CRLF first
|
||||
const crlfPos = findSequence(buffer, Buffer.from('\r\n'), startOffset);
|
||||
if (crlfPos !== -1) {
|
||||
lineEnd = crlfPos;
|
||||
skipBytes = 2;
|
||||
} else {
|
||||
// Look for LF only
|
||||
for (let i = startOffset; i < buffer.length; i++) {
|
||||
if (buffer[i] === 0x0A) { // LF
|
||||
lineEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lineEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const line = buffer.slice(startOffset, lineEnd).toString('utf8');
|
||||
return {
|
||||
line,
|
||||
nextOffset: lineEnd + skipBytes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if buffer starts with a string (case-insensitive)
|
||||
*/
|
||||
export function startsWithString(buffer: Buffer, str: string, offset = 0): boolean {
|
||||
if (offset + str.length > buffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bufferStr = buffer.slice(offset, offset + str.length).toString('utf8');
|
||||
return bufferStr.toLowerCase() === str.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe buffer slice that doesn't throw on out-of-bounds
|
||||
*/
|
||||
export function safeSlice(buffer: Buffer, start: number, end?: number): Buffer {
|
||||
const safeStart = Math.max(0, Math.min(start, buffer.length));
|
||||
const safeEnd = end === undefined
|
||||
? buffer.length
|
||||
: Math.max(safeStart, Math.min(end, buffer.length));
|
||||
|
||||
return buffer.slice(safeStart, safeEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if buffer contains printable ASCII
|
||||
*/
|
||||
export function isPrintableAscii(buffer: Buffer, length?: number): boolean {
|
||||
const checkLength = length || buffer.length;
|
||||
|
||||
for (let i = 0; i < checkLength && i < buffer.length; i++) {
|
||||
const byte = buffer[i];
|
||||
// Check if byte is printable ASCII (0x20-0x7E) or tab/newline/carriage return
|
||||
if (byte < 0x20 || byte > 0x7E) {
|
||||
if (byte !== 0x09 && byte !== 0x0A && byte !== 0x0D) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
Reference in New Issue
Block a user