@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.
- package/changelog.md +23 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/core/models/socket-augmentation.d.ts +3 -0
- package/dist_ts/core/models/socket-augmentation.js +1 -1
- package/dist_ts/core/utils/socket-tracker.d.ts +16 -0
- package/dist_ts/core/utils/socket-tracker.js +49 -0
- package/dist_ts/detection/detectors/http-detector.js +14 -2
- package/dist_ts/detection/protocol-detector.js +2 -1
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +5 -1
- package/dist_ts/proxies/http-proxy/http-proxy.js +63 -39
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +5 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +20 -6
- package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/index.js +2 -1
- package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/metrics-collector.js +52 -7
- package/dist_ts/proxies/smart-proxy/route-orchestrator.d.ts +56 -0
- package/dist_ts/proxies/smart-proxy/route-orchestrator.js +204 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -11
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -237
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +42 -7
- package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +58 -0
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +405 -0
- package/package.json +3 -2
- package/readme.md +321 -828
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/models/socket-augmentation.ts +5 -0
- package/ts/core/utils/socket-tracker.ts +63 -0
- package/ts/detection/detectors/http-detector.ts +14 -1
- package/ts/detection/protocol-detector.ts +1 -0
- package/ts/proxies/http-proxy/http-proxy.ts +73 -48
- package/ts/proxies/smart-proxy/certificate-manager.ts +21 -5
- package/ts/proxies/smart-proxy/index.ts +1 -0
- package/ts/proxies/smart-proxy/metrics-collector.ts +58 -6
- package/ts/proxies/smart-proxy/route-orchestrator.ts +297 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +66 -270
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +45 -6
- 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:
|
|
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: '
|
|
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
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
//
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
//
|
|
762
|
-
this.
|
|
763
|
-
|
|
764
|
-
//
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1137
|
+
// Use tracker to manage listeners
|
|
1138
|
+
tracker.addListener('data', processData);
|
|
1108
1139
|
|
|
1109
|
-
|
|
1140
|
+
tracker.addListener('error', (err) => {
|
|
1110
1141
|
if (!requestParsed) {
|
|
1111
|
-
|
|
1142
|
+
tracker.safeDestroy(err);
|
|
1112
1143
|
}
|
|
1113
1144
|
});
|
|
1114
1145
|
|
|
1115
|
-
|
|
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
|
};
|