feat(protocols): refactor protocol utilities into centralized protocols module
This commit is contained in:
219
ts/protocols/http/parser.ts
Normal file
219
ts/protocols/http/parser.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* HTTP Protocol Parser
|
||||
* Generic HTTP parsing utilities
|
||||
*/
|
||||
|
||||
import { HTTP_METHODS, type THttpMethod, type THttpVersion } from './constants.js';
|
||||
import type { IHttpRequestLine, IHttpHeader } from './types.js';
|
||||
|
||||
/**
|
||||
* HTTP parser utilities
|
||||
*/
|
||||
export class HttpParser {
|
||||
/**
|
||||
* Check if string is a valid HTTP method
|
||||
*/
|
||||
static isHttpMethod(str: string): str is THttpMethod {
|
||||
return HTTP_METHODS.includes(str as THttpMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTTP request line
|
||||
*/
|
||||
static parseRequestLine(line: string): IHttpRequestLine | null {
|
||||
const parts = line.trim().split(' ');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [method, path, version] = parts;
|
||||
|
||||
// Validate method
|
||||
if (!this.isHttpMethod(method)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate version
|
||||
if (!version.startsWith('HTTP/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
method: method as THttpMethod,
|
||||
path,
|
||||
version: version as THttpVersion
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTTP header line
|
||||
*/
|
||||
static parseHeaderLine(line: string): IHttpHeader | null {
|
||||
const colonIndex = line.indexOf(':');
|
||||
|
||||
if (colonIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = line.slice(0, colonIndex).trim();
|
||||
const value = line.slice(colonIndex + 1).trim();
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTTP headers from lines
|
||||
*/
|
||||
static parseHeaders(lines: string[]): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
for (const line of lines) {
|
||||
const header = this.parseHeaderLine(line);
|
||||
if (header) {
|
||||
// Convert header names to lowercase for consistency
|
||||
headers[header.name.toLowerCase()] = header.value;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domain from Host header value
|
||||
*/
|
||||
static extractDomainFromHost(hostHeader: string): string {
|
||||
// Remove port if present
|
||||
const colonIndex = hostHeader.lastIndexOf(':');
|
||||
if (colonIndex !== -1) {
|
||||
// Check if it's not part of IPv6 address
|
||||
const beforeColon = hostHeader.slice(0, colonIndex);
|
||||
if (!beforeColon.includes(']')) {
|
||||
return beforeColon;
|
||||
}
|
||||
}
|
||||
return hostHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain name
|
||||
*/
|
||||
static isValidDomain(domain: string): boolean {
|
||||
// Basic domain validation
|
||||
if (!domain || domain.length > 253) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for valid characters and structure
|
||||
const domainRegex = /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*$/;
|
||||
return domainRegex.test(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract line from buffer
|
||||
*/
|
||||
static extractLine(buffer: Buffer, offset: number = 0): { line: string; nextOffset: number } | null {
|
||||
// Look for CRLF
|
||||
const crlfIndex = buffer.indexOf('\r\n', offset);
|
||||
if (crlfIndex === -1) {
|
||||
// Look for just LF
|
||||
const lfIndex = buffer.indexOf('\n', offset);
|
||||
if (lfIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
line: buffer.slice(offset, lfIndex).toString('utf8'),
|
||||
nextOffset: lfIndex + 1
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
line: buffer.slice(offset, crlfIndex).toString('utf8'),
|
||||
nextOffset: crlfIndex + 2
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if buffer contains printable ASCII
|
||||
*/
|
||||
static isPrintableAscii(buffer: Buffer, length?: number): boolean {
|
||||
const checkLength = Math.min(length || buffer.length, buffer.length);
|
||||
|
||||
for (let i = 0; i < checkLength; i++) {
|
||||
const byte = buffer[i];
|
||||
// Allow printable ASCII (32-126) plus tab (9), LF (10), and CR (13)
|
||||
if (byte < 32 || byte > 126) {
|
||||
if (byte !== 9 && byte !== 10 && byte !== 13) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse query string
|
||||
*/
|
||||
static parseQueryString(queryString: string): Record<string, string> {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (!queryString) {
|
||||
return params;
|
||||
}
|
||||
|
||||
// Remove leading '?' if present
|
||||
if (queryString.startsWith('?')) {
|
||||
queryString = queryString.slice(1);
|
||||
}
|
||||
|
||||
const pairs = queryString.split('&');
|
||||
for (const pair of pairs) {
|
||||
const [key, value] = pair.split('=');
|
||||
if (key) {
|
||||
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query string from params
|
||||
*/
|
||||
static buildQueryString(params: Record<string, string>): string {
|
||||
const pairs: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||
}
|
||||
|
||||
return pairs.length > 0 ? '?' + pairs.join('&') : '';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user