@push.rocks/smartproxy 21.1.3 → 21.1.5

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.
Files changed (38) hide show
  1. package/changelog.md +23 -0
  2. package/dist_ts/00_commitinfo_data.js +2 -2
  3. package/dist_ts/core/models/socket-augmentation.d.ts +3 -0
  4. package/dist_ts/core/models/socket-augmentation.js +1 -1
  5. package/dist_ts/core/utils/socket-tracker.d.ts +16 -0
  6. package/dist_ts/core/utils/socket-tracker.js +49 -0
  7. package/dist_ts/detection/detectors/http-detector.js +14 -2
  8. package/dist_ts/detection/protocol-detector.js +2 -1
  9. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +5 -1
  10. package/dist_ts/proxies/http-proxy/http-proxy.js +63 -39
  11. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +5 -0
  12. package/dist_ts/proxies/smart-proxy/certificate-manager.js +20 -6
  13. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
  14. package/dist_ts/proxies/smart-proxy/index.js +2 -1
  15. package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +4 -0
  16. package/dist_ts/proxies/smart-proxy/metrics-collector.js +52 -7
  17. package/dist_ts/proxies/smart-proxy/route-orchestrator.d.ts +56 -0
  18. package/dist_ts/proxies/smart-proxy/route-orchestrator.js +204 -0
  19. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -11
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -237
  21. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +42 -7
  22. package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +58 -0
  23. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +405 -0
  24. package/package.json +3 -2
  25. package/readme.md +321 -828
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/core/models/socket-augmentation.ts +5 -0
  28. package/ts/core/utils/socket-tracker.ts +63 -0
  29. package/ts/detection/detectors/http-detector.ts +14 -1
  30. package/ts/detection/protocol-detector.ts +1 -0
  31. package/ts/proxies/http-proxy/http-proxy.ts +73 -48
  32. package/ts/proxies/smart-proxy/certificate-manager.ts +21 -5
  33. package/ts/proxies/smart-proxy/index.ts +1 -0
  34. package/ts/proxies/smart-proxy/metrics-collector.ts +58 -6
  35. package/ts/proxies/smart-proxy/route-orchestrator.ts +297 -0
  36. package/ts/proxies/smart-proxy/smart-proxy.ts +66 -270
  37. package/ts/proxies/smart-proxy/utils/route-helpers.ts +45 -6
  38. package/ts/proxies/smart-proxy/utils/route-validator.ts +453 -0
@@ -25,6 +25,12 @@ import type { IRouteConfig } from './models/route-types.js';
25
25
  // Import mutex for route update synchronization
26
26
  import { Mutex } from './utils/mutex.js';
27
27
 
28
+ // Import route validator
29
+ import { RouteValidator } from './utils/route-validator.js';
30
+
31
+ // Import route orchestrator for route management
32
+ import { RouteOrchestrator } from './route-orchestrator.js';
33
+
28
34
  // Import ACME state manager
29
35
  import { AcmeStateManager } from './acme-state-manager.js';
30
36
 
