diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index 84e9574..99eaa7e 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -1,5 +1,5 @@ { - "expiryDate": "2025-10-18T13:15:48.916Z", - "issueDate": "2025-07-20T13:15:48.916Z", - "savedAt": "2025-07-20T13:15:48.916Z" + "expiryDate": "2025-10-19T22:36:33.093Z", + "issueDate": "2025-07-21T22:36:33.093Z", + "savedAt": "2025-07-21T22:36:33.094Z" } \ No newline at end of file diff --git a/changelog.md b/changelog.md index fb87b87..640ad3a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-07-21 - 21.1.0 - feat(protocols) +Refactor protocol utilities into centralized protocols module + +- Moved TLS utilities from `ts/tls/` to `ts/protocols/tls/` +- Created centralized protocol modules for HTTP, WebSocket, Proxy, and TLS +- Core utilities now delegate to protocol modules for parsing and utilities +- Maintains backward compatibility through re-exports in original locations +- Improves code organization and separation of concerns + ## 2025-07-22 - 21.0.0 - BREAKING_CHANGE(forwarding) Remove legacy forwarding module diff --git a/package.json b/package.json index fe65893..5408f81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@push.rocks/smartproxy", - "version": "21.0.0", + "version": "21.1.0", "private": false, "description": "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.", "main": "dist_ts/index.js", diff --git a/ts/core/utils/proxy-protocol.ts b/ts/core/utils/proxy-protocol.ts index 7fa2141..7652895 100644 --- a/ts/core/utils/proxy-protocol.ts +++ b/ts/core/utils/proxy-protocol.ts @@ -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); } /** diff --git a/ts/core/utils/websocket-utils.ts b/ts/core/utils/websocket-utils.ts index aedfadf..93e5bed 100644 --- a/ts/core/utils/websocket-utils.ts +++ b/ts/core/utils/websocket-utils.ts @@ -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); } \ No newline at end of file diff --git a/ts/detection/detectors/tls-detector.ts b/ts/detection/detectors/tls-detector.ts index 05e1fdd..1ddfe39 100644 --- a/ts/detection/detectors/tls-detector.ts +++ b/ts/detection/detectors/tls-detector.ts @@ -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 { diff --git a/ts/detection/utils/buffer-utils.ts b/ts/detection/utils/buffer-utils.ts index 722990d..880a5f7 100644 --- a/ts/detection/utils/buffer-utils.ts +++ b/ts/detection/utils/buffer-utils.ts @@ -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); } \ No newline at end of file diff --git a/ts/detection/utils/parser-utils.ts b/ts/detection/utils/parser-utils.ts index d381ca1..b2887ee 100644 --- a/ts/detection/utils/parser-utils.ts +++ b/ts/detection/utils/parser-utils.ts @@ -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 { - const headers: Record = {}; - - 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}(? = { + // 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'; +} \ No newline at end of file diff --git a/ts/protocols/http/index.ts b/ts/protocols/http/index.ts new file mode 100644 index 0000000..8f602ff --- /dev/null +++ b/ts/protocols/http/index.ts @@ -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'; \ No newline at end of file diff --git a/ts/protocols/http/parser.ts b/ts/protocols/http/parser.ts new file mode 100644 index 0000000..22aa3bf --- /dev/null +++ b/ts/protocols/http/parser.ts @@ -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 { + 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('&') : ''; + } +} \ No newline at end of file diff --git a/ts/protocols/http/types.ts b/ts/protocols/http/types.ts new file mode 100644 index 0000000..91c342f --- /dev/null +++ b/ts/protocols/http/types.ts @@ -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; + body?: Buffer; +} + +/** + * HTTP request structure + */ +export interface IHttpRequest extends IHttpMessage { + method: THttpMethod; + path: string; + version: THttpVersion; + query?: Record; +} + +/** + * 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; +} \ No newline at end of file diff --git a/ts/protocols/index.ts b/ts/protocols/index.ts new file mode 100644 index 0000000..80125c3 --- /dev/null +++ b/ts/protocols/index.ts @@ -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'; \ No newline at end of file diff --git a/ts/protocols/proxy/index.ts b/ts/protocols/proxy/index.ts new file mode 100644 index 0000000..cfd20cd --- /dev/null +++ b/ts/protocols/proxy/index.ts @@ -0,0 +1,7 @@ +/** + * PROXY Protocol Module + * HAProxy PROXY protocol implementation + */ + +export * from './types.js'; +export * from './parser.js'; \ No newline at end of file diff --git a/ts/protocols/proxy/parser.ts b/ts/protocols/proxy/parser.ts new file mode 100644 index 0000000..61bd36d --- /dev/null +++ b/ts/protocols/proxy/parser.ts @@ -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}`; + } +} \ No newline at end of file diff --git a/ts/protocols/proxy/types.ts b/ts/protocols/proxy/types.ts new file mode 100644 index 0000000..5d726a1 --- /dev/null +++ b/ts/protocols/proxy/types.ts @@ -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; +} \ No newline at end of file diff --git a/ts/tls/alerts/index.ts b/ts/protocols/tls/alerts/index.ts similarity index 100% rename from ts/tls/alerts/index.ts rename to ts/protocols/tls/alerts/index.ts diff --git a/ts/tls/alerts/tls-alert.ts b/ts/protocols/tls/alerts/tls-alert.ts similarity index 99% rename from ts/tls/alerts/tls-alert.ts rename to ts/protocols/tls/alerts/tls-alert.ts index d196713..aac4d84 100644 --- a/ts/tls/alerts/tls-alert.ts +++ b/ts/protocols/tls/alerts/tls-alert.ts @@ -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'; /** diff --git a/ts/protocols/tls/index.ts b/ts/protocols/tls/index.ts new file mode 100644 index 0000000..55e2341 --- /dev/null +++ b/ts/protocols/tls/index.ts @@ -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; +} \ No newline at end of file diff --git a/ts/tls/sni/client-hello-parser.ts b/ts/protocols/tls/sni/client-hello-parser.ts similarity index 100% rename from ts/tls/sni/client-hello-parser.ts rename to ts/protocols/tls/sni/client-hello-parser.ts diff --git a/ts/protocols/tls/sni/index.ts b/ts/protocols/tls/sni/index.ts new file mode 100644 index 0000000..896f329 --- /dev/null +++ b/ts/protocols/tls/sni/index.ts @@ -0,0 +1,6 @@ +/** + * TLS SNI (Server Name Indication) protocol utilities + */ + +export * from './client-hello-parser.js'; +export * from './sni-extraction.js'; \ No newline at end of file diff --git a/ts/tls/sni/sni-extraction.ts b/ts/protocols/tls/sni/sni-extraction.ts similarity index 100% rename from ts/tls/sni/sni-extraction.ts rename to ts/protocols/tls/sni/sni-extraction.ts diff --git a/ts/tls/utils/index.ts b/ts/protocols/tls/utils/index.ts similarity index 100% rename from ts/tls/utils/index.ts rename to ts/protocols/tls/utils/index.ts diff --git a/ts/tls/utils/tls-utils.ts b/ts/protocols/tls/utils/tls-utils.ts similarity index 99% rename from ts/tls/utils/tls-utils.ts rename to ts/protocols/tls/utils/tls-utils.ts index f6bc760..25c1cb0 100644 --- a/ts/tls/utils/tls-utils.ts +++ b/ts/protocols/tls/utils/tls-utils.ts @@ -1,4 +1,4 @@ -import * as plugins from '../../plugins.js'; +import * as plugins from '../../../plugins.js'; /** * TLS record types as defined in various RFCs diff --git a/ts/protocols/websocket/constants.ts b/ts/protocols/websocket/constants.ts new file mode 100644 index 0000000..bd30728 --- /dev/null +++ b/ts/protocols/websocket/constants.ts @@ -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; \ No newline at end of file diff --git a/ts/protocols/websocket/index.ts b/ts/protocols/websocket/index.ts new file mode 100644 index 0000000..bc28921 --- /dev/null +++ b/ts/protocols/websocket/index.ts @@ -0,0 +1,8 @@ +/** + * WebSocket Protocol Module + * WebSocket protocol utilities and constants + */ + +export * from './constants.js'; +export * from './types.js'; +export * from './utils.js'; \ No newline at end of file diff --git a/ts/protocols/websocket/types.ts b/ts/protocols/websocket/types.ts new file mode 100644 index 0000000..36336b3 --- /dev/null +++ b/ts/protocols/websocket/types.ts @@ -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; \ No newline at end of file diff --git a/ts/protocols/websocket/utils.ts b/ts/protocols/websocket/utils.ts new file mode 100644 index 0000000..ee26701 --- /dev/null +++ b/ts/protocols/websocket/utils.ts @@ -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): 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'); +} \ No newline at end of file diff --git a/ts/proxies/http-proxy/models/http-types.ts b/ts/proxies/http-proxy/models/http-types.ts index e74e217..8dffd50 100644 --- a/ts/proxies/http-proxy/models/http-types.ts +++ b/ts/proxies/http-proxy/models/http-types.ts @@ -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.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 diff --git a/ts/tls/index.ts b/ts/tls/index.ts index 0f1e9ca..1eb8e9b 100644 --- a/ts/tls/index.ts +++ b/ts/tls/index.ts @@ -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, -}; +}; \ No newline at end of file diff --git a/ts/tls/sni/sni-handler.ts b/ts/tls/sni/sni-handler.ts index 49d9feb..6bd1009 100644 --- a/ts/tls/sni/sni-handler.ts +++ b/ts/tls/sni/sni-handler.ts @@ -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.