@push.rocks/smartproxy 19.3.1 → 19.3.2

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.
@@ -1,14 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import type {
3
- IConnectionRecord,
4
- ISmartProxyOptions
5
- } from './models/interfaces.js';
2
+ import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
6
3
  // Route checking functions have been removed
7
- import type {
8
- IRouteConfig,
9
- IRouteAction,
10
- IRouteContext
11
- } from './models/route-types.js';
4
+ import type { IRouteConfig, IRouteAction, IRouteContext } from './models/route-types.js';
12
5
  import { ConnectionManager } from './connection-manager.js';
13
6
  import { SecurityManager } from './security-manager.js';
14
7
  import { TlsManager } from './tls-manager.js';
@@ -75,7 +68,7 @@ export class RouteConnectionHandler {
75
68
 
76
69
  // Additional properties
77
70
  timestamp: Date.now(),
78
- connectionId: options.connectionId
71
+ connectionId: options.connectionId,
79
72
  };
80
73
  }
81
74
 
@@ -232,16 +225,19 @@ export class RouteConnectionHandler {
232
225
  console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`);
233
226
  if (record.incomingTerminationReason === null) {
234
227
  record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
235
- this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
228
+ this.connectionManager.incrementTerminationStat(
229
+ 'incoming',
230
+ 'session_ticket_blocked_no_sni'
231
+ );
236
232
  }
237
233
  const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
238
- try {
239
- socket.cork();
240
- socket.write(alert);
241
- socket.uncork();
242
- socket.end();
243
- } catch {
244
- socket.end();
234
+ try {
235
+ socket.cork();
236
+ socket.write(alert);
237
+ socket.uncork();
238
+ socket.end();
239
+ } catch {
240
+ socket.end();
245
241
  }
246
242
  this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
247
243
  return;
@@ -262,7 +258,7 @@ export class RouteConnectionHandler {
262
258
  * Route the connection based on match criteria
263
259
  */
264
260
  private routeConnection(
265
- socket: plugins.net.Socket,
261
+ socket: plugins.net.Socket,
266
262
  record: IConnectionRecord,
267
263
  serverName: string,
268
264
  initialChunk?: Buffer
@@ -277,11 +273,13 @@ export class RouteConnectionHandler {
277
273
  domain: serverName,
278
274
  clientIp: remoteIP,
279
275
  path: undefined, // We don't have path info at this point
280
- tlsVersion: undefined // We don't extract TLS version yet
276
+ tlsVersion: undefined, // We don't extract TLS version yet
281
277
  });
282
278
 
283
279
  if (!routeMatch) {
284
- console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`);
280
+ console.log(
281
+ `[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`
282
+ );
285
283
 
286
284
  // No matching route, use default/fallback handling
287
285
  console.log(`[${connectionId}] Using default route handling for connection`);
@@ -304,7 +302,7 @@ export class RouteConnectionHandler {
304
302
  }
305
303
  }
306
304
  }
307
-
305
+
308
306
  // Setup direct connection with default settings
309
307
  if (this.settings.defaults?.target) {
310
308
  // Use defaults from configuration
@@ -328,54 +326,56 @@ export class RouteConnectionHandler {
328
326
  return;
329
327
  }
330
328
  }
331
-
329
+
332
330
  // A matching route was found
333
331
  const route = routeMatch.route;
334
-
332
+
335
333
  if (this.settings.enableDetailedLogging) {
336
334
  console.log(
337
- `[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${serverName || 'connection'} on port ${localPort}`
335
+ `[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${
336
+ serverName || 'connection'
337
+ } on port ${localPort}`
338
338
  );
339
339
  }
340
-
340
+
341
341
  // Check if this route uses NFTables for forwarding
342
342
  if (route.action.forwardingEngine === 'nftables') {
343
343
  // For NFTables routes, we don't need to do anything at the application level
344
344
  // The packet is forwarded at the kernel level
345
-
345
+
346
346
  // Log the connection
347
347
  console.log(
348
348
  `[${connectionId}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}`
349
349
  );
350
-
350
+
351
351
  // Just close the socket in our application since it's handled at kernel level
352
352
  socket.end();
353
353
  this.connectionManager.cleanupConnection(record, 'nftables_handled');
354
354
  return;
355
355
  }
356
-
356
+
357
357
  // Handle the route based on its action type
358
358
  switch (route.action.type) {
359
359
  case 'forward':
360
360
  return this.handleForwardAction(socket, record, route, initialChunk);
361
-
361
+
362
362
  case 'redirect':
363
363
  return this.handleRedirectAction(socket, record, route);
364
-
364
+
365
365
  case 'block':
366
366
  return this.handleBlockAction(socket, record, route);
367
-
367
+
368
368
  case 'static':
369
369
  this.handleStaticAction(socket, record, route);
370
370
  return;
371
-
371
+
372
372
  default:
373
373
  console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
374
374
  socket.end();
375
375
  this.connectionManager.cleanupConnection(record, 'unknown_action');
376
376
  }
