183 lines
5.6 KiB
TypeScript
183 lines
5.6 KiB
TypeScript
![]() |
/**
|
||
|
* 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}`;
|
||
|
}
|
||
|
}
|