import { Buffer } from 'node:buffer';
import { TlsExtensionType, TlsUtils } from '../utils/tls-utils.js';
import { ClientHelloParser } from './client-hello-parser.js';
/**
 * Utilities for extracting SNI information from TLS handshakes
 */
export class SniExtraction {
    /**
     * Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
     *
     * @param buffer The buffer containing the TLS ClientHello message
     * @param logger Optional logging function
     * @returns The extracted server name or undefined if not found
     */
    static extractSNI(buffer, logger) {
        const log = logger || (() => { });
        try {
            // Parse the ClientHello
            const parseResult = ClientHelloParser.parseClientHello(buffer, logger);
            if (!parseResult.isValid) {
                log(`Failed to parse ClientHello: ${parseResult.error}`);
                return undefined;
            }
            // Check if ServerName extension was found
            if (parseResult.serverNameList && parseResult.serverNameList.length > 0) {
                // Use the first hostname (most common case)
                const serverName = parseResult.serverNameList[0];
                log(`Found SNI: ${serverName}`);
                return serverName;
            }
            log('No SNI extension found in ClientHello');
            return undefined;
        }
        catch (error) {
            log(`Error extracting SNI: ${error instanceof Error ? error.message : String(error)}`);
            return undefined;
        }
    }
    /**
     * Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
     *
     * In TLS 1.3, when a client attempts to resume a session, it may include
     * the server name in the PSK identity hint rather than in the SNI extension.
     *
     * @param buffer The buffer containing the TLS ClientHello message
     * @param logger Optional logging function
     * @returns The extracted server name or undefined if not found
     */
    static extractSNIFromPSKExtension(buffer, logger) {
        const log = logger || (() => { });
        try {
            // Ensure this is a ClientHello
            if (!TlsUtils.isClientHello(buffer)) {
                log('Not a ClientHello message');
                return undefined;
            }
            // Parse the ClientHello to find PSK extension
            const parseResult = ClientHelloParser.parseClientHello(buffer, logger);
            if (!parseResult.isValid || !parseResult.extensions) {
                return undefined;
            }
            // Find the PSK extension
            const pskExtension = parseResult.extensions.find(ext => ext.type === TlsExtensionType.PRE_SHARED_KEY);
            if (!pskExtension) {
                log('No PSK extension found');
                return undefined;
            }
            // Parse the PSK extension data
            const data = pskExtension.data;
            // PSK extension structure:
            // 2 bytes: identities list length
            if (data.length < 2)
                return undefined;
            const identitiesLength = (data[0] << 8) + data[1];
            let pos = 2;
            // End of identities list
            const identitiesEnd = pos + identitiesLength;
            if (identitiesEnd > data.length)
                return undefined;
            // Process each PSK identity
            while (pos + 2 <= identitiesEnd) {
                // Identity length (2 bytes)
                if (pos + 2 > identitiesEnd)
                    break;
                const identityLength = (data[pos] << 8) + data[pos + 1];
                pos += 2;
                if (pos + identityLength > identitiesEnd)
                    break;
                // Try to extract hostname from identity
                // Chrome often embeds the hostname in the PSK identity
                // This is a heuristic as there's no standard format
                if (identityLength > 0) {
                    const identity = data.slice(pos, pos + identityLength);
                    // Skip identity bytes
                    pos += identityLength;
                    // Skip obfuscated ticket age (4 bytes)
                    if (pos + 4 <= identitiesEnd) {
                        pos += 4;
                    }
                    else {
                        break;
                    }
                    // Try to parse the identity as UTF-8
                    try {
                        const identityStr = identity.toString('utf8');
                        log(`PSK identity: ${identityStr}`);
                        // Check if the identity contains hostname hints
                        // Chrome often embeds the hostname in a known format
                        // Try to extract using common patterns
                        // Pattern 1: Look for domain name pattern
                        const domainPattern = /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i;
                        const domainMatch = identityStr.match(domainPattern);
                        if (domainMatch && domainMatch[0]) {
                            log(`Found domain in PSK identity: ${domainMatch[0]}`);
                            return domainMatch[0];
                        }
                        // Pattern 2: Chrome sometimes uses a specific format with delimiters
                        // This is a heuristic approach since the format isn't standardized
                        const parts = identityStr.split('|');
                        if (parts.length > 1) {
                            for (const part of parts) {
                                if (part.includes('.') && !part.includes('/')) {
                                    const possibleDomain = part.trim();
                                    if (/^[a-z0-9.-]+$/i.test(possibleDomain)) {
                                        log(`Found possible domain in PSK delimiter format: ${possibleDomain}`);
                                        return possibleDomain;
                                    }
                                }
                            }
                        }
                    }
                    catch (e) {
                        log('Failed to parse PSK identity as UTF-8');
                    }
                }
            }
            log('No hostname found in PSK extension');
            return undefined;
        }
        catch (error) {
            log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`);
            return undefined;
        }
    }
    /**
     * Main entry point for SNI extraction with support for fragmented messages
     * and session resumption edge cases.
     *
     * @param buffer The buffer containing TLS data
     * @param connectionInfo Connection tracking information
     * @param logger Optional logging function
     * @param cachedSni Optional previously cached SNI value
     * @returns The extracted server name or undefined
     */
    static extractSNIWithResumptionSupport(buffer, connectionInfo, logger, cachedSni) {
        const log = logger || (() => { });
        // Log buffer details for debugging
        if (logger) {
            log(`Buffer size: ${buffer.length} bytes`);
            log(`Buffer starts with: ${buffer.slice(0, Math.min(10, buffer.length)).toString('hex')}`);
            if (buffer.length >= 5) {
                const recordType = buffer[0];
                const majorVersion = buffer[1];
                const minorVersion = buffer[2];
                const recordLength = (buffer[3] << 8) + buffer[4];
                log(`TLS Record: type=${recordType}, version=${majorVersion}.${minorVersion}, length=${recordLength}`);
            }
        }
        // Check if we need to handle fragmented packets
        let processBuffer = buffer;
        if (connectionInfo) {
            const connectionId = TlsUtils.createConnectionId(connectionInfo);
            const reassembledBuffer = ClientHelloParser.handleFragmentedClientHello(buffer, connectionId, logger);
            if (!reassembledBuffer) {
                log(`Waiting for more fragments on connection ${connectionId}`);
                return undefined; // Need more fragments to complete ClientHello
            }
            processBuffer = reassembledBuffer;
            log(`Using reassembled buffer of length ${processBuffer.length}`);
        }
        // First try the standard SNI extraction
        const standardSni = this.extractSNI(processBuffer, logger);
        if (standardSni) {
            log(`Found standard SNI: ${standardSni}`);
            return standardSni;
        }
        // Check for session resumption when standard SNI extraction fails
        if (TlsUtils.isClientHello(processBuffer)) {
            const resumptionInfo = ClientHelloParser.hasSessionResumption(processBuffer, logger);
            if (resumptionInfo.isResumption) {
                log(`Detected session resumption in ClientHello without standard SNI`);
                // Try to extract SNI from PSK extension
                const pskSni = this.extractSNIFromPSKExtension(processBuffer, logger);
                if (pskSni) {
                    log(`Extracted SNI from PSK extension: ${pskSni}`);
                    return pskSni;
                }
            }
        }
        // If cached SNI was provided, use it for application data packets
        if (cachedSni && TlsUtils.isTlsApplicationData(buffer)) {
            log(`Using provided cached SNI for application data: ${cachedSni}`);
            return cachedSni;
        }
        return undefined;
    }
    /**
     * Unified method for processing a TLS packet and extracting SNI.
     * Main entry point for SNI extraction that handles all edge cases.
     *
     * @param buffer The buffer containing TLS data
     * @param connectionInfo Connection tracking information
     * @param logger Optional logging function
     * @param cachedSni Optional previously cached SNI value
     * @returns The extracted server name or undefined
     */
    static processTlsPacket(buffer, connectionInfo, logger, cachedSni) {
        const log = logger || (() => { });
        // Add timestamp if not provided
        if (!connectionInfo.timestamp) {
            connectionInfo.timestamp = Date.now();
        }
        // Check if this is a TLS handshake or application data
        if (!TlsUtils.isTlsHandshake(buffer) && !TlsUtils.isTlsApplicationData(buffer)) {
            log('Not a TLS handshake or application data packet');
            return undefined;
        }
        // Create connection ID for tracking
        const connectionId = TlsUtils.createConnectionId(connectionInfo);
        log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
        // Handle application data with cached SNI (for connection racing)
        if (TlsUtils.isTlsApplicationData(buffer)) {
            // If explicit cachedSni was provided, use it
            if (cachedSni) {
                log(`Using provided cached SNI for application data: ${cachedSni}`);
                return cachedSni;
            }
            log('Application data packet without cached SNI, cannot determine hostname');
            return undefined;
        }
        // Enhanced session resumption detection
        if (TlsUtils.isClientHello(buffer)) {
            const resumptionInfo = ClientHelloParser.hasSessionResumption(buffer, logger);
            if (resumptionInfo.isResumption) {
                log(`Session resumption detected in TLS packet`);
                // Always try standard SNI extraction first
                const standardSni = this.extractSNI(buffer, logger);
                if (standardSni) {
                    log(`Found standard SNI in session resumption: ${standardSni}`);
                    return standardSni;
                }
                // Enhanced session resumption SNI extraction
                // Try extracting from PSK identity
                const pskSni = this.extractSNIFromPSKExtension(buffer, logger);
                if (pskSni) {
                    log(`Extracted SNI from PSK extension: ${pskSni}`);
                    return pskSni;
                }
                log(`Session resumption without extractable SNI`);
            }
        }
        // For handshake messages, try the full extraction process
        const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, logger);
        if (sni) {
            log(`Successfully extracted SNI: ${sni}`);
            return sni;
        }
        // If we couldn't extract an SNI, check if this is a valid ClientHello
        if (TlsUtils.isClientHello(buffer)) {
            log('Valid ClientHello detected, but no SNI extracted - might need more data');
        }
        return undefined;
    }
}
//# sourceMappingURL=data:application/json;base64, 