@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.
- 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 +19 -7
- package/dist_ts/detection/detectors/tls-detector.d.ts +1 -9
- package/dist_ts/detection/detectors/tls-detector.js +6 -29
- package/dist_ts/detection/protocol-detector.d.ts +7 -2
- package/dist_ts/detection/protocol-detector.js +80 -8
- 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/connection-manager.js +12 -1
- 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-connection-handler.js +7 -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 +20 -7
- package/ts/detection/protocol-detector.ts +57 -6
- 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
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '
|
|
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
|
-
//
|
|
56
|
-
|
|
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
|
-
//
|
|
126
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
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
|
-
|
|
200
|
-
this.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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 =
|
|
413
|
-
const connIP =
|
|
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
|
-
|
|
395
|
-
|
|
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
|
|
402
|
-
issueDate
|
|
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
|
-
//
|
|
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
|
-
|
|
218
|
-
|
|
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: {
|
|
228
|
-
|
|
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) => {
|