@push.rocks/smartproxy 3.37.1 → 3.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,8 @@ export class SniHandler {
11
11
  private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
12
12
  private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
13
13
  private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
14
+ private static readonly TLS_PSK_EXTENSION_TYPE = 0x0029; // Pre-Shared Key extension type for TLS 1.3
15
+ private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE = 0x002D; // PSK Key Exchange Modes
14
16
 
15
17
  /**
16
18
  * Checks if a buffer contains a TLS handshake message (record type 22)
@@ -178,6 +180,7 @@ export class SniHandler {
178
180
 
179
181
  // Track if we found session tickets (for improved resumption handling)
180
182
  let hasSessionTicket = false;
183
+ let hasPskExtension = false;
181
184
 
182
185
  // Iterate through extensions
183
186
  while (pos + 4 <= extensionsEnd) {
@@ -275,15 +278,21 @@ export class SniHandler {
275
278
  log('Found session ticket extension');
276
279
  hasSessionTicket = true;
277
280
  pos += extensionLength; // Skip this extension
281
+ } else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
282
+ // TLS 1.3 PSK extension - mark for resumption support
283
+ log('Found PSK extension (TLS 1.3 resumption indicator)');
284
+ hasPskExtension = true;
285
+ // We'll skip the extension here and process it separately if needed
286
+ pos += extensionLength;
278
287
  } else {
279
288
  // Skip this extension
280
289
  pos += extensionLength;
281
290
  }
282
291
  }
283
292
 
284
- // Log if we found a session ticket but no SNI
285
- if (hasSessionTicket) {
286
- log('Session ticket present but no SNI found - possible resumption scenario');
293
+ // Log if we found session resumption indicators but no SNI
294
+ if (hasSessionTicket || hasPskExtension) {
295
+ log('Session resumption indicators present but no SNI found');
287
296
  }
288
297
 
289
298
  log('No SNI extension found in ClientHello');
@@ -294,12 +303,177 @@ export class SniHandler {
294
303
  }
295
304
  }
296
305
 
306
+ /**
307
+ * Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
308
+ *
309
+ * In TLS 1.3, when a client attempts to resume a session, it may include
310
+ * the server name in the PSK identity hint rather than in the SNI extension.
311
+ *
312
+ * @param buffer - The buffer containing the TLS ClientHello message
313
+ * @param enableLogging - Whether to enable detailed debug logging
314
+ * @returns The extracted server name or undefined if not found
315
+ */
316
+ public static extractSNIFromPSKExtension(
317
+ buffer: Buffer,
318
+ enableLogging: boolean = false
319
+ ): string | undefined {
320
+ const log = (message: string) => {
321
+ if (enableLogging) {
322
+ console.log(`[PSK-SNI Extraction] ${message}`);
323
+ }
324
+ };
325
+
326
+ try {
327
+ // Ensure this is a ClientHello
328
+ if (!this.isClientHello(buffer)) {
329
+ log('Not a ClientHello message');
330
+ return undefined;
331
+ }
332
+
333
+ // Find the start position of extensions
334
+ let pos = 5; // Start after record header
335
+
336
+ // Skip handshake type (1 byte)
337
+ pos += 1;
338
+
339
+ // Skip handshake length (3 bytes)
340
+ pos += 3;
341
+
342
+ // Skip client version (2 bytes)
343
+ pos += 2;
344
+
345
+ // Skip client random (32 bytes)
346
+ pos += 32;
347
+
348
+ // Skip session ID
349
+ if (pos + 1 > buffer.length) return undefined;
350
+ const sessionIdLength = buffer[pos];
351
+ pos += 1 + sessionIdLength;
352
+
353
+ // Skip cipher suites
354
+ if (pos + 2 > buffer.length) return undefined;
355
+ const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
356
+ pos += 2 + cipherSuitesLength;
357
+
358
+ // Skip compression methods
359
+ if (pos + 1 > buffer.length) return undefined;
360
+ const compressionMethodsLength = buffer[pos];
361
+ pos += 1 + compressionMethodsLength;
362
+
363
+ // Check if we have extensions
364
+ if (pos + 2 > buffer.length) {
365
+ log('No extensions present');
366
+ return undefined;
367
+ }
368
+
369
+ // Get extensions length
370
+ const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
371
+ pos += 2;
372
+
373
+ // Extensions end position
374
+ const extensionsEnd = pos + extensionsLength;
375
+ if (extensionsEnd > buffer.length) return undefined;
376
+
377
+ // Look for PSK extension
378
+ while (pos + 4 <= extensionsEnd) {
379
+ const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
380
+ pos += 2;
381
+
382
+ const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
383
+ pos += 2;
384
+
385
+ if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
386
+ log('Found PSK extension');
387
+
388
+ // PSK extension structure:
389
+ // 2 bytes: identities list length
390
+ if (pos + 2 > extensionsEnd) break;
391
+ const identitiesLength = (buffer[pos] << 8) + buffer[pos + 1];
392
+ pos += 2;
393
+
394
+ // End of identities list
395
+ const identitiesEnd = pos + identitiesLength;
396
+ if (identitiesEnd > extensionsEnd) break;
397
+
398
+ // Process each PSK identity
399
+ while (pos + 2 <= identitiesEnd) {
400
+ // Identity length (2 bytes)
401
+ if (pos + 2 > identitiesEnd) break;
402
+ const identityLength = (buffer[pos] << 8) + buffer[pos + 1];
403
+ pos += 2;
404
+
405
+ if (pos + identityLength > identitiesEnd) break;
406
+
407
+ // Try to extract hostname from identity
408
+ // Chrome often embeds the hostname in the PSK identity
409
+ // This is a heuristic as there's no standard format
410
+ if (identityLength > 0) {
411
+ const identity = buffer.slice(pos, pos + identityLength);
412
+
413
+ // Skip identity bytes
414
+ pos += identityLength;
415
+
416
+ // Skip obfuscated ticket age (4 bytes)
417
+ pos += 4;
418
+
419
+ // Try to parse the identity as UTF-8
420
+ try {
421
+ const identityStr = identity.toString('utf8');
422
+ log(`PSK identity: ${identityStr}`);
423
+
424
+ // Check if the identity contains hostname hints
425
+ // Chrome often embeds the hostname in a known format
426
+ // Try to extract using common patterns
427
+
428
+ // Pattern 1: Look for domain name pattern
429
+ 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;
430
+ const domainMatch = identityStr.match(domainPattern);
431
+ if (domainMatch && domainMatch[0]) {
432
+ log(`Found domain in PSK identity: ${domainMatch[0]}`);
433
+ return domainMatch[0];
434
+ }
435
+
436
+ // Pattern 2: Chrome sometimes uses a specific format with delimiters
437
+ // This is a heuristic approach since the format isn't standardized
438
+ const parts = identityStr.split('|');
439
+ if (parts.length > 1) {
440
+ for (const part of parts) {
441
+ if (part.includes('.') && !part.includes('/')) {
442
+ const possibleDomain = part.trim();
443
+ if (/^[a-z0-9.-]+$/i.test(possibleDomain)) {
444
+ log(`Found possible domain in PSK delimiter format: ${possibleDomain}`);
445
+ return possibleDomain;
446
+ }
447
+ }
448
+ }
449
+ }
450
+ } catch (e) {
451
+ log('Failed to parse PSK identity as UTF-8');
452
+ }
453
+ }
454
+ }
455
+ } else {
456
+ // Skip this extension
457
+ pos += extensionLength;
458
+ }
459
+ }
460
+
461
+ log('No hostname found in PSK extension');
462
+ return undefined;
463
+ } catch (error) {
464
+ log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`);
465
+ return undefined;
466
+ }
467
+ }
468
+
297
469
  /**
298
470
  * Attempts to extract SNI from an initial ClientHello packet and handles
299
471
  * session resumption edge cases more robustly than the standard extraction.
300
472
  *
301
- * This method is specifically designed for Chrome and other browsers that
302
- * may send different ClientHello formats during session resumption.
473
+ * This method handles:
474
+ * 1. Standard SNI extraction
475
+ * 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
476
+ * 3. Session ticket-based resumption
303
477
  *
304
478
  * @param buffer - The buffer containing the TLS ClientHello message
305
479
  * @param enableLogging - Whether to enable detailed debug logging
@@ -312,6 +486,9 @@ export class SniHandler {
312
486
  // First try the standard SNI extraction
313
487
  const standardSni = this.extractSNI(buffer, enableLogging);
314
488
  if (standardSni) {
489
+ if (enableLogging) {
490
+ console.log(`[SNI Extraction] Found standard SNI: ${standardSni}`);
491
+ }
315
492
  return standardSni;
316
493
  }
317
494
 
@@ -322,8 +499,19 @@ export class SniHandler {
322
499
  console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption');
323
500
  }
324
501
 
325
- // Additional handling could be implemented here for specific browser behaviors
326
- // For now, this is a placeholder for future improvements
502
+ // Try to extract from PSK extension (TLS 1.3 resumption)
503
+ const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
504
+ if (pskSni) {
505
+ if (enableLogging) {
506
+ console.log(`[SNI Extraction] Extracted SNI from PSK extension: ${pskSni}`);
507
+ }
508
+ return pskSni;
509
+ }
510
+
511
+ // Could add more browser-specific heuristics here if needed
512
+ if (enableLogging) {
513
+ console.log('[SNI Extraction] Failed to extract SNI from resumption mechanisms');
514
+ }
327
515
  }
328
516
 
329
517
  return undefined;