/** * 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 { const headers: Record = {}; 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}(? 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 { const params: Record = {}; 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 { const pairs: string[] = []; for (const [key, value] of Object.entries(params)) { pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); } return pairs.length > 0 ? '?' + pairs.join('&') : ''; } }