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); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user