@push.rocks/smartproxy 19.5.26 → 19.6.1

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 (30) hide show
  1. package/dist_ts/plugins.d.ts +2 -1
  2. package/dist_ts/plugins.js +3 -2
  3. package/dist_ts/proxies/http-proxy/function-cache.d.ts +5 -0
  4. package/dist_ts/proxies/http-proxy/function-cache.js +19 -2
  5. package/dist_ts/proxies/http-proxy/http-proxy.js +5 -1
  6. package/dist_ts/proxies/http-proxy/request-handler.d.ts +5 -0
  7. package/dist_ts/proxies/http-proxy/request-handler.js +27 -2
  8. package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +80 -0
  9. package/dist_ts/proxies/smart-proxy/metrics-collector.js +239 -0
  10. package/dist_ts/proxies/smart-proxy/models/index.d.ts +1 -0
  11. package/dist_ts/proxies/smart-proxy/models/index.js +2 -1
  12. package/dist_ts/proxies/smart-proxy/models/metrics-types.d.ts +55 -0
  13. package/dist_ts/proxies/smart-proxy/models/metrics-types.js +2 -0
  14. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +2 -2
  15. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +14 -6
  16. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +12 -2
  17. package/dist_ts/proxies/smart-proxy/smart-proxy.js +17 -1
  18. package/package.json +18 -8
  19. package/readme.md +118 -0
  20. package/readme.memory-leaks-fixed.md +45 -0
  21. package/readme.metrics.md +591 -0
  22. package/ts/plugins.ts +2 -0
  23. package/ts/proxies/http-proxy/function-cache.ts +21 -1
  24. package/ts/proxies/http-proxy/http-proxy.ts +5 -0
  25. package/ts/proxies/http-proxy/request-handler.ts +32 -1
  26. package/ts/proxies/smart-proxy/metrics-collector.ts +289 -0
  27. package/ts/proxies/smart-proxy/models/index.ts +1 -0
  28. package/ts/proxies/smart-proxy/models/metrics-types.ts +54 -0
  29. package/ts/proxies/smart-proxy/route-connection-handler.ts +18 -5
  30. package/ts/proxies/smart-proxy/smart-proxy.ts +27 -2
