From 87d26c86a1040ec9f2a17f696965bbe5be1ed4fa Mon Sep 17 00:00:00 2001
From: Philipp Kunz <code@philkunz.com>
Date: Tue, 11 Mar 2025 17:01:07 +0000
Subject: [PATCH] fix(PortProxy/SNI): Refactor SNI extraction in PortProxy to
 use the dedicated SniHandler class

---
 changelog.md             |   8 +
 ts/00_commitinfo_data.ts |   2 +-
 ts/classes.portproxy.ts  | 212 ++-----------------------
 ts/classes.snihandler.ts | 331 +++++++++++++++++++++++++++++++++++++++
 ts/index.ts              |   1 +
 5 files changed, 354 insertions(+), 200 deletions(-)
 create mode 100644 ts/classes.snihandler.ts

diff --git a/changelog.md b/changelog.md
index 48bac9b..bbaaeae 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## 2025-03-11 - 3.37.1 - fix(PortProxy/SNI)
+Refactor SNI extraction in PortProxy to use the dedicated SniHandler class
+
+- Removed local SNI extraction and handshake detection functions from classes.portproxy.ts
+- Introduced a standalone SniHandler class in ts/classes.snihandler.ts for robust SNI extraction and improved logging
+- Replaced inlined calls to isTlsHandshake and extractSNI with calls to SniHandler methods
+- Ensured consistency in handling TLS ClientHello messages across the codebase
+
 ## 2025-03-11 - 3.37.0 - feat(portproxy)
 Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions
 
diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts
index 4c92f18..f822ee8 100644
--- a/ts/00_commitinfo_data.ts
+++ b/ts/00_commitinfo_data.ts
@@ -3,6 +3,6 @@
  */
 export const commitinfo = {
   name: '@push.rocks/smartproxy',
-  version: '3.37.0',
+  version: '3.37.1',
   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
 }
diff --git a/ts/classes.portproxy.ts b/ts/classes.portproxy.ts
index f702358..3e6e129 100644
--- a/ts/classes.portproxy.ts
+++ b/ts/classes.portproxy.ts
@@ -1,5 +1,6 @@
 import * as plugins from './plugins.js';
 import { NetworkProxy } from './classes.networkproxy.js';
