feat(protocols): refactor protocol utilities into centralized protocols module
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiryDate": "2025-10-18T13:15:48.916Z",
|
"expiryDate": "2025-10-19T22:36:33.093Z",
|
||||||
"issueDate": "2025-07-20T13:15:48.916Z",
|
"issueDate": "2025-07-21T22:36:33.093Z",
|
||||||
"savedAt": "2025-07-20T13:15:48.916Z"
|
"savedAt": "2025-07-21T22:36:33.094Z"
|
||||||
}
|
}
|
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-07-22 - 21.0.0 - BREAKING_CHANGE(forwarding)
|
||||||
Remove legacy forwarding module
|
Remove legacy forwarding module
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "21.0.0",
|
"version": "21.1.0",
|
||||||
"private": false,
|
"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.",
|
"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",
|
"main": "dist_ts/index.js",
|
||||||
|
@@ -1,161 +1,44 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
import { ProxyProtocolParser as ProtocolParser, type IProxyInfo, type IProxyParseResult } from '../../protocols/proxy/index.js';
|
||||||
|
|
||||||
/**
|
// Re-export types from protocols for backward compatibility
|
||||||
* Interface representing parsed PROXY protocol information
|
export type { IProxyInfo, IProxyParseResult } from '../../protocols/proxy/index.js';
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parser for PROXY protocol v1 (text format)
|
* Parser for PROXY protocol v1 (text format)
|
||||||
* Spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
* 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 {
|
export class ProxyProtocolParser {
|
||||||
static readonly PROXY_V1_SIGNATURE = 'PROXY ';
|
static readonly PROXY_V1_SIGNATURE = ProtocolParser.PROXY_V1_SIGNATURE;
|
||||||
static readonly MAX_HEADER_LENGTH = 107; // Max length for v1 header
|
static readonly MAX_HEADER_LENGTH = ProtocolParser.MAX_HEADER_LENGTH;
|
||||||
static readonly HEADER_TERMINATOR = '\r\n';
|
static readonly HEADER_TERMINATOR = ProtocolParser.HEADER_TERMINATOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse PROXY protocol v1 header from buffer
|
* Parse PROXY protocol v1 header from buffer
|
||||||
* Returns proxy info and remaining data after header
|
* Returns proxy info and remaining data after header
|
||||||
*/
|
*/
|
||||||
static parse(data: Buffer): IProxyParseResult {
|
static parse(data: Buffer): IProxyParseResult {
|
||||||
// Check if buffer starts with PROXY signature
|
// Delegate to protocol parser
|
||||||
if (!data.toString('ascii', 0, 6).startsWith(this.PROXY_V1_SIGNATURE)) {
|
return ProtocolParser.parse(data);
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate PROXY protocol v1 header
|
* Generate PROXY protocol v1 header
|
||||||
*/
|
*/
|
||||||
static generate(info: IProxyInfo): Buffer {
|
static generate(info: IProxyInfo): Buffer {
|
||||||
if (info.protocol === 'UNKNOWN') {
|
// Delegate to protocol parser
|
||||||
return Buffer.from(`PROXY UNKNOWN\r\n`, 'ascii');
|
return ProtocolParser.generate(info);
|
||||||
}
|
|
||||||
|
|
||||||
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
|
* Validate IP address format
|
||||||
*/
|
*/
|
||||||
private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean {
|
private static isValidIP(ip: string, protocol: 'TCP4' | 'TCP6' | 'UNKNOWN'): boolean {
|
||||||
if (protocol === 'TCP4') {
|
return ProtocolParser.isValidIP(ip, protocol);
|
||||||
return plugins.net.isIPv4(ip);
|
|
||||||
} else if (protocol === 'TCP6') {
|
|
||||||
return plugins.net.isIPv6(ip);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* WebSocket utility functions
|
* WebSocket utility functions
|
||||||
|
*
|
||||||
|
* This module provides smartproxy-specific WebSocket utilities
|
||||||
|
* and re-exports protocol utilities from the protocols module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
// Import and re-export from protocols
|
||||||
* Type for WebSocket RawData that can be different types in different environments
|
import { getMessageSize as protocolGetMessageSize, toBuffer as protocolToBuffer } from '../../protocols/websocket/index.js';
|
||||||
* This matches the ws library's type definition
|
export type { RawData } from '../../protocols/websocket/index.js';
|
||||||
*/
|
|
||||||
export type RawData = Buffer | ArrayBuffer | Buffer[] | any;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the length of a WebSocket message regardless of its type
|
* 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)
|
* @param data - The data message from WebSocket (could be any RawData type)
|
||||||
* @returns The length of the data in bytes
|
* @returns The length of the data in bytes
|
||||||
*/
|
*/
|
||||||
export function getMessageSize(data: RawData): number {
|
export function getMessageSize(data: import('../../protocols/websocket/index.js').RawData): number {
|
||||||
if (typeof data === 'string') {
|
// Delegate to protocol implementation
|
||||||
// For string data, get the byte length
|
return protocolGetMessageSize(data);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,30 +27,7 @@ export function getMessageSize(data: RawData): number {
|
|||||||
* @param data - The data message from WebSocket (could be any RawData type)
|
* @param data - The data message from WebSocket (could be any RawData type)
|
||||||
* @returns A Buffer containing the data
|
* @returns A Buffer containing the data
|
||||||
*/
|
*/
|
||||||
export function toBuffer(data: RawData): Buffer {
|
export function toBuffer(data: import('../../protocols/websocket/index.js').RawData): Buffer {
|
||||||
if (typeof data === 'string') {
|
// Delegate to protocol implementation
|
||||||
return Buffer.from(data, 'utf8');
|
return protocolToBuffer(data);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -8,10 +8,12 @@ import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../mo
|
|||||||
import { readUInt16BE, readUInt24BE, BufferAccumulator } from '../utils/buffer-utils.js';
|
import { readUInt16BE, readUInt24BE, BufferAccumulator } from '../utils/buffer-utils.js';
|
||||||
import { tlsVersionToString } from '../utils/parser-utils.js';
|
import { tlsVersionToString } from '../utils/parser-utils.js';
|
||||||
|
|
||||||
// Import existing TLS utilities
|
// Import from protocols
|
||||||
import { TlsUtils, TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../tls/utils/tls-utils.js';
|
import { TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../protocols/tls/index.js';
|
||||||
import { SniExtraction } from '../../tls/sni/sni-extraction.js';
|
|
||||||
import { ClientHelloParser } from '../../tls/sni/client-hello-parser.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
|
* TLS detector implementation
|
||||||
@@ -92,7 +94,7 @@ export class TlsDetector implements IProtocolDetector {
|
|||||||
protocol: 'tls',
|
protocol: 'tls',
|
||||||
connectionInfo,
|
connectionInfo,
|
||||||
remainingBuffer: buffer.length > totalRecordLength
|
remainingBuffer: buffer.length > totalRecordLength
|
||||||
? buffer.slice(totalRecordLength)
|
? buffer.subarray(totalRecordLength)
|
||||||
: undefined,
|
: undefined,
|
||||||
isComplete: true
|
isComplete: true
|
||||||
};
|
};
|
||||||
@@ -114,7 +116,7 @@ export class TlsDetector implements IProtocolDetector {
|
|||||||
connectionInfo,
|
connectionInfo,
|
||||||
isComplete: true,
|
isComplete: true,
|
||||||
remainingBuffer: buffer.length > recordLength + 5
|
remainingBuffer: buffer.length > recordLength + 5
|
||||||
? buffer.slice(recordLength + 5)
|
? buffer.subarray(recordLength + 5)
|
||||||
: undefined
|
: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -185,7 +187,7 @@ export class TlsDetector implements IProtocolDetector {
|
|||||||
offset++;
|
offset++;
|
||||||
|
|
||||||
if (offset + protoLength <= data.length) {
|
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);
|
protocols.push(protocol);
|
||||||
offset += protoLength;
|
offset += protoLength;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -2,6 +2,9 @@
|
|||||||
* Buffer manipulation utilities for protocol detection
|
* Buffer manipulation utilities for protocol detection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Import from protocols
|
||||||
|
import { HttpParser } from '../../protocols/http/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BufferAccumulator class for handling fragmented data
|
* 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)
|
* Extract a line from buffer (up to CRLF or LF)
|
||||||
*/
|
*/
|
||||||
export function extractLine(buffer: Buffer, startOffset = 0): { line: string; nextOffset: number } | null {
|
export function extractLine(buffer: Buffer, startOffset = 0): { line: string; nextOffset: number } | null {
|
||||||
let lineEnd = -1;
|
// Delegate to protocol parser
|
||||||
let skipBytes = 1;
|
return HttpParser.extractLine(buffer, startOffset);
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,17 +136,6 @@ export function safeSlice(buffer: Buffer, start: number, end?: number): Buffer {
|
|||||||
* Check if buffer contains printable ASCII
|
* Check if buffer contains printable ASCII
|
||||||
*/
|
*/
|
||||||
export function isPrintableAscii(buffer: Buffer, length?: number): boolean {
|
export function isPrintableAscii(buffer: Buffer, length?: number): boolean {
|
||||||
const checkLength = length || buffer.length;
|
// Delegate to protocol parser
|
||||||
|
return HttpParser.isPrintableAscii(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;
|
|
||||||
}
|
}
|
@@ -1,20 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Parser utilities for protocol detection
|
* Parser utilities for protocol detection
|
||||||
|
* Now delegates to protocol modules for actual parsing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { THttpMethod, TTlsVersion } from '../models/detection-types.js';
|
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';
|
||||||
|
|
||||||
/**
|
// Re-export constants for backward compatibility
|
||||||
* Valid HTTP methods
|
export { HTTP_METHODS, HTTP_VERSIONS };
|
||||||
*/
|
|
||||||
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'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse HTTP request line
|
* Parse HTTP request line
|
||||||
@@ -24,118 +18,60 @@ export function parseHttpRequestLine(line: string): {
|
|||||||
path: string;
|
path: string;
|
||||||
version: string;
|
version: string;
|
||||||
} | null {
|
} | null {
|
||||||
const parts = line.trim().split(' ');
|
// Delegate to protocol parser
|
||||||
|
const result = HttpParser.parseRequestLine(line);
|
||||||
if (parts.length !== 3) {
|
return result ? {
|
||||||
return null;
|
method: result.method as THttpMethod,
|
||||||
}
|
path: result.path,
|
||||||
|
version: result.version
|
||||||
const [method, path, version] = parts;
|
} : null;
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse HTTP header line
|
* Parse HTTP header line
|
||||||
*/
|
*/
|
||||||
export function parseHttpHeader(line: string): { name: string; value: string } | null {
|
export function parseHttpHeader(line: string): { name: string; value: string } | null {
|
||||||
const colonIndex = line.indexOf(':');
|
// Delegate to protocol parser
|
||||||
|
return HttpParser.parseHeaderLine(line);
|
||||||
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
|
* Parse HTTP headers from lines
|
||||||
*/
|
*/
|
||||||
export function parseHttpHeaders(lines: string[]): Record<string, string> {
|
export function parseHttpHeaders(lines: string[]): Record<string, string> {
|
||||||
const headers: Record<string, string> = {};
|
// Delegate to protocol parser
|
||||||
|
return HttpParser.parseHeaders(lines);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert TLS version bytes to version string
|
* Convert TLS version bytes to version string
|
||||||
*/
|
*/
|
||||||
export function tlsVersionToString(major: number, minor: number): TTlsVersion | null {
|
export function tlsVersionToString(major: number, minor: number): TTlsVersion | null {
|
||||||
if (major === 0x03) {
|
// Delegate to protocol parser
|
||||||
switch (minor) {
|
return protocolTlsVersionToString(major, minor) as TTlsVersion;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract domain from Host header value
|
* Extract domain from Host header value
|
||||||
*/
|
*/
|
||||||
export function extractDomainFromHost(hostHeader: string): string {
|
export function extractDomainFromHost(hostHeader: string): string {
|
||||||
// Remove port if present
|
// Delegate to protocol parser
|
||||||
const colonIndex = hostHeader.lastIndexOf(':');
|
return HttpParser.extractDomainFromHost(hostHeader);
|
||||||
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
|
* Validate domain name
|
||||||
*/
|
*/
|
||||||
export function isValidDomain(domain: string): boolean {
|
export function isValidDomain(domain: string): boolean {
|
||||||
// Basic domain validation
|
// Delegate to protocol parser
|
||||||
if (!domain || domain.length > 253) {
|
return HttpParser.isValidDomain(domain);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if string is a valid HTTP method
|
* Check if string is a valid HTTP method
|
||||||
*/
|
*/
|
||||||
export function isHttpMethod(str: string): str is THttpMethod {
|
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
|
// Certificate module has been removed - use SmartCertManager instead
|
||||||
export * as tls from './tls/index.js';
|
export * as tls from './tls/index.js';
|
||||||
export * as routing from './routing/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';
|
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
|
* 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 * 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
|
* HTTP-specific event types
|
||||||
@@ -10,34 +12,33 @@ export enum HttpEvents {
|
|||||||
REQUEST_ERROR = 'request-error',
|
REQUEST_ERROR = 'request-error',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP status codes as an enum for better type safety
|
// Re-export for backward compatibility with subset of commonly used codes
|
||||||
*/
|
export const HttpStatus = {
|
||||||
export enum HttpStatus {
|
OK: ProtocolHttpStatus.OK,
|
||||||
OK = 200,
|
MOVED_PERMANENTLY: ProtocolHttpStatus.MOVED_PERMANENTLY,
|
||||||
MOVED_PERMANENTLY = 301,
|
FOUND: ProtocolHttpStatus.FOUND,
|
||||||
FOUND = 302,
|
TEMPORARY_REDIRECT: ProtocolHttpStatus.TEMPORARY_REDIRECT,
|
||||||
TEMPORARY_REDIRECT = 307,
|
PERMANENT_REDIRECT: ProtocolHttpStatus.PERMANENT_REDIRECT,
|
||||||
PERMANENT_REDIRECT = 308,
|
BAD_REQUEST: ProtocolHttpStatus.BAD_REQUEST,
|
||||||
BAD_REQUEST = 400,
|
UNAUTHORIZED: ProtocolHttpStatus.UNAUTHORIZED,
|
||||||
UNAUTHORIZED = 401,
|
FORBIDDEN: ProtocolHttpStatus.FORBIDDEN,
|
||||||
FORBIDDEN = 403,
|
NOT_FOUND: ProtocolHttpStatus.NOT_FOUND,
|
||||||
NOT_FOUND = 404,
|
METHOD_NOT_ALLOWED: ProtocolHttpStatus.METHOD_NOT_ALLOWED,
|
||||||
METHOD_NOT_ALLOWED = 405,
|
REQUEST_TIMEOUT: ProtocolHttpStatus.REQUEST_TIMEOUT,
|
||||||
REQUEST_TIMEOUT = 408,
|
TOO_MANY_REQUESTS: ProtocolHttpStatus.TOO_MANY_REQUESTS,
|
||||||
TOO_MANY_REQUESTS = 429,
|
INTERNAL_SERVER_ERROR: ProtocolHttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
INTERNAL_SERVER_ERROR = 500,
|
NOT_IMPLEMENTED: ProtocolHttpStatus.NOT_IMPLEMENTED,
|
||||||
NOT_IMPLEMENTED = 501,
|
BAD_GATEWAY: ProtocolHttpStatus.BAD_GATEWAY,
|
||||||
BAD_GATEWAY = 502,
|
SERVICE_UNAVAILABLE: ProtocolHttpStatus.SERVICE_UNAVAILABLE,
|
||||||
SERVICE_UNAVAILABLE = 503,
|
GATEWAY_TIMEOUT: ProtocolHttpStatus.GATEWAY_TIMEOUT,
|
||||||
GATEWAY_TIMEOUT = 504,
|
} as const;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base error class for HTTP-related errors
|
* Base error class for HTTP-related errors
|
||||||
*/
|
*/
|
||||||
export class HttpError extends Error {
|
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);
|
super(message);
|
||||||
this.name = 'HttpError';
|
this.name = 'HttpError';
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,7 @@ export class CertificateError extends HttpError {
|
|||||||
* Error related to server operations
|
* Error related to server operations
|
||||||
*/
|
*/
|
||||||
export class ServerError extends HttpError {
|
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);
|
super(message, statusCode);
|
||||||
this.name = 'ServerError';
|
this.name = 'ServerError';
|
||||||
}
|
}
|
||||||
@@ -93,7 +94,7 @@ export class NotFoundError extends HttpError {
|
|||||||
export interface IRedirectConfig {
|
export interface IRedirectConfig {
|
||||||
source: string; // Source path or pattern
|
source: string; // Source path or pattern
|
||||||
destination: string; // Destination URL
|
destination: string; // Destination URL
|
||||||
type: HttpStatus; // Redirect status code
|
type: number; // Redirect status code
|
||||||
preserveQuery?: boolean; // Whether to preserve query parameters
|
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';
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to get HTTP status text
|
* Helper function to get HTTP status text
|
||||||
*/
|
*/
|
||||||
export function getStatusText(status: HttpStatus): string {
|
export function getStatusText(status: number): string {
|
||||||
const statusTexts: Record<HttpStatus, string> = {
|
return getProtocolStatusText(status as ProtocolHttpStatus);
|
||||||
[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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy interfaces for backward compatibility
|
// 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
|
// Re-export all protocol components from protocols/tls
|
||||||
export * from './alerts/tls-alert.js';
|
export * from '../protocols/tls/index.js';
|
||||||
|
|
||||||
// Export SNI handling
|
// Export smartproxy-specific SNI handler
|
||||||
export * from './sni/sni-handler.js';
|
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
|
// Create a namespace for SNI utilities
|
||||||
import { SniHandler } from './sni/sni-handler.js';
|
import { SniHandler } from './sni/sni-handler.js';
|
||||||
import { SniExtraction } from './sni/sni-extraction.js';
|
import { SniExtraction } from '../protocols/tls/sni/sni-extraction.js';
|
||||||
import { ClientHelloParser } from './sni/client-hello-parser.js';
|
import { ClientHelloParser } from '../protocols/tls/sni/client-hello-parser.js';
|
||||||
|
|
||||||
// Export utility objects for convenience
|
// Export utility objects for convenience
|
||||||
export const SNI = {
|
export const SNI = {
|
||||||
@@ -30,4 +26,4 @@ export const SNI = {
|
|||||||
// Convenience functions
|
// Convenience functions
|
||||||
extractSNI: SniHandler.extractSNI,
|
extractSNI: SniHandler.extractSNI,
|
||||||
processTlsPacket: SniHandler.processTlsPacket,
|
processTlsPacket: SniHandler.processTlsPacket,
|
||||||
};
|
};
|
@@ -4,15 +4,15 @@ import {
|
|||||||
TlsHandshakeType,
|
TlsHandshakeType,
|
||||||
TlsExtensionType,
|
TlsExtensionType,
|
||||||
TlsUtils
|
TlsUtils
|
||||||
} from '../utils/tls-utils.js';
|
} from '../../protocols/tls/utils/tls-utils.js';
|
||||||
import {
|
import {
|
||||||
ClientHelloParser,
|
ClientHelloParser,
|
||||||
type LoggerFunction
|
type LoggerFunction
|
||||||
} from './client-hello-parser.js';
|
} from '../../protocols/tls/sni/client-hello-parser.js';
|
||||||
import {
|
import {
|
||||||
SniExtraction,
|
SniExtraction,
|
||||||
type ConnectionInfo
|
type ConnectionInfo
|
||||||
} from './sni-extraction.js';
|
} from '../../protocols/tls/sni/sni-extraction.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SNI (Server Name Indication) handler for TLS connections.
|
* SNI (Server Name Indication) handler for TLS connections.
|
||||||
|
Reference in New Issue
Block a user