feat(protocols): refactor protocol utilities into centralized protocols module
This commit is contained in:
		| @@ -1,161 +1,44 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { logger } from './logger.js'; | ||||
| import { ProxyProtocolParser as ProtocolParser, type IProxyInfo, type IProxyParseResult } from '../../protocols/proxy/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Interface representing parsed PROXY protocol information | ||||
|  */ | ||||
| export interface IProxyInfo { | ||||
|   protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'; | ||||
|   sourceIP: string; | ||||
|   sourcePort: number; | ||||
|   destinationIP: string; | ||||
|   destinationPort: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Interface for parse result including remaining data | ||||
|  */ | ||||
| export interface IProxyParseResult { | ||||
|   proxyInfo: IProxyInfo | null; | ||||
|   remainingData: Buffer; | ||||
| } | ||||
| // Re-export types from protocols for backward compatibility | ||||
| export type { IProxyInfo, IProxyParseResult } from '../../protocols/proxy/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Parser for PROXY protocol v1 (text format) | ||||
|  * Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt | ||||
|  *  | ||||
|  * This class now delegates to the protocol parser but adds  | ||||
|  * smartproxy-specific features like socket reading and logging | ||||
|  */ | ||||
| export class ProxyProtocolParser { | ||||
|   static readonly PROXY_V1_SIGNATURE = 'PROXY '; | ||||
|   static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header | ||||
|   static readonly HEADER_TERMINATOR = '\r\n'; | ||||
|   static readonly PROXY_V1_SIGNATURE = ProtocolParser.PROXY_V1_SIGNATURE; | ||||
|   static readonly MAX_HEADER_LENGTH = ProtocolParser.MAX_HEADER_LENGTH; | ||||
|   static readonly HEADER_TERMINATOR = ProtocolParser.HEADER_TERMINATOR; | ||||
|    | ||||
|   /** | ||||
|    * Parse PROXY protocol v1 header from buffer | ||||
|    * Returns proxy info and remaining data after header | ||||
|    */ | ||||
|   static parse(data: Buffer): IProxyParseResult { | ||||
|     // Check if buffer starts with PROXY signature | ||||
|     if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) { | ||||
|       return { | ||||
|         proxyInfo: null, | ||||
|         remainingData: data | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Find header terminator | ||||
|     const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR); | ||||
|     if (headerEndIndex === -1) { | ||||
|       // Header incomplete, need more data | ||||
|       if (data.length > this.MAX_HEADER_LENGTH) { | ||||
|         // Header too long, invalid | ||||
|         throw new Error('PROXY protocol header exceeds maximum length'); | ||||
|       } | ||||
|       return { | ||||
|         proxyInfo: null, | ||||
|         remainingData: data | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Extract header line | ||||
|     const headerLine = data.toString('ascii', 0, headerEndIndex); | ||||
|     const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n | ||||
|      | ||||
|     // Parse header | ||||
|     const parts = headerLine.split(' '); | ||||
|      | ||||
|     if (parts.length < 2) { | ||||
|       throw new Error(`Invalid PROXY protocol header format: ${headerLine}`); | ||||
|     } | ||||
|      | ||||
|     const [signature, protocol] = parts; | ||||
|      | ||||
|     // Validate protocol | ||||
|     if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) { | ||||
|       throw new Error(`Invalid PROXY protocol: ${protocol}`); | ||||
|     } | ||||
|      | ||||
|     // For UNKNOWN protocol, ignore addresses | ||||
|     if (protocol === 'UNKNOWN') { | ||||
|       return { | ||||
|         proxyInfo: { | ||||
|           protocol: 'UNKNOWN', | ||||
|           sourceIP: '', | ||||
|           sourcePort: 0, | ||||
|           destinationIP: '', | ||||
|           destinationPort: 0 | ||||
|         }, | ||||
|         remainingData | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // For TCP4/TCP6, we need all 6 parts | ||||
|     if (parts.length !== 6) { | ||||
|       throw new Error(`Invalid PROXY protocol header format: ${headerLine}`); | ||||
|     } | ||||
|      | ||||
|     const [, , srcIP, dstIP, srcPort, dstPort] = parts; | ||||
|      | ||||
|     // Validate and parse ports | ||||
|     const sourcePort = parseInt(srcPort, 10); | ||||
|     const destinationPort = parseInt(dstPort, 10); | ||||
|      | ||||
|     if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) { | ||||
|       throw new Error(`Invalid source port: ${srcPort}`); | ||||
|     } | ||||
|      | ||||
|     if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) { | ||||
|       throw new Error(`Invalid destination port: ${dstPort}`); | ||||
|     } | ||||
|      | ||||
|     // Validate IP addresses | ||||
|     const protocolType = protocol as 'TCP4' | 'TCP6' | 'UNKNOWN'; | ||||
|     if (!this.isValidIP(srcIP, protocolType)) { | ||||
|       throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`); | ||||
|     } | ||||
|      | ||||
|     if (!this.isValidIP(dstIP, protocolType)) { | ||||
|       throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`); | ||||
|     } | ||||
|      | ||||
|     return { | ||||
|       proxyInfo: { | ||||
|         protocol: protocol as 'TCP4' | 'TCP6', | ||||
|         sourceIP: srcIP, | ||||
|         sourcePort, | ||||
|         destinationIP: dstIP, | ||||
|         destinationPort | ||||
|       }, | ||||
|       remainingData | ||||
|     }; | ||||
|     // Delegate to protocol parser | ||||
|     return ProtocolParser.parse(data); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Generate PROXY protocol v1 header | ||||
|    */ | ||||
|   static generate(info: IProxyInfo): Buffer { | ||||
|     if (info.protocol === 'UNKNOWN') { | ||||
|       return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii'); | ||||
|     } | ||||
|      | ||||
|     const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`; | ||||
|      | ||||
|     if (header.length > this.MAX_HEADER_LENGTH) { | ||||
|       throw new Error('Generated PROXY protocol header exceeds maximum length'); | ||||
|     } | ||||
|      | ||||
|     return Buffer.from(header, 'ascii'); | ||||
|     // Delegate to protocol parser | ||||
|     return ProtocolParser.generate(info); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate IP address format | ||||
|    */ | ||||
|   private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean { | ||||
|     if (protocol === 'TCP4') { | ||||
|       return plugins.net.isIPv4(ip); | ||||
|     } else if (protocol === 'TCP6') { | ||||
|       return plugins.net.isIPv6(ip); | ||||
|     } | ||||
|     return false; | ||||
|     return ProtocolParser.isValidIP(ip, protocol); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| /** | ||||
|  * WebSocket utility functions | ||||
|  *  | ||||
|  * This module provides smartproxy-specific WebSocket utilities | ||||
|  * and re-exports protocol utilities from the protocols module | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Type for WebSocket RawData that can be different types in different environments | ||||
|  * This matches the ws library's type definition | ||||
|  */ | ||||
| export type RawData = Buffer | ArrayBuffer | Buffer[] | any; | ||||
| // Import and re-export from protocols | ||||
| import { getMessageSize as protocolGetMessageSize, toBuffer as protocolToBuffer } from '../../protocols/websocket/index.js'; | ||||
| export type { RawData } from '../../protocols/websocket/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Get the length of a WebSocket message regardless of its type | ||||
| @@ -15,35 +16,9 @@ export type RawData = Buffer | ArrayBuffer | Buffer[] | any; | ||||
|  * @param data - The data message from WebSocket (could be any RawData type) | ||||
|  * @returns The length of the data in bytes | ||||
|  */ | ||||
| export function getMessageSize(data: RawData): number { | ||||
|   if (typeof data === 'string') { | ||||
|     // For string data, get the byte length | ||||
|     return Buffer.from(data, 'utf8').length; | ||||
|   } else if (data instanceof Buffer) { | ||||
|     // For Node.js Buffer | ||||
|     return data.length; | ||||
|   } else if (data instanceof ArrayBuffer) { | ||||
|     // For ArrayBuffer | ||||
|     return data.byteLength; | ||||
|   } else if (Array.isArray(data)) { | ||||
|     // For array of buffers, sum their lengths | ||||
|     return data.reduce((sum, chunk) => { | ||||
|       if (chunk instanceof Buffer) { | ||||
|         return sum + chunk.length; | ||||
|       } else if (chunk instanceof ArrayBuffer) { | ||||
|         return sum + chunk.byteLength; | ||||
|       } | ||||
|       return sum; | ||||
|     }, 0); | ||||
|   } else { | ||||
|     // For other types, try to determine the size or return 0 | ||||
|     try { | ||||
|       return Buffer.from(data).length; | ||||
|     } catch (e) { | ||||
|       console.warn('Could not determine message size', e); | ||||
|       return 0; | ||||
|     } | ||||
|   } | ||||
| export function getMessageSize(data: import('../../protocols/websocket/index.js').RawData): number { | ||||
|   // Delegate to protocol implementation | ||||
|   return protocolGetMessageSize(data); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -52,30 +27,7 @@ export function getMessageSize(data: RawData): number { | ||||
|  * @param data - The data message from WebSocket (could be any RawData type) | ||||
|  * @returns A Buffer containing the data | ||||
|  */ | ||||
| export function toBuffer(data: RawData): Buffer { | ||||
|   if (typeof data === 'string') { | ||||
|     return Buffer.from(data, 'utf8'); | ||||
|   } else if (data instanceof Buffer) { | ||||
|     return data; | ||||
|   } else if (data instanceof ArrayBuffer) { | ||||
|     return Buffer.from(data); | ||||
|   } else if (Array.isArray(data)) { | ||||
|     // For array of buffers, concatenate them | ||||
|     return Buffer.concat(data.map(chunk => { | ||||
|       if (chunk instanceof Buffer) { | ||||
|         return chunk; | ||||
|       } else if (chunk instanceof ArrayBuffer) { | ||||
|         return Buffer.from(chunk); | ||||
|       } | ||||
|       return Buffer.from(chunk); | ||||
|     })); | ||||
|   } else { | ||||
|     // For other types, try to convert to Buffer or return empty Buffer | ||||
|     try { | ||||
|       return Buffer.from(data); | ||||
|     } catch (e) { | ||||
|       console.warn('Could not convert message to Buffer', e); | ||||
|       return Buffer.alloc(0); | ||||
|     } | ||||
|   } | ||||
| export function toBuffer(data: import('../../protocols/websocket/index.js').RawData): Buffer { | ||||
|   // Delegate to protocol implementation | ||||
|   return protocolToBuffer(data); | ||||
| } | ||||
| @@ -8,10 +8,12 @@ import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../mo | ||||
| import { readUInt16BE, readUInt24BE, BufferAccumulator } from '../utils/buffer-utils.js'; | ||||
| import { tlsVersionToString } from '../utils/parser-utils.js'; | ||||
|  | ||||
| // Import existing TLS utilities | ||||
| import { TlsUtils, TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../tls/utils/tls-utils.js'; | ||||
| import { SniExtraction } from '../../tls/sni/sni-extraction.js'; | ||||
| import { ClientHelloParser } from '../../tls/sni/client-hello-parser.js'; | ||||
| // Import from protocols | ||||
| import { TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../protocols/tls/index.js'; | ||||
|  | ||||
| // Import TLS utilities for SNI extraction from protocols | ||||
| import { SniExtraction } from '../../protocols/tls/sni/sni-extraction.js'; | ||||
| import { ClientHelloParser } from '../../protocols/tls/sni/client-hello-parser.js'; | ||||
|  | ||||
| /** | ||||
|  * TLS detector implementation | ||||
| @@ -92,7 +94,7 @@ export class TlsDetector implements IProtocolDetector { | ||||
|             protocol: 'tls', | ||||
|             connectionInfo, | ||||
|             remainingBuffer: buffer.length > totalRecordLength  | ||||
|               ? buffer.slice(totalRecordLength)  | ||||
|               ? buffer.subarray(totalRecordLength)  | ||||
|               : undefined, | ||||
|             isComplete: true | ||||
|           }; | ||||
| @@ -114,7 +116,7 @@ export class TlsDetector implements IProtocolDetector { | ||||
|       connectionInfo, | ||||
|       isComplete: true, | ||||
|       remainingBuffer: buffer.length > recordLength + 5  | ||||
|         ? buffer.slice(recordLength + 5)  | ||||
|         ? buffer.subarray(recordLength + 5)  | ||||
|         : undefined | ||||
|     }; | ||||
|   } | ||||
| @@ -185,7 +187,7 @@ export class TlsDetector implements IProtocolDetector { | ||||
|       offset++; | ||||
|        | ||||
|       if (offset + protoLength <= data.length) { | ||||
|         const protocol = data.slice(offset, offset + protoLength).toString('ascii'); | ||||
|         const protocol = data.subarray(offset, offset + protoLength).toString('ascii'); | ||||
|         protocols.push(protocol); | ||||
|         offset += protoLength; | ||||
|       } else { | ||||
|   | ||||
| @@ -2,6 +2,9 @@ | ||||
|  * Buffer manipulation utilities for protocol detection | ||||
|  */ | ||||
|  | ||||
| // Import from protocols | ||||
| import { HttpParser } from '../../protocols/http/index.js'; | ||||
|  | ||||
| /** | ||||
|  * BufferAccumulator class for handling fragmented data | ||||
|  */ | ||||
| @@ -101,33 +104,8 @@ export function findSequence(buffer: Buffer, sequence: Buffer, startOffset = 0): | ||||
|  * 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 | ||||
|   }; | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.extractLine(buffer, startOffset); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -158,17 +136,6 @@ export function safeSlice(buffer: Buffer, start: number, end?: number): Buffer { | ||||
|  * 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; | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.isPrintableAscii(buffer, length); | ||||
| } | ||||
| @@ -1,20 +1,14 @@ | ||||
| /** | ||||
|  * Parser utilities for protocol detection | ||||
|  * Now delegates to protocol modules for actual parsing | ||||
|  */ | ||||
|  | ||||
| import type { THttpMethod, TTlsVersion } from '../models/detection-types.js'; | ||||
| import { HttpParser, HTTP_METHODS, HTTP_VERSIONS } from '../../protocols/http/index.js'; | ||||
| import { tlsVersionToString as protocolTlsVersionToString } from '../../protocols/tls/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Valid HTTP methods | ||||
|  */ | ||||
| export const HTTP_METHODS: THttpMethod[] = [ | ||||
|   'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE' | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * HTTP version strings | ||||
|  */ | ||||
| export const HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/3']; | ||||
| // Re-export constants for backward compatibility | ||||
| export { HTTP_METHODS, HTTP_VERSIONS }; | ||||
|  | ||||
| /** | ||||
|  * Parse HTTP request line | ||||
| @@ -24,118 +18,60 @@ export function parseHttpRequestLine(line: string): { | ||||
|   path: string; | ||||
|   version: string; | ||||
| } | null { | ||||
|   const parts = line.trim().split(' '); | ||||
|    | ||||
|   if (parts.length !== 3) { | ||||
|     return null; | ||||
|   } | ||||
|    | ||||
|   const [method, path, version] = parts; | ||||
|    | ||||
|   // Validate method | ||||
|   if (!HTTP_METHODS.includes(method as THttpMethod)) { | ||||
|     return null; | ||||
|   } | ||||
|    | ||||
|   // Validate version | ||||
|   if (!version.startsWith('HTTP/')) { | ||||
|     return null; | ||||
|   } | ||||
|    | ||||
|   return { | ||||
|     method: method as THttpMethod, | ||||
|     path, | ||||
|     version | ||||
|   }; | ||||
|   // Delegate to protocol parser | ||||
|   const result = HttpParser.parseRequestLine(line); | ||||
|   return result ? { | ||||
|     method: result.method as THttpMethod, | ||||
|     path: result.path, | ||||
|     version: result.version | ||||
|   } : null; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Parse HTTP header line | ||||
|  */ | ||||
| export function parseHttpHeader(line: string): { name: string; value: string } | 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 }; | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.parseHeaderLine(line); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Parse HTTP headers from lines | ||||
|  */ | ||||
| export function parseHttpHeaders(lines: string[]): Record<string, string> { | ||||
|   const headers: Record<string, string> = {}; | ||||
|    | ||||
|   for (const line of lines) { | ||||
|     const header = parseHttpHeader(line); | ||||
|     if (header) { | ||||
|       // Convert header names to lowercase for consistency | ||||
|       headers[header.name.toLowerCase()] = header.value; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return headers; | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.parseHeaders(lines); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert TLS version bytes to version string | ||||
|  */ | ||||
| export function tlsVersionToString(major: number, minor: number): TTlsVersion | null { | ||||
|   if (major === 0x03) { | ||||
|     switch (minor) { | ||||
|       case 0x00: return 'SSLv3'; | ||||
|       case 0x01: return 'TLSv1.0'; | ||||
|       case 0x02: return 'TLSv1.1'; | ||||
|       case 0x03: return 'TLSv1.2'; | ||||
|       case 0x04: return 'TLSv1.3'; | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
|   // Delegate to protocol parser | ||||
|   return protocolTlsVersionToString(major, minor) as TTlsVersion; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Extract domain from Host header value | ||||
|  */ | ||||
| export function 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; | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.extractDomainFromHost(hostHeader); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Validate domain name | ||||
|  */ | ||||
| export function 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); | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.isValidDomain(domain); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if string is a valid HTTP method | ||||
|  */ | ||||
| export function isHttpMethod(str: string): str is THttpMethod { | ||||
|   return HTTP_METHODS.includes(str as THttpMethod); | ||||
|   // Delegate to protocol parser | ||||
|   return HttpParser.isHttpMethod(str) && (str as THttpMethod) !== undefined; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -35,4 +35,5 @@ export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js'; | ||||
| // Certificate module has been removed - use SmartCertManager instead | ||||
| export * as tls from './tls/index.js'; | ||||
| export * as routing from './routing/index.js'; | ||||
| export * as detection from './detection/index.js'; | ||||
| export * as detection from './detection/index.js'; | ||||
| export * as protocols from './protocols/index.js'; | ||||
							
								
								
									
										219
									
								
								ts/protocols/http/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								ts/protocols/http/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | ||||
| /** | ||||
|  * HTTP Protocol Constants | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * HTTP methods | ||||
|  */ | ||||
| export const HTTP_METHODS = [ | ||||
|   'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE' | ||||
| ] as const; | ||||
|  | ||||
| export type THttpMethod = typeof HTTP_METHODS[number]; | ||||
|  | ||||
| /** | ||||
|  * HTTP version strings | ||||
|  */ | ||||
| export const HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/3'] as const; | ||||
|  | ||||
| export type THttpVersion = typeof HTTP_VERSIONS[number]; | ||||
|  | ||||
| /** | ||||
|  * HTTP status codes | ||||
|  */ | ||||
| export enum HttpStatus { | ||||
|   // 1xx Informational | ||||
|   CONTINUE = 100, | ||||
|   SWITCHING_PROTOCOLS = 101, | ||||
|   PROCESSING = 102, | ||||
|   EARLY_HINTS = 103, | ||||
|    | ||||
|   // 2xx Success | ||||
|   OK = 200, | ||||
|   CREATED = 201, | ||||
|   ACCEPTED = 202, | ||||
|   NON_AUTHORITATIVE_INFORMATION = 203, | ||||
|   NO_CONTENT = 204, | ||||
|   RESET_CONTENT = 205, | ||||
|   PARTIAL_CONTENT = 206, | ||||
|   MULTI_STATUS = 207, | ||||
|   ALREADY_REPORTED = 208, | ||||
|   IM_USED = 226, | ||||
|    | ||||
|   // 3xx Redirection | ||||
|   MULTIPLE_CHOICES = 300, | ||||
|   MOVED_PERMANENTLY = 301, | ||||
|   FOUND = 302, | ||||
|   SEE_OTHER = 303, | ||||
|   NOT_MODIFIED = 304, | ||||
|   USE_PROXY = 305, | ||||
|   TEMPORARY_REDIRECT = 307, | ||||
|   PERMANENT_REDIRECT = 308, | ||||
|    | ||||
|   // 4xx Client Error | ||||
|   BAD_REQUEST = 400, | ||||
|   UNAUTHORIZED = 401, | ||||
|   PAYMENT_REQUIRED = 402, | ||||
|   FORBIDDEN = 403, | ||||
|   NOT_FOUND = 404, | ||||
|   METHOD_NOT_ALLOWED = 405, | ||||
|   NOT_ACCEPTABLE = 406, | ||||
|   PROXY_AUTHENTICATION_REQUIRED = 407, | ||||
|   REQUEST_TIMEOUT = 408, | ||||
|   CONFLICT = 409, | ||||
|   GONE = 410, | ||||
|   LENGTH_REQUIRED = 411, | ||||
|   PRECONDITION_FAILED = 412, | ||||
|   PAYLOAD_TOO_LARGE = 413, | ||||
|   URI_TOO_LONG = 414, | ||||
|   UNSUPPORTED_MEDIA_TYPE = 415, | ||||
|   RANGE_NOT_SATISFIABLE = 416, | ||||
|   EXPECTATION_FAILED = 417, | ||||
|   IM_A_TEAPOT = 418, | ||||
|   MISDIRECTED_REQUEST = 421, | ||||
|   UNPROCESSABLE_ENTITY = 422, | ||||
|   LOCKED = 423, | ||||
|   FAILED_DEPENDENCY = 424, | ||||
|   TOO_EARLY = 425, | ||||
|   UPGRADE_REQUIRED = 426, | ||||
|   PRECONDITION_REQUIRED = 428, | ||||
|   TOO_MANY_REQUESTS = 429, | ||||
|   REQUEST_HEADER_FIELDS_TOO_LARGE = 431, | ||||
|   UNAVAILABLE_FOR_LEGAL_REASONS = 451, | ||||
|    | ||||
|   // 5xx Server Error | ||||
|   INTERNAL_SERVER_ERROR = 500, | ||||
|   NOT_IMPLEMENTED = 501, | ||||
|   BAD_GATEWAY = 502, | ||||
|   SERVICE_UNAVAILABLE = 503, | ||||
|   GATEWAY_TIMEOUT = 504, | ||||
|   HTTP_VERSION_NOT_SUPPORTED = 505, | ||||
|   VARIANT_ALSO_NEGOTIATES = 506, | ||||
|   INSUFFICIENT_STORAGE = 507, | ||||
|   LOOP_DETECTED = 508, | ||||
|   NOT_EXTENDED = 510, | ||||
|   NETWORK_AUTHENTICATION_REQUIRED = 511, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP status text mapping | ||||
|  */ | ||||
| export const HTTP_STATUS_TEXT: Record<HttpStatus, string> = { | ||||
|   // 1xx | ||||
|   [HttpStatus.CONTINUE]: 'Continue', | ||||
|   [HttpStatus.SWITCHING_PROTOCOLS]: 'Switching Protocols', | ||||
|   [HttpStatus.PROCESSING]: 'Processing', | ||||
|   [HttpStatus.EARLY_HINTS]: 'Early Hints', | ||||
|    | ||||
|   // 2xx | ||||
|   [HttpStatus.OK]: 'OK', | ||||
|   [HttpStatus.CREATED]: 'Created', | ||||
|   [HttpStatus.ACCEPTED]: 'Accepted', | ||||
|   [HttpStatus.NON_AUTHORITATIVE_INFORMATION]: 'Non-Authoritative Information', | ||||
|   [HttpStatus.NO_CONTENT]: 'No Content', | ||||
|   [HttpStatus.RESET_CONTENT]: 'Reset Content', | ||||
|   [HttpStatus.PARTIAL_CONTENT]: 'Partial Content', | ||||
|   [HttpStatus.MULTI_STATUS]: 'Multi-Status', | ||||
|   [HttpStatus.ALREADY_REPORTED]: 'Already Reported', | ||||
|   [HttpStatus.IM_USED]: 'IM Used', | ||||
|    | ||||
|   // 3xx | ||||
|   [HttpStatus.MULTIPLE_CHOICES]: 'Multiple Choices', | ||||
|   [HttpStatus.MOVED_PERMANENTLY]: 'Moved Permanently', | ||||
|   [HttpStatus.FOUND]: 'Found', | ||||
|   [HttpStatus.SEE_OTHER]: 'See Other', | ||||
|   [HttpStatus.NOT_MODIFIED]: 'Not Modified', | ||||
|   [HttpStatus.USE_PROXY]: 'Use Proxy', | ||||
|   [HttpStatus.TEMPORARY_REDIRECT]: 'Temporary Redirect', | ||||
|   [HttpStatus.PERMANENT_REDIRECT]: 'Permanent Redirect', | ||||
|    | ||||
|   // 4xx | ||||
|   [HttpStatus.BAD_REQUEST]: 'Bad Request', | ||||
|   [HttpStatus.UNAUTHORIZED]: 'Unauthorized', | ||||
|   [HttpStatus.PAYMENT_REQUIRED]: 'Payment Required', | ||||
|   [HttpStatus.FORBIDDEN]: 'Forbidden', | ||||
|   [HttpStatus.NOT_FOUND]: 'Not Found', | ||||
|   [HttpStatus.METHOD_NOT_ALLOWED]: 'Method Not Allowed', | ||||
|   [HttpStatus.NOT_ACCEPTABLE]: 'Not Acceptable', | ||||
|   [HttpStatus.PROXY_AUTHENTICATION_REQUIRED]: 'Proxy Authentication Required', | ||||
|   [HttpStatus.REQUEST_TIMEOUT]: 'Request Timeout', | ||||
|   [HttpStatus.CONFLICT]: 'Conflict', | ||||
|   [HttpStatus.GONE]: 'Gone', | ||||
|   [HttpStatus.LENGTH_REQUIRED]: 'Length Required', | ||||
|   [HttpStatus.PRECONDITION_FAILED]: 'Precondition Failed', | ||||
|   [HttpStatus.PAYLOAD_TOO_LARGE]: 'Payload Too Large', | ||||
|   [HttpStatus.URI_TOO_LONG]: 'URI Too Long', | ||||
|   [HttpStatus.UNSUPPORTED_MEDIA_TYPE]: 'Unsupported Media Type', | ||||
|   [HttpStatus.RANGE_NOT_SATISFIABLE]: 'Range Not Satisfiable', | ||||
|   [HttpStatus.EXPECTATION_FAILED]: 'Expectation Failed', | ||||
|   [HttpStatus.IM_A_TEAPOT]: "I'm a teapot", | ||||
|   [HttpStatus.MISDIRECTED_REQUEST]: 'Misdirected Request', | ||||
|   [HttpStatus.UNPROCESSABLE_ENTITY]: 'Unprocessable Entity', | ||||
|   [HttpStatus.LOCKED]: 'Locked', | ||||
|   [HttpStatus.FAILED_DEPENDENCY]: 'Failed Dependency', | ||||
|   [HttpStatus.TOO_EARLY]: 'Too Early', | ||||
|   [HttpStatus.UPGRADE_REQUIRED]: 'Upgrade Required', | ||||
|   [HttpStatus.PRECONDITION_REQUIRED]: 'Precondition Required', | ||||
|   [HttpStatus.TOO_MANY_REQUESTS]: 'Too Many Requests', | ||||
|   [HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE]: 'Request Header Fields Too Large', | ||||
|   [HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS]: 'Unavailable For Legal Reasons', | ||||
|    | ||||
|   // 5xx | ||||
|   [HttpStatus.INTERNAL_SERVER_ERROR]: 'Internal Server Error', | ||||
|   [HttpStatus.NOT_IMPLEMENTED]: 'Not Implemented', | ||||
|   [HttpStatus.BAD_GATEWAY]: 'Bad Gateway', | ||||
|   [HttpStatus.SERVICE_UNAVAILABLE]: 'Service Unavailable', | ||||
|   [HttpStatus.GATEWAY_TIMEOUT]: 'Gateway Timeout', | ||||
|   [HttpStatus.HTTP_VERSION_NOT_SUPPORTED]: 'HTTP Version Not Supported', | ||||
|   [HttpStatus.VARIANT_ALSO_NEGOTIATES]: 'Variant Also Negotiates', | ||||
|   [HttpStatus.INSUFFICIENT_STORAGE]: 'Insufficient Storage', | ||||
|   [HttpStatus.LOOP_DETECTED]: 'Loop Detected', | ||||
|   [HttpStatus.NOT_EXTENDED]: 'Not Extended', | ||||
|   [HttpStatus.NETWORK_AUTHENTICATION_REQUIRED]: 'Network Authentication Required', | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Common HTTP headers | ||||
|  */ | ||||
| export const HTTP_HEADERS = { | ||||
|   // Request headers | ||||
|   HOST: 'host', | ||||
|   USER_AGENT: 'user-agent', | ||||
|   ACCEPT: 'accept', | ||||
|   ACCEPT_LANGUAGE: 'accept-language', | ||||
|   ACCEPT_ENCODING: 'accept-encoding', | ||||
|   AUTHORIZATION: 'authorization', | ||||
|   CACHE_CONTROL: 'cache-control', | ||||
|   CONNECTION: 'connection', | ||||
|   CONTENT_TYPE: 'content-type', | ||||
|   CONTENT_LENGTH: 'content-length', | ||||
|   COOKIE: 'cookie', | ||||
|    | ||||
|   // Response headers | ||||
|   SET_COOKIE: 'set-cookie', | ||||
|   LOCATION: 'location', | ||||
|   SERVER: 'server', | ||||
|   DATE: 'date', | ||||
|   EXPIRES: 'expires', | ||||
|   LAST_MODIFIED: 'last-modified', | ||||
|   ETAG: 'etag', | ||||
|    | ||||
|   // CORS headers | ||||
|   ACCESS_CONTROL_ALLOW_ORIGIN: 'access-control-allow-origin', | ||||
|   ACCESS_CONTROL_ALLOW_METHODS: 'access-control-allow-methods', | ||||
|   ACCESS_CONTROL_ALLOW_HEADERS: 'access-control-allow-headers', | ||||
|    | ||||
|   // Security headers | ||||
|   STRICT_TRANSPORT_SECURITY: 'strict-transport-security', | ||||
|   X_CONTENT_TYPE_OPTIONS: 'x-content-type-options', | ||||
|   X_FRAME_OPTIONS: 'x-frame-options', | ||||
|   X_XSS_PROTECTION: 'x-xss-protection', | ||||
|   CONTENT_SECURITY_POLICY: 'content-security-policy', | ||||
| } as const; | ||||
|  | ||||
| /** | ||||
|  * Get HTTP status text | ||||
|  */ | ||||
| export function getStatusText(status: HttpStatus): string { | ||||
|   return HTTP_STATUS_TEXT[status] || 'Unknown'; | ||||
| } | ||||
							
								
								
									
										8
									
								
								ts/protocols/http/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/protocols/http/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /** | ||||
|  * HTTP Protocol Module | ||||
|  * Generic HTTP protocol knowledge and parsing utilities | ||||
|  */ | ||||
|  | ||||
| export * from './constants.js'; | ||||
| export * from './types.js'; | ||||
| export * from './parser.js'; | ||||
							
								
								
									
										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('&') : ''; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										70
									
								
								ts/protocols/http/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								ts/protocols/http/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| /** | ||||
|  * HTTP Protocol Type Definitions | ||||
|  */ | ||||
|  | ||||
| import type { THttpMethod, THttpVersion, HttpStatus } from './constants.js'; | ||||
|  | ||||
| /** | ||||
|  * HTTP request line structure | ||||
|  */ | ||||
| export interface IHttpRequestLine { | ||||
|   method: THttpMethod; | ||||
|   path: string; | ||||
|   version: THttpVersion; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP response line structure | ||||
|  */ | ||||
| export interface IHttpResponseLine { | ||||
|   version: THttpVersion; | ||||
|   status: HttpStatus; | ||||
|   statusText: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP header structure | ||||
|  */ | ||||
| export interface IHttpHeader { | ||||
|   name: string; | ||||
|   value: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP message structure (base for request and response) | ||||
|  */ | ||||
| export interface IHttpMessage { | ||||
|   headers: Record<string, string>; | ||||
|   body?: Buffer; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP request structure | ||||
|  */ | ||||
| export interface IHttpRequest extends IHttpMessage { | ||||
|   method: THttpMethod; | ||||
|   path: string; | ||||
|   version: THttpVersion; | ||||
|   query?: Record<string, string>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP response structure   | ||||
|  */ | ||||
| export interface IHttpResponse extends IHttpMessage { | ||||
|   status: HttpStatus; | ||||
|   statusText: string; | ||||
|   version: THttpVersion; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Parsed URL structure | ||||
|  */ | ||||
| export interface IParsedUrl { | ||||
|   protocol?: string; | ||||
|   hostname?: string; | ||||
|   port?: number; | ||||
|   path?: string; | ||||
|   query?: string; | ||||
|   fragment?: string; | ||||
| } | ||||
							
								
								
									
										11
									
								
								ts/protocols/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ts/protocols/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| /** | ||||
|  * Protocol-specific modules for smartproxy | ||||
|  *  | ||||
|  * This directory contains generic protocol knowledge separated from | ||||
|  * smartproxy-specific implementation details. | ||||
|  */ | ||||
|  | ||||
| export * as tls from './tls/index.js'; | ||||
| export * as http from './http/index.js'; | ||||
| export * as proxy from './proxy/index.js'; | ||||
| export * as websocket from './websocket/index.js'; | ||||
							
								
								
									
										7
									
								
								ts/protocols/proxy/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ts/protocols/proxy/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| /** | ||||
|  * PROXY Protocol Module | ||||
|  * HAProxy PROXY protocol implementation | ||||
|  */ | ||||
|  | ||||
| export * from './types.js'; | ||||
| export * from './parser.js'; | ||||
							
								
								
									
										183
									
								
								ts/protocols/proxy/parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								ts/protocols/proxy/parser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | ||||
| /** | ||||
|  * PROXY Protocol Parser | ||||
|  * Implementation of HAProxy PROXY protocol v1 (text format) | ||||
|  * Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt | ||||
|  */ | ||||
|  | ||||
| import type { IProxyInfo, IProxyParseResult, TProxyProtocol } from './types.js'; | ||||
|  | ||||
| /** | ||||
|  * PROXY protocol parser | ||||
|  */ | ||||
| export class ProxyProtocolParser { | ||||
|   static readonly PROXY_V1_SIGNATURE = 'PROXY '; | ||||
|   static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header | ||||
|   static readonly HEADER_TERMINATOR = '\r\n'; | ||||
|    | ||||
|   /** | ||||
|    * Parse PROXY protocol v1 header from buffer | ||||
|    * Returns proxy info and remaining data after header | ||||
|    */ | ||||
|   static parse(data: Buffer): IProxyParseResult { | ||||
|     // Check if buffer starts with PROXY signature | ||||
|     if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) { | ||||
|       return { | ||||
|         proxyInfo: null, | ||||
|         remainingData: data | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Find header terminator | ||||
|     const headerEndIndex = data.indexOf(this.HEADER_TERMINATOR); | ||||
|     if (headerEndIndex === -1) { | ||||
|       // Header incomplete, need more data | ||||
|       if (data.length > this.MAX_HEADER_LENGTH) { | ||||
|         // Header too long, invalid | ||||
|         throw new Error('PROXY protocol header exceeds maximum length'); | ||||
|       } | ||||
|       return { | ||||
|         proxyInfo: null, | ||||
|         remainingData: data | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Extract header line | ||||
|     const headerLine = data.toString('ascii', 0, headerEndIndex); | ||||
|     const remainingData = data.slice(headerEndIndex + 2); // Skip \r\n | ||||
|      | ||||
|     // Parse header | ||||
|     const parts = headerLine.split(' '); | ||||
|      | ||||
|     if (parts.length < 2) { | ||||
|       throw new Error(`Invalid PROXY protocol header format: ${headerLine}`); | ||||
|     } | ||||
|      | ||||
|     const [signature, protocol] = parts; | ||||
|      | ||||
|     // Validate protocol | ||||
|     if (!['TCP4', 'TCP6', 'UNKNOWN'].includes(protocol)) { | ||||
|       throw new Error(`Invalid PROXY protocol: ${protocol}`); | ||||
|     } | ||||
|      | ||||
|     // For UNKNOWN protocol, ignore addresses | ||||
|     if (protocol === 'UNKNOWN') { | ||||
|       return { | ||||
|         proxyInfo: { | ||||
|           protocol: 'UNKNOWN', | ||||
|           sourceIP: '', | ||||
|           sourcePort: 0, | ||||
|           destinationIP: '', | ||||
|           destinationPort: 0 | ||||
|         }, | ||||
|         remainingData | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // For TCP4/TCP6, we need all 6 parts | ||||
|     if (parts.length !== 6) { | ||||
|       throw new Error(`Invalid PROXY protocol header format: ${headerLine}`); | ||||
|     } | ||||
|      | ||||
|     const [, , srcIP, dstIP, srcPort, dstPort] = parts; | ||||
|      | ||||
|     // Validate and parse ports | ||||
|     const sourcePort = parseInt(srcPort, 10); | ||||
|     const destinationPort = parseInt(dstPort, 10); | ||||
|      | ||||
|     if (isNaN(sourcePort) || sourcePort < 0 || sourcePort > 65535) { | ||||
|       throw new Error(`Invalid source port: ${srcPort}`); | ||||
|     } | ||||
|      | ||||
|     if (isNaN(destinationPort) || destinationPort < 0 || destinationPort > 65535) { | ||||
|       throw new Error(`Invalid destination port: ${dstPort}`); | ||||
|     } | ||||
|      | ||||
|     // Validate IP addresses | ||||
|     const protocolType = protocol as TProxyProtocol; | ||||
|     if (!this.isValidIP(srcIP, protocolType)) { | ||||
|       throw new Error(`Invalid source IP for ${protocol}: ${srcIP}`); | ||||
|     } | ||||
|      | ||||
|     if (!this.isValidIP(dstIP, protocolType)) { | ||||
|       throw new Error(`Invalid destination IP for ${protocol}: ${dstIP}`); | ||||
|     } | ||||
|      | ||||
|     return { | ||||
|       proxyInfo: { | ||||
|         protocol: protocolType, | ||||
|         sourceIP: srcIP, | ||||
|         sourcePort, | ||||
|         destinationIP: dstIP, | ||||
|         destinationPort | ||||
|       }, | ||||
|       remainingData | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Generate PROXY protocol v1 header | ||||
|    */ | ||||
|   static generate(info: IProxyInfo): Buffer { | ||||
|     if (info.protocol === 'UNKNOWN') { | ||||
|       return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii'); | ||||
|     } | ||||
|      | ||||
|     const header = `PROXY ${info.protocol} ${info.sourceIP} ${info.destinationIP} ${info.sourcePort} ${info.destinationPort}\r\n`; | ||||
|      | ||||
|     if (header.length > this.MAX_HEADER_LENGTH) { | ||||
|       throw new Error('Generated PROXY protocol header exceeds maximum length'); | ||||
|     } | ||||
|      | ||||
|     return Buffer.from(header, 'ascii'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate IP address format | ||||
|    */ | ||||
|   static isValidIP(ip: string, protocol: TProxyProtocol): boolean { | ||||
|     if (protocol === 'TCP4') { | ||||
|       return this.isIPv4(ip); | ||||
|     } else if (protocol === 'TCP6') { | ||||
|       return this.isIPv6(ip); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if string is valid IPv4 | ||||
|    */ | ||||
|   static isIPv4(ip: string): boolean { | ||||
|     const parts = ip.split('.'); | ||||
|     if (parts.length !== 4) return false; | ||||
|      | ||||
|     for (const part of parts) { | ||||
|       const num = parseInt(part, 10); | ||||
|       if (isNaN(num) || num < 0 || num > 255 || part !== num.toString()) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if string is valid IPv6 | ||||
|    */ | ||||
|   static isIPv6(ip: string): boolean { | ||||
|     // Basic IPv6 validation | ||||
|     const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; | ||||
|     return ipv6Regex.test(ip); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create a connection ID string for tracking | ||||
|    */ | ||||
|   static createConnectionId(connectionInfo: { | ||||
|     sourceIp?: string; | ||||
|     sourcePort?: number; | ||||
|     destIp?: string; | ||||
|     destPort?: number; | ||||
|   }): string { | ||||
|     const { sourceIp, sourcePort, destIp, destPort } = connectionInfo; | ||||
|     return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										53
									
								
								ts/protocols/proxy/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ts/protocols/proxy/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| /** | ||||
|  * PROXY Protocol Type Definitions | ||||
|  * Based on HAProxy PROXY protocol specification | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * PROXY protocol version | ||||
|  */ | ||||
| export type TProxyProtocolVersion = 'v1' | 'v2'; | ||||
|  | ||||
| /** | ||||
|  * Connection protocol type | ||||
|  */ | ||||
| export type TProxyProtocol = 'TCP4' | 'TCP6' | 'UNKNOWN'; | ||||
|  | ||||
| /** | ||||
|  * Interface representing parsed PROXY protocol information | ||||
|  */ | ||||
| export interface IProxyInfo { | ||||
|   protocol: TProxyProtocol; | ||||
|   sourceIP: string; | ||||
|   sourcePort: number; | ||||
|   destinationIP: string; | ||||
|   destinationPort: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Interface for parse result including remaining data | ||||
|  */ | ||||
| export interface IProxyParseResult { | ||||
|   proxyInfo: IProxyInfo | null; | ||||
|   remainingData: Buffer; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * PROXY protocol v2 header format | ||||
|  */ | ||||
| export interface IProxyV2Header { | ||||
|   signature: Buffer; | ||||
|   versionCommand: number; | ||||
|   family: number; | ||||
|   length: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Connection information for PROXY protocol | ||||
|  */ | ||||
| export interface IProxyConnectionInfo { | ||||
|   sourceIp?: string; | ||||
|   sourcePort?: number; | ||||
|   destIp?: string; | ||||
|   destPort?: number; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import { TlsAlertLevel, TlsAlertDescription, TlsVersion } from '../utils/tls-utils.js'; | ||||
| 
 | ||||
| /** | ||||
							
								
								
									
										37
									
								
								ts/protocols/tls/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ts/protocols/tls/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| /** | ||||
|  * TLS Protocol Module | ||||
|  * Contains generic TLS protocol knowledge including parsers, constants, and utilities | ||||
|  */ | ||||
|  | ||||
| // Export all sub-modules | ||||
| export * from './alerts/index.js'; | ||||
| export * from './sni/index.js'; | ||||
| export * from './utils/index.js'; | ||||
|  | ||||
| // Re-export main utilities and types for convenience | ||||
| export {  | ||||
|   TlsUtils, | ||||
|   TlsRecordType, | ||||
|   TlsHandshakeType, | ||||
|   TlsExtensionType, | ||||
|   TlsAlertLevel, | ||||
|   TlsAlertDescription, | ||||
|   TlsVersion | ||||
| } from './utils/tls-utils.js'; | ||||
| export { TlsAlert } from './alerts/tls-alert.js'; | ||||
| export { ClientHelloParser } from './sni/client-hello-parser.js'; | ||||
| export { SniExtraction } from './sni/sni-extraction.js'; | ||||
|  | ||||
| // Export tlsVersionToString helper | ||||
| export function tlsVersionToString(major: number, minor: number): string | null { | ||||
|   if (major === 0x03) { | ||||
|     switch (minor) { | ||||
|       case 0x00: return 'SSLv3'; | ||||
|       case 0x01: return 'TLSv1.0'; | ||||
|       case 0x02: return 'TLSv1.1'; | ||||
|       case 0x03: return 'TLSv1.2'; | ||||
|       case 0x04: return 'TLSv1.3'; | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
							
								
								
									
										6
									
								
								ts/protocols/tls/sni/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ts/protocols/tls/sni/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| /** | ||||
|  * TLS SNI (Server Name Indication) protocol utilities | ||||
|  */ | ||||
|  | ||||
| export * from './client-hello-parser.js'; | ||||
| export * from './sni-extraction.js'; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| 
 | ||||
| /** | ||||
|  * TLS record types as defined in various RFCs | ||||
							
								
								
									
										60
									
								
								ts/protocols/websocket/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								ts/protocols/websocket/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| /** | ||||
|  * WebSocket Protocol Constants | ||||
|  * Based on RFC 6455 | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * WebSocket opcode types | ||||
|  */ | ||||
| export enum WebSocketOpcode { | ||||
|   CONTINUATION = 0x0, | ||||
|   TEXT = 0x1, | ||||
|   BINARY = 0x2, | ||||
|   CLOSE = 0x8, | ||||
|   PING = 0x9, | ||||
|   PONG = 0xa, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * WebSocket close codes | ||||
|  */ | ||||
| export enum WebSocketCloseCode { | ||||
|   NORMAL_CLOSURE = 1000, | ||||
|   GOING_AWAY = 1001, | ||||
|   PROTOCOL_ERROR = 1002, | ||||
|   UNSUPPORTED_DATA = 1003, | ||||
|   NO_STATUS_RECEIVED = 1005, | ||||
|   ABNORMAL_CLOSURE = 1006, | ||||
|   INVALID_FRAME_PAYLOAD_DATA = 1007, | ||||
|   POLICY_VIOLATION = 1008, | ||||
|   MESSAGE_TOO_BIG = 1009, | ||||
|   MISSING_EXTENSION = 1010, | ||||
|   INTERNAL_ERROR = 1011, | ||||
|   SERVICE_RESTART = 1012, | ||||
|   TRY_AGAIN_LATER = 1013, | ||||
|   BAD_GATEWAY = 1014, | ||||
|   TLS_HANDSHAKE = 1015, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * WebSocket protocol version | ||||
|  */ | ||||
| export const WEBSOCKET_VERSION = 13; | ||||
|  | ||||
| /** | ||||
|  * WebSocket magic string for handshake | ||||
|  */ | ||||
| export const WEBSOCKET_MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; | ||||
|  | ||||
| /** | ||||
|  * WebSocket headers | ||||
|  */ | ||||
| export const WEBSOCKET_HEADERS = { | ||||
|   UPGRADE: 'upgrade', | ||||
|   CONNECTION: 'connection', | ||||
|   SEC_WEBSOCKET_KEY: 'sec-websocket-key', | ||||
|   SEC_WEBSOCKET_VERSION: 'sec-websocket-version', | ||||
|   SEC_WEBSOCKET_ACCEPT: 'sec-websocket-accept', | ||||
|   SEC_WEBSOCKET_PROTOCOL: 'sec-websocket-protocol', | ||||
|   SEC_WEBSOCKET_EXTENSIONS: 'sec-websocket-extensions', | ||||
| } as const; | ||||
							
								
								
									
										8
									
								
								ts/protocols/websocket/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts/protocols/websocket/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /** | ||||
|  * WebSocket Protocol Module | ||||
|  * WebSocket protocol utilities and constants | ||||
|  */ | ||||
|  | ||||
| export * from './constants.js'; | ||||
| export * from './types.js'; | ||||
| export * from './utils.js'; | ||||
							
								
								
									
										53
									
								
								ts/protocols/websocket/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ts/protocols/websocket/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| /** | ||||
|  * WebSocket Protocol Type Definitions | ||||
|  */ | ||||
|  | ||||
| import type { WebSocketOpcode, WebSocketCloseCode } from './constants.js'; | ||||
|  | ||||
| /** | ||||
|  * WebSocket frame header | ||||
|  */ | ||||
| export interface IWebSocketFrameHeader { | ||||
|   fin: boolean; | ||||
|   rsv1: boolean; | ||||
|   rsv2: boolean; | ||||
|   rsv3: boolean; | ||||
|   opcode: WebSocketOpcode; | ||||
|   masked: boolean; | ||||
|   payloadLength: number; | ||||
|   maskingKey?: Buffer; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * WebSocket frame | ||||
|  */ | ||||
| export interface IWebSocketFrame { | ||||
|   header: IWebSocketFrameHeader; | ||||
|   payload: Buffer; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * WebSocket close frame payload | ||||
|  */ | ||||
| export interface IWebSocketClosePayload { | ||||
|   code: WebSocketCloseCode; | ||||
|   reason?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * WebSocket handshake request headers | ||||
|  */ | ||||
| export interface IWebSocketHandshakeHeaders { | ||||
|   upgrade: string; | ||||
|   connection: string; | ||||
|   'sec-websocket-key': string; | ||||
|   'sec-websocket-version': string; | ||||
|   'sec-websocket-protocol'?: string; | ||||
|   'sec-websocket-extensions'?: string; | ||||
|   [key: string]: string | undefined; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Type for WebSocket raw data (matching ws library) | ||||
|  */ | ||||
| export type RawData = Buffer | ArrayBuffer | Buffer[] | any; | ||||
							
								
								
									
										98
									
								
								ts/protocols/websocket/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								ts/protocols/websocket/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| /** | ||||
|  * WebSocket Protocol Utilities | ||||
|  */ | ||||
|  | ||||
| import * as crypto from 'crypto'; | ||||
| import { WEBSOCKET_MAGIC_STRING } from './constants.js'; | ||||
| import type { RawData } from './types.js'; | ||||
|  | ||||
| /** | ||||
|  * Get the length of a WebSocket message regardless of its type | ||||
|  * (handles all possible WebSocket message data types) | ||||
|  */ | ||||
| export function getMessageSize(data: RawData): number { | ||||
|   if (typeof data === 'string') { | ||||
|     // For string data, get the byte length | ||||
|     return Buffer.from(data, 'utf8').length; | ||||
|   } else if (data instanceof Buffer) { | ||||
|     // For Node.js Buffer | ||||
|     return data.length; | ||||
|   } else if (data instanceof ArrayBuffer) { | ||||
|     // For ArrayBuffer | ||||
|     return data.byteLength; | ||||
|   } else if (Array.isArray(data)) { | ||||
|     // For array of buffers, sum their lengths | ||||
|     return data.reduce((sum, chunk) => { | ||||
|       if (chunk instanceof Buffer) { | ||||
|         return sum + chunk.length; | ||||
|       } else if (chunk instanceof ArrayBuffer) { | ||||
|         return sum + chunk.byteLength; | ||||
|       } | ||||
|       return sum; | ||||
|     }, 0); | ||||
|   } else { | ||||
|     // For other types, try to determine the size or return 0 | ||||
|     try { | ||||
|       return Buffer.from(data).length; | ||||
|     } catch (e) { | ||||
|       return 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert any raw WebSocket data to Buffer for consistent handling | ||||
|  */ | ||||
| export function toBuffer(data: RawData): Buffer { | ||||
|   if (typeof data === 'string') { | ||||
|     return Buffer.from(data, 'utf8'); | ||||
|   } else if (data instanceof Buffer) { | ||||
|     return data; | ||||
|   } else if (data instanceof ArrayBuffer) { | ||||
|     return Buffer.from(data); | ||||
|   } else if (Array.isArray(data)) { | ||||
|     // For array of buffers, concatenate them | ||||
|     return Buffer.concat(data.map(chunk => { | ||||
|       if (chunk instanceof Buffer) { | ||||
|         return chunk; | ||||
|       } else if (chunk instanceof ArrayBuffer) { | ||||
|         return Buffer.from(chunk); | ||||
|       } | ||||
|       return Buffer.from(chunk); | ||||
|     })); | ||||
|   } else { | ||||
|     // For other types, try to convert to Buffer or return empty Buffer | ||||
|     try { | ||||
|       return Buffer.from(data); | ||||
|     } catch (e) { | ||||
|       return Buffer.alloc(0); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Generate WebSocket accept key from client key | ||||
|  */ | ||||
| export function generateAcceptKey(clientKey: string): string { | ||||
|   const hash = crypto.createHash('sha1'); | ||||
|   hash.update(clientKey + WEBSOCKET_MAGIC_STRING); | ||||
|   return hash.digest('base64'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Validate WebSocket upgrade request | ||||
|  */ | ||||
| export function isWebSocketUpgrade(headers: Record<string, string>): boolean { | ||||
|   const upgrade = headers['upgrade']; | ||||
|   const connection = headers['connection']; | ||||
|    | ||||
|   return upgrade?.toLowerCase() === 'websocket' &&  | ||||
|          connection?.toLowerCase().includes('upgrade'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Generate random WebSocket key for client handshake | ||||
|  */ | ||||
| export function generateWebSocketKey(): string { | ||||
|   return crypto.randomBytes(16).toString('base64'); | ||||
| } | ||||
| @@ -1,4 +1,6 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| // Import from protocols for consistent status codes | ||||
| import { HttpStatus as ProtocolHttpStatus, getStatusText as getProtocolStatusText } from '../../../protocols/http/index.js'; | ||||
|  | ||||
| /** | ||||
|  * HTTP-specific event types | ||||
| @@ -10,34 +12,33 @@ export enum HttpEvents { | ||||
|   REQUEST_ERROR = 'request-error', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP status codes as an enum for better type safety | ||||
|  */ | ||||
| export enum HttpStatus { | ||||
|   OK = 200, | ||||
|   MOVED_PERMANENTLY = 301, | ||||
|   FOUND = 302, | ||||
|   TEMPORARY_REDIRECT = 307, | ||||
|   PERMANENT_REDIRECT = 308, | ||||
|   BAD_REQUEST = 400, | ||||
|   UNAUTHORIZED = 401, | ||||
|   FORBIDDEN = 403, | ||||
|   NOT_FOUND = 404, | ||||
|   METHOD_NOT_ALLOWED = 405, | ||||
|   REQUEST_TIMEOUT = 408, | ||||
|   TOO_MANY_REQUESTS = 429, | ||||
|   INTERNAL_SERVER_ERROR = 500, | ||||
|   NOT_IMPLEMENTED = 501, | ||||
|   BAD_GATEWAY = 502, | ||||
|   SERVICE_UNAVAILABLE = 503, | ||||
|   GATEWAY_TIMEOUT = 504, | ||||
| } | ||||
|  | ||||
| // Re-export for backward compatibility with subset of commonly used codes | ||||
| export const HttpStatus = { | ||||
|   OK: ProtocolHttpStatus.OK, | ||||
|   MOVED_PERMANENTLY: ProtocolHttpStatus.MOVED_PERMANENTLY, | ||||
|   FOUND: ProtocolHttpStatus.FOUND, | ||||
|   TEMPORARY_REDIRECT: ProtocolHttpStatus.TEMPORARY_REDIRECT, | ||||
|   PERMANENT_REDIRECT: ProtocolHttpStatus.PERMANENT_REDIRECT, | ||||
|   BAD_REQUEST: ProtocolHttpStatus.BAD_REQUEST, | ||||
|   UNAUTHORIZED: ProtocolHttpStatus.UNAUTHORIZED, | ||||
|   FORBIDDEN: ProtocolHttpStatus.FORBIDDEN, | ||||
|   NOT_FOUND: ProtocolHttpStatus.NOT_FOUND, | ||||
|   METHOD_NOT_ALLOWED: ProtocolHttpStatus.METHOD_NOT_ALLOWED, | ||||
|   REQUEST_TIMEOUT: ProtocolHttpStatus.REQUEST_TIMEOUT, | ||||
|   TOO_MANY_REQUESTS: ProtocolHttpStatus.TOO_MANY_REQUESTS, | ||||
|   INTERNAL_SERVER_ERROR: ProtocolHttpStatus.INTERNAL_SERVER_ERROR, | ||||
|   NOT_IMPLEMENTED: ProtocolHttpStatus.NOT_IMPLEMENTED, | ||||
|   BAD_GATEWAY: ProtocolHttpStatus.BAD_GATEWAY, | ||||
|   SERVICE_UNAVAILABLE: ProtocolHttpStatus.SERVICE_UNAVAILABLE, | ||||
|   GATEWAY_TIMEOUT: ProtocolHttpStatus.GATEWAY_TIMEOUT, | ||||
| } as const; | ||||
|  | ||||
| /** | ||||
|  * Base error class for HTTP-related errors | ||||
|  */ | ||||
| export class HttpError extends Error { | ||||
|   constructor(message: string, public readonly statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR) { | ||||
|   constructor(message: string, public readonly statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR) { | ||||
|     super(message); | ||||
|     this.name = 'HttpError'; | ||||
|   } | ||||
| @@ -61,7 +62,7 @@ export class CertificateError extends HttpError { | ||||
|  * Error related to server operations | ||||
|  */ | ||||
| export class ServerError extends HttpError { | ||||
|   constructor(message: string, public readonly code?: string, statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR) { | ||||
|   constructor(message: string, public readonly code?: string, statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR) { | ||||
|     super(message, statusCode); | ||||
|     this.name = 'ServerError'; | ||||
|   } | ||||
| @@ -93,7 +94,7 @@ export class NotFoundError extends HttpError { | ||||
| export interface IRedirectConfig { | ||||
|   source: string;           // Source path or pattern | ||||
|   destination: string;      // Destination URL   | ||||
|   type: HttpStatus;         // Redirect status code | ||||
|   type: number;             // Redirect status code | ||||
|   preserveQuery?: boolean;  // Whether to preserve query parameters | ||||
| } | ||||
|  | ||||
| @@ -115,30 +116,12 @@ export interface IRouterConfig { | ||||
|  */ | ||||
| export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE'; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Helper function to get HTTP status text | ||||
|  */ | ||||
| export function getStatusText(status: HttpStatus): string { | ||||
|   const statusTexts: Record<HttpStatus, string> = { | ||||
|     [HttpStatus.OK]: 'OK', | ||||
|     [HttpStatus.MOVED_PERMANENTLY]: 'Moved Permanently', | ||||
|     [HttpStatus.FOUND]: 'Found', | ||||
|     [HttpStatus.TEMPORARY_REDIRECT]: 'Temporary Redirect', | ||||
|     [HttpStatus.PERMANENT_REDIRECT]: 'Permanent Redirect', | ||||
|     [HttpStatus.BAD_REQUEST]: 'Bad Request', | ||||
|     [HttpStatus.UNAUTHORIZED]: 'Unauthorized', | ||||
|     [HttpStatus.FORBIDDEN]: 'Forbidden', | ||||
|     [HttpStatus.NOT_FOUND]: 'Not Found', | ||||
|     [HttpStatus.METHOD_NOT_ALLOWED]: 'Method Not Allowed', | ||||
|     [HttpStatus.REQUEST_TIMEOUT]: 'Request Timeout', | ||||
|     [HttpStatus.TOO_MANY_REQUESTS]: 'Too Many Requests', | ||||
|     [HttpStatus.INTERNAL_SERVER_ERROR]: 'Internal Server Error', | ||||
|     [HttpStatus.NOT_IMPLEMENTED]: 'Not Implemented', | ||||
|     [HttpStatus.BAD_GATEWAY]: 'Bad Gateway', | ||||
|     [HttpStatus.SERVICE_UNAVAILABLE]: 'Service Unavailable', | ||||
|     [HttpStatus.GATEWAY_TIMEOUT]: 'Gateway Timeout', | ||||
|   }; | ||||
|   return statusTexts[status] || 'Unknown'; | ||||
| export function getStatusText(status: number): string { | ||||
|   return getProtocolStatusText(status as ProtocolHttpStatus); | ||||
| } | ||||
|  | ||||
| // Legacy interfaces for backward compatibility | ||||
|   | ||||
| @@ -1,22 +1,18 @@ | ||||
| /** | ||||
|  * TLS module providing SNI extraction, TLS alerts, and other TLS-related utilities | ||||
|  * TLS module for smartproxy | ||||
|  * Re-exports protocol components and provides smartproxy-specific functionality | ||||
|  */ | ||||
|  | ||||
| // Export TLS alert functionality | ||||
| export * from './alerts/tls-alert.js'; | ||||
| // Re-export all protocol components from protocols/tls | ||||
| export * from '../protocols/tls/index.js'; | ||||
|  | ||||
| // Export SNI handling | ||||
| // Export smartproxy-specific SNI handler | ||||
| export * from './sni/sni-handler.js'; | ||||
| export * from './sni/sni-extraction.js'; | ||||
| export * from './sni/client-hello-parser.js'; | ||||
|  | ||||
| // Export TLS utilities | ||||
| export * from './utils/tls-utils.js'; | ||||
|  | ||||
| // Create a namespace for SNI utilities | ||||
| import { SniHandler } from './sni/sni-handler.js'; | ||||
| import { SniExtraction } from './sni/sni-extraction.js'; | ||||
| import { ClientHelloParser } from './sni/client-hello-parser.js'; | ||||
| import { SniExtraction } from '../protocols/tls/sni/sni-extraction.js'; | ||||
| import { ClientHelloParser } from '../protocols/tls/sni/client-hello-parser.js'; | ||||
|  | ||||
| // Export utility objects for convenience | ||||
| export const SNI = { | ||||
| @@ -30,4 +26,4 @@ export const SNI = { | ||||
|   // Convenience functions | ||||
|   extractSNI: SniHandler.extractSNI, | ||||
|   processTlsPacket: SniHandler.processTlsPacket, | ||||
| }; | ||||
| }; | ||||
| @@ -4,15 +4,15 @@ import { | ||||
|   TlsHandshakeType, | ||||
|   TlsExtensionType, | ||||
|   TlsUtils | ||||
| } from '../utils/tls-utils.js'; | ||||
| } from '../../protocols/tls/utils/tls-utils.js'; | ||||
| import { | ||||
|   ClientHelloParser, | ||||
|   type LoggerFunction | ||||
| } from './client-hello-parser.js'; | ||||
| } from '../../protocols/tls/sni/client-hello-parser.js'; | ||||
| import { | ||||
|   SniExtraction, | ||||
|   type ConnectionInfo | ||||
| } from './sni-extraction.js'; | ||||
| } from '../../protocols/tls/sni/sni-extraction.js'; | ||||
|  | ||||
| /** | ||||
|  * SNI (Server Name Indication) handler for TLS connections. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user