@push.rocks/smartproxy
Version:
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.
464 lines • 40.3 kB
JavaScript
import { Buffer } from 'buffer';
import { TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../protocols/tls/index.js';
import { TlsUtils } from '../utils/tls-utils.js';
/**
* Class for parsing TLS ClientHello messages
*/
export class ClientHelloParser {
// Buffer for handling fragmented ClientHello messages
static { this.fragmentedBuffers = new Map(); }
static { this.fragmentTimeout = 1000; } // ms to wait for fragments before cleanup
/**
* Clean up expired fragments
*/
static cleanupExpiredFragments() {
const now = Date.now();
for (const [connectionId, info] of this.fragmentedBuffers.entries()) {
if (now - info.timestamp > this.fragmentTimeout) {
this.fragmentedBuffers.delete(connectionId);
}
}
}
/**
* Handles potential fragmented ClientHello messages by buffering and reassembling
* TLS record fragments that might span multiple TCP packets.
*
* @param buffer The current buffer fragment
* @param connectionId Unique identifier for the connection
* @param logger Optional logging function
* @returns A complete buffer if reassembly is successful, or undefined if more fragments are needed
*/
static handleFragmentedClientHello(buffer, connectionId, logger) {
const log = logger || (() => { });
// Periodically clean up expired fragments
this.cleanupExpiredFragments();
// Check if we've seen this connection before
if (!this.fragmentedBuffers.has(connectionId)) {
// New connection, start with this buffer
this.fragmentedBuffers.set(connectionId, {
buffer,
timestamp: Date.now(),
connectionId
});
// Evaluate if this buffer already contains a complete ClientHello
try {
if (buffer.length >= 5) {
// Get the record length from TLS header
const recordLength = (buffer[3] << 8) + buffer[4] + 5; // +5 for the TLS record header itself
log(`Initial buffer size: ${buffer.length}, expected record length: ${recordLength}`);
// Check if this buffer already contains a complete TLS record
if (buffer.length >= recordLength) {
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
return buffer;
}
}
else {
log(`Initial buffer too small (${buffer.length} bytes), needs at least 5 bytes for TLS header`);
}
}
catch (e) {
log(`Error checking initial buffer completeness: ${e}`);
}
log(`Started buffering connection ${connectionId}, initial size: ${buffer.length}`);
return undefined; // Need more fragments
}
else {
// Existing connection, append this buffer
const existingInfo = this.fragmentedBuffers.get(connectionId);
const newBuffer = Buffer.concat([existingInfo.buffer, buffer]);
// Update the buffer and timestamp
this.fragmentedBuffers.set(connectionId, {
...existingInfo,
buffer: newBuffer,
timestamp: Date.now()
});
log(`Appended to buffer for ${connectionId}, new size: ${newBuffer.length}`);
// Check if we now have a complete ClientHello
try {
if (newBuffer.length >= 5) {
// Get the record length from TLS header
const recordLength = (newBuffer[3] << 8) + newBuffer[4] + 5; // +5 for the TLS record header itself
log(`Reassembled buffer size: ${newBuffer.length}, expected record length: ${recordLength}`);
// Check if we have a complete TLS record now
if (newBuffer.length >= recordLength) {
log(`Assembled complete ClientHello, length: ${newBuffer.length}, needed: ${recordLength}`);
// Extract the complete TLS record (might be followed by more data)
const completeRecord = newBuffer.slice(0, recordLength);
// Check if this record is indeed a ClientHello (type 1) at position 5
if (completeRecord.length > 5 &&
completeRecord[5] === TlsHandshakeType.CLIENT_HELLO) {
log(`Verified record is a ClientHello handshake message`);
// Complete message received, remove from tracking
this.fragmentedBuffers.delete(connectionId);
return completeRecord;
}
else {
log(`Record is complete but not a ClientHello handshake, continuing to buffer`);
// This might be another TLS record type preceding the ClientHello
// Try checking for a ClientHello starting at the end of this record
if (newBuffer.length > recordLength + 5) {
const nextRecordType = newBuffer[recordLength];
log(`Next record type: ${nextRecordType} (looking for ${TlsRecordType.HANDSHAKE})`);
if (nextRecordType === TlsRecordType.HANDSHAKE) {
const handshakeType = newBuffer[recordLength + 5];
log(`Next handshake type: ${handshakeType} (looking for ${TlsHandshakeType.CLIENT_HELLO})`);
if (handshakeType === TlsHandshakeType.CLIENT_HELLO) {
// Found a ClientHello in the next record, return the entire buffer
log(`Found ClientHello in subsequent record, returning full buffer`);
this.fragmentedBuffers.delete(connectionId);
return newBuffer;
}
}
}
}
}
}
}
catch (e) {
log(`Error checking reassembled buffer completeness: ${e}`);
}
return undefined; // Still need more fragments
}
}
/**
* Parses a TLS ClientHello message and extracts all components
*
* @param buffer The buffer containing the ClientHello message
* @param logger Optional logging function
* @returns Parsed ClientHello or undefined if parsing failed
*/
static parseClientHello(buffer, logger) {
const log = logger || (() => { });
const result = {
isValid: false,
hasSessionId: false,
extensions: [],
hasSessionTicket: false,
hasPsk: false,
hasEarlyData: false
};
try {
// Check basic validity
if (buffer.length < 5) {
result.error = 'Buffer too small for TLS record header';
return result;
}
// Check record type (must be HANDSHAKE)
if (buffer[0] !== TlsRecordType.HANDSHAKE) {
result.error = `Not a TLS handshake record: ${buffer[0]}`;
return result;
}
// Get TLS version from record header
const majorVersion = buffer[1];
const minorVersion = buffer[2];
result.version = [majorVersion, minorVersion];
log(`TLS record version: ${majorVersion}.${minorVersion}`);
// Parse record length (bytes 3-4, big-endian)
const recordLength = (buffer[3] << 8) + buffer[4];
log(`Record length: ${recordLength}`);
// Validate record length against buffer size
if (buffer.length < recordLength + 5) {
result.error = 'Buffer smaller than expected record length';
return result;
}
// Start of handshake message in the buffer
let pos = 5;
// Check handshake type (must be CLIENT_HELLO)
if (buffer[pos] !== TlsHandshakeType.CLIENT_HELLO) {
result.error = `Not a ClientHello message: ${buffer[pos]}`;
return result;
}
// Skip handshake type (1 byte)
pos += 1;
// Parse handshake length (3 bytes, big-endian)
const handshakeLength = (buffer[pos] << 16) + (buffer[pos + 1] << 8) + buffer[pos + 2];
log(`Handshake length: ${handshakeLength}`);
// Skip handshake length (3 bytes)
pos += 3;
// Check client version (2 bytes)
const clientMajorVersion = buffer[pos];
const clientMinorVersion = buffer[pos + 1];
log(`Client version: ${clientMajorVersion}.${clientMinorVersion}`);
// Skip client version (2 bytes)
pos += 2;
// Extract client random (32 bytes)
if (pos + 32 > buffer.length) {
result.error = 'Buffer too small for client random';
return result;
}
result.random = buffer.slice(pos, pos + 32);
log(`Client random: ${result.random.toString('hex')}`);
// Skip client random (32 bytes)
pos += 32;
// Parse session ID
if (pos + 1 > buffer.length) {
result.error = 'Buffer too small for session ID length';
return result;
}
const sessionIdLength = buffer[pos];
log(`Session ID length: ${sessionIdLength}`);
pos += 1;
result.hasSessionId = sessionIdLength > 0;
if (sessionIdLength > 0) {
if (pos + sessionIdLength > buffer.length) {
result.error = 'Buffer too small for session ID';
return result;
}
result.sessionId = buffer.slice(pos, pos + sessionIdLength);
log(`Session ID: ${result.sessionId.toString('hex')}`);
}
// Skip session ID
pos += sessionIdLength;
// Check if we have enough bytes left for cipher suites
if (pos + 2 > buffer.length) {
result.error = 'Buffer too small for cipher suites length';
return result;
}
// Parse cipher suites length (2 bytes, big-endian)
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
log(`Cipher suites length: ${cipherSuitesLength}`);
pos += 2;
// Extract cipher suites
if (pos + cipherSuitesLength > buffer.length) {
result.error = 'Buffer too small for cipher suites';
return result;
}
result.cipherSuites = buffer.slice(pos, pos + cipherSuitesLength);
// Skip cipher suites
pos += cipherSuitesLength;
// Check if we have enough bytes left for compression methods
if (pos + 1 > buffer.length) {
result.error = 'Buffer too small for compression methods length';
return result;
}
// Parse compression methods length (1 byte)
const compressionMethodsLength = buffer[pos];
log(`Compression methods length: ${compressionMethodsLength}`);
pos += 1;
// Extract compression methods
if (pos + compressionMethodsLength > buffer.length) {
result.error = 'Buffer too small for compression methods';
return result;
}
result.compressionMethods = buffer.slice(pos, pos + compressionMethodsLength);
// Skip compression methods
pos += compressionMethodsLength;
// Check if we have enough bytes for extensions length
if (pos + 2 > buffer.length) {
// No extensions present - this is valid for older TLS versions
result.isValid = true;
return result;
}
// Parse extensions length (2 bytes, big-endian)
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
log(`Extensions length: ${extensionsLength}`);
pos += 2;
// Extensions end position
const extensionsEnd = pos + extensionsLength;
// Check if extensions length is valid
if (extensionsEnd > buffer.length) {
result.error = 'Extensions length exceeds buffer size';
return result;
}
// Iterate through extensions
const serverNames = [];
while (pos + 4 <= extensionsEnd) {
// Parse extension type (2 bytes, big-endian)
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
log(`Extension type: 0x${extensionType.toString(16).padStart(4, '0')}`);
pos += 2;
// Parse extension length (2 bytes, big-endian)
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
log(`Extension length: ${extensionLength}`);
pos += 2;
// Extract extension data
if (pos + extensionLength > extensionsEnd) {
result.error = `Extension ${extensionType} data exceeds bounds`;
return result;
}
const extensionData = buffer.slice(pos, pos + extensionLength);
// Record all extensions
result.extensions.push({
type: extensionType,
length: extensionLength,
data: extensionData
});
// Track specific extension types
if (extensionType === TlsExtensionType.SERVER_NAME) {
// Server Name Indication (SNI)
this.parseServerNameExtension(extensionData, serverNames, logger);
}
else if (extensionType === TlsExtensionType.SESSION_TICKET) {
// Session ticket
result.hasSessionTicket = true;
}
else if (extensionType === TlsExtensionType.PRE_SHARED_KEY) {
// TLS 1.3 PSK
result.hasPsk = true;
}
else if (extensionType === TlsExtensionType.EARLY_DATA) {
// TLS 1.3 Early Data (0-RTT)
result.hasEarlyData = true;
}
// Move to next extension
pos += extensionLength;
}
// Store any server names found
if (serverNames.length > 0) {
result.serverNameList = serverNames;
}
// Mark as valid if we get here
result.isValid = true;
return result;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log(`Error parsing ClientHello: ${errorMessage}`);
result.error = errorMessage;
return result;
}
}
/**
* Parses the server name extension data and extracts hostnames
*
* @param data Extension data buffer
* @param serverNames Array to populate with found server names
* @param logger Optional logging function
* @returns true if parsing succeeded
*/
static parseServerNameExtension(data, serverNames, logger) {
const log = logger || (() => { });
try {
// Need at least 2 bytes for server name list length
if (data.length < 2) {
log('SNI extension too small for server name list length');
return false;
}
// Parse server name list length (2 bytes)
const listLength = (data[0] << 8) + data[1];
// Skip to first name entry
let pos = 2;
// End of list
const listEnd = pos + listLength;
// Validate length
if (listEnd > data.length) {
log('SNI server name list exceeds extension data');
return false;
}
// Process all name entries
while (pos + 3 <= listEnd) {
// Name type (1 byte)
const nameType = data[pos];
pos += 1;
// For hostname, type must be 0
if (nameType !== 0) {
// Skip this entry
if (pos + 2 <= listEnd) {
const nameLength = (data[pos] << 8) + data[pos + 1];
pos += 2 + nameLength;
continue;
}
else {
log('Malformed SNI entry');
return false;
}
}
// Parse hostname length (2 bytes)
if (pos + 2 > listEnd) {
log('SNI extension truncated');
return false;
}
const nameLength = (data[pos] << 8) + data[pos + 1];
pos += 2;
// Extract hostname
if (pos + nameLength > listEnd) {
log('SNI hostname truncated');
return false;
}
// Extract the hostname as UTF-8
try {
const hostname = data.slice(pos, pos + nameLength).toString('utf8');
log(`Found SNI hostname: ${hostname}`);
serverNames.push(hostname);
}
catch (err) {
log(`Error extracting hostname: ${err}`);
}
// Move to next entry
pos += nameLength;
}
return serverNames.length > 0;
}
catch (error) {
log(`Error parsing SNI extension: ${error}`);
return false;
}
}
/**
* Determines if a ClientHello contains session resumption indicators
*
* @param buffer The ClientHello buffer
* @param logger Optional logging function
* @returns Session resumption result
*/
static hasSessionResumption(buffer, logger) {
const log = logger || (() => { });
if (!TlsUtils.isClientHello(buffer)) {
return { isResumption: false, hasSNI: false };
}
const parseResult = this.parseClientHello(buffer, logger);
if (!parseResult.isValid) {
log(`ClientHello parse failed: ${parseResult.error}`);
return { isResumption: false, hasSNI: false };
}
// Check resumption indicators
const hasSessionId = parseResult.hasSessionId;
const hasSessionTicket = parseResult.hasSessionTicket;
const hasPsk = parseResult.hasPsk;
const hasEarlyData = parseResult.hasEarlyData;
// Check for SNI
const hasSNI = !!parseResult.serverNameList && parseResult.serverNameList.length > 0;
// Consider it a resumption if any resumption mechanism is present
const isResumption = hasSessionTicket || hasPsk || hasEarlyData ||
(hasSessionId && !hasPsk); // Legacy resumption
// Log details
if (isResumption) {
log('Session resumption detected: ' +
(hasSessionTicket ? 'session ticket, ' : '') +
(hasPsk ? 'PSK, ' : '') +
(hasEarlyData ? 'early data, ' : '') +
(hasSessionId ? 'session ID' : '') +
(hasSNI ? ', with SNI' : ', without SNI'));
}
return { isResumption, hasSNI };
}
/**
* Checks if a ClientHello appears to be from a tab reactivation
*
* @param buffer The ClientHello buffer
* @param logger Optional logging function
* @returns true if it appears to be a tab reactivation
*/
static isTabReactivationHandshake(buffer, logger) {
const log = logger || (() => { });
if (!TlsUtils.isClientHello(buffer)) {
return false;
}
// Parse the ClientHello
const parseResult = this.parseClientHello(buffer, logger);
if (!parseResult.isValid) {
return false;
}
// Tab reactivation pattern: session identifier + (ticket or PSK) but no SNI
const hasSessionId = parseResult.hasSessionId;
const hasSessionTicket = parseResult.hasSessionTicket;
const hasPsk = parseResult.hasPsk;
const hasSNI = !!parseResult.serverNameList && parseResult.serverNameList.length > 0;
if ((hasSessionId && (hasSessionTicket || hasPsk)) && !hasSNI) {
log('Detected tab reactivation pattern: session resumption without SNI');
return true;
}
return false;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpZW50LWhlbGxvLXBhcnNlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL3Rscy9zbmkvY2xpZW50LWhlbGxvLXBhcnNlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ2hDLE9BQU8sRUFDTCxhQUFhLEVBQ2IsZ0JBQWdCLEVBQ2hCLGdCQUFnQixFQUNqQixNQUFNLDhCQUE4QixDQUFDO0FBQ3RDLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQW9EakQ7O0dBRUc7QUFDSCxNQUFNLE9BQU8saUJBQWlCO0lBQzVCLHNEQUFzRDthQUN2QyxzQkFBaUIsR0FBc0MsSUFBSSxHQUFHLEVBQUUsQ0FBQzthQUNqRSxvQkFBZSxHQUFXLElBQUksQ0FBQyxHQUFDLDBDQUEwQztJQUV6Rjs7T0FFRztJQUNLLE1BQU0sQ0FBQyx1QkFBdUI7UUFDcEMsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLEtBQUssTUFBTSxDQUFDLFlBQVksRUFBRSxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUMsaUJBQWlCLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQztZQUNwRSxJQUFJLEdBQUcsR0FBRyxJQUFJLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQyxlQUFlLEVBQUUsQ0FBQztnQkFDaEQsSUFBSSxDQUFDLGlCQUFpQixDQUFDLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQztZQUM5QyxDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFFRDs7Ozs7Ozs7T0FRRztJQUNJLE1BQU0sQ0FBQywyQkFBMkIsQ0FDdkMsTUFBYyxFQUNkLFlBQW9CLEVBQ3BCLE1BQXVCO1FBRXZCLE1BQU0sR0FBRyxHQUFHLE1BQU0sSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFFLENBQUMsQ0FBQyxDQUFDO1FBRWpDLDBDQUEwQztRQUMxQyxJQUFJ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