@push.rocks/smartproxy 21.1.2 → 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 (43) 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 +19 -7
  8. package/dist_ts/detection/detectors/tls-detector.d.ts +1 -9
  9. package/dist_ts/detection/detectors/tls-detector.js +6 -29
  10. package/dist_ts/detection/protocol-detector.d.ts +7 -2
  11. package/dist_ts/detection/protocol-detector.js +80 -8
  12. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +5 -1
  13. package/dist_ts/proxies/http-proxy/http-proxy.js +63 -39
  14. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +5 -0
  15. package/dist_ts/proxies/smart-proxy/certificate-manager.js +20 -6
  16. package/dist_ts/proxies/smart-proxy/connection-manager.js +12 -1
  17. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
  18. package/dist_ts/proxies/smart-proxy/index.js +2 -1
  19. package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +4 -0
  20. package/dist_ts/proxies/smart-proxy/metrics-collector.js +52 -7
  21. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +7 -7
  22. package/dist_ts/proxies/smart-proxy/route-orchestrator.d.ts +56 -0
  23. package/dist_ts/proxies/smart-proxy/route-orchestrator.js +204 -0
  24. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -11
  25. package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -237
  26. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +42 -7
  27. package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +58 -0
  28. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +405 -0
  29. package/package.json +3 -2
  30. package/readme.md +321 -828
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/core/models/socket-augmentation.ts +5 -0
  33. package/ts/core/utils/socket-tracker.ts +63 -0
  34. package/ts/detection/detectors/http-detector.ts +20 -7
  35. package/ts/detection/protocol-detector.ts +57 -6
  36. package/ts/proxies/http-proxy/http-proxy.ts +73 -48
  37. package/ts/proxies/smart-proxy/certificate-manager.ts +21 -5
  38. package/ts/proxies/smart-proxy/index.ts +1 -0
  39. package/ts/proxies/smart-proxy/metrics-collector.ts +58 -6
  40. package/ts/proxies/smart-proxy/route-orchestrator.ts +297 -0
  41. package/ts/proxies/smart-proxy/smart-proxy.ts +66 -270
  42. package/ts/proxies/smart-proxy/utils/route-helpers.ts +45 -6
  43. package/ts/proxies/smart-proxy/utils/route-validator.ts +453 -0
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '19.5.19',
6
+ version: '21.1.5',
7
7
  description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
8
8
  }
@@ -12,6 +12,11 @@ declare module 'net' {
12
12
  getTLSVersion?(): string; // Returns the TLS version (e.g., 'TLSv1.2', 'TLSv1.3')
13
13
  getPeerCertificate?(detailed?: boolean): any; // Returns the peer's certificate
14
14
  getSession?(): Buffer; // Returns the TLS session data
15
+
16
+ // Connection tracking properties (used by HttpProxy)
17
+ _connectionId?: string; // Unique identifier for the connection
18
+ _remoteIP?: string; // Remote IP address
19
+ _realRemoteIP?: string; // Real remote IP (when proxied)
15
20
  }
16
21
  }
