UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.

219 lines (184 loc) 5.42 kB
/** * 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('&') : ''; } }