@push.rocks/smartproxy 22.4.2 → 22.6.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.
Files changed (72) hide show
  1. package/changelog.md +28 -0
  2. package/dist_rust/rustproxy +0 -0
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/index.d.ts +1 -5
  5. package/dist_ts/index.js +3 -9
  6. package/dist_ts/protocols/common/fragment-handler.js +5 -1
  7. package/dist_ts/proxies/index.d.ts +1 -5
  8. package/dist_ts/proxies/index.js +2 -6
  9. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
  10. package/dist_ts/proxies/smart-proxy/index.js +7 -13
  11. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
  12. package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
  13. package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
  14. package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
  15. package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
  16. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
  17. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
  18. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
  19. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
  21. package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
  22. package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
  23. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
  24. package/dist_ts/routing/index.d.ts +1 -1
  25. package/dist_ts/routing/index.js +3 -3
  26. package/dist_ts/routing/models/http-types.d.ts +119 -4
  27. package/dist_ts/routing/models/http-types.js +93 -5
  28. package/package.json +1 -1
  29. package/readme.md +470 -219
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/index.ts +4 -12
  32. package/ts/protocols/common/fragment-handler.ts +4 -0
  33. package/ts/proxies/index.ts +1 -9
  34. package/ts/proxies/smart-proxy/index.ts +6 -13
  35. package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
  36. package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
  37. package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
  38. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
  39. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
  40. package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
  41. package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
  42. package/ts/routing/index.ts +2 -2
  43. package/ts/routing/models/http-types.ts +147 -4
  44. package/ts/proxies/http-proxy/connection-pool.ts +0 -228
  45. package/ts/proxies/http-proxy/context-creator.ts +0 -145
  46. package/ts/proxies/http-proxy/default-certificates.ts +0 -150
  47. package/ts/proxies/http-proxy/function-cache.ts +0 -279
  48. package/ts/proxies/http-proxy/handlers/index.ts +0 -5
  49. package/ts/proxies/http-proxy/http-proxy.ts +0 -669
  50. package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
  51. package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
  52. package/ts/proxies/http-proxy/index.ts +0 -18
  53. package/ts/proxies/http-proxy/models/http-types.ts +0 -148
  54. package/ts/proxies/http-proxy/models/index.ts +0 -5
  55. package/ts/proxies/http-proxy/models/types.ts +0 -125
  56. package/ts/proxies/http-proxy/request-handler.ts +0 -878
  57. package/ts/proxies/http-proxy/security-manager.ts +0 -413
  58. package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
  59. package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
  60. package/ts/proxies/smart-proxy/cert-store.ts +0 -92
  61. package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
  62. package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
  63. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
  64. package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
  65. package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
  66. package/ts/proxies/smart-proxy/port-manager.ts +0 -358
  67. package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
  68. package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
  69. package/ts/proxies/smart-proxy/security-manager.ts +0 -269
  70. package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
  71. package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
  72. package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