+import { SniHandler } from './classes.snihandler.js';
 
 /** Domain configuration with per-domain allowed port ranges */
 export interface IDomainConfig {
@@ -117,192 +118,8 @@ interface IConnectionRecord {
   domainSwitches?: number; // Number of times the domain has been switched on this connection
 }
 
-/**
- * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
- * Enhanced for robustness and detailed logging.
- * @param buffer - Buffer containing the TLS ClientHello.
- * @param enableLogging - Whether to enable detailed logging.
- * @returns The server name if found, otherwise undefined.
- */
-function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
-  try {
-    // Check if buffer is too small for TLS
-    if (buffer.length < 5) {
-      if (enableLogging) console.log('Buffer too small for TLS header');
-      return undefined;
-    }
-
-    // Check record type (has to be handshake - 22)
-    const recordType = buffer.readUInt8(0);
-    if (recordType !== 22) {
-      if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
-      return undefined;
-    }
-
-    // Check TLS version (has to be 3.1 or higher)
-    const majorVersion = buffer.readUInt8(1);
-    const minorVersion = buffer.readUInt8(2);
-    if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
-
-    // Check record length
-    const recordLength = buffer.readUInt16BE(3);
-    if (buffer.length < 5 + recordLength) {
-      if (enableLogging)
-        console.log(
-          `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
-        );
-      return undefined;
-    }
-
-    let offset = 5;
-    const handshakeType = buffer.readUInt8(offset);
-    if (handshakeType !== 1) {
-      if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
-      return undefined;
-    }
-
-    offset += 4; // Skip handshake header (type + length)
-
-    // Client version
-    const clientMajorVersion = buffer.readUInt8(offset);
-    const clientMinorVersion = buffer.readUInt8(offset + 1);
-    if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
-
-    offset += 2 + 32; // Skip client version and random
-
-    // Session ID
-    const sessionIDLength = buffer.readUInt8(offset);
-    if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
-    offset += 1 + sessionIDLength; // Skip session ID
-
-    // Cipher suites
-    if (offset + 2 > buffer.length) {
-      if (enableLogging) console.log('Buffer too small for cipher suites length');
-      return undefined;
-    }
-    const cipherSuitesLength = buffer.readUInt16BE(offset);
-    if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
-    offset += 2 + cipherSuitesLength; // Skip cipher suites
-
-    // Compression methods
-    if (offset + 1 > buffer.length) {
-      if (enableLogging) console.log('Buffer too small for compression methods length');
-      return undefined;
-    }
-    const compressionMethodsLength = buffer.readUInt8(offset);
-    if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
-    offset += 1 + compressionMethodsLength; // Skip compression methods
-
-    // Extensions
-    if (offset + 2 > buffer.length) {
-      if (enableLogging) console.log('Buffer too small for extensions length');
-      return undefined;
-    }
-    const extensionsLength = buffer.readUInt16BE(offset);
-    if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
-    offset += 2;
-    const extensionsEnd = offset + extensionsLength;
-
-    if (extensionsEnd > buffer.length) {
-      if (enableLogging)
-        console.log(
-          `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
-        );
-      return undefined;
-    }
-
-    // Parse extensions
-    while (offset + 4 <= extensionsEnd) {
-      const extensionType = buffer.readUInt16BE(offset);
-      const extensionLength = buffer.readUInt16BE(offset + 2);
-
-      if (enableLogging)
-        console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
-
-      offset += 4;
-
-      if (extensionType === 0x0000) {
-        // SNI extension
-        if (offset + 2 > buffer.length) {
-          if (enableLogging) console.log('Buffer too small for SNI list length');
-          return undefined;
-        }
-
-        const sniListLength = buffer.readUInt16BE(offset);
-        if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
-        offset += 2;
-        const sniListEnd = offset + sniListLength;
-
-        if (sniListEnd > buffer.length) {
-          if (enableLogging)
-            console.log(
-              `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
-            );
-          return undefined;
-        }
-
-        while (offset + 3 < sniListEnd) {
-          const nameType = buffer.readUInt8(offset++);
-          const nameLen = buffer.readUInt16BE(offset);
-          offset += 2;
-
-          if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
-
-          if (nameType === 0) {
-            // host_name
-            if (offset + nameLen > buffer.length) {
-              if (enableLogging)
-                console.log(
-                  `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
-                    buffer.length
-                  }`
-                );
-              return undefined;
-            }
-
-            const serverName = buffer.toString('utf8', offset, offset + nameLen);
-            if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
-            return serverName;
-          }
-
-          offset += nameLen;
-        }
-        break;
-      } else {
-        offset += extensionLength;
-      }
-    }
-
-    if (enableLogging) console.log('No SNI extension found');
-    return undefined;
-  } catch (err) {
-    console.log(`Error extracting SNI: ${err}`);
-    return undefined;
-  }
-}
-
-/**
- * Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
- * @param buffer - Buffer containing the TLS record
- * @returns true if the buffer contains a proper ClientHello message
- */
-function isClientHello(buffer: Buffer): boolean {
-  try {
-    if (buffer.length < 9) return false; // Too small for a proper ClientHello
-
-    // Check record type (has to be handshake - 22)
-    if (buffer.readUInt8(0) !== 22) return false;
-
-    // After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
-    if (buffer.readUInt8(5) !== 1) return false;
-
-    // Basic checks passed, this appears to be a ClientHello
-    return true;
-  } catch (err) {
-    console.log(`Error checking for ClientHello: ${err}`);
-    return false;
-  }
-}
+// SNI functions are now imported from SniHandler class
+// No need for wrapper functions
 
 // Helper: Check if a port falls within any of the given port ranges
 const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
@@ -346,10 +163,7 @@ const generateConnectionId = (): string => {
   return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
 };
 
-// Helper: Check if a buffer contains a TLS handshake
-const isTlsHandshake = (buffer: Buffer): boolean => {
-  return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
-};
+// SNI functions are now imported from SniHandler class
 
 // Helper: Ensure timeout values don't exceed Node.js max safe integer
 const ensureSafeTimeout = (timeout: number): number => {
@@ -761,7 +575,7 @@ export class PortProxy {
       record.bytesReceived += chunk.length;
 
       // Check for TLS handshake
-      if (!record.isTLS && isTlsHandshake(chunk)) {
+      if (!record.isTLS && SniHandler.isTlsHandshake(chunk)) {
         record.isTLS = true;
 
         if (this.settings.enableTlsDebugLogging) {
@@ -1049,10 +863,10 @@ export class PortProxy {
         // Define a handler for checking renegotiation with improved detection
         const renegotiationHandler = (renegChunk: Buffer) => {
           // Only process if this looks like a TLS ClientHello
-          if (isClientHello(renegChunk)) {
+          if (SniHandler.isClientHello(renegChunk)) {
             try {
               // Extract SNI from ClientHello
-              const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
+              const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, this.settings.enableTlsDebugLogging);
 
               // Skip if no SNI was found
               if (!newSNI) return;
@@ -1644,7 +1458,7 @@ export class PortProxy {
           connectionRecord.hasReceivedInitialData = true;
 
           // Check if this looks like a TLS handshake
-          if (isTlsHandshake(chunk)) {
+          if (SniHandler.isTlsHandshake(chunk)) {
             connectionRecord.isTLS = true;
             
             // Forward directly to NetworkProxy without SNI processing
@@ -1706,7 +1520,7 @@ export class PortProxy {
           this.updateActivity(connectionRecord);
 
           // Check for TLS handshake if this is the first chunk
-          if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
+          if (!connectionRecord.isTLS && SniHandler.isTlsHandshake(chunk)) {
             connectionRecord.isTLS = true;
 
             if (this.settings.enableTlsDebugLogging) {
@@ -1714,7 +1528,7 @@ export class PortProxy {
                 `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
               );
               // Try to extract SNI and log detailed debug info
-              extractSNI(chunk, true);
+              SniHandler.extractSNIWithResumptionSupport(chunk, true);
             }
           }
         });
