@push.rocks/smartproxy 19.3.0 → 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,55 +734,222 @@ 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
-
731
- try {
732
- // Build route context
733
- const context: IRouteContext = {
734
- port: record.localPort,
735
- domain: record.lockedDomain,
736
- clientIp: record.remoteIP,
737
- serverIp: socket.localAddress!,
738
- path: undefined, // Will need to be extracted from HTTP request
739
- isTls: record.isTLS,
740
- tlsVersion: record.tlsVersion,
741
- routeName: route.name,
742
- routeId: route.name,
743
- timestamp: Date.now(),
744
- connectionId
745
- };
746
-
747
- // Call the handler
748
- const response = await route.action.handler(context);
749
-
750
- // Send HTTP response
751
- const headers = response.headers || {};
752
- headers['Content-Length'] = Buffer.byteLength(response.body).toString();
753
-
754
- let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
755
- for (const [key, value] of Object.entries(headers)) {
756
- httpResponse += `${key}: ${value}\r\n`;
744
+
745
+ let buffer = Buffer.alloc(0);
746
+ let processingData = false;
747
+
748
+ const handleHttpData = async (chunk: Buffer) => {
749
+ // Accumulate the data
750
+ buffer = Buffer.concat([buffer, chunk]);
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;
757
761
  }
758
- httpResponse += '\r\n';
759
-
760
- socket.write(httpResponse);
761
- socket.write(response.body);
762
- socket.end();
763
-
764
- this.connectionManager.cleanupConnection(record, 'completed');
765
- } catch (error) {
766
- console.error(`[${connectionId}] Error in static handler: ${error}`);
767
- socket.end();
768
- this.connectionManager.cleanupConnection(record, 'handler_error');
769
- }
762
+ };
763
+
764
+ const processBuffer = async () => {
765
+ // Look for end of HTTP headers
766
+ const headerEndIndex = buffer.indexOf('\r\n\r\n');
767
+ if (headerEndIndex === -1) {
768
+ // Need more data
769
+ if (buffer.length > 8192) {
770
+ // Prevent excessive buffering
771
+ console.error(`[${connectionId}] HTTP headers too large`);
772
+ socket.end();
773
+ this.connectionManager.cleanupConnection(record, 'headers_too_large');
774
+ }
775
+ return; // Wait for more data to arrive
776
+ }
777
+
778
+ // Parse the HTTP request
779
+ const headerBuffer = buffer.slice(0, headerEndIndex);
780
+ const headers = headerBuffer.toString();
781
+ const lines = headers.split('\r\n');
782
+
783
+ if (lines.length === 0) {
784
+ console.error(`[${connectionId}] Invalid HTTP request`);
785
+ socket.end();
786
+ this.connectionManager.cleanupConnection(record, 'invalid_request');
787
+ return;
788
+ }
789
+
790
+ // Parse request line
791
+ const requestLine = lines[0];
792
+ const requestParts = requestLine.split(' ');
793
+ if (requestParts.length < 3) {
794
+ console.error(`[${connectionId}] Invalid HTTP request line`);
795
+ socket.end();
796
+ this.connectionManager.cleanupConnection(record, 'invalid_request_line');
797
+ return;
798
+ }
799
+
800
+ const [method, path, httpVersion] = requestParts;
801
+
802
+ // Parse headers
803
+ const headersMap: Record<string, string> = {};
804
+ for (let i = 1; i < lines.length; i++) {
805
+ const colonIndex = lines[i].indexOf(':');
806
+ if (colonIndex > 0) {
807
+ const key = lines[i].slice(0, colonIndex).trim().toLowerCase();
808
+ const value = lines[i].slice(colonIndex + 1).trim();
809
+ headersMap[key] = value;
810
+ }
811
+ }
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
+
835
+ // Extract query string if present
836
+ let pathname = path;
837
+ let query: string | undefined;
838
+ const queryIndex = path.indexOf('?');
839
+ if (queryIndex !== -1) {
840
+ pathname = path.slice(0, queryIndex);
841
+ query = path.slice(queryIndex + 1);
842
+ }
843
+
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
+
857
+ // Build route context with parsed HTTP information
858
+ const context: IRouteContext = {
859
+ port: record.localPort,
860
+ domain: record.lockedDomain || headersMap['host']?.split(':')[0],
861
+ clientIp: record.remoteIP,
862
+ serverIp: socket.localAddress!,
863
+ path: pathname,
864
+ query: query,
865
+ headers: headersMap,
866
+ method: method,
867
+ isTls: record.isTLS,
868
+ tlsVersion: record.tlsVersion,
869
+ routeName: route.name,
870
+ routeId: route.id,
871
+ timestamp: Date.now(),
872
+ connectionId,
873
+ };
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
+
903
+ // Prepare the HTTP response
904
+ const responseHeaders = response.headers || {};
905
+ const contentLength = Buffer.byteLength(response.body || '');
906
+ responseHeaders['Content-Length'] = contentLength.toString();
907
+
908
+ if (!responseHeaders['Content-Type']) {
909
+ responseHeaders['Content-Type'] = 'text/plain';
910
+ }
911
+
912
+ // Build the response
913
+ let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
914
+ for (const [key, value] of Object.entries(responseHeaders)) {
915
+ httpResponse += `${key}: ${value}\r\n`;
916
+ }
917
+ httpResponse += '\r\n';
918
+
919
+ // Send response
920
+ socket.write(httpResponse);
921
+ if (response.body) {
922
+ socket.write(response.body);
923
+ }
924
+ socket.end();
925
+
926
+ this.connectionManager.cleanupConnection(record, 'completed');
927
+ } catch (error) {
928
+ console.error(`[${connectionId}] Error in static handler: ${error}`);
929
+
930
+ // Send error response
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';
937
+ socket.write(errorResponse);
938
+ socket.end();
939
+
940
+ this.connectionManager.cleanupConnection(record, 'handler_error');
941
+ }
942
+ };
943
+
944
+ // Listen for data
945
+ socket.on('data', handleHttpData);
946
+
947
+ // Ensure cleanup on socket close
948
+ socket.once('close', () => {
949
+ socket.removeListener('data', handleHttpData);
950
+ });
770
951
  }