@@ -1,581 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import '../../core/models/socket-augmentation.js';
3
- import { type IHttpProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger } from './models/types.js';
4
- import { ConnectionPool } from './connection-pool.js';
5
- import { HttpRouter } from '../../routing/router/index.js';
6
- import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
7
- import type { IRouteContext } from '../../core/models/route-context.js';
8
- import { toBaseContext } from '../../core/models/route-context.js';
9
- import { ContextCreator } from './context-creator.js';
10
- import { SecurityManager } from './security-manager.js';
11
- import { TemplateUtils } from '../../core/utils/template-utils.js';
12
- import { getMessageSize, toBuffer } from '../../core/utils/websocket-utils.js';
13
-
14
- /**
15
- * Handles WebSocket connections and proxying
16
- */
17
- export class WebSocketHandler {
18
- private heartbeatInterval: NodeJS.Timeout | null = null;
19
- private wsServer: plugins.ws.WebSocketServer | null = null;
20
- private logger: ILogger;
21
- private contextCreator: ContextCreator = new ContextCreator();
22
- private router: HttpRouter | null = null;
23
- private securityManager: SecurityManager;
24
-
25
- constructor(
26
- private options: IHttpProxyOptions,
27
- private connectionPool: ConnectionPool,
28
- private routes: IRouteConfig[] = []
29
- ) {
30
- this.logger = createLogger(options.logLevel || 'info');
31
- this.securityManager = new SecurityManager(this.logger, routes);
32
-
33
- // Initialize router if we have routes
34
- if (routes.length > 0) {
35
- this.router = new HttpRouter(routes, this.logger);
36
- }
37
- }
38
-
39
- /**
40
- * Set the route configurations
41
- */
42
- public setRoutes(routes: IRouteConfig[]): void {
43
- this.routes = routes;
44
-
45
- // Initialize or update the route router
46
- if (!this.router) {
47
- this.router = new HttpRouter(routes, this.logger);
48
- } else {
49
- this.router.setRoutes(routes);
50
- }
51
-
52
- // Update the security manager
53
- this.securityManager.setRoutes(routes);
54
- }
55
-
56
- /**
57
- * Select the appropriate target from the targets array based on sub-matching criteria
58
- */
59
- private selectTarget(
60
- targets: IRouteTarget[],
61
- context: {
62
- port: number;
63
- path?: string;
64
- headers?: Record<string, string>;
65
- method?: string;
66
- }
67
- ): IRouteTarget | null {
68
- // Sort targets by priority (higher first)
69
- const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
70
-
71
- // Find the first matching target
72
- for (const target of sortedTargets) {
73
- if (!target.match) {
74
- // No match criteria means this is a default/fallback target
75
- return target;
76
- }
77
-
78
- // Check port match
79
- if (target.match.ports && !target.match.ports.includes(context.port)) {
80
- continue;
81
- }
82
-
83
- // Check path match (supports wildcards)
84
- if (target.match.path && context.path) {
85
- const pathPattern = target.match.path.replace(/\*/g, '.*');
86
- const pathRegex = new RegExp(`^${pathPattern}$`);
87
- if (!pathRegex.test(context.path)) {
88
- continue;
89
- }
90
- }
91
-
92
- // Check method match
93
- if (target.match.method && context.method && !target.match.method.includes(context.method)) {
94
- continue;
95
- }
96
-
97
- // Check headers match
98
- if (target.match.headers && context.headers) {
99
- let headersMatch = true;
100
- for (const [key, pattern] of Object.entries(target.match.headers)) {
101
- const headerValue = context.headers[key.toLowerCase()];
102
- if (!headerValue) {
103
- headersMatch = false;
104
- break;
105
- }
106
-
107
- if (pattern instanceof RegExp) {
108
- if (!pattern.test(headerValue)) {
109
- headersMatch = false;
110
- break;
111
- }
112
- } else if (headerValue !== pattern) {
113
- headersMatch = false;
114
- break;
115
- }
116
- }
117
- if (!headersMatch) {
118
- continue;
119
- }
120
- }
121
-
122
- // All criteria matched
123
- return target;
124
- }
125
-
126
- // No matching target found, return the first target without match criteria (default)
127
- return sortedTargets.find(t => !t.match) || null;
128
- }
129
-
130
- /**
131
- * Initialize WebSocket server on an existing HTTPS server
132
- */
133
- public initialize(server: plugins.https.Server): void {
134
- // Create WebSocket server
135
- this.wsServer = new plugins.ws.WebSocketServer({
136
- server: server,
137
- clientTracking: true
138
- });
139
-
140
- // Handle WebSocket connections
141
- this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage) => {
142
- this.handleWebSocketConnection(wsIncoming, req);
143
- });
144
-
145
- // Start the heartbeat interval
146
- this.startHeartbeat();
147
-
148
- this.logger.info('WebSocket handler initialized');
149
- }
150
-
151
- /**
152
- * Start the heartbeat interval to check for inactive WebSocket connections
153
- */
154
- private startHeartbeat(): void {
155
- // Clean up existing interval if any
156
- if (this.heartbeatInterval) {
157
- clearInterval(this.heartbeatInterval);
158
- }
159
-
160
- // Set up the heartbeat interval (check every 30 seconds)
161
- this.heartbeatInterval = setInterval(() => {
162
- if (!this.wsServer || this.wsServer.clients.size === 0) {
163
- return; // Skip if no active connections
164
- }
165
-
166
- this.logger.debug(`WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
167
-
168
- this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
169
- const wsWithHeartbeat = ws as IWebSocketWithHeartbeat;
170
-
171
- if (wsWithHeartbeat.isAlive === false) {
172
- this.logger.debug('Terminating inactive WebSocket connection');
173
- return wsWithHeartbeat.terminate();
174
- }
175
-
176
- wsWithHeartbeat.isAlive = false;
177
- wsWithHeartbeat.ping();
178
- });
179
- }, 30000);
180
-
181
- // Make sure the interval doesn't keep the process alive
182
- if (this.heartbeatInterval.unref) {
183
- this.heartbeatInterval.unref();
184
- }
185
- }
186
-
187
- /**
188
- * Handle a new WebSocket connection
189
- */
190
- private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage): void {
191
- this.logger.debug(`WebSocket connection initiated from ${req.headers.host}`);
192
-
193
- try {
194
- // Initialize heartbeat tracking
195
- wsIncoming.isAlive = true;
196
- wsIncoming.lastPong = Date.now();
197
-
198
- // Handle pong messages to track liveness
199
- wsIncoming.on('pong', () => {
200
- wsIncoming.isAlive = true;
201
- wsIncoming.lastPong = Date.now();
202
- });
203
-
204
- // Create a context for routing
205
- const connectionId = `ws-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
206
- const routeContext = this.contextCreator.createHttpRouteContext(req, {
207
- connectionId,
208
- clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0',
209
- serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0',
210
- tlsVersion: req.socket.getTLSVersion?.() || undefined
211
- });
212
-
213
- // Try modern router first if available
214
- let route: IRouteConfig | undefined;
215
- if (this.router) {
216
- route = this.router.routeReq(req);
217
- }
218
-
219
- // Define destination variables
220
- let destination: { host: string; port: number };
221
-
222
- // If we found a route with the modern router, use it
223
- if (route && route.action.type === 'forward' && route.action.targets && route.action.targets.length > 0) {
224
- this.logger.debug(`Found matching WebSocket route: ${route.name || 'unnamed'}`);
225
-
226
- // Select the appropriate target from the targets array
227
- const selectedTarget = this.selectTarget(route.action.targets, {
228
- port: routeContext.port,
229
- path: routeContext.path,
230
- headers: routeContext.headers,
231
- method: routeContext.method
232
- });
233
-
234
- if (!selectedTarget) {
235
- this.logger.error(`No matching target found for route ${route.name}`);
236
- wsIncoming.close(1003, 'No matching target');
237
- return;
238
- }
239
-
240
- // Check if WebSockets are enabled for this route
241
- if (route.action.websocket?.enabled === false) {
242
- this.logger.debug(`WebSockets are disabled for route: ${route.name || 'unnamed'}`);
243
- wsIncoming.close(1003, 'WebSockets not supported for this route');
244
- return;
245
- }
246
-
247
- // Check security restrictions if configured to authenticate WebSocket requests
248
- if (route.action.websocket?.authenticateRequest !== false && route.security) {
249
- if (!this.securityManager.isAllowed(route, toBaseContext(routeContext))) {
250
- this.logger.warn(`WebSocket connection denied by security policy for ${routeContext.clientIp}`);
251
- wsIncoming.close(1008, 'Access denied by security policy');
252
- return;
253
- }
254
-
255
- // Check origin restrictions if configured
256
- const origin = req.headers.origin;
257
- if (origin && route.action.websocket?.allowedOrigins && route.action.websocket.allowedOrigins.length > 0) {
258
- const isAllowed = route.action.websocket.allowedOrigins.some(allowedOrigin => {
259
- // Handle wildcards and template variables
260
- if (allowedOrigin.includes('*') || allowedOrigin.includes('{')) {
261
- const pattern = allowedOrigin.replace(/\*/g, '.*');
262
- const resolvedPattern = TemplateUtils.resolveTemplateVariables(pattern, routeContext);
263
- const regex = new RegExp(`^${resolvedPattern}$`);
264
- return regex.test(origin);
265
- }
266
- return allowedOrigin === origin;
267
- });
268
-
269
- if (!isAllowed) {
270
- this.logger.warn(`WebSocket origin ${origin} not allowed for route: ${route.name || 'unnamed'}`);
271
- wsIncoming.close(1008, 'Origin not allowed');
272
- return;
273
- }
274
- }
275
- }
276
-
277
- // Extract target information, resolving functions if needed
278
- let targetHost: string | string[];
279
- let targetPort: number;
280
-
281
- try {
282
- // Resolve host if it's a function
283
- if (typeof selectedTarget.host === 'function') {
284
- const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
285
- targetHost = resolvedHost;
286
- this.logger.debug(`Resolved function-based host for WebSocket: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
287
- } else {
288
- targetHost = selectedTarget.host;
289
- }
290
-
291
- // Resolve port if it's a function
292
- if (typeof selectedTarget.port === 'function') {
293
- targetPort = selectedTarget.port(toBaseContext(routeContext));
294
- this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`);
295
- } else {
296
- targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
297
- }
298
-
299
- // Select a single host if an array was provided
300
- const selectedHost = Array.isArray(targetHost)
301
- ? targetHost[Math.floor(Math.random() * targetHost.length)]
302
- : targetHost;
303
-
304
- // Create a destination for the WebSocket connection
305
- destination = {
306
- host: selectedHost,
307
- port: targetPort
308
- };
309
-
310
- this.logger.debug(`WebSocket destination resolved: ${selectedHost}:${targetPort}`);
311
- } catch (err) {
312
- this.logger.error(`Error evaluating function-based target for WebSocket: ${err}`);
313
- wsIncoming.close(1011, 'Internal server error');
314
- return;
315
- }
316
- } else {
317
- // No route found
318
- this.logger.warn(`No route configuration for WebSocket host: ${req.headers.host}`);
319
- wsIncoming.close(1008, 'No route configuration for this host');
320
- return;
321
- }
322
-
323
- // Build target URL with potential path rewriting
324
- // Determine protocol based on the target's configuration
325
- // For WebSocket connections, we use ws for HTTP backends and wss for HTTPS backends
326
- const isTargetSecure = destination.port === 443;
327
- const protocol = isTargetSecure ? 'wss' : 'ws';
328
- let targetPath = req.url || '/';
329
-
330
- // Apply path rewriting if configured
331
- if (route?.action.websocket?.rewritePath) {
332
- const originalPath = targetPath;
333
- targetPath = TemplateUtils.resolveTemplateVariables(
334
- route.action.websocket.rewritePath,
335
- {...routeContext, path: targetPath}
336
- );
337
- this.logger.debug(`WebSocket path rewritten: ${originalPath} -> ${targetPath}`);
338
- }
339
-
340
- const targetUrl = `${protocol}://${destination.host}:${destination.port}${targetPath}`;
341
-
342
- this.logger.debug(`WebSocket connection from ${req.socket.remoteAddress} to ${targetUrl}`);
343
-
344
- // Create headers for outgoing WebSocket connection
345
- const headers: { [key: string]: string } = {};
346
-
347
- // Copy relevant headers from incoming request
348
- for (const [key, value] of Object.entries(req.headers)) {
349
- if (value && typeof value === 'string' &&
350
- key.toLowerCase() !== 'connection' &&
351
- key.toLowerCase() !== 'upgrade' &&
352
- key.toLowerCase() !== 'sec-websocket-key' &&
353
- key.toLowerCase() !== 'sec-websocket-version') {
354
- headers[key] = value;
355
- }
356
- }
357
-
358
- // Always rewrite host header for WebSockets for consistency
359
- headers['host'] = `${destination.host}:${destination.port}`;
360
-
361
- // Add custom headers from route configuration
362
- if (route?.action.websocket?.customHeaders) {
363
- for (const [key, value] of Object.entries(route.action.websocket.customHeaders)) {
364
- // Skip if header already exists and we're not overriding
365
- if (headers[key.toLowerCase()] && !value.startsWith('!')) {
366
- continue;
367
- }
368
-
369
- // Handle special delete directive (!delete)
370
- if (value === '!delete') {
371
- delete headers[key.toLowerCase()];
372
- continue;
373
- }
374
-
375
- // Handle forced override (!value)
376
- let finalValue: string;
377
- if (value.startsWith('!') && value !== '!delete') {
378
- // Keep the ! but resolve any templates in the rest
379
- const templateValue = value.substring(1);
380
- finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, routeContext);
381
- } else {
382
- // Resolve templates in the entire value
383
- finalValue = TemplateUtils.resolveTemplateVariables(value, routeContext);
384
- }
385
-
386
- // Set the header
387
- headers[key.toLowerCase()] = finalValue;
388
- }
389
- }
390
-
391
- // Create WebSocket connection options
392
- const wsOptions: any = {
393
- headers: headers,
394
- followRedirects: true
395
- };
396
-
397
- // Add subprotocols if configured
398
- if (route?.action.websocket?.subprotocols && route.action.websocket.subprotocols.length > 0) {
399
- wsOptions.protocols = route.action.websocket.subprotocols;
400
- } else if (req.headers['sec-websocket-protocol']) {
401
- // Pass through client requested protocols
402
- wsOptions.protocols = req.headers['sec-websocket-protocol'].split(',').map(p => p.trim());
403
- }
404
-
405
- // Create outgoing WebSocket connection
406
- this.logger.debug(`Creating WebSocket connection to ${targetUrl} with options:`, {
407
- headers: wsOptions.headers,
408
- protocols: wsOptions.protocols
409
- });
410
- const wsOutgoing = new plugins.wsDefault(targetUrl, wsOptions);
411
- this.logger.debug(`WebSocket instance created, waiting for connection...`);
412
-
413
- // Handle connection errors
414
- wsOutgoing.on('error', (err) => {
415
- this.logger.error(`WebSocket target connection error: ${err.message}`);
416
- if (wsIncoming.readyState === wsIncoming.OPEN) {
417
- wsIncoming.close(1011, 'Internal server error');
418
- }
419
- });
420
-
421
- // Handle outgoing connection open
422
- wsOutgoing.on('open', () => {
423
- this.logger.debug(`WebSocket target connection opened to ${targetUrl}`);
424
- // Set up custom ping interval if configured
425
- let pingInterval: NodeJS.Timeout | null = null;
426
- if (route?.action.websocket?.pingInterval && route.action.websocket.pingInterval > 0) {
427
- pingInterval = setInterval(() => {
428
- if (wsIncoming.readyState === wsIncoming.OPEN) {
429
- wsIncoming.ping();
430
- this.logger.debug(`Sent WebSocket ping to client for route: ${route.name || 'unnamed'}`);
431
- }
432
- }, route.action.websocket.pingInterval);
433
-
434
- // Don't keep process alive just for pings
435
- if (pingInterval.unref) pingInterval.unref();
436
- }
437
-
438
- // Set up custom ping timeout if configured
439
- let pingTimeout: NodeJS.Timeout | null = null;
440
- const pingTimeoutMs = route?.action.websocket?.pingTimeout || 60000; // Default 60s
441
-
442
- // Define timeout function for cleaner code
443
- const resetPingTimeout = () => {
444
- if (pingTimeout) clearTimeout(pingTimeout);
445
- pingTimeout = setTimeout(() => {
446
- this.logger.debug(`WebSocket ping timeout for client connection on route: ${route?.name || 'unnamed'}`);
447
- wsIncoming.terminate();
448
- }, pingTimeoutMs);
449
-
450
- // Don't keep process alive just for timeouts
451
- if (pingTimeout.unref) pingTimeout.unref();
452
- };
453
-
454
- // Reset timeout on pong
455
- wsIncoming.on('pong', () => {
456
- wsIncoming.isAlive = true;
457
- wsIncoming.lastPong = Date.now();
458
- resetPingTimeout();
459
- });
460
-
461
- // Initial ping timeout
462
- resetPingTimeout();
463
-
464
- // Handle potential message size limits
465
- const maxSize = route?.action.websocket?.maxPayloadSize || 0;
466
-
467
- // Forward incoming messages to outgoing connection
468
- wsIncoming.on('message', (data, isBinary) => {
469
- this.logger.debug(`WebSocket forwarding message from client to target: ${data.toString()}`);
470
- if (wsOutgoing.readyState === wsOutgoing.OPEN) {
471
- // Check message size if limit is set
472
- const messageSize = getMessageSize(data);
473
- if (maxSize > 0 && messageSize > maxSize) {
474
- this.logger.warn(`WebSocket message exceeds max size (${messageSize} > ${maxSize})`);
475
- wsIncoming.close(1009, 'Message too big');
476
- return;
477
- }
478
-
479
- wsOutgoing.send(data, { binary: isBinary });
480
- } else {
481
- this.logger.warn(`WebSocket target connection not open (state: ${wsOutgoing.readyState})`);
482
- }
483
- });
484
-
485
- // Forward outgoing messages to incoming connection
486
- wsOutgoing.on('message', (data, isBinary) => {
487
- this.logger.debug(`WebSocket forwarding message from target to client: ${data.toString()}`);
488
- if (wsIncoming.readyState === wsIncoming.OPEN) {
489
- wsIncoming.send(data, { binary: isBinary });
490
- } else {
491
- this.logger.warn(`WebSocket client connection not open (state: ${wsIncoming.readyState})`);
492
- }
493
- });
494
-
495
- // Handle closing of connections
496
- wsIncoming.on('close', (code, reason) => {
497
- this.logger.debug(`WebSocket client connection closed: ${code} ${reason}`);
498
- if (wsOutgoing.readyState === wsOutgoing.OPEN) {
499
- // Ensure code is a valid WebSocket close code number
500
- const validCode = typeof code === 'number' && code >= 1000 && code <= 4999 ? code : 1000;
501
- try {
502
- const reasonString = reason ? toBuffer(reason).toString() : '';
503
- wsOutgoing.close(validCode, reasonString);
504
- } catch (err) {
505
- this.logger.error('Error closing wsOutgoing:', err);
506
- wsOutgoing.close(validCode);
507
- }
508
- }
509
-
510
- // Clean up timers
511
- if (pingInterval) clearInterval(pingInterval);
512
- if (pingTimeout) clearTimeout(pingTimeout);
513
- });
514
-
515
- wsOutgoing.on('close', (code, reason) => {
516
- this.logger.debug(`WebSocket target connection closed: ${code} ${reason}`);
517
- if (wsIncoming.readyState === wsIncoming.OPEN) {
518
- // Ensure code is a valid WebSocket close code number
519
- const validCode = typeof code === 'number' && code >= 1000 && code <= 4999 ? code : 1000;
520
- try {
521
- const reasonString = reason ? toBuffer(reason).toString() : '';
522
- wsIncoming.close(validCode, reasonString);
523
- } catch (err) {
524
- this.logger.error('Error closing wsIncoming:', err);
525
- wsIncoming.close(validCode);
526
- }
527
- }
528
-
529
- // Clean up timers
530
- if (pingInterval) clearInterval(pingInterval);
531
- if (pingTimeout) clearTimeout(pingTimeout);
532
- });
533
-
534
- this.logger.debug(`WebSocket connection established: ${req.headers.host} -> ${destination.host}:${destination.port}`);
535
- });
536
-
537
- } catch (error) {
538
- this.logger.error(`Error handling WebSocket connection: ${error.message}`);
539
- if (wsIncoming.readyState === wsIncoming.OPEN) {
540
- wsIncoming.close(1011, 'Internal server error');
541
- }
542
- }
543
- }
544
-
545
- /**
546
- * Get information about active WebSocket connections
547
- */
548
- public getConnectionInfo(): { activeConnections: number } {
549
- return {
550
- activeConnections: this.wsServer ? this.wsServer.clients.size : 0
551
- };
552
- }
553
-
554
- /**
555
- * Shutdown the WebSocket handler
556
- */
557
- public shutdown(): void {
558
- // Stop heartbeat interval
559
- if (this.heartbeatInterval) {
560
- clearInterval(this.heartbeatInterval);
561
- this.heartbeatInterval = null;
562
- }
563
-
564
- // Close all WebSocket connections
565
- if (this.wsServer) {
566
- this.logger.info(`Closing ${this.wsServer.clients.size} WebSocket connections`);
567
-
568
- for (const client of this.wsServer.clients) {
569
- try {
570
- client.terminate();
571
- } catch (error) {
572
- this.logger.error('Error terminating WebSocket client', error);
573
- }
574
- }
575
-
576
- // Close the server
577
- this.wsServer.close();
578
- this.wsServer = null;
579
- }
580
- }
581
- }
@@ -1,112 +0,0 @@
1
- import type { IRouteConfig } from './models/route-types.js';
2
-
3
- /**
4
- * Global state store for ACME operations
5
- * Tracks active challenge routes and port allocations
6
- */
7
- export class AcmeStateManager {
8
- private activeChallengeRoutes: Map<string, IRouteConfig> = new Map();
9
- private acmePortAllocations: Set<number> = new Set();
10
- private primaryChallengeRoute: IRouteConfig | null = null;
11
-
12
- /**
13
- * Check if a challenge route is active
14
- */
15
- public isChallengeRouteActive(): boolean {
16
- return this.activeChallengeRoutes.size > 0;
17
- }
18
-
19
- /**
20
- * Register a challenge route as active
21
- */
22
- public addChallengeRoute(route: IRouteConfig): void {
23
- this.activeChallengeRoutes.set(route.name, route);
24
-
25
- // Track the primary challenge route
26
- if (!this.primaryChallengeRoute || route.priority > (this.primaryChallengeRoute.priority || 0)) {
27
- this.primaryChallengeRoute = route;
28
- }
29
-
30
- // Track port allocations
31
- const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
32
- ports.forEach(port => this.acmePortAllocations.add(port));
33
- }
34
-
35
- /**
36
- * Remove a challenge route
37
- */
38
- public removeChallengeRoute(routeName: string): void {
39
- const route = this.activeChallengeRoutes.get(routeName);
40
- if (!route) return;
41
-
42
- this.activeChallengeRoutes.delete(routeName);
43
-
44
- // Update primary challenge route if needed
45
- if (this.primaryChallengeRoute?.name === routeName) {
46
- this.primaryChallengeRoute = null;
47
- // Find new primary route with highest priority
48
- let highestPriority = -1;
49
- for (const [_, activeRoute] of this.activeChallengeRoutes) {
50
- const priority = activeRoute.priority || 0;
51
- if (priority > highestPriority) {
52
- highestPriority = priority;
53
- this.primaryChallengeRoute = activeRoute;
54
- }
55
- }
56
- }
57
-
58
- // Update port allocations - only remove if no other routes use this port
59
- const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
60
- ports.forEach(port => {
61
- let portStillUsed = false;
62
- for (const [_, activeRoute] of this.activeChallengeRoutes) {
63
- const activePorts = Array.isArray(activeRoute.match.ports) ?
64
- activeRoute.match.ports : [activeRoute.match.ports];
65
- if (activePorts.includes(port)) {
66
- portStillUsed = true;
67
- break;
68
- }
69
- }
70
- if (!portStillUsed) {
71
- this.acmePortAllocations.delete(port);
72
- }
73
- });
74
- }
75
-
76
- /**
77
- * Get all active challenge routes
78
- */
79
- public getActiveChallengeRoutes(): IRouteConfig[] {
80
- return Array.from(this.activeChallengeRoutes.values());
81
- }
82
-
83
- /**
84
- * Get the primary challenge route
85
- */
86
- public getPrimaryChallengeRoute(): IRouteConfig | null {
87
- return this.primaryChallengeRoute;
88
- }
89
-
90
- /**
91
- * Check if a port is allocated for ACME
92
- */
93
- public isPortAllocatedForAcme(port: number): boolean {
94
- return this.acmePortAllocations.has(port);
95
- }
96
-
97
- /**
98
- * Get all ACME ports
99
- */
100
- public getAcmePorts(): number[] {
101
- return Array.from(this.acmePortAllocations);
102
- }
103
-
104
- /**
105
- * Clear all state (for shutdown or reset)
106
- */
107
- public clear(): void {
108
- this.activeChallengeRoutes.clear();
109
- this.acmePortAllocations.clear();
110
- this.primaryChallengeRoute = null;
111
- }
112
- }