@@ -0,0 +1,289 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { SmartProxy } from './smart-proxy.js';
3
+ import type { IProxyStats, IProxyStatsExtended } from './models/metrics-types.js';
4
+ import { logger } from '../../core/utils/logger.js';
5
+
6
+ /**
7
+ * Collects and computes metrics for SmartProxy on-demand
8
+ */
9
+ export class MetricsCollector implements IProxyStatsExtended {
10
+ // RPS tracking (the only state we need to maintain)
11
+ private requestTimestamps: number[] = [];
12
+ private readonly RPS_WINDOW_SIZE = 60000; // 1 minute window
13
+ private readonly MAX_TIMESTAMPS = 5000; // Maximum timestamps to keep
14
+
15
+ // Optional caching for performance
16
+ private cachedMetrics: {
17
+ timestamp: number;
18
+ connectionsByRoute?: Map<string, number>;
19
+ connectionsByIP?: Map<string, number>;
20
+ } = { timestamp: 0 };
21
+
22
+ private readonly CACHE_TTL = 1000; // 1 second cache
23
+
24
+ // RxJS subscription for connection events
25
+ private connectionSubscription?: plugins.smartrx.rxjs.Subscription;
26
+
27
+ constructor(
28
+ private smartProxy: SmartProxy
29
+ ) {
30
+ // Subscription will be set up in start() method
31
+ }
32
+
33
+ /**
34
+ * Get the current number of active connections
35
+ */
36
+ public getActiveConnections(): number {
37
+ return this.smartProxy.connectionManager.getConnectionCount();
38
+ }
39
+
40
+ /**
41
+ * Get connection counts grouped by route name
42
+ */
43
+ public getConnectionsByRoute(): Map<string, number> {
44
+ const now = Date.now();
45
+
46
+ // Return cached value if fresh
47
+ if (this.cachedMetrics.connectionsByRoute &&
48
+ now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
49
+ return new Map(this.cachedMetrics.connectionsByRoute);
50
+ }
51
+
52
+ // Compute fresh value
53
+ const routeCounts = new Map<string, number>();
54
+ const connections = this.smartProxy.connectionManager.getConnections();
55
+
56
+ if (this.smartProxy.settings?.enableDetailedLogging) {
57
+ logger.log('debug', `MetricsCollector: Computing route connections`, {
58
+ totalConnections: connections.size,
59
+ component: 'metrics'
60
+ });
61
+ }
62
+
63
+ for (const [_, record] of connections) {
64
+ // Try different ways to get the route name
65
+ const routeName = (record as any).routeName ||
66
+ record.routeConfig?.name ||
67
+ (record.routeConfig as any)?.routeName ||
68
+ 'unknown';
69
+
70
+ if (this.smartProxy.settings?.enableDetailedLogging) {
71
+ logger.log('debug', `MetricsCollector: Connection route info`, {
72
+ connectionId: record.id,
73
+ routeName,
74
+ hasRouteConfig: !!record.routeConfig,
75
+ routeConfigName: record.routeConfig?.name,
76
+ routeConfigKeys: record.routeConfig ? Object.keys(record.routeConfig) : [],
77
+ component: 'metrics'
78
+ });
79
+ }
80
+
81
+ const current = routeCounts.get(routeName) || 0;
82
+ routeCounts.set(routeName, current + 1);
83
+ }
84
+
85
+ // Cache and return
86
+ this.cachedMetrics.connectionsByRoute = routeCounts;
87
+ this.cachedMetrics.timestamp = now;
88
+ return new Map(routeCounts);
89
+ }
90
+
91
+ /**
92
+ * Get connection counts grouped by IP address
93
+ */
94
+ public getConnectionsByIP(): Map<string, number> {
95
+ const now = Date.now();
96
+
97
+ // Return cached value if fresh
98
+ if (this.cachedMetrics.connectionsByIP &&
99
+ now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
100
+ return new Map(this.cachedMetrics.connectionsByIP);
101
+ }
102
+
103
+ // Compute fresh value
104
+ const ipCounts = new Map<string, number>();
105
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
106
+ const ip = record.remoteIP;
107
+ const current = ipCounts.get(ip) || 0;
108
+ ipCounts.set(ip, current + 1);
109
+ }
110
+
111
+ // Cache and return
112
+ this.cachedMetrics.connectionsByIP = ipCounts;
113
+ this.cachedMetrics.timestamp = now;
114
+ return new Map(ipCounts);
115
+ }
116
+
117
+ /**
118
+ * Get the total number of connections since proxy start
119
+ */
120
+ public getTotalConnections(): number {
121
+ // Get from termination stats
122
+ const stats = this.smartProxy.connectionManager.getTerminationStats();
123
+ let total = this.smartProxy.connectionManager.getConnectionCount(); // Add active connections
124
+
125
+ // Add all terminated connections
126
+ for (const reason in stats.incoming) {
127
+ total += stats.incoming[reason];
128
+ }
129
+
130
+ return total;
131
+ }
132
+
133
+ /**
134
+ * Get the current requests per second rate
135
+ */
136
+ public getRequestsPerSecond(): number {
137
+ const now = Date.now();
138
+ const windowStart = now - this.RPS_WINDOW_SIZE;
139
+
140
+ // Clean old timestamps
141
+ this.requestTimestamps = this.requestTimestamps.filter(ts => ts > windowStart);
142
+
143
+ // Calculate RPS based on window
144
+ const requestsInWindow = this.requestTimestamps.length;
145
+ return requestsInWindow / (this.RPS_WINDOW_SIZE / 1000);
146
+ }
147
+
148
+ /**
149
+ * Record a new request for RPS tracking
150
+ */
151
+ public recordRequest(): void {
152
+ const now = Date.now();
153
+ this.requestTimestamps.push(now);
154
+
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);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get total throughput (bytes transferred)
165
+ */
166
+ public getThroughput(): { bytesIn: number; bytesOut: number } {
167
+ let bytesIn = 0;
168
+ let bytesOut = 0;
169
+
170
+ // Sum bytes from all active connections
171
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
172
+ bytesIn += record.bytesReceived;
173
+ bytesOut += record.bytesSent;
174
+ }
175
+
176
+ return { bytesIn, bytesOut };
177
+ }
178
+
179
+ /**
180
+ * Get throughput rate (bytes per second) for last minute
181
+ */
182
+ public getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number } {
183
+ const now = Date.now();
184
+ let recentBytesIn = 0;
185
+ let recentBytesOut = 0;
186
+
187
+ // Calculate bytes transferred in last minute from active connections
188
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
189
+ const connectionAge = now - record.incomingStartTime;
190
+ if (connectionAge < 60000) { // Connection started within last minute
191
+ recentBytesIn += record.bytesReceived;
192
+ recentBytesOut += record.bytesSent;
193
+ } else {
194
+ // For older connections, estimate rate based on average
195
+ const rate = connectionAge / 60000;
196
+ recentBytesIn += record.bytesReceived / rate;
197
+ recentBytesOut += record.bytesSent / rate;
198
+ }
199
+ }
200
+
201
+ return {
202
+ bytesInPerSec: Math.round(recentBytesIn / 60),
203
+ bytesOutPerSec: Math.round(recentBytesOut / 60)
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Get top IPs by connection count
209
+ */
210
+ public getTopIPs(limit: number = 10): Array<{ ip: string; connections: number }> {
211
+ const ipCounts = this.getConnectionsByIP();
212
+ const sorted = Array.from(ipCounts.entries())
213
+ .sort((a, b) => b[1] - a[1])
214
+ .slice(0, limit)
215
+ .map(([ip, connections]) => ({ ip, connections }));
216
+
217
+ return sorted;
218
+ }
219
+
220
+ /**
221
+ * Check if an IP has reached the connection limit
222
+ */
223
+ public isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean {
224
+ const ipCounts = this.getConnectionsByIP();
225
+ const currentConnections = ipCounts.get(ip) || 0;
226
+ return currentConnections >= maxConnectionsPerIP;
227
+ }
228
+
229
+ /**
230
+ * Clean up old request timestamps
231
+ */
232
+ private cleanupOldRequests(): void {
233
+ const cutoff = Date.now() - this.RPS_WINDOW_SIZE;
234
+ this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
235
+ }
236
+
237
+ /**
238
+ * Start the metrics collector and set up subscriptions
239
+ */
240
+ public start(): void {
241
+ if (!this.smartProxy.routeConnectionHandler) {
242
+ throw new Error('MetricsCollector: RouteConnectionHandler not available');
243
+ }
244
+
245
+ // Subscribe to the newConnectionSubject from RouteConnectionHandler
246
+ this.connectionSubscription = this.smartProxy.routeConnectionHandler.newConnectionSubject.subscribe({
247
+ next: (record) => {
248
+ this.recordRequest();
249
+
250
+ // Optional: Log connection details
251
+ if (this.smartProxy.settings?.enableDetailedLogging) {
252
+ logger.log('debug', `MetricsCollector: New connection recorded`, {
253
+ connectionId: record.id,
254
+ remoteIP: record.remoteIP,
255
+ routeName: record.routeConfig?.name || 'unknown',
256
+ component: 'metrics'
257
+ });
258
+ }
259
+ },
260
+ error: (err) => {
261
+ logger.log('error', `MetricsCollector: Error in connection subscription`, {
262
+ error: err.message,
263
+ component: 'metrics'
264
+ });
265
+ }
266
+ });
267
+
268
+ logger.log('debug', 'MetricsCollector started', { component: 'metrics' });
269
+ }
270
+
271
+ /**
272
+ * Stop the metrics collector and clean up resources
273
+ */
274
+ public stop(): void {
275
+ if (this.connectionSubscription) {
276
+ this.connectionSubscription.unsubscribe();
277
+ this.connectionSubscription = undefined;
278
+ }
279
+
280
+ logger.log('debug', 'MetricsCollector stopped', { component: 'metrics' });
281
+ }
282
+
283
+ /**
284
+ * Alias for stop() for backward compatibility
285
+ */
286
+ public destroy(): void {
287
+ this.stop();
288
+ }
289
+ }
@@ -4,3 +4,4 @@
4
4
  // Export everything except IAcmeOptions from interfaces
