@push.rocks/smartproxy 22.4.2 → 23.0.0
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 +36 -0
- package/dist_rust/rustproxy +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -6
- package/dist_ts/index.js +3 -11
- package/dist_ts/protocols/common/fragment-handler.js +5 -1
- package/dist_ts/proxies/index.d.ts +1 -6
- package/dist_ts/proxies/index.js +2 -8
- package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
- package/dist_ts/proxies/smart-proxy/index.js +7 -13
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
- package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
- package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
- package/dist_ts/routing/index.d.ts +1 -1
- package/dist_ts/routing/index.js +3 -3
- package/dist_ts/routing/models/http-types.d.ts +119 -4
- package/dist_ts/routing/models/http-types.js +93 -5
- package/package.json +1 -1
- package/readme.md +444 -219
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -15
- package/ts/protocols/common/fragment-handler.ts +4 -0
- package/ts/proxies/index.ts +1 -12
- package/ts/proxies/smart-proxy/index.ts +6 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
- package/ts/proxies/smart-proxy/models/route-types.ts +0 -2
- package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
- package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
- package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
- package/ts/routing/index.ts +2 -2
- package/ts/routing/models/http-types.ts +147 -4
- package/dist_ts/proxies/nftables-proxy/index.d.ts +0 -6
- package/dist_ts/proxies/nftables-proxy/index.js +0 -7
- package/dist_ts/proxies/nftables-proxy/models/errors.d.ts +0 -15
- package/dist_ts/proxies/nftables-proxy/models/errors.js +0 -28
- package/dist_ts/proxies/nftables-proxy/models/index.d.ts +0 -5
- package/dist_ts/proxies/nftables-proxy/models/index.js +0 -6
- package/dist_ts/proxies/nftables-proxy/models/interfaces.d.ts +0 -75
- package/dist_ts/proxies/nftables-proxy/models/interfaces.js +0 -5
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +0 -124
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +0 -1374
- package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +0 -9
- package/dist_ts/proxies/nftables-proxy/utils/index.js +0 -12
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +0 -66
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +0 -131
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +0 -39
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +0 -112
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +0 -59
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +0 -130
- package/ts/proxies/http-proxy/connection-pool.ts +0 -228
- package/ts/proxies/http-proxy/context-creator.ts +0 -145
- package/ts/proxies/http-proxy/default-certificates.ts +0 -150
- package/ts/proxies/http-proxy/function-cache.ts +0 -279
- package/ts/proxies/http-proxy/handlers/index.ts +0 -5
- package/ts/proxies/http-proxy/http-proxy.ts +0 -669
- package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
- package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
- package/ts/proxies/http-proxy/index.ts +0 -18
- package/ts/proxies/http-proxy/models/http-types.ts +0 -148
- package/ts/proxies/http-proxy/models/index.ts +0 -5
- package/ts/proxies/http-proxy/models/types.ts +0 -125
- package/ts/proxies/http-proxy/request-handler.ts +0 -878
- package/ts/proxies/http-proxy/security-manager.ts +0 -413
- package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
- package/ts/proxies/nftables-proxy/index.ts +0 -6
- package/ts/proxies/nftables-proxy/models/errors.ts +0 -30
- package/ts/proxies/nftables-proxy/models/index.ts +0 -5
- package/ts/proxies/nftables-proxy/models/interfaces.ts +0 -94
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +0 -1754
- package/ts/proxies/nftables-proxy/utils/index.ts +0 -38
- package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +0 -162
- package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +0 -125
- package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +0 -156
- package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
- package/ts/proxies/smart-proxy/cert-store.ts +0 -92
- package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
- package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
- package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
- package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
- package/ts/proxies/smart-proxy/port-manager.ts +0 -358
- package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
- package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
- package/ts/proxies/smart-proxy/security-manager.ts +0 -269
- package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
- package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
- package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../plugins.js';
|
|
2
|
-
import { HttpProxy } from '../http-proxy/index.js';
|
|
3
|
-
import { setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
|
4
|
-
import type { IConnectionRecord } from './models/interfaces.js';
|
|
5
|
-
import type { IRouteConfig } from './models/route-types.js';
|
|
6
|
-
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
|
7
|
-
import type { SmartProxy } from './smart-proxy.js';
|
|
8
|
-
|
|
9
|
-
export class HttpProxyBridge {
|
|
10
|
-
private httpProxy: HttpProxy | null = null;
|
|
11
|
-
|
|
12
|
-
constructor(private smartProxy: SmartProxy) {}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Get the HttpProxy instance
|
|
16
|
-
*/
|
|
17
|
-
public getHttpProxy(): HttpProxy | null {
|
|
18
|
-
return this.httpProxy;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Initialize HttpProxy instance
|
|
23
|
-
*/
|
|
24
|
-
public async initialize(): Promise<void> {
|
|
25
|
-
if (!this.httpProxy && this.smartProxy.settings.useHttpProxy && this.smartProxy.settings.useHttpProxy.length > 0) {
|
|
26
|
-
const httpProxyOptions: any = {
|
|
27
|
-
port: this.smartProxy.settings.httpProxyPort!,
|
|
28
|
-
portProxyIntegration: true,
|
|
29
|
-
logLevel: this.smartProxy.settings.enableDetailedLogging ? 'debug' : 'info'
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
this.httpProxy = new HttpProxy(httpProxyOptions);
|
|
33
|
-
console.log(`Initialized HttpProxy on port ${this.smartProxy.settings.httpProxyPort}`);
|
|
34
|
-
|
|
35
|
-
// Apply route configurations to HttpProxy
|
|
36
|
-
await this.syncRoutesToHttpProxy(this.smartProxy.settings.routes || []);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Sync routes to HttpProxy
|
|
42
|
-
*/
|
|
43
|
-
public async syncRoutesToHttpProxy(routes: IRouteConfig[]): Promise<void> {
|
|
44
|
-
if (!this.httpProxy) return;
|
|
45
|
-
|
|
46
|
-
// Convert routes to HttpProxy format
|
|
47
|
-
const httpProxyConfigs = routes
|
|
48
|
-
.filter(route => {
|
|
49
|
-
// Check if this route matches any of the specified network proxy ports
|
|
50
|
-
const routePorts = Array.isArray(route.match.ports)
|
|
51
|
-
? route.match.ports
|
|
52
|
-
: [route.match.ports];
|
|
53
|
-
|
|
54
|
-
return routePorts.some(port =>
|
|
55
|
-
this.smartProxy.settings.useHttpProxy?.includes(port)
|
|
56
|
-
);
|
|
57
|
-
})
|
|
58
|
-
.map(route => this.routeToHttpProxyConfig(route));
|
|
59
|
-
|
|
60
|
-
// Apply configurations to HttpProxy
|
|
61
|
-
await this.httpProxy.updateRouteConfigs(httpProxyConfigs);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Convert route to HttpProxy configuration
|
|
66
|
-
*/
|
|
67
|
-
private routeToHttpProxyConfig(route: IRouteConfig): any {
|
|
68
|
-
// Convert route to HttpProxy domain config format
|
|
69
|
-
let domain = '*';
|
|
70
|
-
if (route.match.domains) {
|
|
71
|
-
if (Array.isArray(route.match.domains)) {
|
|
72
|
-
domain = route.match.domains[0] || '*';
|
|
73
|
-
} else {
|
|
74
|
-
domain = route.match.domains;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
...route, // Keep the original route structure
|
|
80
|
-
match: {
|
|
81
|
-
...route.match,
|
|
82
|
-
domains: domain // Ensure domains is always set for HttpProxy
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Check if connection should use HttpProxy
|
|
89
|
-
*/
|
|
90
|
-
public shouldUseHttpProxy(connection: IConnectionRecord, routeMatch: any): boolean {
|
|
91
|
-
// Only use HttpProxy for TLS termination
|
|
92
|
-
return (
|
|
93
|
-
routeMatch.route.action.tls?.mode === 'terminate' ||
|
|
94
|
-
routeMatch.route.action.tls?.mode === 'terminate-and-reencrypt'
|
|
95
|
-
) && this.httpProxy !== null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Forward connection to HttpProxy
|
|
100
|
-
*/
|
|
101
|
-
public async forwardToHttpProxy(
|
|
102
|
-
connectionId: string,
|
|
103
|
-
socket: plugins.net.Socket | WrappedSocket,
|
|
104
|
-
record: IConnectionRecord,
|
|
105
|
-
initialChunk: Buffer,
|
|
106
|
-
httpProxyPort: number,
|
|
107
|
-
cleanupCallback: (reason: string) => void
|
|
108
|
-
): Promise<void> {
|
|
109
|
-
if (!this.httpProxy) {
|
|
110
|
-
throw new Error('HttpProxy not initialized');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Check if client socket is already destroyed before proceeding
|
|
114
|
-
const underlyingSocket = socket instanceof WrappedSocket ? socket.socket : socket;
|
|
115
|
-
if (underlyingSocket.destroyed) {
|
|
116
|
-
console.log(`[${connectionId}] Client socket already destroyed, skipping HttpProxy forwarding`);
|
|
117
|
-
cleanupCallback('client_disconnected_before_proxy');
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const proxySocket = new plugins.net.Socket();
|
|
122
|
-
|
|
123
|
-
// Handle client disconnect during proxy connection setup
|
|
124
|
-
const clientDisconnectHandler = () => {
|
|
125
|
-
console.log(`[${connectionId}] Client disconnected during HttpProxy connection setup`);
|
|
126
|
-
proxySocket.destroy();
|
|
127
|
-
cleanupCallback('client_disconnected_during_setup');
|
|
128
|
-
};
|
|
129
|
-
underlyingSocket.once('close', clientDisconnectHandler);
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
await new Promise<void>((resolve, reject) => {
|
|
133
|
-
proxySocket.connect(httpProxyPort, 'localhost', () => {
|
|
134
|
-
console.log(`[${connectionId}] Connected to HttpProxy for termination`);
|
|
135
|
-
resolve();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
proxySocket.on('error', reject);
|
|
139
|
-
});
|
|
140
|
-
} finally {
|
|
141
|
-
// Remove the disconnect handler after connection attempt
|
|
142
|
-
underlyingSocket.removeListener('close', clientDisconnectHandler);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Double-check client socket is still connected after async operation
|
|
146
|
-
if (underlyingSocket.destroyed) {
|
|
147
|
-
console.log(`[${connectionId}] Client disconnected while connecting to HttpProxy`);
|
|
148
|
-
proxySocket.destroy();
|
|
149
|
-
cleanupCallback('client_disconnected_after_proxy_connect');
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Send client IP information header first (custom protocol)
|
|
154
|
-
// Format: "CLIENT_IP:<ip>\r\n"
|
|
155
|
-
const clientIPHeader = Buffer.from(`CLIENT_IP:${record.remoteIP}\r\n`);
|
|
156
|
-
proxySocket.write(clientIPHeader);
|
|
157
|
-
|
|
158
|
-
// Send initial chunk if present
|
|
159
|
-
if (initialChunk) {
|
|
160
|
-
// Count the initial chunk bytes
|
|
161
|
-
record.bytesReceived += initialChunk.length;
|
|
162
|
-
if (this.smartProxy.metricsCollector) {
|
|
163
|
-
this.smartProxy.metricsCollector.recordBytes(record.id, initialChunk.length, 0);
|
|
164
|
-
}
|
|
165
|
-
proxySocket.write(initialChunk);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Use centralized bidirectional forwarding (underlyingSocket already extracted above)
|
|
169
|
-
setupBidirectionalForwarding(underlyingSocket, proxySocket, {
|
|
170
|
-
onClientData: (chunk) => {
|
|
171
|
-
// Update stats - this is the ONLY place bytes are counted for HttpProxy connections
|
|
172
|
-
if (record) {
|
|
173
|
-
record.bytesReceived += chunk.length;
|
|
174
|
-
if (this.smartProxy.metricsCollector) {
|
|
175
|
-
this.smartProxy.metricsCollector.recordBytes(record.id, chunk.length, 0);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
onServerData: (chunk) => {
|
|
180
|
-
// Update stats - this is the ONLY place bytes are counted for HttpProxy connections
|
|
181
|
-
if (record) {
|
|
182
|
-
record.bytesSent += chunk.length;
|
|
183
|
-
if (this.smartProxy.metricsCollector) {
|
|
184
|
-
this.smartProxy.metricsCollector.recordBytes(record.id, 0, chunk.length);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
onCleanup: (reason) => {
|
|
189
|
-
cleanupCallback(reason);
|
|
190
|
-
},
|
|
191
|
-
enableHalfOpen: false // Close both when one closes (required for proxy chains)
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Start HttpProxy
|
|
197
|
-
*/
|
|
198
|
-
public async start(): Promise<void> {
|
|
199
|
-
if (this.httpProxy) {
|
|
200
|
-
await this.httpProxy.start();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Stop HttpProxy
|
|
206
|
-
*/
|
|
207
|
-
public async stop(): Promise<void> {
|
|
208
|
-
if (this.httpProxy) {
|
|
209
|
-
await this.httpProxy.stop();
|
|
210
|
-
this.httpProxy = null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../plugins.js';
|
|
2
|
-
import type { SmartProxy } from './smart-proxy.js';
|
|
3
|
-
import type {
|
|
4
|
-
IMetrics,
|
|
5
|
-
IThroughputData,
|
|
6
|
-
IThroughputHistoryPoint,
|
|
7
|
-
IByteTracker
|
|
8
|
-
} from './models/metrics-types.js';
|
|
9
|
-
import { ThroughputTracker } from './throughput-tracker.js';
|
|
10
|
-
import { logger } from '../../core/utils/logger.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Collects and provides metrics for SmartProxy with clean API
|
|
14
|
-
*/
|
|
15
|
-
export class MetricsCollector implements IMetrics {
|
|
16
|
-
// Throughput tracking
|
|
17
|
-
private throughputTracker: ThroughputTracker;
|
|
18
|
-
private routeThroughputTrackers = new Map<string, ThroughputTracker>();
|
|
19
|
-
private ipThroughputTrackers = new Map<string, ThroughputTracker>();
|
|
20
|
-
|
|
21
|
-
// Request tracking
|
|
22
|
-
private requestTimestamps: number[] = [];
|
|
23
|
-
private totalRequests: number = 0;
|
|
24
|
-
|
|
25
|
-
// Connection byte tracking for per-route/IP metrics
|
|
26
|
-
private connectionByteTrackers = new Map<string, IByteTracker>();
|
|
27
|
-
|
|
28
|
-
// Subscriptions
|
|
29
|
-
private samplingInterval?: NodeJS.Timeout;
|
|
30
|
-
private connectionSubscription?: plugins.smartrx.rxjs.Subscription;
|
|
31
|
-
|
|
32
|
-
// Configuration
|
|
33
|
-
private readonly sampleIntervalMs: number;
|
|
34
|
-
private readonly retentionSeconds: number;
|
|
35
|
-
|
|
36
|
-
// Track connection durations for percentile calculations
|
|
37
|
-
private connectionDurations: number[] = [];
|
|
38
|
-
private bytesInArray: number[] = [];
|
|
39
|
-
private bytesOutArray: number[] = [];
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
private smartProxy: SmartProxy,
|
|
43
|
-
config?: {
|
|
44
|
-
sampleIntervalMs?: number;
|
|
45
|
-
retentionSeconds?: number;
|
|
46
|
-
}
|
|
47
|
-
) {
|
|
48
|
-
this.sampleIntervalMs = config?.sampleIntervalMs || 1000;
|
|
49
|
-
this.retentionSeconds = config?.retentionSeconds || 3600;
|
|
50
|
-
this.throughputTracker = new ThroughputTracker(this.retentionSeconds);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Connection metrics implementation
|
|
54
|
-
public connections = {
|
|
55
|
-
active: (): number => {
|
|
56
|
-
return this.smartProxy.connectionManager.getConnectionCount();
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
total: (): number => {
|
|
60
|
-
const stats = this.smartProxy.connectionManager.getTerminationStats();
|
|
61
|
-
let total = this.smartProxy.connectionManager.getConnectionCount();
|
|
62
|
-
|
|
63
|
-
for (const reason in stats.incoming) {
|
|
64
|
-
total += stats.incoming[reason];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return total;
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
byRoute: (): Map<string, number> => {
|
|
71
|
-
const routeCounts = new Map<string, number>();
|
|
72
|
-
const connections = this.smartProxy.connectionManager.getConnections();
|
|
73
|
-
|
|
74
|
-
for (const [_, record] of connections) {
|
|
75
|
-
const routeName = (record as any).routeName ||
|
|
76
|
-
record.routeConfig?.name ||
|
|
77
|
-
'unknown';
|
|
78
|
-
|
|
79
|
-
const current = routeCounts.get(routeName) || 0;
|
|
80
|
-
routeCounts.set(routeName, current + 1);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return routeCounts;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
byIP: (): Map<string, number> => {
|
|
87
|
-
const ipCounts = new Map<string, number>();
|
|
88
|
-
|
|
89
|
-
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
|
90
|
-
const ip = record.remoteIP;
|
|
91
|
-
const current = ipCounts.get(ip) || 0;
|
|
92
|
-
ipCounts.set(ip, current + 1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return ipCounts;
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
topIPs: (limit: number = 10): Array<{ ip: string; count: number }> => {
|
|
99
|
-
const ipCounts = this.connections.byIP();
|
|
100
|
-
return Array.from(ipCounts.entries())
|
|
101
|
-
.sort((a, b) => b[1] - a[1])
|
|
102
|
-
.slice(0, limit)
|
|
103
|
-
.map(([ip, count]) => ({ ip, count }));
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Throughput metrics implementation
|
|
108
|
-
public throughput = {
|
|
109
|
-
instant: (): IThroughputData => {
|
|
110
|
-
return this.throughputTracker.getRate(1);
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
recent: (): IThroughputData => {
|
|
114
|
-
return this.throughputTracker.getRate(10);
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
average: (): IThroughputData => {
|
|
118
|
-
return this.throughputTracker.getRate(60);
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
custom: (seconds: number): IThroughputData => {
|
|
122
|
-
return this.throughputTracker.getRate(seconds);
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
history: (seconds: number): Array<IThroughputHistoryPoint> => {
|
|
126
|
-
return this.throughputTracker.getHistory(seconds);
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
byRoute: (windowSeconds: number = 1): Map<string, IThroughputData> => {
|
|
130
|
-
const routeThroughput = new Map<string, IThroughputData>();
|
|
131
|
-
|
|
132
|
-
// Get throughput from each route's dedicated tracker
|
|
133
|
-
for (const [route, tracker] of this.routeThroughputTrackers) {
|
|
134
|
-
const rate = tracker.getRate(windowSeconds);
|
|
135
|
-
if (rate.in > 0 || rate.out > 0) {
|
|
136
|
-
routeThroughput.set(route, rate);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return routeThroughput;
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
byIP: (windowSeconds: number = 1): Map<string, IThroughputData> => {
|
|
144
|
-
const ipThroughput = new Map<string, IThroughputData>();
|
|
145
|
-
|
|
146
|
-
// Get throughput from each IP's dedicated tracker
|
|
147
|
-
for (const [ip, tracker] of this.ipThroughputTrackers) {
|
|
148
|
-
const rate = tracker.getRate(windowSeconds);
|
|
149
|
-
if (rate.in > 0 || rate.out > 0) {
|
|
150
|
-
ipThroughput.set(ip, rate);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return ipThroughput;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Request metrics implementation
|
|
159
|
-
public requests = {
|
|
160
|
-
perSecond: (): number => {
|
|
161
|
-
const now = Date.now();
|
|
162
|
-
const oneSecondAgo = now - 1000;
|
|
163
|
-
|
|
164
|
-
// Clean old timestamps
|
|
165
|
-
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > now - 60000);
|
|
166
|
-
|
|
167
|
-
// Count requests in last second
|
|
168
|
-
const recentRequests = this.requestTimestamps.filter(ts => ts > oneSecondAgo);
|
|
169
|
-
return recentRequests.length;
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
perMinute: (): number => {
|
|
173
|
-
const now = Date.now();
|
|
174
|
-
const oneMinuteAgo = now - 60000;
|
|
175
|
-
|
|
176
|
-
// Count requests in last minute
|
|
177
|
-
const recentRequests = this.requestTimestamps.filter(ts => ts > oneMinuteAgo);
|
|
178
|
-
return recentRequests.length;
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
total: (): number => {
|
|
182
|
-
return this.totalRequests;
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Totals implementation
|
|
187
|
-
public totals = {
|
|
188
|
-
bytesIn: (): number => {
|
|
189
|
-
let total = 0;
|
|
190
|
-
|
|
191
|
-
// Sum from all active connections
|
|
192
|
-
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
|
193
|
-
total += record.bytesReceived;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// TODO: Add historical data from terminated connections
|
|
197
|
-
|
|
198
|
-
return total;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
bytesOut: (): number => {
|
|
202
|
-
let total = 0;
|
|
203
|
-
|
|
204
|
-
// Sum from all active connections
|
|
205
|
-
for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
|
|
206
|
-
total += record.bytesSent;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// TODO: Add historical data from terminated connections
|
|
210
|
-
|
|
211
|
-
return total;
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
connections: (): number => {
|
|
215
|
-
return this.connections.total();
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
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
|
|
228
|
-
public percentiles = {
|
|
229
|
-
connectionDuration: (): { p50: number; p95: number; p99: number } => {
|
|
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
|
-
};
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
bytesTransferred: (): {
|
|
238
|
-
in: { p50: number; p95: number; p99: number };
|
|
239
|
-
out: { p50: number; p95: number; p99: number };
|
|
240
|
-
} => {
|
|
241
|
-
return {
|
|
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
|
-
}
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Record a new request
|
|
258
|
-
*/
|
|
259
|
-
public recordRequest(connectionId: string, routeName: string, remoteIP: string): void {
|
|
260
|
-
const now = Date.now();
|
|
261
|
-
this.requestTimestamps.push(now);
|
|
262
|
-
this.totalRequests++;
|
|
263
|
-
|
|
264
|
-
// Initialize byte tracker for this connection
|
|
265
|
-
this.connectionByteTrackers.set(connectionId, {
|
|
266
|
-
connectionId,
|
|
267
|
-
routeName,
|
|
268
|
-
remoteIP,
|
|
269
|
-
bytesIn: 0,
|
|
270
|
-
bytesOut: 0,
|
|
271
|
-
startTime: now,
|
|
272
|
-
lastUpdate: now
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Cleanup old request timestamps
|
|
276
|
-
if (this.requestTimestamps.length > 5000) {
|
|
277
|
-
// First try to clean up old timestamps (older than 1 minute)
|
|
278
|
-
const cutoff = now - 60000;
|
|
279
|
-
this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
|
|
280
|
-
|
|
281
|
-
// If still too many, enforce hard cap of 5000 most recent
|
|
282
|
-
if (this.requestTimestamps.length > 5000) {
|
|
283
|
-
this.requestTimestamps = this.requestTimestamps.slice(-5000);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Record bytes transferred for a connection
|
|
290
|
-
*/
|
|
291
|
-
public recordBytes(connectionId: string, bytesIn: number, bytesOut: number): void {
|
|
292
|
-
// Update global throughput tracker
|
|
293
|
-
this.throughputTracker.recordBytes(bytesIn, bytesOut);
|
|
294
|
-
|
|
295
|
-
// Update connection-specific tracker
|
|
296
|
-
const tracker = this.connectionByteTrackers.get(connectionId);
|
|
297
|
-
if (tracker) {
|
|
298
|
-
tracker.bytesIn += bytesIn;
|
|
299
|
-
tracker.bytesOut += bytesOut;
|
|
300
|
-
tracker.lastUpdate = Date.now();
|
|
301
|
-
|
|
302
|
-
// Update per-route throughput tracker
|
|
303
|
-
let routeTracker = this.routeThroughputTrackers.get(tracker.routeName);
|
|
304
|
-
if (!routeTracker) {
|
|
305
|
-
routeTracker = new ThroughputTracker(this.retentionSeconds);
|
|
306
|
-
this.routeThroughputTrackers.set(tracker.routeName, routeTracker);
|
|
307
|
-
}
|
|
308
|
-
routeTracker.recordBytes(bytesIn, bytesOut);
|
|
309
|
-
|
|
310
|
-
// Update per-IP throughput tracker
|
|
311
|
-
let ipTracker = this.ipThroughputTrackers.get(tracker.remoteIP);
|
|
312
|
-
if (!ipTracker) {
|
|
313
|
-
ipTracker = new ThroughputTracker(this.retentionSeconds);
|
|
314
|
-
this.ipThroughputTrackers.set(tracker.remoteIP, ipTracker);
|
|
315
|
-
}
|
|
316
|
-
ipTracker.recordBytes(bytesIn, bytesOut);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Clean up tracking for a closed connection
|
|
322
|
-
*/
|
|
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
|
-
|
|
348
|
-
this.connectionByteTrackers.delete(connectionId);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Start the metrics collector
|
|
353
|
-
*/
|
|
354
|
-
public start(): void {
|
|
355
|
-
if (!this.smartProxy.routeConnectionHandler) {
|
|
356
|
-
throw new Error('MetricsCollector: RouteConnectionHandler not available');
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Start periodic sampling
|
|
360
|
-
this.samplingInterval = setInterval(() => {
|
|
361
|
-
// Sample global throughput
|
|
362
|
-
this.throughputTracker.takeSample();
|
|
363
|
-
|
|
364
|
-
// Sample per-route throughput
|
|
365
|
-
for (const [_, tracker] of this.routeThroughputTrackers) {
|
|
366
|
-
tracker.takeSample();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Sample per-IP throughput
|
|
370
|
-
for (const [_, tracker] of this.ipThroughputTrackers) {
|
|
371
|
-
tracker.takeSample();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Clean up old connection trackers (connections closed more than 5 minutes ago)
|
|
375
|
-
const cutoff = Date.now() - 300000;
|
|
376
|
-
for (const [id, tracker] of this.connectionByteTrackers) {
|
|
377
|
-
if (tracker.lastUpdate < cutoff) {
|
|
378
|
-
this.connectionByteTrackers.delete(id);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Clean up unused route trackers
|
|
383
|
-
const activeRoutes = new Set(Array.from(this.connectionByteTrackers.values()).map(t => t.routeName));
|
|
384
|
-
for (const [route, _] of this.routeThroughputTrackers) {
|
|
385
|
-
if (!activeRoutes.has(route)) {
|
|
386
|
-
this.routeThroughputTrackers.delete(route);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Clean up unused IP trackers
|
|
391
|
-
const activeIPs = new Set(Array.from(this.connectionByteTrackers.values()).map(t => t.remoteIP));
|
|
392
|
-
for (const [ip, _] of this.ipThroughputTrackers) {
|
|
393
|
-
if (!activeIPs.has(ip)) {
|
|
394
|
-
this.ipThroughputTrackers.delete(ip);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}, this.sampleIntervalMs);
|
|
398
|
-
|
|
399
|
-
// Unref the interval so it doesn't keep the process alive
|
|
400
|
-
if (this.samplingInterval.unref) {
|
|
401
|
-
this.samplingInterval.unref();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Subscribe to new connections
|
|
405
|
-
this.connectionSubscription = this.smartProxy.routeConnectionHandler.newConnectionSubject.subscribe({
|
|
406
|
-
next: (record) => {
|
|
407
|
-
const routeName = record.routeConfig?.name || 'unknown';
|
|
408
|
-
this.recordRequest(record.id, routeName, record.remoteIP);
|
|
409
|
-
|
|
410
|
-
if (this.smartProxy.settings?.enableDetailedLogging) {
|
|
411
|
-
logger.log('debug', `MetricsCollector: New connection recorded`, {
|
|
412
|
-
connectionId: record.id,
|
|
413
|
-
remoteIP: record.remoteIP,
|
|
414
|
-
routeName,
|
|
415
|
-
component: 'metrics'
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
error: (err) => {
|
|
420
|
-
logger.log('error', `MetricsCollector: Error in connection subscription`, {
|
|
421
|
-
error: err.message,
|
|
422
|
-
component: 'metrics'
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
logger.log('debug', 'MetricsCollector started', { component: 'metrics' });
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Stop the metrics collector
|
|
432
|
-
*/
|
|
433
|
-
public stop(): void {
|
|
434
|
-
if (this.samplingInterval) {
|
|
435
|
-
clearInterval(this.samplingInterval);
|
|
436
|
-
this.samplingInterval = undefined;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (this.connectionSubscription) {
|
|
440
|
-
this.connectionSubscription.unsubscribe();
|
|
441
|
-
this.connectionSubscription = undefined;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
logger.log('debug', 'MetricsCollector stopped', { component: 'metrics' });
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Alias for stop() for compatibility
|
|
449
|
-
*/
|
|
450
|
-
public destroy(): void {
|
|
451
|
-
this.stop();
|
|
452
|
-
}
|
|
453
|
-
}
|