@@ -1743,7 +1557,7 @@ export class PortProxy {
           connectionRecord.hasReceivedInitialData = true;
 
           // Check if this looks like a TLS handshake
-          const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
+          const isTlsHandshakeDetected = initialChunk && SniHandler.isTlsHandshake(initialChunk);
           if (isTlsHandshakeDetected) {
             connectionRecord.isTLS = true;
 
@@ -1912,7 +1726,7 @@ export class PortProxy {
             // Try to extract SNI
             let serverName = '';
 
-            if (isTlsHandshake(chunk)) {
+            if (SniHandler.isTlsHandshake(chunk)) {
               connectionRecord.isTLS = true;
 
               if (this.settings.enableTlsDebugLogging) {
@@ -1921,7 +1735,7 @@ export class PortProxy {
                 );
               }
 
-              serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
+              serverName = SniHandler.extractSNIWithResumptionSupport(chunk, this.settings.enableTlsDebugLogging) || '';
             }
 
             // Lock the connection to the negotiated SNI.
diff --git a/ts/classes.snihandler.ts b/ts/classes.snihandler.ts
new file mode 100644
index 0000000..3c521d1
--- /dev/null
+++ b/ts/classes.snihandler.ts
@@ -0,0 +1,331 @@
+import { Buffer } from 'buffer';
+
+/**
+ * SNI (Server Name Indication) handler for TLS connections.
+ * Provides robust extraction of SNI values from TLS ClientHello messages.
+ */
+export class SniHandler {
+  // TLS record types and constants
+  private static readonly TLS_HANDSHAKE_RECORD_TYPE = 22;
+  private static readonly TLS_CLIENT_HELLO_HANDSHAKE_TYPE = 1;
+  private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
+  private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
+  private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
+
+  /**
+   * Checks if a buffer contains a TLS handshake message (record type 22)
+   * @param buffer - The buffer to check
+   * @returns true if the buffer starts with a TLS handshake record type
+   */
+  public static isTlsHandshake(buffer: Buffer): boolean {
+    return buffer.length > 0 && buffer[0] === this.TLS_HANDSHAKE_RECORD_TYPE;
+  }
+
+  /**
+   * Checks if a buffer contains a TLS ClientHello message
+   * @param buffer - The buffer to check
+   * @returns true if the buffer appears to be a ClientHello message
+   */
+  public static isClientHello(buffer: Buffer): boolean {
+    // Minimum ClientHello size (TLS record header + handshake header)
+    if (buffer.length < 9) {
+      return false;
+    }
+
+    // Check record type (must be TLS_HANDSHAKE_RECORD_TYPE)
+    if (buffer[0] !== this.TLS_HANDSHAKE_RECORD_TYPE) {
+      return false;
+    }
+
+    // Skip version and length in TLS record header (5 bytes total)
+    // Check handshake type at byte 5 (must be CLIENT_HELLO)
+    return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
+  }
+
+  /**
+   * Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
+   * Implements robust parsing with support for session resumption edge cases.
+   * 
+   * @param buffer - The buffer containing the TLS ClientHello message
+   * @param enableLogging - Whether to enable detailed debug logging
+   * @returns The extracted server name or undefined if not found
+   */
+  public static extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
+    // Logging helper
+    const log = (message: string) => {
+      if (enableLogging) {
+        console.log(`[SNI Extraction] ${message}`);
+      }
+    };
+
+    try {
+      // Buffer must be at least 5 bytes (TLS record header)
+      if (buffer.length < 5) {
+        log('Buffer too small for TLS record header');
+        return undefined;
+      }
+
+      // Check record type (must be TLS_HANDSHAKE_RECORD_TYPE = 22)
+      if (buffer[0] !== this.TLS_HANDSHAKE_RECORD_TYPE) {
+        log(`Not a TLS handshake record: ${buffer[0]}`);
+        return undefined;
+      }
+
+      // Check TLS version
+      const majorVersion = buffer[1];
+      const minorVersion = buffer[2];
+      log(`TLS 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) {
+        log('Buffer smaller than expected record length');
+        return undefined;
+      }
+
+      // Start of handshake message in the buffer
+      let pos = 5;
+
+      // Check handshake type (must be CLIENT_HELLO = 1)
+      if (buffer[pos] !== this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
+        log(`Not a ClientHello message: ${buffer[pos]}`);
+        return undefined;
+      }
+
+      // 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;
+
+      // Skip client random (32 bytes)
+      pos += 32;
+
+      // Parse session ID
+      if (pos + 1 > buffer.length) {
+        log('Buffer too small for session ID length');
+        return undefined;
+      }
+
+      const sessionIdLength = buffer[pos];
+      log(`Session ID length: ${sessionIdLength}`);
+
+      // Skip session ID length (1 byte) and session ID
+      pos += 1 + sessionIdLength;
+
+      // Check if we have enough bytes left
+      if (pos + 2 > buffer.length) {
+        log('Buffer too small for cipher suites length');
+        return undefined;
+      }
+
+      // Parse cipher suites length (2 bytes, big-endian)
+      const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
+      log(`Cipher suites length: ${cipherSuitesLength}`);
+
+      // Skip cipher suites length (2 bytes) and cipher suites
+      pos += 2 + cipherSuitesLength;
+
+      // Check if we have enough bytes left
+      if (pos + 1 > buffer.length) {
+        log('Buffer too small for compression methods length');
+        return undefined;
+      }
+
+      // Parse compression methods length (1 byte)
+      const compressionMethodsLength = buffer[pos];
+      log(`Compression methods length: ${compressionMethodsLength}`);
+
+      // Skip compression methods length (1 byte) and compression methods
+      pos += 1 + compressionMethodsLength;
+
+      // Check if we have enough bytes for extensions length
+      if (pos + 2 > buffer.length) {
+        log('No extensions present or buffer too small');
+        return undefined;
+      }
+
+      // Parse extensions length (2 bytes, big-endian)
+      const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
+      log(`Extensions length: ${extensionsLength}`);
+
+      // Skip extensions length (2 bytes)
+      pos += 2;
+
+      // Extensions end position
+      const extensionsEnd = pos + extensionsLength;
+
+      // Check if extensions length is valid
+      if (extensionsEnd > buffer.length) {
+        log('Extensions length exceeds buffer size');
+        return undefined;
+      }
+
+      // Track if we found session tickets (for improved resumption handling)
+      let hasSessionTicket = false;
+
+      // Iterate through extensions
+      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')}`);
+
+        // Skip extension type (2 bytes)
+        pos += 2;
+
+        // Parse extension length (2 bytes, big-endian)
+        const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
+        log(`Extension length: ${extensionLength}`);
+
+        // Skip extension length (2 bytes)
+        pos += 2;
+
+        // Check if this is the SNI extension
+        if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
+          log('Found SNI extension');
+
+          // Ensure we have enough bytes for the server name list
+          if (pos + 2 > extensionsEnd) {
+            log('Extension too small for server name list length');
+            pos += extensionLength; // Skip this extension
+            continue;
+          }
+
+          // Parse server name list length (2 bytes, big-endian)
+          const serverNameListLength = (buffer[pos] << 8) + buffer[pos + 1];
+          log(`Server name list length: ${serverNameListLength}`);
+
+          // Skip server name list length (2 bytes)
+          pos += 2;
+
+          // Ensure server name list length is valid
+          if (pos + serverNameListLength > extensionsEnd) {
+            log('Server name list length exceeds extension size');
+            break; // Exit the loop, extension parsing is broken
+          }
+
+          // End position of server name list
+          const serverNameListEnd = pos + serverNameListLength;
+
+          // Iterate through server names
+          while (pos + 3 <= serverNameListEnd) {
+            // Check name type (must be HOST_NAME_TYPE = 0 for hostname)
+            const nameType = buffer[pos];
+            log(`Name type: ${nameType}`);
+
+            if (nameType !== this.TLS_SNI_HOST_NAME_TYPE) {
+              log(`Unsupported name type: ${nameType}`);
+              pos += 1; // Skip name type (1 byte)
+
+              // Skip name length (2 bytes) and name data
+              if (pos + 2 <= serverNameListEnd) {
+                const nameLength = (buffer[pos] << 8) + buffer[pos + 1];
+                pos += 2 + nameLength;
+              } else {
+                log('Invalid server name entry');
+                break;
+              }
+              continue;
+            }
+
+            // Skip name type (1 byte)
+            pos += 1;
+
+            // Ensure we have enough bytes for name length
+            if (pos + 2 > serverNameListEnd) {
+              log('Server name entry too small for name length');
+              break;
+            }
+
+            // Parse name length (2 bytes, big-endian)
+            const nameLength = (buffer[pos] << 8) + buffer[pos + 1];
+            log(`Name length: ${nameLength}`);
+
+            // Skip name length (2 bytes)
+            pos += 2;
+
+            // Ensure we have enough bytes for the name
+            if (pos + nameLength > serverNameListEnd) {
+              log('Name length exceeds server name list size');
+              break;
+            }
+
+            // Extract server name (hostname)
+            const serverName = buffer.slice(pos, pos + nameLength).toString('utf8');
+            log(`Extracted server name: ${serverName}`);
+            return serverName;
+          }
+        } else if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
+          // If we encounter a session ticket extension, mark it for later
+          log('Found session ticket extension');
+          hasSessionTicket = true;
+          pos += extensionLength; // Skip this extension
+        } else {
+          // Skip this extension
+          pos += extensionLength;
+        }
+      }
+
+      // Log if we found a session ticket but no SNI
+      if (hasSessionTicket) {
+        log('Session ticket present but no SNI found - possible resumption scenario');
+      }
+
+      log('No SNI extension found in ClientHello');
+      return undefined;
+    } catch (error) {
+      log(`Error parsing SNI: ${error instanceof Error ? error.message : String(error)}`);
+      return undefined;
+    }
+  }
+
+  /**
+   * Attempts to extract SNI from an initial ClientHello packet and handles
+   * session resumption edge cases more robustly than the standard extraction.
+   * 
+   * This method is specifically designed for Chrome and other browsers that
+   * may send different ClientHello formats during session resumption.
+   * 
+   * @param buffer - The buffer containing the TLS ClientHello message
+   * @param enableLogging - Whether to enable detailed debug logging
+   * @returns The extracted server name or undefined if not found
+   */
+  public static extractSNIWithResumptionSupport(
+    buffer: Buffer, 
+    enableLogging: boolean = false
+  ): string | undefined {
+    // First try the standard SNI extraction
+    const standardSni = this.extractSNI(buffer, enableLogging);
+    if (standardSni) {
+      return standardSni;
+    }
+    
+    // If standard extraction failed and we have a valid ClientHello,
+    // this might be a session resumption with non-standard format
+    if (this.isClientHello(buffer)) {
+      if (enableLogging) {
+        console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption');
+      }
+      
+      // Additional handling could be implemented here for specific browser behaviors
+      // For now, this is a placeholder for future improvements
+    }
+    
+    return undefined;
+  }
+}
\ No newline at end of file
diff --git a/ts/index.ts b/ts/index.ts
index 4956c92..804308f 100644
--- a/ts/index.ts
+++ b/ts/index.ts
@@ -3,3 +3,4 @@ export * from './classes.networkproxy.js';
 export * from './classes.portproxy.js';
 export * from './classes.port80handler.js';
 export * from './classes.sslredirect.js';
+export * from './classes.snihandler.js';