- 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.
174 lines
4.0 KiB
TypeScript
174 lines
4.0 KiB
TypeScript
/**
|
|
* 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;
|
|
} |