5
5
  export type { ISmartProxyOptions, IConnectionRecord, TSmartProxyCertProvisionObject } from './interfaces.js';
6
6
  export * from './route-types.js';
7
+ export * from './metrics-types.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Interface for proxy statistics and metrics
3
+ */
4
+ export interface IProxyStats {
5
+ /**
6
+ * Get the current number of active connections
7
+ */
8
+ getActiveConnections(): number;
9
+
10
+ /**
11
+ * Get connection counts grouped by route name
12
+ */
13
+ getConnectionsByRoute(): Map<string, number>;
14
+
15
+ /**
16
+ * Get connection counts grouped by IP address
17
+ */
18
+ getConnectionsByIP(): Map<string, number>;
19
+
20
+ /**
21
+ * Get the total number of connections since proxy start
22
+ */
23
+ getTotalConnections(): number;
24
+
25
+ /**
26
+ * Get the current requests per second rate
27
+ */
28
+ getRequestsPerSecond(): number;
29
+
30
+ /**
31
+ * Get total throughput (bytes transferred)
32
+ */
33
+ getThroughput(): { bytesIn: number; bytesOut: number };
34
+ }
35
+
36
+ /**
37
+ * Extended interface for additional metrics helpers
38
+ */
39
+ export interface IProxyStatsExtended extends IProxyStats {
40
+ /**
41
+ * Get throughput rate (bytes per second) for last minute
42
+ */
43
+ getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number };
44
+
45
+ /**
46
+ * Get top IPs by connection count
47
+ */
48
+ getTopIPs(limit?: number): Array<{ ip: string; connections: number }>;
49
+
50
+ /**
51
+ * Check if an IP has reached the connection limit
52
+ */
53
+ isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean;
54
+ }
@@ -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, createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
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,12 @@ import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
21
21
  export class RouteConnectionHandler {
22
22
  private settings: ISmartProxyOptions;
23
23
 
24
- // Cache for route contexts to avoid recreation
25
- private routeContextCache: Map<string, IRouteContext> = new Map();
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
27
+
28
+ // RxJS Subject for new connections
29
+ public newConnectionSubject = new plugins.smartrx.rxjs.Subject<IConnectionRecord>();
26
30
 
27
31
  constructor(
28
32
  settings: ISmartProxyOptions,
@@ -35,6 +39,7 @@ export class RouteConnectionHandler {
35
39
  ) {
36
40
  this.settings = settings;
37
41
  }
42
+
38
43
 
39
44
  /**
40
45
  * Create a route context object for port and host mapping functions
@@ -110,6 +115,9 @@ export class RouteConnectionHandler {
110
115
  // Connection was rejected due to limit - socket already destroyed by connection manager
111
116
  return;
112
117
  }
118
+
119
+ // Emit new connection event
120
+ this.newConnectionSubject.next(record);
113
121
  const connectionId = record.id;
114
122
 
115
123
  // Apply socket optimizations (apply to underlying socket)
@@ -640,6 +648,9 @@ export class RouteConnectionHandler {
640
648
  ): void {
641
649
  const connectionId = record.id;
642
650
  const action = route.action as IRouteAction;
651
+
652
+ // Store the route config in the connection record for metrics and other uses
653
+ record.routeConfig = route;
643
654
 
644
655
  // Check if this route uses NFTables for forwarding
645
656
  if (action.forwardingEngine === 'nftables') {
@@ -720,8 +731,7 @@ export class RouteConnectionHandler {
720
731
  routeId: route.id,
721
732
  });
722
733
 
723
- // Cache the context for potential reuse
724
- this.routeContextCache.set(connectionId, routeContext);
734
+ // Note: Route contexts are not cached to ensure fresh data for each connection
725
735
 
726
736
  // Determine host using function or static value
727
737
  let targetHost: string | string[];
@@ -957,6 +967,9 @@ export class RouteConnectionHandler {
957
967
  ): Promise<void> {
958
968
  const connectionId = record.id;
959
969
 
970
+ // Store the route config in the connection record for metrics and other uses
971
+ record.routeConfig = route;
972
+
960
973
  if (!route.action.socketHandler) {
961
974
  logger.log('error', 'socket-handler action missing socketHandler function', {
962
975
  connectionId,
@@ -27,6 +27,10 @@ import { Mutex } from './utils/mutex.js';
27
27
  // Import ACME state manager
28
28
  import { AcmeStateManager } from './acme-state-manager.js';
29
29
 
30
+ // Import metrics collector
31
+ import { MetricsCollector } from './metrics-collector.js';
32
+ import type { IProxyStats } from './models/metrics-types.js';
33
+
30
34
  /**
31
35
  * SmartProxy - Pure route-based API
32
36
  *
@@ -47,13 +51,13 @@ export class SmartProxy extends plugins.EventEmitter {
47
51
  private isShuttingDown: boolean = false;
48
52
 
49
53
  // Component managers
50
- private connectionManager: ConnectionManager;
54
+ public connectionManager: ConnectionManager;
51
55
  private securityManager: SecurityManager;
52
56
  private tlsManager: TlsManager;
53
57
  private httpProxyBridge: HttpProxyBridge;
54
58
  private timeoutManager: TimeoutManager;
55
59
  public routeManager: RouteManager; // Made public for route management
56
- private routeConnectionHandler: RouteConnectionHandler;
60
+ public routeConnectionHandler: RouteConnectionHandler; // Made public for metrics
57
61
  private nftablesManager: NFTablesManager;
58
62
 
59
63
  // Certificate manager for ACME and static certificates
@@ -64,6 +68,9 @@ export class SmartProxy extends plugins.EventEmitter {
64
68
  private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
65
69
  private acmeStateManager: AcmeStateManager;
66
70
 
71
+ // Metrics collector
72
+ private metricsCollector: MetricsCollector;
73
+
67
74
  // Track port usage across route updates
68
75
  private portUsageMap: Map<number, Set<string>> = new Map();
69
76
 
@@ -204,6 +211,9 @@ export class SmartProxy extends plugins.EventEmitter {
204
211
 
205
212
  // Initialize ACME state manager
206
213
  this.acmeStateManager = new AcmeStateManager();
214
+
215
+ // Initialize metrics collector with reference to this SmartProxy instance
216
+ this.metricsCollector = new MetricsCollector(this);
207
217
  }
208
218
 
209
219
  /**
@@ -383,6 +393,9 @@ export class SmartProxy extends plugins.EventEmitter {
383
393
  logger.log('info', 'Starting certificate provisioning now that ports are ready', { component: 'certificate-manager' });
384
394
  await this.certManager.provisionAllCertificates();
385
395
  }
396
+
397
+ // Start the metrics collector now that all components are initialized
398
+ this.metricsCollector.start();
386
399
 
387
400
  // Set up periodic connection logging and inactivity checks
388
401
  this.connectionLogger = setInterval(() => {
@@ -508,6 +521,9 @@ export class SmartProxy extends plugins.EventEmitter {
508
521
 
509
522
  // Clear ACME state manager
510
523
  this.acmeStateManager.clear();
524
+
525
+ // Stop metrics collector
526
+ this.metricsCollector.stop();
511
527
 
512
528
  logger.log('info', 'SmartProxy shutdown complete.');
513
529
  }
@@ -905,6 +921,15 @@ export class SmartProxy extends plugins.EventEmitter {
905
921
  return this.certManager.getCertificateStatus(routeName);
906
922
  }
907
923
 
924
+ /**
925
+ * Get proxy statistics and metrics
926
+ *
927
+ * @returns IProxyStats interface with various metrics methods
928
+ */
929
+ public getStats(): IProxyStats {
930
+ return this.metricsCollector;
931
+ }
932
+
908
933
  /**
909
934
  * Validates if a domain name is valid for certificate issuance
910
935
  */