17
22
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Socket Tracker Utility
3
+ * Provides standardized socket cleanup with proper listener and timer management
4
+ */
5
+
6
+ import type { Socket } from 'net';
7
+
8
+ export type SocketTracked = {
9
+ cleanup: () => void;
10
+ addListener: <E extends string>(event: E, listener: (...args: any[]) => void) => void;
11
+ addTimer: (t: NodeJS.Timeout | null | undefined) => void;
12
+ safeDestroy: (reason?: Error) => void;
13
+ };
14
+
15
+ /**
16
+ * Create a socket tracker to manage listeners and timers
17
+ * Ensures proper cleanup and prevents memory leaks
18
+ */
19
+ export function createSocketTracker(socket: Socket): SocketTracked {
20
+ const listeners: Array<{ event: string; listener: (...args: any[]) => void }> = [];
21
+ const timers: NodeJS.Timeout[] = [];
22
+ let cleaned = false;
23
+
24
+ const addListener = (event: string, listener: (...args: any[]) => void) => {
25
+ socket.on(event, listener);
26
+ listeners.push({ event, listener });
27
+ };
28
+
29
+ const addTimer = (t: NodeJS.Timeout | null | undefined) => {
30
+ if (!t) return;
31
+ timers.push(t);
32
+ // Unref timer so it doesn't keep process alive
33
+ if (typeof t.unref === 'function') {
34
+ t.unref();
35
+ }
36
+ };
37
+
38
+ const cleanup = () => {
39
+ if (cleaned) return;
40
+ cleaned = true;
41
+
42
+ // Clear all tracked timers
43
+ for (const t of timers) {
44
+ clearTimeout(t);
45
+ }
46
+ timers.length = 0;
47
+
48
+ // Remove all tracked listeners
49
+ for (const { event, listener } of listeners) {
50
+ socket.off(event, listener);
51
+ }
52
+ listeners.length = 0;
53
+ };
54
+
55
+ const safeDestroy = (reason?: Error) => {
56
+ cleanup();
57
+ if (!socket.destroyed) {
58
+ socket.destroy(reason);
59
+ }
60
+ };
61
+
62
+ return { cleanup, addListener, addTimer, safeDestroy };
63
+ }
@@ -11,6 +11,7 @@ import type { THttpMethod } from '../../protocols/http/index.js';
11
11
  import { QuickProtocolDetector } from './quick-detector.js';
12
12
  import { RoutingExtractor } from './routing-extractor.js';
13
13
  import { DetectionFragmentManager } from '../utils/fragment-manager.js';
14
+ import { HttpParser } from '../../protocols/http/parser.js';
14
15
 
