@push.rocks/smartproxy 16.0.2 → 16.0.3

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 (115) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/core/models/index.d.ts +2 -0
  3. package/dist_ts/core/models/index.js +3 -1
  4. package/dist_ts/core/models/route-context.d.ts +62 -0
  5. package/dist_ts/core/models/route-context.js +43 -0
  6. package/dist_ts/core/models/socket-augmentation.d.ts +12 -0
  7. package/dist_ts/core/models/socket-augmentation.js +18 -0
  8. package/dist_ts/core/utils/event-system.d.ts +200 -0
  9. package/dist_ts/core/utils/event-system.js +224 -0
  10. package/dist_ts/core/utils/index.d.ts +7 -0
  11. package/dist_ts/core/utils/index.js +8 -1
  12. package/dist_ts/core/utils/route-manager.d.ts +118 -0
  13. package/dist_ts/core/utils/route-manager.js +383 -0
  14. package/dist_ts/core/utils/route-utils.d.ts +94 -0
  15. package/dist_ts/core/utils/route-utils.js +264 -0
  16. package/dist_ts/core/utils/security-utils.d.ts +111 -0
  17. package/dist_ts/core/utils/security-utils.js +212 -0
  18. package/dist_ts/core/utils/shared-security-manager.d.ts +110 -0
  19. package/dist_ts/core/utils/shared-security-manager.js +252 -0
  20. package/dist_ts/core/utils/template-utils.d.ts +37 -0
  21. package/dist_ts/core/utils/template-utils.js +104 -0
  22. package/dist_ts/core/utils/websocket-utils.d.ts +23 -0
  23. package/dist_ts/core/utils/websocket-utils.js +86 -0
  24. package/dist_ts/http/router/index.d.ts +5 -1
  25. package/dist_ts/http/router/index.js +4 -2
  26. package/dist_ts/http/router/route-router.d.ts +108 -0
  27. package/dist_ts/http/router/route-router.js +393 -0
  28. package/dist_ts/index.d.ts +8 -2
  29. package/dist_ts/index.js +10 -3
  30. package/dist_ts/proxies/index.d.ts +7 -2
  31. package/dist_ts/proxies/index.js +10 -4
  32. package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +21 -0
  33. package/dist_ts/proxies/network-proxy/certificate-manager.js +92 -1
  34. package/dist_ts/proxies/network-proxy/context-creator.d.ts +34 -0
  35. package/dist_ts/proxies/network-proxy/context-creator.js +108 -0
  36. package/dist_ts/proxies/network-proxy/function-cache.d.ts +90 -0
  37. package/dist_ts/proxies/network-proxy/function-cache.js +198 -0
  38. package/dist_ts/proxies/network-proxy/http-request-handler.d.ts +40 -0
  39. package/dist_ts/proxies/network-proxy/http-request-handler.js +256 -0
  40. package/dist_ts/proxies/network-proxy/http2-request-handler.d.ts +24 -0
  41. package/dist_ts/proxies/network-proxy/http2-request-handler.js +201 -0
  42. package/dist_ts/proxies/network-proxy/models/types.d.ts +73 -1
  43. package/dist_ts/proxies/network-proxy/models/types.js +242 -1
  44. package/dist_ts/proxies/network-proxy/network-proxy.d.ts +23 -20
  45. package/dist_ts/proxies/network-proxy/network-proxy.js +147 -60
  46. package/dist_ts/proxies/network-proxy/request-handler.d.ts +38 -5
  47. package/dist_ts/proxies/network-proxy/request-handler.js +584 -198
  48. package/dist_ts/proxies/network-proxy/security-manager.d.ts +65 -0
  49. package/dist_ts/proxies/network-proxy/security-manager.js +255 -0
  50. package/dist_ts/proxies/network-proxy/websocket-handler.d.ts +13 -2
  51. package/dist_ts/proxies/network-proxy/websocket-handler.js +238 -20
  52. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -1
  53. package/dist_ts/proxies/smart-proxy/index.js +3 -3
  54. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +3 -5
  55. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +56 -3
  56. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +4 -57
  57. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +19 -228
  58. package/dist_ts/proxies/smart-proxy/port-manager.d.ts +81 -0
  59. package/dist_ts/proxies/smart-proxy/port-manager.js +166 -0
  60. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +5 -0
  61. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +131 -15
  62. package/dist_ts/proxies/smart-proxy/route-helpers/index.d.ts +3 -1
  63. package/dist_ts/proxies/smart-proxy/route-helpers/index.js +5 -3
  64. package/dist_ts/proxies/smart-proxy/route-helpers.d.ts +5 -178
  65. package/dist_ts/proxies/smart-proxy/route-helpers.js +8 -296
  66. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +11 -2
  67. package/dist_ts/proxies/smart-proxy/route-manager.js +79 -10
  68. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +29 -2
  69. package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -43
  70. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +67 -1
  71. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +120 -1
  72. package/dist_ts/proxies/smart-proxy/utils/route-validators.d.ts +3 -3
  73. package/dist_ts/proxies/smart-proxy/utils/route-validators.js +27 -5
  74. package/package.json +1 -1
  75. package/readme.md +102 -14
  76. package/readme.plan.md +103 -168
  77. package/ts/00_commitinfo_data.ts +1 -1
  78. package/ts/core/models/index.ts +2 -0
  79. package/ts/core/models/route-context.ts +113 -0
  80. package/ts/core/models/socket-augmentation.ts +33 -0
  81. package/ts/core/utils/event-system.ts +376 -0
  82. package/ts/core/utils/index.ts +7 -0
  83. package/ts/core/utils/route-manager.ts +489 -0
  84. package/ts/core/utils/route-utils.ts +312 -0
  85. package/ts/core/utils/security-utils.ts +309 -0
  86. package/ts/core/utils/shared-security-manager.ts +333 -0
  87. package/ts/core/utils/template-utils.ts +124 -0
  88. package/ts/core/utils/websocket-utils.ts +81 -0
  89. package/ts/http/router/index.ts +8 -1
  90. package/ts/http/router/route-router.ts +482 -0
  91. package/ts/index.ts +14 -2
  92. package/ts/proxies/index.ts +12 -3
  93. package/ts/proxies/network-proxy/certificate-manager.ts +114 -10
  94. package/ts/proxies/network-proxy/context-creator.ts +145 -0
  95. package/ts/proxies/network-proxy/function-cache.ts +259 -0
  96. package/ts/proxies/network-proxy/http-request-handler.ts +330 -0
  97. package/ts/proxies/network-proxy/http2-request-handler.ts +255 -0
  98. package/ts/proxies/network-proxy/models/types.ts +312 -1
  99. package/ts/proxies/network-proxy/network-proxy.ts +195 -86
  100. package/ts/proxies/network-proxy/request-handler.ts +698 -246
  101. package/ts/proxies/network-proxy/security-manager.ts +298 -0
  102. package/ts/proxies/network-proxy/websocket-handler.ts +276 -33
  103. package/ts/proxies/smart-proxy/index.ts +2 -12
  104. package/ts/proxies/smart-proxy/models/interfaces.ts +7 -4
  105. package/ts/proxies/smart-proxy/models/route-types.ts +78 -10
  106. package/ts/proxies/smart-proxy/network-proxy-bridge.ts +20 -257
  107. package/ts/proxies/smart-proxy/port-manager.ts +195 -0
  108. package/ts/proxies/smart-proxy/route-connection-handler.ts +156 -21
  109. package/ts/proxies/smart-proxy/route-manager.ts +98 -14
  110. package/ts/proxies/smart-proxy/smart-proxy.ts +56 -55
  111. package/ts/proxies/smart-proxy/utils/route-helpers.ts +167 -1
  112. package/ts/proxies/smart-proxy/utils/route-validators.ts +24 -5
  113. package/ts/proxies/smart-proxy/domain-config-manager.ts.bak +0 -441
  114. package/ts/proxies/smart-proxy/route-helpers/index.ts +0 -9
  115. package/ts/proxies/smart-proxy/route-helpers.ts +0 -498