377
377
  }
378
-
378
+
379
379
  /**
380
380
  * Handle a forward action for a route
381
381
  */
@@ -394,33 +394,35 @@ export class RouteConnectionHandler {
394
394
  if (this.settings.enableDetailedLogging) {
395
395
  console.log(
396
396
  `[${record.id}] Connection forwarded by NFTables (kernel-level): ` +
397
- `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
398
- ` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
397
+ `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
398
+ ` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
399
399
  );
400
400
  } else {
401
401
  console.log(
402
- `[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${record.localPort} (Route: "${route.name || 'unnamed'}")`
402
+ `[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${
403
+ record.localPort
404
+ } (Route: "${route.name || 'unnamed'}")`
403
405
  );
404
406
  }
405
-
407
+
406
408
  // Additional NFTables-specific logging if configured
407
409
  if (action.nftables) {
408
410
  const nftConfig = action.nftables;
409
411
  if (this.settings.enableDetailedLogging) {
410
412
  console.log(
411
413
  `[${record.id}] NFTables config: ` +
412
- `protocol=${nftConfig.protocol || 'tcp'}, ` +
413
- `preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` +
414
- `priority=${nftConfig.priority || 'default'}, ` +
415
- `maxRate=${nftConfig.maxRate || 'unlimited'}`
414
+ `protocol=${nftConfig.protocol || 'tcp'}, ` +
415
+ `preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` +
416
+ `priority=${nftConfig.priority || 'default'}, ` +
417
+ `maxRate=${nftConfig.maxRate || 'unlimited'}`
416
418
  );
417
419
  }
418
420
  }
419
-
421
+
420
422
  // This connection is handled at the kernel level, no need to process at application level
421
423
  // Close the socket gracefully in our application layer
422
424
  socket.end();
423
-
425
+
424
426
  // Mark the connection as handled by NFTables for proper cleanup
425
427
  record.nftablesHandled = true;
426
428
  this.connectionManager.initiateCleanupOnce(record, 'nftables_handled');
@@ -445,7 +447,7 @@ export class RouteConnectionHandler {
445
447
  isTls: record.isTLS || false,
446
448
  tlsVersion: record.tlsVersion,
447
449
  routeName: route.name,
448
- routeId: route.id
450
+ routeId: route.id,
449
451
  });
450
452
 
451
453
  // Cache the context for potential reuse