771
-
952
+
772
953
  /**
773
954
  * Sets up a direct connection to the target
774
955
  */
@@ -784,22 +965,23 @@ export class RouteConnectionHandler {
784
965
  const connectionId = record.id;
785
966
 
786
967
  // Determine target host and port if not provided
787
- const finalTargetHost = targetHost ||
788
- record.targetHost ||
789
- (this.settings.defaults?.target?.host || 'localhost');
968
+ const finalTargetHost =
969
+ targetHost || record.targetHost || this.settings.defaults?.target?.host || 'localhost';
790
970
 
791
971
  // Determine target port
792
- const finalTargetPort = targetPort ||
972
+ const finalTargetPort =
973
+ targetPort ||
793
974
  record.targetPort ||
794
- (overridePort !== undefined ? overridePort :
795
- (this.settings.defaults?.target?.port || 443));
975
+ (overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443);
796
976
 
797
977
  // Update record with final target information
798
978
  record.targetHost = finalTargetHost;
799
979
  record.targetPort = finalTargetPort;
800
980
 
801
981
  if (this.settings.enableDetailedLogging) {
802
- console.log(`[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}`);
982
+ console.log(
983
+ `[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}`
984
+ );
803
985
  }
804
986
 
805
987
  // Setup connection options
@@ -812,53 +994,53 @@ export class RouteConnectionHandler {
812
994
  if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
813
995
  connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
814
996
  }
815
-
997
+
816
998
  // Create a safe queue for incoming data
817
999
  const dataQueue: Buffer[] = [];
818
1000
  let queueSize = 0;
819
1001
  let processingQueue = false;
820
1002
  let drainPending = false;
821
1003
  let pipingEstablished = false;
822
-
1004
+
823
1005
  // Pause the incoming socket to prevent buffer overflows
824
1006
  socket.pause();
825
-
1007
+
826
1008
  // Function to safely process the data queue without losing events
827
1009
  const processDataQueue = () => {
828
1010
  if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
829
-
1011
+
830
1012
  processingQueue = true;
831
-
1013
+
832
1014
  try {
833
1015
  // Process all queued chunks with the current active handler
834
1016
  while (dataQueue.length > 0) {
835
1017
  const chunk = dataQueue.shift()!;
836
1018
  queueSize -= chunk.length;
837
-
1019
+
838
1020
  // Once piping is established, we shouldn't get here,
839
1021
  // but just in case, pass to the outgoing socket directly
840
1022
  if (pipingEstablished && record.outgoing) {
841
1023
  record.outgoing.write(chunk);
842
1024
  continue;
843
1025
  }
844
-
1026
+
845
1027
  // Track bytes received
846
1028
  record.bytesReceived += chunk.length;
847
-
1029
+
848
1030
  // Check for TLS handshake
849
1031
  if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
850
1032
  record.isTLS = true;
851
-
1033
+
852
1034
  if (this.settings.enableTlsDebugLogging) {
853
1035
  console.log(
854
1036
  `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
855
1037
  );
856
1038
  }
857
1039
  }
858
-
1040
+
859
1041
  // Check if adding this chunk would exceed the buffer limit
860
1042
  const newSize = record.pendingDataSize + chunk.length;
861
-
1043
+
862
1044
  if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
863
1045
  console.log(
864
1046
  `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
@@ -867,7 +1049,7 @@ export class RouteConnectionHandler {
867
1049
  this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
868
1050
  return;
869
1051
  }
870
-
1052
+
871
1053
  // Buffer the chunk and update the size counter
872
1054
  record.pendingData.push(Buffer.from(chunk));
873
1055
  record.pendingDataSize = newSize;
@@ -875,7 +1057,7 @@ export class RouteConnectionHandler {
875
1057
  }
876
1058
  } finally {
877
1059
  processingQueue = false;
878
-
1060
+
879
1061
  // If there's a pending drain and we've processed everything,
880
1062
  // signal we're ready for more data if we haven't established piping yet
881
1063
  if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
@@ -884,48 +1066,48 @@ export class RouteConnectionHandler {
884
1066
  }
885
1067
  }
886
1068
  };
887
-
1069
+
888
1070
  // Unified data handler that safely queues incoming data
889
1071
  const safeDataHandler = (chunk: Buffer) => {
890
1072
  // If piping is already established, just let the pipe handle it
891
1073
  if (pipingEstablished) return;
892
-
1074
+
893
1075
  // Add to our queue for orderly processing
894
1076
  dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
895
1077
  queueSize += chunk.length;
896
-
1078
+
897
1079
  // If queue is getting large, pause socket until we catch up
898
1080
  if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
899
1081
  socket.pause();
900
1082
  drainPending = true;
901
1083
  }
902
-
1084
+
903
1085
  // Process the queue
904
1086
  processDataQueue();
905
1087
  };
906
-
1088
+
907
1089
  // Add our safe data handler
908
1090
  socket.on('data', safeDataHandler);
909
-
1091
+
910
1092
  // Add initial chunk to pending data if present
911
1093
  if (initialChunk) {
912
1094
  record.bytesReceived += initialChunk.length;
913
1095
  record.pendingData.push(Buffer.from(initialChunk));
914
1096
  record.pendingDataSize = initialChunk.length;
915
1097
  }
916
-
1098
+
917
1099
  // Create the target socket but don't set up piping immediately
918
1100
  const targetSocket = plugins.net.connect(connectionOptions);
919
1101
  record.outgoing = targetSocket;
920
1102
  record.outgoingStartTime = Date.now();
921
-
1103
+
922
1104
  // Apply socket optimizations
923
1105
  targetSocket.setNoDelay(this.settings.noDelay);
924
-
1106
+
925
1107
  // Apply keep-alive settings to the outgoing connection as well
926
1108
  if (this.settings.keepAlive) {
927
1109
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
928
-
1110
+
929
1111
  // Apply enhanced TCP keep-alive options if enabled
930
1112
  if (this.settings.enableKeepAliveProbes) {
931
1113
  try {
@@ -945,7 +1127,7 @@ export class RouteConnectionHandler {
945
1127
  }
946
1128
  }
947
1129
  }
948
-
1130
+
949
1131
  // Setup specific error handler for connection phase
950
1132
  targetSocket.once('error', (err) => {
951
1133
  // This handler runs only once during the initial connection phase
@@ -953,10 +1135,10 @@ export class RouteConnectionHandler {
953
1135
  console.log(
954
1136
  `[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})`
955
1137
  );
956
-
1138
+
957
1139
  // Resume the incoming socket to prevent it from hanging
958
1140
  socket.resume();
959
-
1141
+
960
1142
  if (code === 'ECONNREFUSED') {
961
1143
  console.log(
962
1144
  `[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection`
@@ -972,28 +1154,28 @@ export class RouteConnectionHandler {
972
1154
  } else if (code === 'EHOSTUNREACH') {
973
1155
  console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`);
974
1156
  }
975
-
1157
+
976
1158
  // Clear any existing error handler after connection phase
977
1159
  targetSocket.removeAllListeners('error');
978
-
1160
+
979
1161
  // Re-add the normal error handler for established connections
980
1162
  targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
981
-
1163
+
982
1164
  if (record.outgoingTerminationReason === null) {
983
1165
  record.outgoingTerminationReason = 'connection_failed';
984
1166
  this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
985
1167
  }
986
-
1168
+
987
1169
  // Route-based configuration doesn't use domain handlers
988
-
1170
+
989
1171
  // Clean up the connection
990
1172
  this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
991
1173
  });
992
-
1174
+
993
1175
  // Setup close handler
994
1176
  targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
995
1177
  socket.on('close', this.connectionManager.handleClose('incoming', record));
996
-
1178
+
997
1179
  // Handle timeouts with keep-alive awareness
998
1180
  socket.on('timeout', () => {
999
1181
  // For keep-alive connections, just log a warning instead of closing
@@ -1007,7 +1189,7 @@ export class RouteConnectionHandler {
1007
1189
  );
1008
1190
  return;
1009
1191
  }
1010
-
1192
+
1011
1193
  // For non-keep-alive connections, proceed with normal cleanup
1012
1194
  console.log(
1013
1195
  `[${connectionId}] Timeout on incoming side from ${
@@ -1020,7 +1202,7 @@ export class RouteConnectionHandler {
1020
1202
  }
1021
1203
  this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
1022
1204
  });
1023
-
1205
+
1024
1206
  targetSocket.on('timeout', () => {
1025
1207
  // For keep-alive connections, just log a warning instead of closing
1026
1208
  if (record.hasKeepAlive) {
@@ -1033,7 +1215,7 @@ export class RouteConnectionHandler {
1033
1215
  );
1034
1216
  return;
1035
1217
  }
1036
-
1218
+
1037
1219
  // For non-keep-alive connections, proceed with normal cleanup
1038
1220
  console.log(
1039
1221
  `[${connectionId}] Timeout on outgoing side from ${
@@ -1046,40 +1228,40 @@ export class RouteConnectionHandler {
1046
1228
  }
1047
1229
  this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
1048
1230
  });
1049
-
1231
+
1050
1232
  // Apply socket timeouts
1051
1233
  this.timeoutManager.applySocketTimeouts(record);
1052
-
1234
+
1053
1235
  // Track outgoing data for bytes counting
1054
1236
  targetSocket.on('data', (chunk: Buffer) => {
1055
1237
  record.bytesSent += chunk.length;
1056
1238
  this.timeoutManager.updateActivity(record);
1057
1239
  });
1058
-
1240
+
1059
1241
  // Wait for the outgoing connection to be ready before setting up piping
1060
1242
  targetSocket.once('connect', () => {
1061
1243
  // Clear the initial connection error handler
1062
1244
  targetSocket.removeAllListeners('error');
1063
-
1245
+
1064
1246
  // Add the normal error handler for established connections
1065
1247
  targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
1066
-
1248
+
1067
1249
  // Process any remaining data in the queue before switching to piping
1068
1250
  processDataQueue();
1069
-
1251
+
1070
1252
  // Set up piping immediately
1071
1253
  pipingEstablished = true;
1072
-
1254
+
1073
1255
  // Flush all pending data to target
1074
1256
  if (record.pendingData.length > 0) {
1075
1257
  const combinedData = Buffer.concat(record.pendingData);
1076
-
1258
+
1077
1259
  if (this.settings.enableDetailedLogging) {
1078
1260
  console.log(
1079
1261
  `[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
1080
1262
  );
1081
1263
  }
1082
-
1264
+
1083
1265
  // Write pending data immediately
1084
1266
  targetSocket.write(combinedData, (err) => {
1085
1267
  if (err) {
@@ -1087,19 +1269,19 @@ export class RouteConnectionHandler {
1087
1269
  return this.connectionManager.initiateCleanupOnce(record, 'write_error');
1088
1270
  }
1089
1271
  });
1090
-
1272
+
1091
1273
  // Clear the buffer now that we've processed it
1092
1274
  record.pendingData = [];
1093
1275
  record.pendingDataSize = 0;
1094
1276
  }
1095
-
1277
+
1096
1278
  // Setup piping in both directions without any delays
1097
1279
  socket.pipe(targetSocket);
1098
1280
  targetSocket.pipe(socket);
1099
-
1281
+
1100
1282
  // Resume the socket to ensure data flows
1101
1283
  socket.resume();
1102
-
1284
+
1103
1285
  // Process any data that might be queued in the interim
1104
1286
  if (dataQueue.length > 0) {
1105
1287
  // Write any remaining queued data directly to the target socket
@@ -1110,7 +1292,7 @@ export class RouteConnectionHandler {
1110
1292
  dataQueue.length = 0;
1111
1293
  queueSize = 0;
1112
1294
  }
1113
-
1295
+
1114
1296
  if (this.settings.enableDetailedLogging) {
1115
1297
  console.log(
1116
1298
  `[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
@@ -1137,7 +1319,7 @@ export class RouteConnectionHandler {
1137
1319
  }`
1138
1320
  );
1139
1321
  }
1140
-
1322
+
1141
1323
  // Add the renegotiation handler for SNI validation
1142
1324
  if (serverName) {
1143
1325
  // Create connection info object for the existing connection
@@ -1147,7 +1329,7 @@ export class RouteConnectionHandler {
1147
1329
  destIp: record.incoming.localAddress || '',
1148
1330
  destPort: record.incoming.localPort || 0,
1149
1331
  };
1150
-
1332
+
1151
1333
  // Create a renegotiation handler function
1152
1334
  const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
1153
1335
  connectionId,
@@ -1155,13 +1337,13 @@ export class RouteConnectionHandler {
1155
1337
  connInfo,
1156
1338
  (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
1157
1339
  );
1158
-
1340
+
1159
1341
  // Store the handler in the connection record so we can remove it during cleanup
1160
1342
  record.renegotiationHandler = renegotiationHandler;
1161
-
1343
+
1162
1344
  // Add the handler to the socket
1163
1345
  socket.on('data', renegotiationHandler);
1164
-
1346
+
1165
1347
  if (this.settings.enableDetailedLogging) {
1166
1348
  console.log(
1167
1349
  `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
@@ -1173,7 +1355,7 @@ export class RouteConnectionHandler {
1173
1355
  }
1174
1356
  }
1175
1357
  }
1176
-
1358
+
1177
1359
  // Set connection timeout
1178
1360
  record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
1179
1361
  console.log(
@@ -1181,11 +1363,11 @@ export class RouteConnectionHandler {
1181
1363
  );
1182
1364
  this.connectionManager.initiateCleanupOnce(record, reason);
1183
1365
  });
1184
-
1366
+
1185
1367
  // Mark TLS handshake as complete for TLS connections
1186
1368
  if (record.isTLS) {
1187
1369
  record.tlsHandshakeComplete = true;
1188
-
1370
+
1189
1371
  if (this.settings.enableTlsDebugLogging) {
1190
1372
  console.log(
1191
1373
  `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
@@ -1201,7 +1383,7 @@ function getStatusText(status: number): string {
1201
1383
  const statusTexts: Record<number, string> = {
1202
1384
  200: 'OK',
1203
1385
  404: 'Not Found',
1204
- 500: 'Internal Server Error'
1386
+ 500: 'Internal Server Error',
1205
1387
  };
1206
1388
  return statusTexts[status] || 'Unknown';
1207
- }
1389
+ }