@@ -0,0 +1,298 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { ILogger } from './models/types.js';
3
+ import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
4
+ import type { IRouteContext } from '../../core/models/route-context.js';
5
+
6
+ /**
7
+ * Manages security features for the NetworkProxy
8
+ * Implements Phase 5.4: Security features like IP filtering and rate limiting
9
+ */
10
+ export class SecurityManager {
11
+ // Cache IP filtering results to avoid constant regex matching
12
+ private ipFilterCache: Map<string, Map<string, boolean>> = new Map();
13
+
14
+ // Store rate limits per route and key
15
+ private rateLimits: Map<string, Map<string, { count: number, expiry: number }>> = new Map();
16
+
17
+ constructor(private logger: ILogger, private routes: IRouteConfig[] = []) {}
18
+
19
+ /**
20
+ * Update the routes configuration
21
+ */
22
+ public setRoutes(routes: IRouteConfig[]): void {
23
+ this.routes = routes;
24
+ // Reset caches when routes change
25
+ this.ipFilterCache.clear();
26
+ }
27
+
28
+ /**
29
+ * Check if a client is allowed to access a specific route
30
+ *
31
+ * @param route The route to check access for
32
+ * @param context The route context with client information
33
+ * @returns True if access is allowed, false otherwise
34
+ */
35
+ public isAllowed(route: IRouteConfig, context: IRouteContext): boolean {
36
+ if (!route.security) {
37
+ return true; // No security restrictions
38
+ }
39
+
40
+ // --- IP filtering ---
41
+ if (!this.isIpAllowed(route, context.clientIp)) {
42
+ this.logger.debug(`IP ${context.clientIp} is blocked for route ${route.name || route.id || 'unnamed'}`);
43
+ return false;
44
+ }
45
+
46
+ // --- Rate limiting ---
47
+ if (route.security.rateLimit?.enabled && !this.isWithinRateLimit(route, context)) {
48
+ this.logger.debug(`Rate limit exceeded for route ${route.name || route.id || 'unnamed'}`);
49
+ return false;
50
+ }
51
+
52
+ // --- Basic Auth (handled at HTTP level) ---
53
+ // Basic auth is not checked here as it requires HTTP headers
54
+ // and is handled in the RequestHandler
55
+
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Check if an IP is allowed based on route security settings
61
+ */
62
+ private isIpAllowed(route: IRouteConfig, clientIp: string): boolean {
63
+ if (!route.security) {
64
+ return true; // No security restrictions
65
+ }
66
+
67
+ const routeId = route.id || route.name || 'unnamed';
68
+
69
+ // Check cache first
70
+ if (!this.ipFilterCache.has(routeId)) {
71
+ this.ipFilterCache.set(routeId, new Map());
72
+ }
73
+
74
+ const routeCache = this.ipFilterCache.get(routeId)!;
75
+ if (routeCache.has(clientIp)) {
76
+ return routeCache.get(clientIp)!;
77
+ }
78
+
79
+ let allowed = true;
80
+
81
+ // Check block list first (deny has priority over allow)
82
+ if (route.security.ipBlockList && route.security.ipBlockList.length > 0) {
83
+ if (this.ipMatchesPattern(clientIp, route.security.ipBlockList)) {
84
+ allowed = false;
85
+ }
86
+ }
87
+
88
+ // Then check allow list (overrides block list if specified)
89
+ if (route.security.ipAllowList && route.security.ipAllowList.length > 0) {
90
+ // If allow list is specified, IP must match an entry to be allowed
91
+ allowed = this.ipMatchesPattern(clientIp, route.security.ipAllowList);
92
+ }
93
+
94
+ // Cache the result
95
+ routeCache.set(clientIp, allowed);
96
+
97
+ return allowed;
98
+ }
99
+
100
+ /**
101
+ * Check if IP matches any pattern in the list
102
+ */
103
+ private ipMatchesPattern(ip: string, patterns: string[]): boolean {
104
+ for (const pattern of patterns) {
105
+ // CIDR notation
106
+ if (pattern.includes('/')) {
107
+ if (this.ipMatchesCidr(ip, pattern)) {
108
+ return true;
109
+ }
110
+ }
111
+ // Wildcard notation
112
+ else if (pattern.includes('*')) {
113
+ const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
114
+ if (regex.test(ip)) {
115
+ return true;
116
+ }
117
+ }
118
+ // Exact match
119
+ else if (pattern === ip) {
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+
126
+ /**
127
+ * Check if IP matches CIDR notation
128
+ * Very basic implementation - for production use, consider a dedicated IP library
129
+ */
130
+ private ipMatchesCidr(ip: string, cidr: string): boolean {
131
+ try {
132
+ const [subnet, bits] = cidr.split('/');
133
+ const mask = parseInt(bits, 10);
134
+
135
+ // Convert IP to numeric format
136
+ const ipParts = ip.split('.').map(part => parseInt(part, 10));
137
+ const subnetParts = subnet.split('.').map(part => parseInt(part, 10));
138
+
139
+ // Calculate the numeric IP and subnet
140
+ const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
141
+ const subnetNum = (subnetParts[0] << 24) | (subnetParts[1] << 16) | (subnetParts[2] << 8) | subnetParts[3];
142
+
143
+ // Calculate the mask
144
+ const maskNum = ~((1 << (32 - mask)) - 1);
145
+
146
+ // Check if IP is in subnet
147
+ return (ipNum & maskNum) === (subnetNum & maskNum);
148
+ } catch (e) {
149
+ this.logger.error(`Invalid CIDR notation: ${cidr}`);
150
+ return false;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Check if request is within rate limit
156
+ */
157
+ private isWithinRateLimit(route: IRouteConfig, context: IRouteContext): boolean {
158
+ if (!route.security?.rateLimit?.enabled) {
159
+ return true;
160
+ }
161
+
162
+ const rateLimit = route.security.rateLimit;
163
+ const routeId = route.id || route.name || 'unnamed';
164
+
165
+ // Determine rate limit key (by IP, path, or header)
166
+ let key = context.clientIp; // Default to IP
167
+
168
+ if (rateLimit.keyBy === 'path' && context.path) {
169
+ key = `${context.clientIp}:${context.path}`;
170
+ } else if (rateLimit.keyBy === 'header' && rateLimit.headerName && context.headers) {
171
+ const headerValue = context.headers[rateLimit.headerName.toLowerCase()];
172
+ if (headerValue) {
173
+ key = `${context.clientIp}:${headerValue}`;
174
+ }
175
+ }
176
+
177
+ // Get or create rate limit tracking for this route
178
+ if (!this.rateLimits.has(routeId)) {
179
+ this.rateLimits.set(routeId, new Map());
180
+ }
181
+
182
+ const routeLimits = this.rateLimits.get(routeId)!;
183
+ const now = Date.now();
184
+
185
+ // Get or create rate limit tracking for this key
186
+ let limit = routeLimits.get(key);
187
+ if (!limit || limit.expiry < now) {
188
+ // Create new rate limit or reset expired one
189
+ limit = {
190
+ count: 1,
191
+ expiry: now + (rateLimit.window * 1000)
192
+ };
193
+ routeLimits.set(key, limit);
194
+ return true;
195
+ }
196
+
197
+ // Increment the counter
198
+ limit.count++;
199
+
200
+ // Check if rate limit is exceeded
201
+ return limit.count <= rateLimit.maxRequests;
202
+ }
203
+
204
+ /**
205
+ * Clean up expired rate limits
206
+ * Should be called periodically to prevent memory leaks
207
+ */
208
+ public cleanupExpiredRateLimits(): void {
209
+ const now = Date.now();
210
+ for (const [routeId, routeLimits] of this.rateLimits.entries()) {
211
+ let removed = 0;
212
+ for (const [key, limit] of routeLimits.entries()) {
213
+ if (limit.expiry < now) {
214
+ routeLimits.delete(key);
215
+ removed++;
216
+ }
217
+ }
218
+ if (removed > 0) {
219
+ this.logger.debug(`Cleaned up ${removed} expired rate limits for route ${routeId}`);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Check basic auth credentials
226
+ *
227
+ * @param route The route to check auth for
228
+ * @param username The provided username
229
+ * @param password The provided password
230
+ * @returns True if credentials are valid, false otherwise
231
+ */
232
+ public checkBasicAuth(route: IRouteConfig, username: string, password: string): boolean {
233
+ if (!route.security?.basicAuth?.enabled) {
234
+ return true;
235
+ }
236
+
237
+ const basicAuth = route.security.basicAuth;
238
+
239
+ // Check credentials against configured users
240
+ for (const user of basicAuth.users) {
241
+ if (user.username === username && user.password === password) {
242
+ return true;
243
+ }
244
+ }
245
+
246
+ return false;
247
+ }
248
+
249
+ /**
250
+ * Verify a JWT token
251
+ *
252
+ * @param route The route to verify the token for
253
+ * @param token The JWT token to verify
254
+ * @returns True if the token is valid, false otherwise
255
+ */
256
+ public verifyJwtToken(route: IRouteConfig, token: string): boolean {
257
+ if (!route.security?.jwtAuth?.enabled) {
258
+ return true;
259
+ }
260
+
261
+ try {
262
+ // This is a simplified version - in production you'd use a proper JWT library
263
+ const jwtAuth = route.security.jwtAuth;
264
+
265
+ // Verify structure
266
+ const parts = token.split('.');
267
+ if (parts.length !== 3) {
268
+ return false;
269
+ }
270
+
271
+ // Decode payload
272
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
273
+
274
+ // Check expiration
275
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
276
+ return false;
277
+ }
278
+
279
+ // Check issuer
280
+ if (jwtAuth.issuer && payload.iss !== jwtAuth.issuer) {
281
+ return false;
282
+ }
283
+
284
+ // Check audience
285
+ if (jwtAuth.audience && payload.aud !== jwtAuth.audience) {
286
+ return false;
287
+ }
288
+
289
+ // In a real implementation, you'd also verify the signature
290
+ // using the secret and algorithm specified in jwtAuth
291
+
292
+ return true;
293
+ } catch (err) {
294
+ this.logger.error(`Error verifying JWT: ${err}`);
295
+ return false;
296
+ }
297
+ }
298
+ }
@@ -1,7 +1,15 @@
1
1
  import * as plugins from '../../plugins.js';
2
+ import '../../core/models/socket-augmentation.js';
2
3
  import { type INetworkProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger, type IReverseProxyConfig } from './models/types.js';
3
4
  import { ConnectionPool } from './connection-pool.js';
4
- import { ProxyRouter } from '../../http/router/index.js';
5
+ import { ProxyRouter, RouteRouter } from '../../http/router/index.js';
6
+ import type { IRouteConfig } 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';
5
13
 
6
14
  /**
7
15
  * Handles WebSocket connections and proxying
@@ -10,13 +18,40 @@ export class WebSocketHandler {
10
18
  private heartbeatInterval: NodeJS.Timeout | null = null;
11
19
  private wsServer: plugins.ws.WebSocketServer | null = null;
12
20
  private logger: ILogger;
21
+ private contextCreator: ContextCreator = new ContextCreator();
22
+ private routeRouter: RouteRouter | null = null;
23
+ private securityManager: SecurityManager;
13
24
 
14
25
  constructor(
15
26
  private options: INetworkProxyOptions,
16
27
  private connectionPool: ConnectionPool,
17
- private router: ProxyRouter
28
+ private legacyRouter: ProxyRouter, // Legacy router for backward compatibility
29
+ private routes: IRouteConfig[] = [] // Routes for modern router
18
30
  ) {
19
31
  this.logger = createLogger(options.logLevel || 'info');
32
+ this.securityManager = new SecurityManager(this.logger, routes);
33
+
34
+ // Initialize modern router if we have routes
35
+ if (routes.length > 0) {
36
+ this.routeRouter = new RouteRouter(routes, this.logger);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Set the route configurations
42
+ */
43
+ public setRoutes(routes: IRouteConfig[]): void {
44
+ this.routes = routes;
45
+
46
+ // Initialize or update the route router
47
+ if (!this.routeRouter) {
48
+ this.routeRouter = new RouteRouter(routes, this.logger);
49
+ } else {
50
+ this.routeRouter.setRoutes(routes);
51
+ }
52
+
53
+ // Update the security manager
54
+ this.securityManager.setRoutes(routes);
20
55
  }
21
56
 
22
57
  /**
@@ -91,51 +126,200 @@ export class WebSocketHandler {
91
126
  wsIncoming.lastPong = Date.now();
92
127
  });
93
128
 
94
- // Find target configuration based on request
95
- const proxyConfig = this.router.routeReq(req);
96
-
97
- if (!proxyConfig) {
98
- this.logger.warn(`No proxy configuration for WebSocket host: ${req.headers.host}`);
99
- wsIncoming.close(1008, 'No proxy configuration for this host');
100
- return;
129
+ // Create a context for routing
130
+ const connectionId = `ws-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
131
+ const routeContext = this.contextCreator.createHttpRouteContext(req, {
132
+ connectionId,
133
+ clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0',
134
+ serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0',
135
+ tlsVersion: req.socket.getTLSVersion?.() || undefined
136
+ });
137
+
138
+ // Try modern router first if available
139
+ let route: IRouteConfig | undefined;
140
+ if (this.routeRouter) {
141
+ route = this.routeRouter.routeReq(req);
142
+ }
143
+
144
+ // Define destination variables
145
+ let destination: { host: string; port: number };
146
+
147
+ // If we found a route with the modern router, use it
148
+ if (route && route.action.type === 'forward' && route.action.target) {
149
+ this.logger.debug(`Found matching WebSocket route: ${route.name || 'unnamed'}`);
150
+
151
+ // Check if WebSockets are enabled for this route
152
+ if (route.action.websocket?.enabled === false) {
153
+ this.logger.debug(`WebSockets are disabled for route: ${route.name || 'unnamed'}`);
154
+ wsIncoming.close(1003, 'WebSockets not supported for this route');
155
+ return;
156
+ }
157
+
158
+ // Check security restrictions if configured to authenticate WebSocket requests
159
+ if (route.action.websocket?.authenticateRequest !== false && route.security) {
160
+ if (!this.securityManager.isAllowed(route, toBaseContext(routeContext))) {
161
+ this.logger.warn(`WebSocket connection denied by security policy for ${routeContext.clientIp}`);
162
+ wsIncoming.close(1008, 'Access denied by security policy');
163
+ return;
164
+ }
165
+
166
+ // Check origin restrictions if configured
167
+ const origin = req.headers.origin;
168
+ if (origin && route.action.websocket?.allowedOrigins && route.action.websocket.allowedOrigins.length > 0) {
169
+ const isAllowed = route.action.websocket.allowedOrigins.some(allowedOrigin => {
170
+ // Handle wildcards and template variables
171
+ if (allowedOrigin.includes('*') || allowedOrigin.includes('{')) {
172
+ const pattern = allowedOrigin.replace(/\*/g, '.*');
173
+ const resolvedPattern = TemplateUtils.resolveTemplateVariables(pattern, routeContext);
174
+ const regex = new RegExp(`^${resolvedPattern}$`);
175
+ return regex.test(origin);
176
+ }
177
+ return allowedOrigin === origin;
178
+ });
179
+
180
+ if (!isAllowed) {
181
+ this.logger.warn(`WebSocket origin ${origin} not allowed for route: ${route.name || 'unnamed'}`);
182
+ wsIncoming.close(1008, 'Origin not allowed');
183
+ return;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Extract target information, resolving functions if needed
189
+ let targetHost: string | string[];
190
+ let targetPort: number;
191
+
192
+ try {
193
+ // Resolve host if it's a function
194
+ if (typeof route.action.target.host === 'function') {
195
+ const resolvedHost = route.action.target.host(toBaseContext(routeContext));
196
+ targetHost = resolvedHost;
197
+ this.logger.debug(`Resolved function-based host for WebSocket: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
198
+ } else {
199
+ targetHost = route.action.target.host;
200
+ }
201
+
202
+ // Resolve port if it's a function
203
+ if (typeof route.action.target.port === 'function') {
204
+ targetPort = route.action.target.port(toBaseContext(routeContext));
205
+ this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`);
206
+ } else {
207
+ targetPort = route.action.target.port;
208
+ }
209
+
210
+ // Select a single host if an array was provided
211
+ const selectedHost = Array.isArray(targetHost)
212
+ ? targetHost[Math.floor(Math.random() * targetHost.length)]
213
+ : targetHost;
214
+
215
+ // Create a destination for the WebSocket connection
216
+ destination = {
217
+ host: selectedHost,
218
+ port: targetPort
219
+ };
220
+ } catch (err) {
221
+ this.logger.error(`Error evaluating function-based target for WebSocket: ${err}`);
222
+ wsIncoming.close(1011, 'Internal server error');
223
+ return;
224
+ }
225
+ } else {
226
+ // Fall back to legacy routing if no matching route found via modern router
227
+ const proxyConfig = this.legacyRouter.routeReq(req);
228
+
229
+ if (!proxyConfig) {
230
+ this.logger.warn(`No proxy configuration for WebSocket host: ${req.headers.host}`);
231
+ wsIncoming.close(1008, 'No proxy configuration for this host');
232
+ return;
233
+ }
234
+
235
+ // Get destination target using round-robin if multiple targets
236
+ destination = this.connectionPool.getNextTarget(
237
+ proxyConfig.destinationIps,
238
+ proxyConfig.destinationPorts[0]
239
+ );
101
240
  }
102
241
 
103
- // Get destination target using round-robin if multiple targets
104
- const destination = this.connectionPool.getNextTarget(
105
- proxyConfig.destinationIps,
106
- proxyConfig.destinationPorts[0]
107
- );
108
-
109
- // Build target URL
242
+ // Build target URL with potential path rewriting
110
243
  const protocol = (req.socket as any).encrypted ? 'wss' : 'ws';
111
- const targetUrl = `${protocol}://${destination.host}:${destination.port}${req.url}`;
112
-
244
+ let targetPath = req.url || '/';
245
+
246
+ // Apply path rewriting if configured
247
+ if (route?.action.websocket?.rewritePath) {
248
+ const originalPath = targetPath;
249
+ targetPath = TemplateUtils.resolveTemplateVariables(
250
+ route.action.websocket.rewritePath,
251
+ {...routeContext, path: targetPath}
252
+ );
253
+ this.logger.debug(`WebSocket path rewritten: ${originalPath} -> ${targetPath}`);
254
+ }
255
+
256
+ const targetUrl = `${protocol}://${destination.host}:${destination.port}${targetPath}`;
257
+
113
258
  this.logger.debug(`WebSocket connection from ${req.socket.remoteAddress} to ${targetUrl}`);
114
-
259
+
115
260
  // Create headers for outgoing WebSocket connection
116
261
  const headers: { [key: string]: string } = {};
117
-
262
+
118
263
  // Copy relevant headers from incoming request
119
264
  for (const [key, value] of Object.entries(req.headers)) {
120
- if (value && typeof value === 'string' &&
121
- key.toLowerCase() !== 'connection' &&
265
+ if (value && typeof value === 'string' &&
266
+ key.toLowerCase() !== 'connection' &&
122
267
  key.toLowerCase() !== 'upgrade' &&
123
268
  key.toLowerCase() !== 'sec-websocket-key' &&
124
269
  key.toLowerCase() !== 'sec-websocket-version') {
125
270
  headers[key] = value;
126
271
  }
127
272
  }
128
-
129
- // Override host header if needed
130
- if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
131
- headers['host'] = `${destination.host}:${destination.port}`;
273
+
274
+ // Always rewrite host header for WebSockets for consistency
275
+ headers['host'] = `${destination.host}:${destination.port}`;
276
+
277
+ // Add custom headers from route configuration
278
+ if (route?.action.websocket?.customHeaders) {
279
+ for (const [key, value] of Object.entries(route.action.websocket.customHeaders)) {
280
+ // Skip if header already exists and we're not overriding
281
+ if (headers[key.toLowerCase()] && !value.startsWith('!')) {
282
+ continue;
283
+ }
284
+
285
+ // Handle special delete directive (!delete)
286
+ if (value === '!delete') {
287
+ delete headers[key.toLowerCase()];
288
+ continue;
289
+ }
290
+
291
+ // Handle forced override (!value)
292
+ let finalValue: string;
293
+ if (value.startsWith('!') && value !== '!delete') {
294
+ // Keep the ! but resolve any templates in the rest
295
+ const templateValue = value.substring(1);
296
+ finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, routeContext);
297
+ } else {
298
+ // Resolve templates in the entire value
299
+ finalValue = TemplateUtils.resolveTemplateVariables(value, routeContext);
300
+ }
301
+
302
+ // Set the header
303
+ headers[key.toLowerCase()] = finalValue;
304
+ }
132
305
  }
133
306
 
134
- // Create outgoing WebSocket connection
135
- const wsOutgoing = new plugins.wsDefault(targetUrl, {
307
+ // Create WebSocket connection options
308
+ const wsOptions: any = {
136
309
  headers: headers,
137
310
  followRedirects: true
138
- });
311
+ };
312
+
313
+ // Add subprotocols if configured
314
+ if (route?.action.websocket?.subprotocols && route.action.websocket.subprotocols.length > 0) {
315
+ wsOptions.protocols = route.action.websocket.subprotocols;
316
+ } else if (req.headers['sec-websocket-protocol']) {
317
+ // Pass through client requested protocols
318
+ wsOptions.protocols = req.headers['sec-websocket-protocol'].split(',').map(p => p.trim());
319
+ }
320
+
321
+ // Create outgoing WebSocket connection
322
+ const wsOutgoing = new plugins.wsDefault(targetUrl, wsOptions);
139
323
 
140
324
  // Handle connection errors
141
325
  wsOutgoing.on('error', (err) => {
@@ -147,35 +331,94 @@ export class WebSocketHandler {
147
331
 
148
332
  // Handle outgoing connection open
149
333
  wsOutgoing.on('open', () => {
334
+ // Set up custom ping interval if configured
335
+ let pingInterval: NodeJS.Timeout | null = null;
336
+ if (route?.action.websocket?.pingInterval && route.action.websocket.pingInterval > 0) {
337
+ pingInterval = setInterval(() => {
338
+ if (wsIncoming.readyState === wsIncoming.OPEN) {
339
+ wsIncoming.ping();
340
+ this.logger.debug(`Sent WebSocket ping to client for route: ${route.name || 'unnamed'}`);
341
+ }
342
+ }, route.action.websocket.pingInterval);
343
+
344
+ // Don't keep process alive just for pings
345
+ if (pingInterval.unref) pingInterval.unref();
346
+ }
347
+
348
+ // Set up custom ping timeout if configured
349
+ let pingTimeout: NodeJS.Timeout | null = null;
350
+ const pingTimeoutMs = route?.action.websocket?.pingTimeout || 60000; // Default 60s
351
+
352
+ // Define timeout function for cleaner code
353
+ const resetPingTimeout = () => {
354
+ if (pingTimeout) clearTimeout(pingTimeout);
355
+ pingTimeout = setTimeout(() => {
356
+ this.logger.debug(`WebSocket ping timeout for client connection on route: ${route?.name || 'unnamed'}`);
357
+ wsIncoming.terminate();
358
+ }, pingTimeoutMs);
359
+
360
+ // Don't keep process alive just for timeouts
361
+ if (pingTimeout.unref) pingTimeout.unref();
362
+ };
363
+
364
+ // Reset timeout on pong
365
+ wsIncoming.on('pong', () => {
366
+ wsIncoming.isAlive = true;
367
+ wsIncoming.lastPong = Date.now();
368
+ resetPingTimeout();
369
+ });
370
+
371
+ // Initial ping timeout
372
+ resetPingTimeout();
373
+
374
+ // Handle potential message size limits
375
+ const maxSize = route?.action.websocket?.maxPayloadSize || 0;
376
+
150
377
  // Forward incoming messages to outgoing connection
151
378
  wsIncoming.on('message', (data, isBinary) => {
152
379
  if (wsOutgoing.readyState === wsOutgoing.OPEN) {
380
+ // Check message size if limit is set
381
+ const messageSize = getMessageSize(data);
382
+ if (maxSize > 0 && messageSize > maxSize) {
383
+ this.logger.warn(`WebSocket message exceeds max size (${messageSize} > ${maxSize})`);
384
+ wsIncoming.close(1009, 'Message too big');
385
+ return;
386
+ }
387
+
153
388
  wsOutgoing.send(data, { binary: isBinary });
154
389
  }
155
390
  });
156
-
391
+
157
392
  // Forward outgoing messages to incoming connection
158
393
  wsOutgoing.on('message', (data, isBinary) => {
159
394
  if (wsIncoming.readyState === wsIncoming.OPEN) {
160
395
  wsIncoming.send(data, { binary: isBinary });
161
396
  }
162
397
  });
163
-
398
+
164
399
  // Handle closing of connections
165
400
  wsIncoming.on('close', (code, reason) => {
166
401
  this.logger.debug(`WebSocket client connection closed: ${code} ${reason}`);
167
402
  if (wsOutgoing.readyState === wsOutgoing.OPEN) {
168
403
  wsOutgoing.close(code, reason);
169
404
  }
405
+
406
+ // Clean up timers
407
+ if (pingInterval) clearInterval(pingInterval);
408
+ if (pingTimeout) clearTimeout(pingTimeout);
170
409
  });
171
-
410
+
172
411
  wsOutgoing.on('close', (code, reason) => {
173
412
  this.logger.debug(`WebSocket target connection closed: ${code} ${reason}`);
174
413
  if (wsIncoming.readyState === wsIncoming.OPEN) {
175
414
  wsIncoming.close(code, reason);
176
415
  }
416
+
417
+ // Clean up timers
418
+ if (pingInterval) clearInterval(pingInterval);
419
+ if (pingTimeout) clearTimeout(pingTimeout);
177
420
  });
178
-
421
+
179
422
  this.logger.debug(`WebSocket connection established: ${req.headers.host} -> ${destination.host}:${destination.port}`);
180
423
  });
181
424