@@ -66,12 +72,15 @@ export class SmartProxy extends plugins.EventEmitter {
66
72
 
67
73
  // Global challenge route tracking
68
74
  private globalChallengeRouteActive: boolean = false;
69
- private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
75
+ private routeUpdateLock: Mutex;
70
76
  public acmeStateManager: AcmeStateManager;
71
77
 
72
78
  // Metrics collector
73
79
  public metricsCollector: MetricsCollector;
74
80
 
81
+ // Route orchestrator for managing route updates
82
+ private routeOrchestrator: RouteOrchestrator;
83
+
75
84
  // Track port usage across route updates
76
85
  private portUsageMap: Map<number, Set<string>> = new Map();
77
86
 
@@ -175,6 +184,15 @@ export class SmartProxy extends plugins.EventEmitter {
175
184
  error: (message: string, data?: any) => logger.log('error', message, data)
176
185
  };
177
186
 
187
+ // Validate initial routes
188
+ if (this.settings.routes && this.settings.routes.length > 0) {
189
+ const validation = RouteValidator.validateRoutes(this.settings.routes);
190
+ if (!validation.valid) {
191
+ RouteValidator.logValidationErrors(validation.errors);
192
+ throw new Error(`Initial route validation failed: ${validation.errors.size} route(s) have errors`);
193
+ }
194
+ }
195
+
178
196
  this.routeManager = new RouteManager({
179
197
  logger: loggerAdapter,
180
198
  enableDetailedLogging: this.settings.enableDetailedLogging,
@@ -206,6 +224,16 @@ export class SmartProxy extends plugins.EventEmitter {
206
224
  sampleIntervalMs: this.settings.metrics?.sampleIntervalMs,
207
225
  retentionSeconds: this.settings.metrics?.retentionSeconds
208
226
  });
227
+
228
+ // Initialize route orchestrator for managing route updates
229
+ this.routeOrchestrator = new RouteOrchestrator(
230
+ this.portManager,
231
+ this.routeManager,
232
+ this.httpProxyBridge,
233
+ this.nftablesManager,
234
+ null, // certManager will be set later
235
+ loggerAdapter
236
+ );
209
237
  }
210
238
 
211
239
  /**
@@ -354,8 +382,8 @@ export class SmartProxy extends plugins.EventEmitter {
354
382
  // Get listening ports from RouteManager
355
383
  const listeningPorts = this.routeManager.getListeningPorts();
356
384
 
357
- // Initialize port usage tracking
358
- this.portUsageMap = this.updatePortUsageMap(this.settings.routes);
385
+ // Initialize port usage tracking using RouteOrchestrator
386
+ this.portUsageMap = this.routeOrchestrator.updatePortUsageMap(this.settings.routes);
359
387
 
360
388
  // Log port usage for startup
361
389
  logger.log('info', `SmartProxy starting with ${listeningPorts.length} ports: ${listeningPorts.join(', ')}`, {
@@ -516,7 +544,7 @@ export class SmartProxy extends plugins.EventEmitter {
516
544
  logger.log('info', 'All servers closed. Cleaning up active connections...');
517
545
 
518
546
  // Clean up all active connections
519
- this.connectionManager.clearConnections();
547
+ await this.connectionManager.clearConnections();
520
548
 
521
549
  // Stop HttpProxy
522
550
  await this.httpProxyBridge.stop();
@@ -527,6 +555,10 @@ export class SmartProxy extends plugins.EventEmitter {
527
555
  // Stop metrics collector
528
556
  this.metricsCollector.stop();
529
557
 
558
+ // Clean up ProtocolDetector singleton
559
+ const detection = await import('../../detection/index.js');
560
+ detection.ProtocolDetector.destroy();
561
+
530
562
  // Flush any pending deduplicated logs
531
563
  connectionLogDeduplicator.flushAll();
532
564
 
@@ -606,202 +638,46 @@ export class SmartProxy extends plugins.EventEmitter {
606
638
  try {
607
639
  logger.log('info', `Updating routes (${newRoutes.length} routes)`, {
608
640
  routeCount: newRoutes.length,
609
- component: 'route-manager'
641
+ component: 'smart-proxy'
610
642
  });
611
643
  } catch (error) {
612
644
  // Silently handle logging errors
613
645
  console.log(`[INFO] Updating routes (${newRoutes.length} routes)`);
614
646
  }
615
647
 
616
- // Track port usage before and after updates
617
- const oldPortUsage = this.updatePortUsageMap(this.settings.routes);
618
- const newPortUsage = this.updatePortUsageMap(newRoutes);
619
-
620
- // Get the lists of currently listening ports and new ports needed
621
- const currentPorts = new Set(this.portManager.getListeningPorts());
622
- const newPortsSet = new Set(newPortUsage.keys());
623
-
624
- // Log the port usage for debugging
625
- try {
626
- logger.log('debug', `Current listening ports: ${Array.from(currentPorts).join(', ')}`, {
627
- ports: Array.from(currentPorts),
628
- component: 'smart-proxy'
629
- });
630
-
631
- logger.log('debug', `Ports needed for new routes: ${Array.from(newPortsSet).join(', ')}`, {
632
- ports: Array.from(newPortsSet),
633
- component: 'smart-proxy'
634
- });
635
- } catch (error) {
636
- // Silently handle logging errors
637
- console.log(`[DEBUG] Current listening ports: ${Array.from(currentPorts).join(', ')}`);
638
- console.log(`[DEBUG] Ports needed for new routes: ${Array.from(newPortsSet).join(', ')}`);
648
+ // Update route orchestrator dependencies if cert manager changed
649
+ if (this.certManager && !this.routeOrchestrator.getCertManager()) {
650
+ this.routeOrchestrator.setCertManager(this.certManager);
639
651
  }
640
652
 
641
- // Find orphaned ports - ports that no longer have any routes
642
- const orphanedPorts = this.findOrphanedPorts(oldPortUsage, newPortUsage);
643
-
644
- // Find new ports that need binding (only ports that we aren't already listening on)
645
- const newBindingPorts = Array.from(newPortsSet).filter(p => !currentPorts.has(p));
646
-
647
- // Check for ACME challenge port to give it special handling
648
- const acmePort = this.settings.acme?.port || 80;
649
- const acmePortNeeded = newPortsSet.has(acmePort);
650
- const acmePortListed = newBindingPorts.includes(acmePort);
651
-
652
- if (acmePortNeeded && acmePortListed) {
653
- try {
654
- logger.log('info', `Adding ACME challenge port ${acmePort} to routes`, {
655
- port: acmePort,
656
- component: 'smart-proxy'
657
- });
658
- } catch (error) {
659
- // Silently handle logging errors
660
- console.log(`[INFO] Adding ACME challenge port ${acmePort} to routes`);
653
+ // Delegate the complex route update logic to RouteOrchestrator
654
+ const updateResult = await this.routeOrchestrator.updateRoutes(
655
+ this.settings.routes,
656
+ newRoutes,
657
+ {
658
+ acmePort: this.settings.acme?.port || 80,
659
+ acmeOptions: this.certManager?.getAcmeOptions(),
660
+ acmeState: this.certManager?.getState(),
661
+ globalChallengeRouteActive: this.globalChallengeRouteActive,
662
+ createCertificateManager: this.createCertificateManager.bind(this),
663
+ verifyChallengeRouteRemoved: this.verifyChallengeRouteRemoved.bind(this)
661
664
  }
662
- }
663
-
664
- // Get existing routes that use NFTables and update them
665
- const oldNfTablesRoutes = this.settings.routes.filter(
666
- r => r.action.forwardingEngine === 'nftables'
667
665
  );
668
666
 
669
- const newNfTablesRoutes = newRoutes.filter(
670
- r => r.action.forwardingEngine === 'nftables'
671
- );
672
-
673
- // Update existing NFTables routes
674
- for (const oldRoute of oldNfTablesRoutes) {
675
- const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
676
-
677
- if (!newRoute) {
678
- // Route was removed
679
- await this.nftablesManager.deprovisionRoute(oldRoute);
680
- } else {
681
- // Route was updated
682
- await this.nftablesManager.updateRoute(oldRoute, newRoute);
683
- }
684
- }
685
-
686
- // Add new NFTables routes
687
- for (const newRoute of newNfTablesRoutes) {
688
- const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
689
-
690
- if (!oldRoute) {
691
- // New route
692
- await this.nftablesManager.provisionRoute(newRoute);
693
- }
694
- }
695
-
696
- // Update routes in RouteManager
697
- this.routeManager.updateRoutes(newRoutes);
698
-
699
- // Release orphaned ports first to free resources
700
- if (orphanedPorts.length > 0) {
701
- try {
702
- logger.log('info', `Releasing ${orphanedPorts.length} orphaned ports: ${orphanedPorts.join(', ')}`, {
703
- ports: orphanedPorts,
704
- component: 'smart-proxy'
705
- });
706
- } catch (error) {
707
- // Silently handle logging errors
708
- console.log(`[INFO] Releasing ${orphanedPorts.length} orphaned ports: ${orphanedPorts.join(', ')}`);
709
- }
710
- await this.portManager.removePorts(orphanedPorts);
711
- }
712
-
713
- // Add new ports if needed
714
- if (newBindingPorts.length > 0) {
715
- try {
716
- logger.log('info', `Binding to ${newBindingPorts.length} new ports: ${newBindingPorts.join(', ')}`, {
717
- ports: newBindingPorts,
718
- component: 'smart-proxy'
719
- });
720
- } catch (error) {
721
- // Silently handle logging errors
722
- console.log(`[INFO] Binding to ${newBindingPorts.length} new ports: ${newBindingPorts.join(', ')}`);
723
- }
724
-
725
- // Handle port binding with improved error recovery
726
- try {
727
- await this.portManager.addPorts(newBindingPorts);
728
- } catch (error) {
729
- // Special handling for port binding errors
730
- // This provides better diagnostics for ACME challenge port conflicts
731
- if ((error as any).code === 'EADDRINUSE') {
732
- const port = (error as any).port || newBindingPorts[0];
733
- const isAcmePort = port === acmePort;
734
-
735
- if (isAcmePort) {
736
- try {
737
- logger.log('warn', `Could not bind to ACME challenge port ${port}. It may be in use by another application.`, {
738
- port,
739
- component: 'smart-proxy'
740
- });
741
- } catch (logError) {
742
- console.log(`[WARN] Could not bind to ACME challenge port ${port}. It may be in use by another application.`);
743
- }
744
-
745
- // Re-throw with more helpful message
746
- throw new Error(
747
- `ACME challenge port ${port} is already in use by another application. ` +
748
- `Configure a different port in settings.acme.port (e.g., 8080) or free up port ${port}.`
749
- );
750
- }
751
- }
752
-
753
- // Re-throw the original error for other cases
754
- throw error;
755
- }
756
- }
757
-
758
667
  // Update settings with the new routes
759
668
  this.settings.routes = newRoutes;
760
669
 
761
- // Save the new port usage map for future reference
762
- this.portUsageMap = newPortUsage;
763
-
764
- // If HttpProxy is initialized, resync the configurations
765
- if (this.httpProxyBridge.getHttpProxy()) {
766
- await this.httpProxyBridge.syncRoutesToHttpProxy(newRoutes);
767
- }
768
-
769
- // Update certificate manager with new routes
770
- if (this.certManager) {
771
- const existingAcmeOptions = this.certManager.getAcmeOptions();
772
- const existingState = this.certManager.getState();
773
-
774
- // Store global state before stopping
775
- this.globalChallengeRouteActive = existingState.challengeRouteActive;
776
-
777
- // Only stop the cert manager if absolutely necessary
778
- // First check if there's an ACME route on the same port already
779
- const acmePort = existingAcmeOptions?.port || 80;
780
- const acmePortInUse = newPortUsage.has(acmePort) && newPortUsage.get(acmePort)!.size > 0;
781
-
782
- try {
783
- logger.log('debug', `ACME port ${acmePort} ${acmePortInUse ? 'is' : 'is not'} already in use by other routes`, {
784
- port: acmePort,
785
- inUse: acmePortInUse,
786
- component: 'smart-proxy'
787
- });
788
- } catch (error) {
789
- // Silently handle logging errors
790
- console.log(`[DEBUG] ACME port ${acmePort} ${acmePortInUse ? 'is' : 'is not'} already in use by other routes`);
791
- }
792
-
793
- await this.certManager.stop();
794
-
795
- // Verify the challenge route has been properly removed
796
- await this.verifyChallengeRouteRemoved();
797
-
798
- // Create new certificate manager with preserved state
799
- this.certManager = await this.createCertificateManager(
800
- newRoutes,
801
- './certs',
802
- existingAcmeOptions,
803
- { challengeRouteActive: this.globalChallengeRouteActive }
804
- );
670
+ // Update global state from orchestrator results
671
+ this.globalChallengeRouteActive = updateResult.newChallengeRouteActive;
672
+
673
+ // Update port usage map from orchestrator
674
+ this.portUsageMap = updateResult.portUsageMap;
675
+
676
+ // If certificate manager was recreated, update our reference
677
+ if (updateResult.newCertManager) {
678
+ this.certManager = updateResult.newCertManager;
679
+ // Update the orchestrator's reference too
680
+ this.routeOrchestrator.setCertManager(this.certManager);
805
681
  }
806
682
  });
807
683
  }
@@ -822,87 +698,7 @@ export class SmartProxy extends plugins.EventEmitter {
822
698
  await this.certManager.provisionCertificate(route);
823
699
  }
824
700
 
825
- /**
826
- * Update the port usage map based on the provided routes
827
- *
828
- * This tracks which ports are used by which routes, allowing us to
829
- * detect when a port is no longer needed and can be released.
830
- */
831
- private updatePortUsageMap(routes: IRouteConfig[]): Map<number, Set<string>> {
832
- // Reset the usage map
833
- const portUsage = new Map<number, Set<string>>();
834
-
835
- for (const route of routes) {
836
- // Get the ports for this route
837
- const portsConfig = Array.isArray(route.match.ports)
838
- ? route.match.ports
839
- : [route.match.ports];
840
-
841
- // Expand port range objects to individual port numbers
842
- const expandedPorts: number[] = [];
843
- for (const portConfig of portsConfig) {
844
- if (typeof portConfig === 'number') {
845
- expandedPorts.push(portConfig);
846
- } else if (typeof portConfig === 'object' && 'from' in portConfig && 'to' in portConfig) {
847
- // Expand the port range
848
- for (let p = portConfig.from; p <= portConfig.to; p++) {
849
- expandedPorts.push(p);
850
- }
851
- }
852
- }
853
-
854
- // Use route name if available, otherwise generate a unique ID
855
- const routeName = route.name || `unnamed_${Math.random().toString(36).substring(2, 9)}`;
856
-
857
- // Add each port to the usage map
858
- for (const port of expandedPorts) {
859
- if (!portUsage.has(port)) {
860
- portUsage.set(port, new Set());
861
- }
862
- portUsage.get(port)!.add(routeName);
863
- }
864
- }
865
-
866
- // Log port usage for debugging
867
- for (const [port, routes] of portUsage.entries()) {
868
- try {
869
- logger.log('debug', `Port ${port} is used by ${routes.size} routes: ${Array.from(routes).join(', ')}`, {
870
- port,
871
- routeCount: routes.size,
872
- component: 'smart-proxy'
873
- });
874
- } catch (error) {
875
- // Silently handle logging errors
876
- console.log(`[DEBUG] Port ${port} is used by ${routes.size} routes: ${Array.from(routes).join(', ')}`);
877
- }
878
- }
879
-
880
- return portUsage;
881
- }
882
-
883
- /**
884
- * Find ports that have no routes in the new configuration
885
- */
886
- private findOrphanedPorts(oldUsage: Map<number, Set<string>>, newUsage: Map<number, Set<string>>): number[] {
887
- const orphanedPorts: number[] = [];
888
-
889
- for (const [port, routes] of oldUsage.entries()) {
890
- if (!newUsage.has(port) || newUsage.get(port)!.size === 0) {
891
- orphanedPorts.push(port);
892
- try {
893
- logger.log('info', `Port ${port} no longer has any associated routes, will be released`, {
894
- port,
895
- component: 'smart-proxy'
896
- });
897
- } catch (error) {
898
- // Silently handle logging errors
899
- console.log(`[INFO] Port ${port} no longer has any associated routes, will be released`);
900
- }
901
- }
902
- }
903
-
904
- return orphanedPorts;
905
- }
701
+ // Port usage tracking methods moved to RouteOrchestrator
906
702
 
907
703
  /**
908
704
  * Force renewal of a certificate
@@ -1024,9 +820,9 @@ export class SmartProxy extends plugins.EventEmitter {
1024
820
  terminationStats,
1025
821
  acmeEnabled: !!this.certManager,
1026
822
  port80HandlerPort: this.certManager ? 80 : null,
1027
- routes: this.routeManager.getListeningPorts().length,
1028
- listeningPorts: this.portManager.getListeningPorts(),
1029
- activePorts: this.portManager.getListeningPorts().length
823
+ routeCount: this.settings.routes.length,
824
+ activePorts: this.portManager.getListeningPorts().length,
825
+ listeningPorts: this.portManager.getListeningPorts()
1030
826
  };
1031
827
  }
1032
828
 
@@ -22,6 +22,7 @@ import * as plugins from '../../../plugins.js';
22
22
  import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
23
23
  import { mergeRouteConfigs } from './route-utils.js';
24
24
  import { ProtocolDetector, HttpDetector } from '../../../detection/index.js';
25
+ import { createSocketTracker } from '../../../core/utils/socket-tracker.js';
25
26
 
26
27
  /**
27
28
  * Create an HTTP-only route configuration
@@ -960,11 +961,12 @@ export const SocketHandlers = {
960
961
  * Now uses the centralized detection module for HTTP parsing
961
962
  */
962
963
  httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
964
+ const tracker = createSocketTracker(socket);
963
965
  const connectionId = ProtocolDetector.createConnectionId({
964
966
  socketId: context.connectionId || `${Date.now()}-${Math.random()}`
965
967
  });
966
968
 
967
- socket.once('data', async (data) => {
969
+ const handleData = async (data: Buffer) => {
968
970
  // Use detection module for parsing
969
971
  const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
970
972
  data,
@@ -1005,6 +1007,19 @@ export const SocketHandlers = {
1005
1007
  socket.end();
1006
1008
  // Clean up detection state
1007
1009
  ProtocolDetector.cleanupConnections();
1010
+ // Clean up all tracked resources
1011
+ tracker.cleanup();
1012
+ };
1013
+
1014
+ // Use tracker to manage the listener
1015
+ socket.once('data', handleData);
1016
+
1017
+ tracker.addListener('error', (err) => {
1018
+ tracker.safeDestroy(err);
1019
+ });
1020
+
1021
+ tracker.addListener('close', () => {
1022
+ tracker.cleanup();
1008
1023
  });
1009
1024
  },
1010
1025
 
@@ -1013,7 +1028,9 @@ export const SocketHandlers = {
1013
1028
  * Now uses the centralized detection module for HTTP parsing
1014
1029
  */
1015
1030
  httpServer: (handler: (req: { method: string; url: string; headers: Record<string, string>; body?: string }, res: { status: (code: number) => void; header: (name: string, value: string) => void; send: (data: string) => void; end: () => void }) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
1031
+ const tracker = createSocketTracker(socket);
1016
1032
  let requestParsed = false;
1033
+ let responseTimer: NodeJS.Timeout | null = null;
1017
1034
  const connectionId = ProtocolDetector.createConnectionId({
1018
1035
  socketId: context.connectionId || `${Date.now()}-${Math.random()}`
1019
1036
  });
@@ -1034,6 +1051,8 @@ export const SocketHandlers = {
1034
1051
  }
1035
1052
 
1036
1053
  requestParsed = true;
1054
+ // Remove data listener after parsing request
1055
+ socket.removeListener('data', processData);
1037
1056
  const connInfo = detectionResult.connectionInfo;
1038
1057
 
1039
1058
  // Create request object from detection result
@@ -1060,6 +1079,12 @@ export const SocketHandlers = {
1060
1079
  if (ended) return;
1061
1080
  ended = true;
1062
1081
 
1082
+ // Clear response timer since we're sending now
1083
+ if (responseTimer) {
1084
+ clearTimeout(responseTimer);
1085
+ responseTimer = null;
1086
+ }
1087
+
1063
1088
  if (!responseHeaders['content-type']) {
1064
1089
  responseHeaders['content-type'] = 'text/plain';
1065
1090
  }
@@ -1091,30 +1116,44 @@ export const SocketHandlers = {
1091
1116
  try {
1092
1117
  handler(req, res);
1093
1118
  // Ensure response is sent even if handler doesn't call send()
1094
- setTimeout(() => {
1119
+ responseTimer = setTimeout(() => {
1095
1120
  if (!ended) {
1096
1121
  res.send('');
1097
1122
  }
1123
+ responseTimer = null;
1098
1124
  }, 1000);
1125
+ // Track and unref the timer
1126
+ tracker.addTimer(responseTimer);
1099
1127
  } catch (error) {
1100
1128
  if (!ended) {
1101
1129
  res.status(500);
1102
1130
  res.send('Internal Server Error');
1103
1131
  }
1132
+ // Use safeDestroy for error cases
1133
+ tracker.safeDestroy(error instanceof Error ? error : new Error('Handler error'));
1104
1134
  }
1105
1135
  };
1106
1136
 
1107
- socket.on('data', processData);
1137
+ // Use tracker to manage listeners
1138
+ tracker.addListener('data', processData);
1108
1139
 
1109
- socket.on('error', () => {
1140
+ tracker.addListener('error', (err) => {
1110
1141
  if (!requestParsed) {
1111
- socket.end();
1142
+ tracker.safeDestroy(err);
1112
1143
  }
1113
1144
  });
1114
1145
 
1115
- socket.on('close', () => {
1146
+ tracker.addListener('close', () => {
1147
+ // Cleanup is handled by tracker
1148
+ // Clear any pending response timer
1149
+ if (responseTimer) {
1150
+ clearTimeout(responseTimer);
1151
+ responseTimer = null;
1152
+ }
1116
1153
  // Clean up detection state
1117
1154
  ProtocolDetector.cleanupConnections();
1155
+ // Clean up all tracked resources
1156
+ tracker.cleanup();
1118
1157
  });
1119
1158
  }
1120
1159
  };