@push.rocks/smartproxy 19.6.0 → 19.6.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.
- package/dist_ts/proxies/http-proxy/function-cache.d.ts +5 -0
- package/dist_ts/proxies/http-proxy/function-cache.js +19 -2
- package/dist_ts/proxies/http-proxy/http-proxy.js +5 -1
- package/dist_ts/proxies/http-proxy/request-handler.d.ts +5 -0
- package/dist_ts/proxies/http-proxy/request-handler.js +27 -2
- package/dist_ts/proxies/smart-proxy/connection-manager.js +13 -5
- package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/metrics-collector.js +9 -5
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +0 -1
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +6 -6
- package/package.json +1 -1
- package/readme.memory-leaks-fixed.md +45 -0
- package/readme.websocket-keepalive-config.md +140 -0
- package/readme.websocket-keepalive-fix.md +63 -0
- package/ts/proxies/http-proxy/function-cache.ts +21 -1
- package/ts/proxies/http-proxy/http-proxy.ts +5 -0
- package/ts/proxies/http-proxy/request-handler.ts +32 -1
- package/ts/proxies/smart-proxy/connection-manager.ts +14 -4
- package/ts/proxies/smart-proxy/metrics-collector.ts +8 -4
- package/ts/proxies/smart-proxy/route-connection-handler.ts +5 -5
|
@@ -30,6 +30,9 @@ export class FunctionCache {
|
|
|
30
30
|
// Logger
|
|
31
31
|
private logger: ILogger;
|
|
32
32
|
|
|
33
|
+
// Cleanup interval timer
|
|
34
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
35
|
+
|
|
33
36
|
/**
|
|
34
37
|
* Creates a new function cache
|
|
35
38
|
*
|
|
@@ -48,7 +51,12 @@ export class FunctionCache {
|
|
|
48
51
|
this.defaultTtl = options.defaultTtl || 5000; // 5 seconds default
|
|
49
52
|
|
|
50
53
|
// Start the cache cleanup timer
|
|
51
|
-
setInterval(() => this.cleanupCache(), 30000); // Cleanup every 30 seconds
|
|
54
|
+
this.cleanupInterval = setInterval(() => this.cleanupCache(), 30000); // Cleanup every 30 seconds
|
|
55
|
+
|
|
56
|
+
// Make sure the interval doesn't keep the process alive
|
|
57
|
+
if (this.cleanupInterval.unref) {
|
|
58
|
+
this.cleanupInterval.unref();
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
/**
|
|
@@ -256,4 +264,16 @@ export class FunctionCache {
|
|
|
256
264
|
this.portCache.clear();
|
|
257
265
|
this.logger.info('Function cache cleared');
|
|
258
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Destroy the cache and cleanup resources
|
|
270
|
+
*/
|
|
271
|
+
public destroy(): void {
|
|
272
|
+
if (this.cleanupInterval) {
|
|
273
|
+
clearInterval(this.cleanupInterval);
|
|
274
|
+
this.cleanupInterval = null;
|
|
275
|
+
}
|
|
276
|
+
this.clearCache();
|
|
277
|
+
this.logger.debug('Function cache destroyed');
|
|
278
|
+
}
|
|
259
279
|
}
|
|
@@ -464,6 +464,11 @@ export class HttpProxy implements IMetricsTracker {
|
|
|
464
464
|
// Stop WebSocket handler
|
|
465
465
|
this.webSocketHandler.shutdown();
|
|
466
466
|
|
|
467
|
+
// Destroy request handler (cleans up intervals and caches)
|
|
468
|
+
if (this.requestHandler && typeof this.requestHandler.destroy === 'function') {
|
|
469
|
+
this.requestHandler.destroy();
|
|
470
|
+
}
|
|
471
|
+
|
|
467
472
|
// Close all tracked sockets
|
|
468
473
|
const socketCleanupPromises = this.socketMap.getArray().map(socket =>
|
|
469
474
|
cleanupSocket(socket, 'http-proxy-stop', { immediate: true })
|
|
@@ -42,6 +42,9 @@ export class RequestHandler {
|
|
|
42
42
|
|
|
43
43
|
// Security manager for IP filtering, rate limiting, etc.
|
|
44
44
|
public securityManager: SecurityManager;
|
|
45
|
+
|
|
46
|
+
// Rate limit cleanup interval
|
|
47
|
+
private rateLimitCleanupInterval: NodeJS.Timeout | null = null;
|
|
45
48
|
|
|
46
49
|
constructor(
|
|
47
50
|
private options: IHttpProxyOptions,
|
|
@@ -54,9 +57,14 @@ export class RequestHandler {
|
|
|
54
57
|
this.securityManager = new SecurityManager(this.logger);
|
|
55
58
|
|
|
56
59
|
// Schedule rate limit cleanup every minute
|
|
57
|
-
setInterval(() => {
|
|
60
|
+
this.rateLimitCleanupInterval = setInterval(() => {
|
|
58
61
|
this.securityManager.cleanupExpiredRateLimits();
|
|
59
62
|
}, 60000);
|
|
63
|
+
|
|
64
|
+
// Make sure the interval doesn't keep the process alive
|
|
65
|
+
if (this.rateLimitCleanupInterval.unref) {
|
|
66
|
+
this.rateLimitCleanupInterval.unref();
|
|
67
|
+
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/**
|
|
@@ -741,4 +749,27 @@ export class RequestHandler {
|
|
|
741
749
|
stream.end('Not Found: No route configuration for this request');
|
|
742
750
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
|
743
751
|
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Cleanup resources and stop intervals
|
|
755
|
+
*/
|
|
756
|
+
public destroy(): void {
|
|
757
|
+
if (this.rateLimitCleanupInterval) {
|
|
758
|
+
clearInterval(this.rateLimitCleanupInterval);
|
|
759
|
+
this.rateLimitCleanupInterval = null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Close all HTTP/2 sessions
|
|
763
|
+
for (const [key, session] of this.h2Sessions) {
|
|
764
|
+
session.close();
|
|
765
|
+
}
|
|
766
|
+
this.h2Sessions.clear();
|
|
767
|
+
|
|
768
|
+
// Clear function cache if it has a destroy method
|
|
769
|
+
if (this.functionCache && typeof this.functionCache.destroy === 'function') {
|
|
770
|
+
this.functionCache.destroy();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
this.logger.debug('RequestHandler destroyed');
|
|
774
|
+
}
|
|
744
775
|
}
|
|
@@ -488,14 +488,19 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
488
488
|
// Check for half-zombie: one socket destroyed
|
|
489
489
|
if (incomingDestroyed || outgoingDestroyed) {
|
|
490
490
|
const age = now - record.incomingStartTime;
|
|
491
|
-
//
|
|
492
|
-
|
|
491
|
+
// Use longer grace period for encrypted connections (5 minutes vs 30 seconds)
|
|
492
|
+
const gracePeriod = record.isTLS ? 300000 : 30000;
|
|
493
|
+
|
|
494
|
+
// Also ensure connection is old enough to avoid premature cleanup
|
|
495
|
+
if (age > gracePeriod && age > 10000) {
|
|
493
496
|
logger.log('warn', `Half-zombie connection detected: ${connectionId} - ${incomingDestroyed ? 'incoming' : 'outgoing'} destroyed`, {
|
|
494
497
|
connectionId,
|
|
495
498
|
remoteIP: record.remoteIP,
|
|
496
499
|
age: plugins.prettyMs(age),
|
|
497
500
|
incomingDestroyed,
|
|
498
501
|
outgoingDestroyed,
|
|
502
|
+
isTLS: record.isTLS,
|
|
503
|
+
gracePeriod: plugins.prettyMs(gracePeriod),
|
|
499
504
|
component: 'connection-manager'
|
|
500
505
|
});
|
|
501
506
|
|
|
@@ -507,8 +512,11 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
507
512
|
// Check for stuck connections: no data sent back to client
|
|
508
513
|
if (!record.connectionClosed && record.outgoing && record.bytesReceived > 0 && record.bytesSent === 0) {
|
|
509
514
|
const age = now - record.incomingStartTime;
|
|
510
|
-
//
|
|
511
|
-
|
|
515
|
+
// Use longer grace period for encrypted connections (5 minutes vs 60 seconds)
|
|
516
|
+
const stuckThreshold = record.isTLS ? 300000 : 60000;
|
|
517
|
+
|
|
518
|
+
// If connection is older than threshold and no data sent back, likely stuck
|
|
519
|
+
if (age > stuckThreshold) {
|
|
512
520
|
logger.log('warn', `Stuck connection detected: ${connectionId} - received ${record.bytesReceived} bytes but sent 0 bytes`, {
|
|
513
521
|
connectionId,
|
|
514
522
|
remoteIP: record.remoteIP,
|
|
@@ -516,6 +524,8 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
516
524
|
bytesReceived: record.bytesReceived,
|
|
517
525
|
targetHost: record.targetHost,
|
|
518
526
|
targetPort: record.targetPort,
|
|
527
|
+
isTLS: record.isTLS,
|
|
528
|
+
threshold: plugins.prettyMs(stuckThreshold),
|
|
519
529
|
component: 'connection-manager'
|
|
520
530
|
});
|
|
521
531
|
|
|
@@ -10,6 +10,7 @@ export class MetricsCollector implements IProxyStatsExtended {
|
|
|
10
10
|
// RPS tracking (the only state we need to maintain)
|
|
11
11
|
private requestTimestamps: number[] = [];
|
|
12
12
|
private readonly RPS_WINDOW_SIZE = 60000; // 1 minute window
|
|
13
|
+
private readonly MAX_TIMESTAMPS = 5000; // Maximum timestamps to keep
|
|
13
14
|
|
|
14
15
|
// Optional caching for performance
|
|
15
16
|
private cachedMetrics: {
|
|
@@ -148,11 +149,14 @@ export class MetricsCollector implements IProxyStatsExtended {
|
|
|
148
149
|
* Record a new request for RPS tracking
|
|
149
150
|
*/
|
|
150
151
|
public recordRequest(): void {
|
|
151
|
-
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
this.requestTimestamps.push(now);
|
|
152
154
|
|
|
153
|
-
// Prevent unbounded growth
|
|
154
|
-
if (this.requestTimestamps.length >
|
|
155
|
-
|
|
155
|
+
// Prevent unbounded growth - clean up more aggressively
|
|
156
|
+
if (this.requestTimestamps.length > this.MAX_TIMESTAMPS) {
|
|
157
|
+
// Keep only timestamps within the window
|
|
158
|
+
const cutoff = now - this.RPS_WINDOW_SIZE;
|
|
159
|
+
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -10,7 +10,7 @@ import { TlsManager } from './tls-manager.js';
|
|
|
10
10
|
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
|
11
11
|
import { TimeoutManager } from './timeout-manager.js';
|
|
12
12
|
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
13
|
-
import { cleanupSocket,
|
|
13
|
+
import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
|
14
14
|
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
|
15
15
|
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
|
|
16
16
|
import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
|
|
@@ -21,8 +21,9 @@ import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
|
|
|
21
21
|
export class RouteConnectionHandler {
|
|
22
22
|
private settings: ISmartProxyOptions;
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
24
|
+
// Note: Route context caching was considered but not implemented
|
|
25
|
+
// as route contexts are lightweight and should be created fresh
|
|
26
|
+
// for each connection to ensure accurate context data
|
|
26
27
|
|
|
27
28
|
// RxJS Subject for new connections
|
|
28
29
|
public newConnectionSubject = new plugins.smartrx.rxjs.Subject<IConnectionRecord>();
|
|
@@ -730,8 +731,7 @@ export class RouteConnectionHandler {
|
|
|
730
731
|
routeId: route.id,
|
|
731
732
|
});
|
|
732
733
|
|
|
733
|
-
//
|
|
734
|
-
this.routeContextCache.set(connectionId, routeContext);
|
|
734
|
+
// Note: Route contexts are not cached to ensure fresh data for each connection
|
|
735
735
|
|
|
736
736
|
// Determine host using function or static value
|
|
737
737
|
let targetHost: string | string[];
|