@@ -457,7 +459,11 @@ export class RouteConnectionHandler {
457
459
  try {
458
460
  targetHost = action.target.host(routeContext);
459
461
  if (this.settings.enableDetailedLogging) {
460
- console.log(`[${connectionId}] Dynamic host resolved to: ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost}`);
462
+ console.log(
463
+ `[${connectionId}] Dynamic host resolved to: ${
464
+ Array.isArray(targetHost) ? targetHost.join(', ') : targetHost
465
+ }`
466
+ );
461
467
  }
462
468
  } catch (err) {
463
469
  console.log(`[${connectionId}] Error in host mapping function: ${err}`);
@@ -480,7 +486,9 @@ export class RouteConnectionHandler {
480
486
  try {
481
487
  targetPort = action.target.port(routeContext);
482
488
  if (this.settings.enableDetailedLogging) {
483
- console.log(`[${connectionId}] Dynamic port mapping: ${record.localPort} -> ${targetPort}`);
489
+ console.log(
490
+ `[${connectionId}] Dynamic port mapping: ${record.localPort} -> ${targetPort}`
491
+ );
484
492
  }
485
493
  // Store the resolved target port in the context for potential future use
486
494
  routeContext.targetPort = targetPort;
@@ -509,7 +517,7 @@ export class RouteConnectionHandler {
509
517
  if (this.settings.enableDetailedLogging) {
510
518
  console.log(`[${connectionId}] Using TLS passthrough to ${selectedHost}:${targetPort}`);
511
519
  }
512
-
520
+
513
521
  return this.setupDirectConnection(
514
522
  socket,
515
523
  record,
@@ -519,7 +527,7 @@ export class RouteConnectionHandler {
519
527
  selectedHost,
520
528
  targetPort
521
529
  );
522
-
530
+
523
531
  case 'terminate':
524
532
  case 'terminate-and-reencrypt':
525
533
  // For TLS termination, use NetworkProxy
@@ -529,7 +537,7 @@ export class RouteConnectionHandler {
529
537
  `[${connectionId}] Using NetworkProxy for TLS termination to ${action.target.host}`
530
538
  );
531
539
  }
532
-
540
+
533
541
  // If we have an initial chunk with TLS data, start processing it
534
542
  if (initialChunk && record.isTLS) {
535
543
  this.networkProxyBridge.forwardToNetworkProxy(
@@ -542,7 +550,7 @@ export class RouteConnectionHandler {
542
550
  );
543
551
  return;
544
552
  }
545
-
553
+
546
554
  // This shouldn't normally happen - we should have TLS data at this point
547
555
  console.log(`[${connectionId}] TLS termination route without TLS data`);
548
556
  socket.end();
@@ -558,9 +566,11 @@ export class RouteConnectionHandler {
558
566
  } else {
559
567
  // No TLS settings - basic forwarding
560
568
  if (this.settings.enableDetailedLogging) {
561
- console.log(`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`);
569
+ console.log(
570
+ `[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`
571
+ );
562
572
  }
563
-
573
+
564
574
  // Get the appropriate host value
565
575
  let targetHost: string;
566
576
 
@@ -602,7 +612,7 @@ export class RouteConnectionHandler {
602
612
  );
603
613
  }
604
614
  }
605
-
615
+
606
616
  /**
607
617
  * Handle a redirect action for a route
608
618
  */
@@ -613,7 +623,7 @@ export class RouteConnectionHandler {
613
623
  ): void {
614
624
  const connectionId = record.id;
615
625
  const action = route.action;
616
-
626
+
617
627
  // We should have a redirect configuration
618
628
  if (!action.redirect) {
619
629
  console.log(`[${connectionId}] Redirect action missing redirect configuration`);
@@ -621,7 +631,7 @@ export class RouteConnectionHandler {
621
631
  this.connectionManager.cleanupConnection(record, 'missing_redirect');
622
632
  return;
623
633
  }
624
-
634
+
625
635
  // For TLS connections, we can't do redirects at the TCP level
626
636
  if (record.isTLS) {
627
637
  console.log(`[${connectionId}] Cannot redirect TLS connection at TCP level`);
@@ -629,16 +639,16 @@ export class RouteConnectionHandler {
629
639
  this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
630
640
  return;
631
641
  }
632
-
642
+
633
643
  // Wait for the first HTTP request to perform the redirect
634
644
  const dataListeners: ((chunk: Buffer) => void)[] = [];
635
-
645
+
636
646
  const httpDataHandler = (chunk: Buffer) => {
637
647
  // Remove all data listeners to avoid duplicated processing
638
648
  for (const listener of dataListeners) {
639
649
  socket.removeListener('data', listener);
640
650
  }
641
-
651
+
642
652
  // Parse HTTP request to get path
643
653
  try {
644
654
  const headersEnd = chunk.indexOf('\r\n\r\n');
@@ -648,21 +658,21 @@ export class RouteConnectionHandler {
648
658
  dataListeners.push(httpDataHandler);
649
659
  return;
650
660
  }
651
-
661
+
652
662
  const httpHeaders = chunk.slice(0, headersEnd).toString();
653
663
  const requestLine = httpHeaders.split('\r\n')[0];
654
664
  const [method, path] = requestLine.split(' ');
655
-
665
+
656
666
  // Extract Host header
657
667
  const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i);
658
668
  const host = hostMatch ? hostMatch[1].trim() : record.lockedDomain || '';
659
-
669
+
660
670
  // Process the redirect URL with template variables
661
671
  let redirectUrl = action.redirect.to;
662
672
  redirectUrl = redirectUrl.replace(/\{domain\}/g, host);
663
673
  redirectUrl = redirectUrl.replace(/\{path\}/g, path || '');
664
674
  redirectUrl = redirectUrl.replace(/\{port\}/g, record.localPort.toString());
665
-
675
+
666
676
  // Prepare the HTTP redirect response
667
677
  const redirectResponse = [
668
678
  `HTTP/1.1 ${action.redirect.status} Moved`,
@@ -670,13 +680,15 @@ export class RouteConnectionHandler {
670
680
  'Connection: close',
671
681
  'Content-Length: 0',
672
682
  '',
673
- ''
683
+ '',
674
684
  ].join('\r\n');
675
-
685
+
676
686
  if (this.settings.enableDetailedLogging) {
677
- console.log(`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`);
687
+ console.log(
688
+ `[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`
689
+ );
678
690
  }
679
-
691
+
680
692
  // Send the redirect response
681
693
  socket.end(redirectResponse);
682
694
  this.connectionManager.initiateCleanupOnce(record, 'redirect_complete');
@@ -686,12 +698,12 @@ export class RouteConnectionHandler {
686
698
  this.connectionManager.initiateCleanupOnce(record, 'redirect_error');
687
699
  }
688
700
  };
689
-
701
+
690
702
  // Setup the HTTP data handler
691
703
  socket.once('data', httpDataHandler);
692
704
  dataListeners.push(httpDataHandler);
693
705
  }
694
-
706
+
695
707
  /**
696
708
  * Handle a block action for a route
697
709
  */
@@ -701,16 +713,18 @@ export class RouteConnectionHandler {
701
713
  route: IRouteConfig
702
714
  ): void {
703
715
  const connectionId = record.id;
704
-
716
+
705
717
  if (this.settings.enableDetailedLogging) {
706
- console.log(`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`);
718
+ console.log(
719
+ `[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`
720
+ );
707
721
  }
708
-
722
+
709
723
  // Simply close the connection
710
724
  socket.end();
711
725
  this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
712
726
  }
713
-
727
+
714
728
  /**
715
729
  * Handle a static action for a route
716
730
  */
@@ -720,43 +734,59 @@ export class RouteConnectionHandler {
720
734
  route: IRouteConfig
721
735
  ): Promise<void> {
722
736
  const connectionId = record.id;
723
-
737
+
724
738
  if (!route.action.handler) {
725
739
  console.error(`[${connectionId}] Static route '${route.name}' has no handler`);
726
740
  socket.end();
727
741
  this.connectionManager.cleanupConnection(record, 'no_handler');
728
742
  return;
729
743
  }
730
-
744
+
731
745
  let buffer = Buffer.alloc(0);
732
-
746
+ let processingData = false;
747
+
733
748
  const handleHttpData = async (chunk: Buffer) => {
749
+ // Accumulate the data
734
750
  buffer = Buffer.concat([buffer, chunk]);
735
-
751
+
752
+ // Prevent concurrent processing of the same buffer
753
+ if (processingData) return;
754
+ processingData = true;
755
+
756
+ try {
757
+ // Process data until we have a complete request or need more data
758
+ await processBuffer();
759
+ } finally {
760
+ processingData = false;
761
+ }
762
+ };
763
+
764
+ const processBuffer = async () => {
736
765
  // Look for end of HTTP headers
737
766
  const headerEndIndex = buffer.indexOf('\r\n\r\n');
738
767
  if (headerEndIndex === -1) {
739
768
  // Need more data
740
- if (buffer.length > 8192) { // Prevent excessive buffering
769
+ if (buffer.length > 8192) {
770
+ // Prevent excessive buffering
741
771
  console.error(`[${connectionId}] HTTP headers too large`);
742
772
  socket.end();
743
773
  this.connectionManager.cleanupConnection(record, 'headers_too_large');
744
774
  }
745
- return;
775
+ return; // Wait for more data to arrive
746
776
  }
747
-
777
+
748
778
  // Parse the HTTP request
749
779
  const headerBuffer = buffer.slice(0, headerEndIndex);
750
780
  const headers = headerBuffer.toString();
751
781
  const lines = headers.split('\r\n');
752
-
782
+
753
783
  if (lines.length === 0) {
754
784
  console.error(`[${connectionId}] Invalid HTTP request`);
755
785
  socket.end();
756
786
  this.connectionManager.cleanupConnection(record, 'invalid_request');
757
787
  return;
758
788
  }
759
-
789
+
760
790
  // Parse request line
761
791
  const requestLine = lines[0];
762
792
  const requestParts = requestLine.split(' ');
@@ -766,9 +796,9 @@ export class RouteConnectionHandler {
766
796
  this.connectionManager.cleanupConnection(record, 'invalid_request_line');
767
797
  return;
768
798
  }
769
-
799
+
770
800
  const [method, path, httpVersion] = requestParts;
771
-
801
+
772
802
  // Parse headers
773
803
  const headersMap: Record<string, string> = {};
774
804
  for (let i = 1; i < lines.length; i++) {
@@ -779,7 +809,29 @@ export class RouteConnectionHandler {
779
809
  headersMap[key] = value;
780
810
  }
781
811
  }
782
-
812
+
813
+ // Check for Content-Length to handle request body
814
+ const requestBodyLength = parseInt(headersMap['content-length'] || '0', 10);
815
+ const bodyStartIndex = headerEndIndex + 4; // Skip the \r\n\r\n
816
+
817
+ // If there's a body, ensure we have the full body
818
+ if (requestBodyLength > 0) {
819
+ const totalExpectedLength = bodyStartIndex + requestBodyLength;
820
+
821
+ // If we don't have the complete body yet, wait for more data
822
+ if (buffer.length < totalExpectedLength) {
823
+ // Implement a reasonable body size limit to prevent memory issues
824
+ if (requestBodyLength > 1024 * 1024) {
825
+ // 1MB limit
826
+ console.error(`[${connectionId}] Request body too large`);
827
+ socket.end();
828
+ this.connectionManager.cleanupConnection(record, 'body_too_large');
829
+ return;
830
+ }
831
+ return; // Wait for more data
832
+ }
833
+ }
834
+
783
835
  // Extract query string if present
784
836
  let pathname = path;
785
837
  let query: string | undefined;
@@ -788,8 +840,20 @@ export class RouteConnectionHandler {
788
840
  pathname = path.slice(0, queryIndex);
789
841
  query = path.slice(queryIndex + 1);
790
842
  }
791
-
843
+
792
844
  try {
845
+ // Get request body if present
846
+ let requestBody: Buffer | undefined;
847
+ if (requestBodyLength > 0) {
848
+ requestBody = buffer.slice(bodyStartIndex, bodyStartIndex + requestBodyLength);
849
+ }
850
+
851
+ // Pause socket to prevent data loss during async processing
852
+ socket.pause();
853
+
854
+ // Remove the data listener since we're handling the request
855
+ socket.removeListener('data', handleHttpData);
856
+
793
857
  // Build route context with parsed HTTP information
794
858
  const context: IRouteContext = {
795
859
  port: record.localPort,
@@ -803,66 +867,89 @@ export class RouteConnectionHandler {
803
867
  isTls: record.isTLS,
804
868
  tlsVersion: record.tlsVersion,
805
869
  routeName: route.name,
806
- routeId: route.name,
870
+ routeId: route.id,
807
871
  timestamp: Date.now(),
808
- connectionId
872
+ connectionId,
809
873
  };
810
-
811
- // Remove the data listener since we're handling the request
812
- socket.removeListener('data', handleHttpData);
813
-
814
- // Call the handler with the properly parsed context
815
- const response = await route.action.handler(context);
816
-
874
+
875
+ // Since IRouteContext doesn't have a body property,
876
+ // we need an alternative approach to handle the body
877
+ let response;
878
+
879
+ if (requestBody) {
880
+ if (this.settings.enableDetailedLogging) {
881
+ console.log(
882
+ `[${connectionId}] Processing request with body (${requestBody.length} bytes)`
883
+ );
884
+ }
885
+
886
+ // Pass the body as an additional parameter by extending the context object
887
+ // This is not type-safe, but it allows handlers that expect a body to work
888
+ const extendedContext = {
889
+ ...context,
890
+ // Provide both raw buffer and string representation
891
+ requestBody: requestBody,
892
+ requestBodyText: requestBody.toString(),
893
+ };
894
+
895
+ // Call the handler with the extended context
896
+ // The handler needs to know to look for the non-standard properties
897
+ response = await route.action.handler(extendedContext as any);
898
+ } else {
899
+ // Call the handler with the standard context
900
+ response = await route.action.handler(context);
901
+ }
902
+
817
903
  // Prepare the HTTP response
818
904
  const responseHeaders = response.headers || {};
819
905
  const contentLength = Buffer.byteLength(response.body || '');
820
906
  responseHeaders['Content-Length'] = contentLength.toString();
821
-
907
+
822
908
  if (!responseHeaders['Content-Type']) {
823
909
  responseHeaders['Content-Type'] = 'text/plain';
824
910
  }
825
-
911
+
826
912
  // Build the response
827
913
  let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
828
914
  for (const [key, value] of Object.entries(responseHeaders)) {
829
915
  httpResponse += `${key}: ${value}\r\n`;
830
916
  }
831
917
  httpResponse += '\r\n';
832
-
918
+
833
919
  // Send response
834
920
  socket.write(httpResponse);
835
921
  if (response.body) {
836
922
  socket.write(response.body);
837
923
  }
838
924
  socket.end();
839
-
925
+
840
926
  this.connectionManager.cleanupConnection(record, 'completed');
841
927
  } catch (error) {
842
928
  console.error(`[${connectionId}] Error in static handler: ${error}`);
843
-
929
+
844
930
  // Send error response
845
- const errorResponse = 'HTTP/1.1 500 Internal Server Error\r\n' +
846
- 'Content-Type: text/plain\r\n' +
847
- 'Content-Length: 21\r\n' +
848
- '\r\n' +
849
- 'Internal Server Error';
931
+ const errorResponse =
932
+ 'HTTP/1.1 500 Internal Server Error\r\n' +
933
+ 'Content-Type: text/plain\r\n' +
934
+ 'Content-Length: 21\r\n' +
935
+ '\r\n' +
936
+ 'Internal Server Error';
850
937
  socket.write(errorResponse);
851
938
  socket.end();
852
-
939
+
853
940
  this.connectionManager.cleanupConnection(record, 'handler_error');
854
941
  }
855
942
  };
856
-
943
+
857
944
  // Listen for data
858
945
  socket.on('data', handleHttpData);
859
-
946
+
860
947
  // Ensure cleanup on socket close
861
948
  socket.once('close', () => {
862
949
  socket.removeListener('data', handleHttpData);
863
950
  });
864
951
  }
865
-
952
+
866
953
  /**
867
954
  * Sets up a direct connection to the target
868
955
  */
@@ -878,22 +965,23 @@ export class RouteConnectionHandler {
878
965
  const connectionId = record.id;
879
966
 
880
967
  // Determine target host and port if not provided
881
- const finalTargetHost = targetHost ||
882
- record.targetHost ||
883
- (this.settings.defaults?.target?.host || 'localhost');
968
+ const finalTargetHost =
969
+ targetHost || record.targetHost || this.settings.defaults?.target?.host || 'localhost';
884
970
 
885
971
  // Determine target port
886
- const finalTargetPort = targetPort ||
972
+ const finalTargetPort =
973
+ targetPort ||
887
974
  record.targetPort ||
888
- (overridePort !== undefined ? overridePort :
889
- (this.settings.defaults?.target?.port || 443));
975
+ (overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443);
890
976
 
891
977
  // Update record with final target information
892
978
  record.targetHost = finalTargetHost;
893
979
  record.targetPort = finalTargetPort;
894
980
 
895
981
  if (this.settings.enableDetailedLogging) {
896
- console.log(`[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}`);
982
+ console.log(
983
+ `[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}`
984
+ );
897
985
  }
898
986
 
899
987
  // Setup connection options
@@ -906,53 +994,53 @@ export class RouteConnectionHandler {
906
994
  if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
907
995
  connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
908
996
  }
909
-
997
+
910
998
  // Create a safe queue for incoming data
911
999
  const dataQueue: Buffer[] = [];
912
1000
  let queueSize = 0;
913
1001
  let processingQueue = false;
914
1002
  let drainPending = false;
915
1003
  let pipingEstablished = false;
916
-
1004
+
917
1005
  // Pause the incoming socket to prevent buffer overflows
918
1006
  socket.pause();
919
-
1007
+
920
1008
  // Function to safely process the data queue without losing events
921
1009
  const processDataQueue = () => {
922
1010
  if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
923
-
1011
+
924
1012
  processingQueue = true;
925
-
1013
+
926
1014
  try {
927
1015
  // Process all queued chunks with the current active handler
928
1016
  while (dataQueue.length > 0) {
929
1017
  const chunk = dataQueue.shift()!;
930
1018
  queueSize -= chunk.length;
931
-
1019
+
932
1020
  // Once piping is established, we shouldn't get here,
933
1021
  // but just in case, pass to the outgoing socket directly
934
1022
  if (pipingEstablished && record.outgoing) {
935
1023
  record.outgoing.write(chunk);
936
1024
  continue;
937
1025
  }
938
-
1026
+
939
1027
  // Track bytes received
940
1028
  record.bytesReceived += chunk.length;
941
-
1029
+
942
1030
  // Check for TLS handshake
943
1031
  if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
944
1032
  record.isTLS = true;
945
-
1033
+
946
1034
  if (this.settings.enableTlsDebugLogging) {
947
1035
  console.log(
948
1036
  `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
949
1037
  );
950
1038
  }
951
1039
  }
952
-
1040
+
953
1041
  // Check if adding this chunk would exceed the buffer limit
954
1042
  const newSize = record.pendingDataSize + chunk.length;
955
-
1043
+
956
1044
  if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
957
1045
  console.log(
958
1046
  `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
@@ -961,7 +1049,7 @@ export class RouteConnectionHandler {
961
1049
  this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
962
1050
  return;
963
1051
  }
964
-
1052
+
965
1053
  // Buffer the chunk and update the size counter
966
1054
  record.pendingData.push(Buffer.from(chunk));
967
1055
  record.pendingDataSize = newSize;
@@ -969,7 +1057,7 @@ export class RouteConnectionHandler {
969
1057
  }
970
1058
  } finally {
971
1059
  processingQueue = false;
972
-
1060
+
973
1061
  // If there's a pending drain and we've processed everything,
974
1062
  // signal we're ready for more data if we haven't established piping yet
975
1063
  if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
@@ -978,48 +1066,48 @@ export class RouteConnectionHandler {
978
1066
  }
979
1067
  }
980
1068
  };
981
-
1069
+
982
1070
  // Unified data handler that safely queues incoming data
983
1071
  const safeDataHandler = (chunk: Buffer) => {
984
1072
  // If piping is already established, just let the pipe handle it
985
1073
  if (pipingEstablished) return;
986
-
1074
+
987
1075
  // Add to our queue for orderly processing
988
1076
  dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
989
1077
  queueSize += chunk.length;
990
-
1078
+
991
1079
  // If queue is getting large, pause socket until we catch up
992
1080
  if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
993
1081
  socket.pause();
994
1082
  drainPending = true;
995
1083
  }
996
-
1084
+
997
1085
  // Process the queue
998
1086
  processDataQueue();
999
1087
  };
1000
-
1088
+
1001
1089
  // Add our safe data handler
1002
1090
  socket.on('data', safeDataHandler);
1003
-
1091
+
1004
1092
  // Add initial chunk to pending data if present
1005
1093
  if (initialChunk) {
1006
1094
  record.bytesReceived += initialChunk.length;
1007
1095
  record.pendingData.push(Buffer.from(initialChunk));
1008
1096
  record.pendingDataSize = initialChunk.length;
1009
1097
  }
1010
-
1098
+
1011
1099
  // Create the target socket but don't set up piping immediately
1012
1100
  const targetSocket = plugins.net.connect(connectionOptions);
1013
1101
  record.outgoing = targetSocket;
1014
1102
  record.outgoingStartTime = Date.now();
1015
-
1103
+
1016
1104
  // Apply socket optimizations
1017
1105
  targetSocket.setNoDelay(this.settings.noDelay);
1018
-
1106
+
1019
1107
  // Apply keep-alive settings to the outgoing connection as well
1020
1108
  if (this.settings.keepAlive) {
1021
1109
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1022
-
1110
+
1023
1111
  // Apply enhanced TCP keep-alive options if enabled
1024
1112
  if (this.settings.enableKeepAliveProbes) {
1025
1113
  try {
@@ -1039,7 +1127,7 @@ export class RouteConnectionHandler {
1039
1127
  }
1040
1128
  }
1041
1129
  }
1042
-
1130
+
1043
1131
  // Setup specific error handler for connection phase
1044
1132
  targetSocket.once('error', (err) => {
1045
1133
  // This handler runs only once during the initial connection phase
@@ -1047,10 +1135,10 @@ export class RouteConnectionHandler {
1047
1135
  console.log(
1048
1136
  `[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})`
1049
1137
  );
1050
-
1138
+
1051
1139
  // Resume the incoming socket to prevent it from hanging
1052
1140
  socket.resume();
1053
-
1141
+
1054
1142
  if (code === 'ECONNREFUSED') {
1055
1143
  console.log(
1056
1144
  `[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection`
@@ -1066,28 +1154,28 @@ export class RouteConnectionHandler {
1066
1154
  } else if (code === 'EHOSTUNREACH') {
1067
1155
  console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`);
1068
1156
  }
1069
-
1157
+
1070
1158
  // Clear any existing error handler after connection phase
1071
1159
  targetSocket.removeAllListeners('error');
1072
-
1160
+
1073
1161
  // Re-add the normal error handler for established connections
1074
1162
  targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
1075
-
1163
+
1076
1164
  if (record.outgoingTerminationReason === null) {
1077
1165
  record.outgoingTerminationReason = 'connection_failed';
1078
1166
  this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
1079
1167
  }
1080
-
1168
+
1081
1169
  // Route-based configuration doesn't use domain handlers
1082
-
1170
+
1083
1171
  // Clean up the connection
1084
1172
  this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
1085
1173
  });
1086
-
1174
+
1087
1175
  // Setup close handler
1088
1176
  targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
1089
1177
  socket.on('close', this.connectionManager.handleClose('incoming', record));
1090
-
1178
+
1091
1179
  // Handle timeouts with keep-alive awareness
1092
1180
  socket.on('timeout', () => {
1093
1181
  // For keep-alive connections, just log a warning instead of closing
@@ -1101,7 +1189,7 @@ export class RouteConnectionHandler {
1101
1189
  );
1102
1190
  return;
1103
1191
  }
1104
-
1192
+
1105
1193
  // For non-keep-alive connections, proceed with normal cleanup
1106
1194
  console.log(
1107
1195
  `[${connectionId}] Timeout on incoming side from ${
@@ -1114,7 +1202,7 @@ export class RouteConnectionHandler {
1114
1202
  }
1115
1203
  this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
1116
1204
  });
1117
-
1205
+
1118
1206
  targetSocket.on('timeout', () => {
1119
1207
  // For keep-alive connections, just log a warning instead of closing
1120
1208
  if (record.hasKeepAlive) {
@@ -1127,7 +1215,7 @@ export class RouteConnectionHandler {
1127
1215
  );
1128
1216
  return;
1129
1217
  }
1130
-
1218
+
1131
1219
  // For non-keep-alive connections, proceed with normal cleanup
1132
1220
  console.log(
1133
1221
  `[${connectionId}] Timeout on outgoing side from ${
@@ -1140,40 +1228,40 @@ export class RouteConnectionHandler {
1140
1228
  }
1141
1229
  this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
1142
1230
  });
1143
-
1231
+
1144
1232
  // Apply socket timeouts
1145
1233
  this.timeoutManager.applySocketTimeouts(record);
1146
-
1234
+
1147
1235
  // Track outgoing data for bytes counting
1148
1236
  targetSocket.on('data', (chunk: Buffer) => {
1149
1237
  record.bytesSent += chunk.length;
1150
1238
  this.timeoutManager.updateActivity(record);
1151
1239
  });
1152
-
1240
+
1153
1241
  // Wait for the outgoing connection to be ready before setting up piping
1154
1242
  targetSocket.once('connect', () => {
1155
1243
  // Clear the initial connection error handler
1156
1244
  targetSocket.removeAllListeners('error');
1157
-
1245
+
1158
1246
  // Add the normal error handler for established connections
1159
1247
  targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
1160
-
1248
+
1161
1249
  // Process any remaining data in the queue before switching to piping
1162
1250
  processDataQueue();
1163
-
1251
+
1164
1252
  // Set up piping immediately
1165
1253
  pipingEstablished = true;
1166
-
1254
+
1167
1255
  // Flush all pending data to target
1168
1256
  if (record.pendingData.length > 0) {
1169
1257
  const combinedData = Buffer.concat(record.pendingData);
1170
-
1258
+
1171
1259
  if (this.settings.enableDetailedLogging) {
1172
1260
  console.log(
1173
1261
  `[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
1174
1262
  );
1175
1263
  }
1176
-
1264
+
1177
1265
  // Write pending data immediately
1178
1266
  targetSocket.write(combinedData, (err) => {
1179
1267
  if (err) {
@@ -1181,19 +1269,19 @@ export class RouteConnectionHandler {
1181
1269
  return this.connectionManager.initiateCleanupOnce(record, 'write_error');
1182
1270
  }
1183
1271
  });
1184
-
1272
+
1185
1273
  // Clear the buffer now that we've processed it
1186
1274
  record.pendingData = [];
1187
1275
  record.pendingDataSize = 0;
1188
1276
  }
1189
-
1277
+
1190
1278
  // Setup piping in both directions without any delays
1191
1279
  socket.pipe(targetSocket);
1192
1280
  targetSocket.pipe(socket);
1193
-
1281
+
1194
1282
  // Resume the socket to ensure data flows
1195
1283
  socket.resume();
1196
-
1284
+
1197
1285
  // Process any data that might be queued in the interim
1198
1286
  if (dataQueue.length > 0) {
1199
1287
  // Write any remaining queued data directly to the target socket
@@ -1204,7 +1292,7 @@ export class RouteConnectionHandler {
1204
1292
  dataQueue.length = 0;
1205
1293
  queueSize = 0;
1206
1294
  }
1207
-
1295
+
1208
1296
  if (this.settings.enableDetailedLogging) {
1209
1297
  console.log(
1210
1298
  `[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
@@ -1231,7 +1319,7 @@ export class RouteConnectionHandler {
1231
1319
  }`
1232
1320
  );
1233
1321
  }
1234
-
1322
+
1235
1323
  // Add the renegotiation handler for SNI validation
1236
1324
  if (serverName) {
1237
1325
  // Create connection info object for the existing connection
@@ -1241,7 +1329,7 @@ export class RouteConnectionHandler {
1241
1329
  destIp: record.incoming.localAddress || '',
1242
1330
  destPort: record.incoming.localPort || 0,
1243
1331
  };
1244
-
1332
+
1245
1333
  // Create a renegotiation handler function
1246
1334
  const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
1247
1335
  connectionId,
@@ -1249,13 +1337,13 @@ export class RouteConnectionHandler {
1249
1337
  connInfo,
1250
1338
  (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
1251
1339
  );
1252
-
1340
+
1253
1341
  // Store the handler in the connection record so we can remove it during cleanup
1254
1342
  record.renegotiationHandler = renegotiationHandler;
1255
-
1343
+
1256
1344
  // Add the handler to the socket
1257
1345
  socket.on('data', renegotiationHandler);
1258
-
1346
+
1259
1347
  if (this.settings.enableDetailedLogging) {
1260
1348
  console.log(
1261
1349
  `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
@@ -1267,7 +1355,7 @@ export class RouteConnectionHandler {
1267
1355
  }
1268
1356
  }
1269
1357
  }
1270
-
1358
+
1271
1359
  // Set connection timeout
1272
1360
  record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
1273
1361
  console.log(
@@ -1275,11 +1363,11 @@ export class RouteConnectionHandler {
1275
1363
  );
1276
1364
  this.connectionManager.initiateCleanupOnce(record, reason);
1277
1365
  });
1278
-
1366
+
1279
1367
  // Mark TLS handshake as complete for TLS connections
1280
1368
  if (record.isTLS) {
1281
1369
  record.tlsHandshakeComplete = true;
1282
-
1370
+
1283
1371
  if (this.settings.enableTlsDebugLogging) {
1284
1372
  console.log(
1285
1373
  `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
@@ -1295,7 +1383,7 @@ function getStatusText(status: number): string {
1295
1383
  const statusTexts: Record<number, string> = {
1296
1384
  200: 'OK',
1297
1385
  404: 'Not Found',
1298
- 500: 'Internal Server Error'
1386
+ 500: 'Internal Server Error',
1299
1387
  };
1300
1388
  return statusTexts[status] || 'Unknown';
1301
- }
1389
+ }