15
16
  /**
16
17
  * Simplified HTTP detector
@@ -49,11 +50,26 @@ export class HttpDetector implements IProtocolDetector {
49
50
  return null;
50
51
  }
51
52
 
53
+ // Check if we have complete headers first
54
+ const headersEnd = buffer.indexOf('\r\n\r\n');
55
+ const isComplete = headersEnd !== -1;
56
+
52
57
  // Extract routing information
53
58
  const routing = RoutingExtractor.extract(buffer, 'http');
54
59
 
55
- // If we don't need full headers, we can return early
56
- if (quickResult.confidence >= 95 && !options?.extractFullHeaders) {
60
+ // Extract headers if requested and we have complete headers
61
+ let headers: Record<string, string> | undefined;
62
+ if (options?.extractFullHeaders && isComplete) {
63
+ const headerSection = buffer.slice(0, headersEnd).toString();
64
+ const lines = headerSection.split('\r\n');
65
+ if (lines.length > 1) {
66
+ // Skip the request line and parse headers
67
+ headers = HttpParser.parseHeaders(lines.slice(1));
68
+ }
69
+ }
70
+
71
+ // If we don't need full headers and we have complete headers, we can return early
72
+ if (quickResult.confidence >= 95 && !options?.extractFullHeaders && isComplete) {
57
73
  return {
58
74
  protocol: 'http',
59
75
  connectionInfo: {
@@ -66,17 +82,14 @@ export class HttpDetector implements IProtocolDetector {
66
82
  };
67
83
  }
68
84
 
69
- // Check if we have complete headers
70
- const headersEnd = buffer.indexOf('\r\n\r\n');
71
- const isComplete = headersEnd !== -1;
72
-
73
85
  return {
74
86
  protocol: 'http',
75
87
  connectionInfo: {
76
88
  protocol: 'http',
77
89
  domain: routing?.domain,
78
90
  path: routing?.path,
79
- method: quickResult.metadata?.method as THttpMethod
91
+ method: quickResult.metadata?.method as THttpMethod,
92
+ headers: headers
80
93
  },
81
94
  isComplete,
82
95
  bytesNeeded: isComplete ? undefined : buffer.length + 512 // Need more for headers
@@ -18,6 +18,7 @@ export class ProtocolDetector {
18
18
  private fragmentManager: DetectionFragmentManager;
19
19
  private tlsDetector: TlsDetector;
20
20
  private httpDetector: HttpDetector;
21
+ private connectionProtocols: Map<string, 'tls' | 'http'> = new Map();
21
22
 
22
23
  constructor() {
23
24
  this.fragmentManager = new DetectionFragmentManager();
@@ -122,14 +123,25 @@ export class ProtocolDetector {
122
123
 
123
124
  const connectionId = DetectionFragmentManager.createConnectionId(context);
124
125
 
125
- // First peek to determine protocol type
126
- if (this.tlsDetector.canHandle(buffer)) {
126
+ // Check if we already know the protocol for this connection
127
+ const knownProtocol = this.connectionProtocols.get(connectionId);
128
+
129
+ if (knownProtocol === 'http') {
130
+ const result = this.httpDetector.detectWithContext(buffer, context, options);
131
+ if (result) {
132
+ if (result.isComplete) {
133
+ this.connectionProtocols.delete(connectionId);
134
+ }
135
+ return result;
136
+ }
137
+ } else if (knownProtocol === 'tls') {
127
138
  // Handle TLS with fragment accumulation
128
139
  const handler = this.fragmentManager.getHandler('tls');
129
140
  const fragmentResult = handler.addFragment(connectionId, buffer);
130
141
 
131
142
  if (fragmentResult.error) {
132
143
  handler.complete(connectionId);
144
+ this.connectionProtocols.delete(connectionId);
133
145
  return {
134
146
  protocol: 'unknown',
135
147
  connectionInfo: { protocol: 'unknown' },
@@ -141,15 +153,50 @@ export class ProtocolDetector {
141
153
  if (result) {
142
154
  if (result.isComplete) {
143
155
  handler.complete(connectionId);
156
+ this.connectionProtocols.delete(connectionId);
144
157
  }
145
158
  return result;
146
159
  }
147
160
  }
148
161
 
149
- if (this.httpDetector.canHandle(buffer)) {
150
- const result = this.httpDetector.detectWithContext(buffer, context, options);
151
- if (result) {
152
- return result;
162
+ // If we don't know the protocol yet, try to detect it
163
+ if (!knownProtocol) {
164
+ // First peek to determine protocol type
165
+ if (this.tlsDetector.canHandle(buffer)) {
166
+ this.connectionProtocols.set(connectionId, 'tls');
167
+ // Handle TLS with fragment accumulation
168
+ const handler = this.fragmentManager.getHandler('tls');
169
+ const fragmentResult = handler.addFragment(connectionId, buffer);
170
+
171
+ if (fragmentResult.error) {
172
+ handler.complete(connectionId);
173
+ this.connectionProtocols.delete(connectionId);
174
+ return {
175
+ protocol: 'unknown',
176
+ connectionInfo: { protocol: 'unknown' },
177
+ isComplete: true
178
+ };
179
+ }
180
+
181
+ const result = this.tlsDetector.detect(fragmentResult.buffer!, options);
182
+ if (result) {
183
+ if (result.isComplete) {
184
+ handler.complete(connectionId);
185
+ this.connectionProtocols.delete(connectionId);
186
+ }
187
+ return result;
188
+ }
189
+ }
190
+
191
+ if (this.httpDetector.canHandle(buffer)) {
192
+ this.connectionProtocols.set(connectionId, 'http');
193
+ const result = this.httpDetector.detectWithContext(buffer, context, options);
194
+ if (result) {
195
+ if (result.isComplete) {
196
+ this.connectionProtocols.delete(connectionId);
197
+ }
198
+ return result;
199
+ }
153
200
  }
154
201
  }
155
202
 
@@ -186,6 +233,7 @@ export class ProtocolDetector {
186
233
 
187
234
  private destroyInstance(): void {
188
235
  this.fragmentManager.destroy();
236
+ this.connectionProtocols.clear();
189
237
  }
190
238
 
191
239
  /**
@@ -208,6 +256,9 @@ export class ProtocolDetector {
208
256
  // Clean up both TLS and HTTP fragments for this connection
209
257
  instance.fragmentManager.getHandler('tls').complete(connectionId);
210
258
  instance.fragmentManager.getHandler('http').complete(connectionId);
259
+
260
+ // Remove from connection protocols tracking
261
+ instance.connectionProtocols.delete(connectionId);
211
262
  }
212
263
 
213
264
  /**
@@ -35,7 +35,7 @@ export class HttpProxy implements IMetricsTracker {
35
35
  public routes: IRouteConfig[] = [];
36
36
 
37
37
  // Server instances (HTTP/2 with HTTP/1 fallback)
38
- public httpsServer: any;
38
+ public httpsServer: plugins.http2.Http2SecureServer;
39
39
 
40
40
  // Core components
41
41
  private certificateManager: CertificateManager;
@@ -196,8 +196,9 @@ export class HttpProxy implements IMetricsTracker {
196
196
  this.options.keepAliveTimeout = keepAliveTimeout;
197
197
 
198
198
  if (this.httpsServer) {
199
- this.httpsServer.keepAliveTimeout = keepAliveTimeout;
200
- this.logger.info(`Updated keep-alive timeout to ${keepAliveTimeout}ms`);
199
+ // HTTP/2 servers have setTimeout method for timeout management
200
+ this.httpsServer.setTimeout(keepAliveTimeout);
201
+ this.logger.info(`Updated server timeout to ${keepAliveTimeout}ms`);
201
202
  }
202
203
  }
203
204
 
@@ -249,18 +250,19 @@ export class HttpProxy implements IMetricsTracker {
249
250
  this.setupConnectionTracking();
250
251
 
251
252
  // Handle incoming HTTP/2 streams
252
- this.httpsServer.on('stream', (stream: any, headers: any) => {
253
+ this.httpsServer.on('stream', (stream: plugins.http2.ServerHttp2Stream, headers: plugins.http2.IncomingHttpHeaders) => {
253
254
  this.requestHandler.handleHttp2(stream, headers);
254
255
  });
255
256
  // Handle HTTP/1.x fallback requests
256
- this.httpsServer.on('request', (req: any, res: any) => {
257
+ this.httpsServer.on('request', (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => {
257
258
  this.requestHandler.handleRequest(req, res);
258
259
  });
259
260
 
260
261
  // Share server with certificate manager for dynamic contexts
261
- this.certificateManager.setHttpsServer(this.httpsServer);
262
+ // Cast to https.Server as Http2SecureServer is compatible for certificate contexts
263
+ this.certificateManager.setHttpsServer(this.httpsServer as any);
262
264
  // Setup WebSocket support on HTTP/1 fallback
263
- this.webSocketHandler.initialize(this.httpsServer);
265
+ this.webSocketHandler.initialize(this.httpsServer as any);
264
266
  // Start metrics logging
265
267
  this.setupMetricsCollection();
266
268
  // Start periodic connection pool cleanup
@@ -275,6 +277,21 @@ export class HttpProxy implements IMetricsTracker {
275
277
  });
276
278
  }
277
279
 
280
+ /**
281
+ * Check if an address is a loopback address (IPv4 or IPv6)
282
+ */
283
+ private isLoopback(addr?: string): boolean {
284
+ if (!addr) return false;
285
+ // Check for IPv6 loopback
286
+ if (addr === '::1') return true;
287
+ // Handle IPv6-mapped IPv4 addresses
288
+ if (addr.startsWith('::ffff:')) {
289
+ addr = addr.substring(7);
290
+ }
291
+ // Check for IPv4 loopback range (127.0.0.0/8)
292
+ return addr.startsWith('127.');
293
+ }
294
+
278
295
  /**
279
296
  * Sets up tracking of TCP connections
280
297
  */
@@ -282,30 +299,47 @@ export class HttpProxy implements IMetricsTracker {
282
299
  this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
283
300
  let remoteIP = connection.remoteAddress || '';
284
301
  const connectionId = Math.random().toString(36).substring(2, 15);
285
- const isFromSmartProxy = this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1');
302
+ const isFromSmartProxy = this.options.portProxyIntegration && this.isLoopback(connection.remoteAddress);
286
303
 
287
304
  // For SmartProxy connections, wait for CLIENT_IP header
288
305
  if (isFromSmartProxy) {
289
- let headerBuffer = Buffer.alloc(0);
290
- let headerParsed = false;
291
-
292
- const parseHeader = (data: Buffer) => {
293
- if (headerParsed) return data;
306
+ const MAX_PREFACE = 256; // bytes - prevent DoS
307
+ const HEADER_TIMEOUT_MS = 500; // timeout for header parsing
308
+ let headerTimer: NodeJS.Timeout | undefined;
309
+ let buffered = Buffer.alloc(0);
310
+
311
+ const onData = (chunk: Buffer) => {
312
+ buffered = Buffer.concat([buffered, chunk]);
294
313
 
295
- headerBuffer = Buffer.concat([headerBuffer, data]);
296
- const headerStr = headerBuffer.toString();
297
- const headerEnd = headerStr.indexOf('\r\n');
314
+ // Prevent unbounded growth
315
+ if (buffered.length > MAX_PREFACE) {
316
+ connection.removeListener('data', onData);
317
+ if (headerTimer) clearTimeout(headerTimer);
318
+ this.logger.warn('Header preface too large, closing connection');
319
+ connection.destroy();
320
+ return;
321
+ }
298
322
 
299
- if (headerEnd !== -1) {
300
- const header = headerStr.substring(0, headerEnd);
301
- if (header.startsWith('CLIENT_IP:')) {
302
- remoteIP = header.substring(10); // Extract IP after "CLIENT_IP:"
323
+ const idx = buffered.indexOf('\r\n');
324
+ if (idx !== -1) {
325
+ const headerLine = buffered.slice(0, idx).toString('utf8');
326
+ if (headerLine.startsWith('CLIENT_IP:')) {
327
+ remoteIP = headerLine.substring(10).trim();
303
328
  this.logger.debug(`Extracted client IP from SmartProxy: ${remoteIP}`);
304
329
  }
305
- headerParsed = true;
330
+
331
+ // Clean up listener and timer
332
+ connection.removeListener('data', onData);
333
+ if (headerTimer) clearTimeout(headerTimer);
334
+
335
+ // Put remaining data back onto the stream
336
+ const remaining = buffered.slice(idx + 2);
337
+ if (remaining.length > 0) {
338
+ connection.unshift(remaining);
339
+ }
306
340
 
307
341
  // Store the real IP on the connection
308
- (connection as any)._realRemoteIP = remoteIP;
342
+ connection._realRemoteIP = remoteIP;
309
343
 
310
344
  // Validate the real IP
311
345
  const ipValidation = this.securityManager.validateIP(remoteIP);
@@ -318,35 +352,26 @@ export class HttpProxy implements IMetricsTracker {
318
352
  remoteIP
319
353
  );
320
354
  connection.destroy();
321
- return null;
355
+ return;
322
356
  }
323
357
 
324
358
  // Track connection by real IP
325
359
  this.securityManager.trackConnectionByIP(remoteIP, connectionId);
326
-
327
- // Return remaining data after header
328
- return headerBuffer.slice(headerEnd + 2);
329
360
  }
330
- return null;
331
361
  };
362
+
363
+ // Set timeout for header parsing
364
+ headerTimer = setTimeout(() => {
365
+ connection.removeListener('data', onData);
366
+ this.logger.warn('Header parsing timeout, closing connection');
367
+ connection.destroy();
368
+ }, HEADER_TIMEOUT_MS);
332
369
 
333
- // Override the first data handler to parse header
334
- const originalEmit = connection.emit;
335
- connection.emit = function(event: string, ...args: any[]) {
336
- if (event === 'data' && !headerParsed) {
337
- const remaining = parseHeader(args[0]);
338
- if (remaining && remaining.length > 0) {
339
- // Call original emit with remaining data
340
- return originalEmit.apply(connection, ['data', remaining]);
341
- } else if (headerParsed) {
342
- // Header parsed but no remaining data
343
- return true;
344
- }
345
- // Header not complete yet, suppress this data event
346
- return true;
347
- }
348
- return originalEmit.apply(connection, [event, ...args]);
349
- } as any;
370
+ // Unref the timer so it doesn't keep the process alive
371
+ if (headerTimer.unref) headerTimer.unref();
372
+
373
+ // Use prependListener to get data first
374
+ connection.prependListener('data', onData);
350
375
  } else {
351
376
  // Direct connection - validate immediately
352
377
  const ipValidation = this.securityManager.validateIP(remoteIP);
@@ -385,8 +410,8 @@ export class HttpProxy implements IMetricsTracker {
385
410
  }
386
411
 
387
412
  // Add connection to tracking with metadata
388
- (connection as any)._connectionId = connectionId;
389
- (connection as any)._remoteIP = remoteIP;
413
+ connection._connectionId = connectionId;
414
+ connection._remoteIP = remoteIP;
390
415
  this.socketMap.add(connection);
391
416
  this.connectedClients = this.socketMap.getArray().length;
392
417
 
@@ -409,8 +434,8 @@ export class HttpProxy implements IMetricsTracker {
409
434
  this.connectedClients = this.socketMap.getArray().length;
410
435
 
411
436
  // Remove IP tracking
412
- const connId = (connection as any)._connectionId;
413
- const connIP = (connection as any)._realRemoteIP || (connection as any)._remoteIP;
437
+ const connId = connection._connectionId;
438
+ const connIP = connection._realRemoteIP || connection._remoteIP;
414
439
  if (connId && connIP) {
415
440
  this.securityManager.removeConnectionByIP(connIP, connId);
416
441
  }
@@ -110,6 +110,14 @@ export class SmartCertManager {
110
110
  this.certProvisionFallbackToAcme = fallback;
111
111
  }
112
112
 
113
+ /**
114
+ * Update the routes array to keep it in sync with SmartProxy
115
+ * This prevents stale route data when adding/removing challenge routes
116
+ */
117
+ public setRoutes(routes: IRouteConfig[]): void {
118
+ this.routes = routes;
119
+ }
120
+
113
121
  /**
114
122
  * Set callback for updating routes (used for challenge routes)
115
123
  */
@@ -391,15 +399,14 @@ export class SmartCertManager {
391
399
  }
392
400
 
393
401
  // Parse certificate to get dates
394
- // Parse certificate to get dates - for now just use defaults
395
- // TODO: Implement actual certificate parsing if needed
396
- const certInfo = { validTo: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), validFrom: new Date() };
402
+ const expiryDate = this.extractExpiryDate(cert);
403
+ const issueDate = new Date(); // Current date as issue date
397
404
 
398
405
  const certData: ICertificateData = {
399
406
  cert,
400
407
  key,
401
- expiryDate: certInfo.validTo,
402
- issueDate: certInfo.validFrom,
408
+ expiryDate,
409
+ issueDate,
403
410
  source: 'static'
404
411
  };
405
412
 
@@ -573,6 +580,8 @@ export class SmartCertManager {
573
580
  // With the re-ordering of start(), port binding should already be done
574
581
  // This updateRoutes call should just add the route without binding again
575
582
  await this.updateRoutesCallback(updatedRoutes);
583
+ // Keep local routes in sync after updating
584
+ this.routes = updatedRoutes;
576
585
  this.challengeRouteActive = true;
577
586
 
578
587
  // Register with state manager
@@ -662,6 +671,8 @@ export class SmartCertManager {
662
671
  try {
663
672
  const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
664
673
  await this.updateRoutesCallback(filteredRoutes);
674
+ // Keep local routes in sync after updating
675
+ this.routes = filteredRoutes;
665
676
  this.challengeRouteActive = false;
666
677
 
667
678
  // Remove from state manager
@@ -697,6 +708,11 @@ export class SmartCertManager {
697
708
  this.checkAndRenewCertificates();
698
709
  }, 12 * 60 * 60 * 1000);
699
710
 
711
+ // Unref the timer so it doesn't keep the process alive
712
+ if (this.renewalTimer.unref) {
713
+ this.renewalTimer.unref();
714
+ }
715
+
700
716
  // Also do an immediate check
701
717
  this.checkAndRenewCertificates();
702
718
  }
@@ -20,6 +20,7 @@ export { HttpProxyBridge } from './http-proxy-bridge.js';
20
20
  export { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
21
21
  export { RouteConnectionHandler } from './route-connection-handler.js';
22
22
  export { NFTablesManager } from './nftables-manager.js';
23
+ export { RouteOrchestrator } from './route-orchestrator.js';
23
24
 
24
25
  // Export certificate management
25
26
  export { SmartCertManager } from './certificate-manager.js';
@@ -33,6 +33,11 @@ export class MetricsCollector implements IMetrics {
33
33
  private readonly sampleIntervalMs: number;
34
34
  private readonly retentionSeconds: number;
35
35
 
36
+ // Track connection durations for percentile calculations
37
+ private connectionDurations: number[] = [];
38
+ private bytesInArray: number[] = [];
39
+ private bytesOutArray: number[] = [];
40
+
36
41
  constructor(
37
42
  private smartProxy: SmartProxy,
38
43
  config?: {
@@ -211,21 +216,39 @@ export class MetricsCollector implements IMetrics {
211
216
  }
212
217
  };
213
218
 
214
- // Percentiles implementation (placeholder for now)
219
+ // Helper to calculate percentiles from an array
220
+ private calculatePercentile(arr: number[], percentile: number): number {
221
+ if (arr.length === 0) return 0;
222
+ const sorted = [...arr].sort((a, b) => a - b);
223
+ const index = Math.floor((sorted.length - 1) * percentile);
224
+ return sorted[index];
225
+ }
226
+
227
+ // Percentiles implementation
215
228
  public percentiles = {
216
229
  connectionDuration: (): { p50: number; p95: number; p99: number } => {
217
- // TODO: Implement percentile calculations
218
- return { p50: 0, p95: 0, p99: 0 };
230
+ return {
231
+ p50: this.calculatePercentile(this.connectionDurations, 0.5),
232
+ p95: this.calculatePercentile(this.connectionDurations, 0.95),
233
+ p99: this.calculatePercentile(this.connectionDurations, 0.99)
234
+ };
219
235
  },
220
236
 
221
237
  bytesTransferred: (): {
222
238
  in: { p50: number; p95: number; p99: number };
223
239
  out: { p50: number; p95: number; p99: number };
224
240
  } => {
225
- // TODO: Implement percentile calculations
226
241
  return {
227
- in: { p50: 0, p95: 0, p99: 0 },
228
- out: { p50: 0, p95: 0, p99: 0 }
242
+ in: {
243
+ p50: this.calculatePercentile(this.bytesInArray, 0.5),
244
+ p95: this.calculatePercentile(this.bytesInArray, 0.95),
245
+ p99: this.calculatePercentile(this.bytesInArray, 0.99)
246
+ },
247
+ out: {
248
+ p50: this.calculatePercentile(this.bytesOutArray, 0.5),
249
+ p95: this.calculatePercentile(this.bytesOutArray, 0.95),
250
+ p99: this.calculatePercentile(this.bytesOutArray, 0.99)
251
+ }
229
252
  };
230
253
  }
231
254
  };
@@ -298,6 +321,30 @@ export class MetricsCollector implements IMetrics {
298
321
  * Clean up tracking for a closed connection
299
322
  */
300
323
  public removeConnection(connectionId: string): void {
324
+ const tracker = this.connectionByteTrackers.get(connectionId);
325
+ if (tracker) {
326
+ // Calculate connection duration
327
+ const duration = Date.now() - tracker.startTime;
328
+
329
+ // Add to arrays for percentile calculations (bounded to prevent memory growth)
330
+ const MAX_SAMPLES = 5000;
331
+
332
+ this.connectionDurations.push(duration);
333
+ if (this.connectionDurations.length > MAX_SAMPLES) {
334
+ this.connectionDurations.shift();
335
+ }
336
+
337
+ this.bytesInArray.push(tracker.bytesIn);
338
+ if (this.bytesInArray.length > MAX_SAMPLES) {
339
+ this.bytesInArray.shift();
340
+ }
341
+
342
+ this.bytesOutArray.push(tracker.bytesOut);
343
+ if (this.bytesOutArray.length > MAX_SAMPLES) {
344
+ this.bytesOutArray.shift();
345
+ }
346
+ }
347
+
301
348
  this.connectionByteTrackers.delete(connectionId);
302
349
  }
303
350
 
@@ -349,6 +396,11 @@ export class MetricsCollector implements IMetrics {
349
396
  }
350
397
  }, this.sampleIntervalMs);
351
398
 
399
+ // Unref the interval so it doesn't keep the process alive
400
+ if (this.samplingInterval.unref) {
401
+ this.samplingInterval.unref();
402
+ }
403
+
352
404
  // Subscribe to new connections
353
405
  this.connectionSubscription = this.smartProxy.routeConnectionHandler.newConnectionSubject.subscribe({
354
406
  next: (record) => {