@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,878 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import '../../core/models/socket-augmentation.js';
3
- import {
4
- type IHttpProxyOptions,
5
- type ILogger,
6
- createLogger,
7
- } from './models/types.js';
8
- import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
9
- import { ConnectionPool } from './connection-pool.js';
10
- import { ContextCreator } from './context-creator.js';
11
- import { HttpRequestHandler } from './http-request-handler.js';
12
- import { Http2RequestHandler } from './http2-request-handler.js';
13
- import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
14
- import type { IRouteContext, IHttpRouteContext } from '../../core/models/route-context.js';
15
- import { toBaseContext } from '../../core/models/route-context.js';
16
- import { TemplateUtils } from '../../core/utils/template-utils.js';
17
- import { SecurityManager } from './security-manager.js';
18
-
19
- /**
20
- * Interface for tracking metrics
21
- */
22
- export interface IMetricsTracker {
23
- incrementRequestsServed(): void;
24
- incrementFailedRequests(): void;
25
- }
26
-
27
- // Backward compatibility
28
- export type MetricsTracker = IMetricsTracker;
29
-
30
- /**
31
- * Handles HTTP request processing and proxying
32
- */
33
- export class RequestHandler {
34
- private defaultHeaders: { [key: string]: string } = {};
35
- private logger: ILogger;
36
- private metricsTracker: IMetricsTracker | null = null;
37
- // HTTP/2 client sessions for backend proxying
38
- private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map();
39
-
40
- // Context creator for route contexts
41
- private contextCreator: ContextCreator = new ContextCreator();
42
-
43
- // Security manager for IP filtering, rate limiting, etc.
44
- public securityManager: SecurityManager;
45
-
46
- // Rate limit cleanup interval
47
- private rateLimitCleanupInterval: NodeJS.Timeout | null = null;
48
-
49
- constructor(
50
- private options: IHttpProxyOptions,
51
- private connectionPool: ConnectionPool,
52
- private routeManager?: RouteManager,
53
- private functionCache?: any, // FunctionCache - using any to avoid circular dependency
54
- private router?: any // HttpRouter - using any to avoid circular dependency
55
- ) {
56
- this.logger = createLogger(options.logLevel || 'info');
57
- this.securityManager = new SecurityManager(this.logger);
58
-
59
- // Schedule rate limit cleanup every minute
60
- this.rateLimitCleanupInterval = setInterval(() => {
61
- this.securityManager.cleanupExpiredRateLimits();
62
- }, 60000);
63
-
64
- // Make sure the interval doesn't keep the process alive
65
- if (this.rateLimitCleanupInterval.unref) {
66
- this.rateLimitCleanupInterval.unref();
67
- }
68
- }
69
-
70
- /**
71
- * Set the route manager instance
72
- */
73
- public setRouteManager(routeManager: RouteManager): void {
74
- this.routeManager = routeManager;
75
- }
76
-
77
- /**
78
- * Set the metrics tracker instance
79
- */
80
- public setMetricsTracker(tracker: IMetricsTracker): void {
81
- this.metricsTracker = tracker;
82
- }
83
-
84
- /**
85
- * Set default headers to be included in all responses
86
- */
87
- public setDefaultHeaders(headers: { [key: string]: string }): void {
88
- this.defaultHeaders = {
89
- ...this.defaultHeaders,
90
- ...headers
91
- };
92
- this.logger.info('Updated default response headers');
93
- }
94
-
95
- /**
96
- * Get all default headers
97
- */
98
- public getDefaultHeaders(): { [key: string]: string } {
99
- return { ...this.defaultHeaders };
100
- }
101
-
102
- /**
103
- * Select the appropriate target from the targets array based on sub-matching criteria
104
- */
105
- private selectTarget(
106
- targets: IRouteTarget[],
107
- context: {
108
- port: number;
109
- path?: string;
110
- headers?: Record<string, string>;
111
- method?: string;
112
- }
113
- ): IRouteTarget | null {
114
- // Sort targets by priority (higher first)
115
- const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
116
-
117
- // Find the first matching target
118
- for (const target of sortedTargets) {
119
- if (!target.match) {
120
- // No match criteria means this is a default/fallback target
121
- return target;
122
- }
123
-
124
- // Check port match
125
- if (target.match.ports && !target.match.ports.includes(context.port)) {
126
- continue;
127
- }
128
-
129
- // Check path match (supports wildcards)
130
- if (target.match.path && context.path) {
131
- const pathPattern = target.match.path.replace(/\*/g, '.*');
132
- const pathRegex = new RegExp(`^${pathPattern}$`);
133
- if (!pathRegex.test(context.path)) {
134
- continue;
135
- }
136
- }
137
-
138
- // Check method match
139
- if (target.match.method && context.method && !target.match.method.includes(context.method)) {
140
- continue;
141
- }
142
-
143
- // Check headers match
144
- if (target.match.headers && context.headers) {
145
- let headersMatch = true;
146
- for (const [key, pattern] of Object.entries(target.match.headers)) {
147
- const headerValue = context.headers[key.toLowerCase()];
148
- if (!headerValue) {
149
- headersMatch = false;
150
- break;
151
- }
152
-
153
- if (pattern instanceof RegExp) {
154
- if (!pattern.test(headerValue)) {
155
- headersMatch = false;
156
- break;
157
- }
158
- } else if (headerValue !== pattern) {
159
- headersMatch = false;
160
- break;
161
- }
162
- }
163
- if (!headersMatch) {
164
- continue;
165
- }
166
- }
167
-
168
- // All criteria matched
169
- return target;
170
- }
171
-
172
- // No matching target found, return the first target without match criteria (default)
173
- return sortedTargets.find(t => !t.match) || null;
174
- }
175
-
176
- /**
177
- * Apply CORS headers to response if configured
178
- * Implements Phase 5.5: Context-aware CORS handling
179
- *
180
- * @param res The server response to apply headers to
181
- * @param req The incoming request
182
- * @param route Optional route config with CORS settings
183
- */
184
- private applyCorsHeaders(
185
- res: plugins.http.ServerResponse,
186
- req: plugins.http.IncomingMessage,
187
- route?: IRouteConfig
188
- ): void {
189
- // Use route-specific CORS config if available, otherwise use global config
190
- let corsConfig: any = null;
191
-
192
- // Route CORS config takes precedence if enabled
193
- if (route?.headers?.cors?.enabled) {
194
- corsConfig = route.headers.cors;
195
- this.logger.debug(`Using route-specific CORS config for ${route.name || 'unnamed route'}`);
196
- }
197
- // Fall back to global CORS config if available
198
- else if (this.options.cors) {
199
- corsConfig = this.options.cors;
200
- this.logger.debug('Using global CORS config');
201
- }
202
-
203
- // If no CORS config available, skip
204
- if (!corsConfig) {
205
- return;
206
- }
207
-
208
- // Get origin from request
209
- const origin = req.headers.origin;
210
-
211
- // Apply Allow-Origin (with dynamic validation if needed)
212
- if (corsConfig.allowOrigin) {
213
- // Handle multiple origins in array format
214
- if (Array.isArray(corsConfig.allowOrigin)) {
215
- if (origin && corsConfig.allowOrigin.includes(origin)) {
216
- // Match found, set specific origin
217
- res.setHeader('Access-Control-Allow-Origin', origin);
218
- res.setHeader('Vary', 'Origin'); // Important for caching
219
- } else if (corsConfig.allowOrigin.includes('*')) {
220
- // Wildcard match
221
- res.setHeader('Access-Control-Allow-Origin', '*');
222
- }
223
- }
224
- // Handle single origin or wildcard
225
- else if (corsConfig.allowOrigin === '*') {
226
- res.setHeader('Access-Control-Allow-Origin', '*');
227
- }
228
- // Match single origin against request
229
- else if (origin && corsConfig.allowOrigin === origin) {
230
- res.setHeader('Access-Control-Allow-Origin', origin);
231
- res.setHeader('Vary', 'Origin');
232
- }
233
- // Use template variables if present
234
- else if (origin && corsConfig.allowOrigin.includes('{')) {
235
- const resolvedOrigin = TemplateUtils.resolveTemplateVariables(
236
- corsConfig.allowOrigin,
237
- { domain: req.headers.host } as any
238
- );
239
- if (resolvedOrigin === origin || resolvedOrigin === '*') {
240
- res.setHeader('Access-Control-Allow-Origin', origin);
241
- res.setHeader('Vary', 'Origin');
242
- }
243
- }
244
- }
245
-
246
- // Apply other CORS headers
247
- if (corsConfig.allowMethods) {
248
- res.setHeader('Access-Control-Allow-Methods', corsConfig.allowMethods);
249
- }
250
-
251
- if (corsConfig.allowHeaders) {
252
- res.setHeader('Access-Control-Allow-Headers', corsConfig.allowHeaders);
253
- }
254
-
255
- if (corsConfig.allowCredentials) {
256
- res.setHeader('Access-Control-Allow-Credentials', 'true');
257
- }
258
-
259
- if (corsConfig.exposeHeaders) {
260
- res.setHeader('Access-Control-Expose-Headers', corsConfig.exposeHeaders);
261
- }
262
-
263
- if (corsConfig.maxAge) {
264
- res.setHeader('Access-Control-Max-Age', corsConfig.maxAge.toString());
265
- }
266
-
267
- // Handle CORS preflight requests if enabled (default: true)
268
- if (req.method === 'OPTIONS' && corsConfig.preflight !== false) {
269
- res.statusCode = 204; // No content
270
- res.end();
271
- return;
272
- }
273
- }
274
-
275
- // First implementation of applyRouteHeaderModifications moved to the second implementation below
276
-
277
- /**
278
- * Apply default headers to response
279
- */
280
- private applyDefaultHeaders(res: plugins.http.ServerResponse): void {
281
- // Apply default headers
282
- for (const [key, value] of Object.entries(this.defaultHeaders)) {
283
- if (!res.hasHeader(key)) {
284
- res.setHeader(key, value);
285
- }
286
- }
287
-
288
- // Add server identifier if not already set
289
- if (!res.hasHeader('Server')) {
290
- res.setHeader('Server', 'NetworkProxy');
291
- }
292
- }
293
-
294
- /**
295
- * Apply URL rewriting based on route configuration
296
- * Implements Phase 5.2: URL rewriting using route context
297
- *
298
- * @param req The request with the URL to rewrite
299
- * @param route The route configuration containing rewrite rules
300
- * @param routeContext Context for template variable resolution
301
- * @returns True if URL was rewritten, false otherwise
302
- */
303
- private applyUrlRewriting(
304
- req: plugins.http.IncomingMessage,
305
- route: IRouteConfig,
306
- routeContext: IHttpRouteContext
307
- ): boolean {
308
- // Check if route has URL rewriting configuration
309
- if (!route.action.advanced?.urlRewrite) {
310
- return false;
311
- }
312
-
313
- const rewriteConfig = route.action.advanced.urlRewrite;
314
-
315
- // Store original URL for logging
316
- const originalUrl = req.url;
317
-
318
- if (rewriteConfig.pattern && rewriteConfig.target) {
319
- try {
320
- // Create a RegExp from the pattern
321
- const regex = new RegExp(rewriteConfig.pattern, rewriteConfig.flags || '');
322
-
323
- // Apply rewriting with template variable resolution
324
- let target = rewriteConfig.target;
325
-
326
- // Replace template variables in target with values from context
327
- target = TemplateUtils.resolveTemplateVariables(target, routeContext);
328
-
329
- // If onlyRewritePath is set, split URL into path and query parts
330
- if (rewriteConfig.onlyRewritePath && req.url) {
331
- const [path, query] = req.url.split('?');
332
- const rewrittenPath = path.replace(regex, target);
333
- req.url = query ? `${rewrittenPath}?${query}` : rewrittenPath;
334
- } else {
335
- // Perform the replacement on the entire URL
336
- req.url = req.url?.replace(regex, target);
337
- }
338
-
339
- this.logger.debug(`URL rewritten: ${originalUrl} -> ${req.url}`);
340
- return true;
341
- } catch (err) {
342
- this.logger.error(`Error in URL rewriting: ${err}`);
343
- return false;
344
- }
345
- }
346
-
347
- return false;
348
- }
349
-
350
- /**
351
- * Apply header modifications from route configuration
352
- * Implements Phase 5.1: Route-based header manipulation
353
- */
354
- private applyRouteHeaderModifications(
355
- route: IRouteConfig,
356
- req: plugins.http.IncomingMessage,
357
- res: plugins.http.ServerResponse
358
- ): void {
359
- // Check if route has header modifications
360
- if (!route.headers) {
361
- return;
362
- }
363
-
364
- // Apply request header modifications (these will be sent to the backend)
365
- if (route.headers.request && req.headers) {
366
- for (const [key, value] of Object.entries(route.headers.request)) {
367
- // Skip if header already exists and we're not overriding
368
- if (req.headers[key.toLowerCase()] && !value.startsWith('!')) {
369
- continue;
370
- }
371
-
372
- // Handle special delete directive (!delete)
373
- if (value === '!delete') {
374
- delete req.headers[key.toLowerCase()];
375
- this.logger.debug(`Deleted request header: ${key}`);
376
- continue;
377
- }
378
-
379
- // Handle forced override (!value)
380
- let finalValue: string;
381
- if (value.startsWith('!') && value !== '!delete') {
382
- // Keep the ! but resolve any templates in the rest
383
- const templateValue = value.substring(1);
384
- finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {} as IRouteContext);
385
- } else {
386
- // Resolve templates in the entire value
387
- finalValue = TemplateUtils.resolveTemplateVariables(value, {} as IRouteContext);
388
- }
389
-
390
- // Set the header
391
- req.headers[key.toLowerCase()] = finalValue;
392
- this.logger.debug(`Modified request header: ${key}=${finalValue}`);
393
- }
394
- }
395
-
396
- // Apply response header modifications (these will be stored for later use)
397
- if (route.headers.response) {
398
- for (const [key, value] of Object.entries(route.headers.response)) {
399
- // Skip if header already exists and we're not overriding
400
- if (res.hasHeader(key) && !value.startsWith('!')) {
401
- continue;
402
- }
403
-
404
- // Handle special delete directive (!delete)
405
- if (value === '!delete') {
406
- res.removeHeader(key);
407
- this.logger.debug(`Deleted response header: ${key}`);
408
- continue;
409
- }
410
-
411
- // Handle forced override (!value)
412
- let finalValue: string;
413
- if (value.startsWith('!') && value !== '!delete') {
414
- // Keep the ! but resolve any templates in the rest
415
- const templateValue = value.substring(1);
416
- finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {} as IRouteContext);
417
- } else {
418
- // Resolve templates in the entire value
419
- finalValue = TemplateUtils.resolveTemplateVariables(value, {} as IRouteContext);
420
- }
421
-
422
- // Set the header
423
- res.setHeader(key, finalValue);
424
- this.logger.debug(`Modified response header: ${key}=${finalValue}`);
425
- }
426
- }
427
- }
428
-
429
- /**
430
- * Handle an HTTP request
431
- */
432
- public async handleRequest(
433
- req: plugins.http.IncomingMessage,
434
- res: plugins.http.ServerResponse
435
- ): Promise<void> {
436
- // Record start time for logging
437
- const startTime = Date.now();
438
-
439
- // Get route before applying CORS (we might need its settings)
440
- // Try to find a matching route using RouteManager
441
- let matchingRoute: IRouteConfig | null = null;
442
- if (this.routeManager) {
443
- try {
444
- // Create a connection ID for this request
445
- const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
446
-
447
- // Create route context for function-based targets
448
- const routeContext = this.contextCreator.createHttpRouteContext(req, {
449
- connectionId,
450
- clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0',
451
- serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0',
452
- tlsVersion: req.socket.getTLSVersion?.() || undefined
453
- });
454
-
455
- const matchResult = this.routeManager.findMatchingRoute(toBaseContext(routeContext));
456
- matchingRoute = matchResult?.route || null;
457
- } catch (err) {
458
- this.logger.error('Error finding matching route', err);
459
- }
460
- }
461
-
462
- // Apply CORS headers with route-specific settings if available
463
- this.applyCorsHeaders(res, req, matchingRoute);
464
-
465
- // If this is an OPTIONS request, the response has already been ended in applyCorsHeaders
466
- // so we should return early to avoid trying to set more headers
467
- if (req.method === 'OPTIONS') {
468
- // Increment metrics for OPTIONS requests too
469
- if (this.metricsTracker) {
470
- this.metricsTracker.incrementRequestsServed();
471
- }
472
- return;
473
- }
474
-
475
- // Apply default headers
476
- this.applyDefaultHeaders(res);
477
-
478
- // We already have the connection ID and routeContext from CORS handling
479
- const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
480
-
481
- // Create route context for function-based targets (if we don't already have one)
482
- const routeContext = this.contextCreator.createHttpRouteContext(req, {
483
- connectionId,
484
- clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0',
485
- serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0',
486
- tlsVersion: req.socket.getTLSVersion?.() || undefined
487
- });
488
-
489
- // Check security restrictions if we have a matching route
490
- if (matchingRoute) {
491
- // Check IP filtering and rate limiting
492
- if (!this.securityManager.isAllowed(matchingRoute, routeContext)) {
493
- this.logger.warn(`Access denied for ${routeContext.clientIp} to ${matchingRoute.name || 'unnamed'}`);
494
- res.statusCode = 403;
495
- res.end('Forbidden: Access denied by security policy');
496
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
497
- return;
498
- }
499
-
500
- // Check basic auth
501
- if (matchingRoute.security?.basicAuth?.enabled) {
502
- const authHeader = req.headers.authorization;
503
- if (!authHeader || !authHeader.startsWith('Basic ')) {
504
- // No auth header provided - send 401 with WWW-Authenticate header
505
- res.statusCode = 401;
506
- const realm = matchingRoute.security.basicAuth.realm || 'Protected Area';
507
- res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`);
508
- res.end('Authentication Required');
509
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
510
- return;
511
- }
512
-
513
- // Verify credentials
514
- try {
515
- const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
516
- const [username, password] = credentials.split(':');
517
-
518
- if (!this.securityManager.checkBasicAuth(matchingRoute, username, password)) {
519
- res.statusCode = 401;
520
- const realm = matchingRoute.security.basicAuth.realm || 'Protected Area';
521
- res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`);
522
- res.end('Invalid Credentials');
523
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
524
- return;
525
- }
526
- } catch (err) {
527
- this.logger.error(`Error verifying basic auth: ${err}`);
528
- res.statusCode = 401;
529
- res.end('Authentication Error');
530
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
531
- return;
532
- }
533
- }
534
-
535
- // Check JWT auth
536
- if (matchingRoute.security?.jwtAuth?.enabled) {
537
- const authHeader = req.headers.authorization;
538
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
539
- // No auth header provided - send 401
540
- res.statusCode = 401;
541
- res.end('Authentication Required: JWT token missing');
542
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
543
- return;
544
- }
545
-
546
- // Verify token
547
- const token = authHeader.substring(7);
548
- if (!this.securityManager.verifyJwtToken(matchingRoute, token)) {
549
- res.statusCode = 401;
550
- res.end('Invalid or Expired JWT');
551
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
552
- return;
553
- }
554
- }
555
- }
556
-
557
- // If we found a matching route with forward action, select appropriate target
558
- if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
559
- this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`);
560
-
561
- // Select the appropriate target from the targets array
562
- const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
563
- port: routeContext.port,
564
- path: routeContext.path,
565
- headers: routeContext.headers,
566
- method: routeContext.method
567
- });
568
-
569
- if (!selectedTarget) {
570
- this.logger.error(`No matching target found for route ${matchingRoute.name}`);
571
- req.socket.end();
572
- return;
573
- }
574
-
575
- // Extract target information, resolving functions if needed
576
- let targetHost: string | string[];
577
- let targetPort: number;
578
-
579
- try {
580
- // Check function cache for host and resolve or use cached value
581
- if (typeof selectedTarget.host === 'function') {
582
- // Generate a function ID for caching (use route name or ID if available)
583
- const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
584
-
585
- // Check if we have a cached result
586
- if (this.functionCache) {
587
- const cachedHost = this.functionCache.getCachedHost(routeContext, functionId);
588
- if (cachedHost !== undefined) {
589
- targetHost = cachedHost;
590
- this.logger.debug(`Using cached host value for ${functionId}`);
591
- } else {
592
- // Resolve the function and cache the result
593
- const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
594
- targetHost = resolvedHost;
595
-
596
- // Cache the result
597
- this.functionCache.cacheHost(routeContext, functionId, resolvedHost);
598
- this.logger.debug(`Resolved and cached function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
599
- }
600
- } else {
601
- // No cache available, just resolve
602
- const resolvedHost = selectedTarget.host(routeContext);
603
- targetHost = resolvedHost;
604
- this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
605
- }
606
- } else {
607
- targetHost = selectedTarget.host;
608
- }
609
-
610
- // Check function cache for port and resolve or use cached value
611
- if (typeof selectedTarget.port === 'function') {
612
- // Generate a function ID for caching
613
- const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
614
-
615
- // Check if we have a cached result
616
- if (this.functionCache) {
617
- const cachedPort = this.functionCache.getCachedPort(routeContext, functionId);
618
- if (cachedPort !== undefined) {
619
- targetPort = cachedPort;
620
- this.logger.debug(`Using cached port value for ${functionId}`);
621
- } else {
622
- // Resolve the function and cache the result
623
- const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
624
- targetPort = resolvedPort;
625
-
626
- // Cache the result
627
- this.functionCache.cachePort(routeContext, functionId, resolvedPort);
628
- this.logger.debug(`Resolved and cached function-based port to: ${resolvedPort}`);
629
- }
630
- } else {
631
- // No cache available, just resolve
632
- const resolvedPort = selectedTarget.port(routeContext);
633
- targetPort = resolvedPort;
634
- this.logger.debug(`Resolved function-based port to: ${resolvedPort}`);
635
- }
636
- } else {
637
- targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
638
- }
639
-
640
- // Select a single host if an array was provided
641
- const selectedHost = Array.isArray(targetHost)
642
- ? targetHost[Math.floor(Math.random() * targetHost.length)]
643
- : targetHost;
644
-
645
- // Create a destination for the connection pool
646
- const destination = {
647
- host: selectedHost,
648
- port: targetPort
649
- };
650
-
651
- // Apply URL rewriting if configured
652
- this.applyUrlRewriting(req, matchingRoute, routeContext);
653
-
654
- // Apply header modifications if configured
655
- this.applyRouteHeaderModifications(matchingRoute, req, res);
656
-
657
- // Continue with handling using the resolved destination
658
- HttpRequestHandler.handleHttpRequestWithDestination(
659
- req,
660
- res,
661
- destination,
662
- routeContext,
663
- startTime,
664
- this.logger,
665
- this.metricsTracker,
666
- matchingRoute // Pass the route config for additional processing
667
- );
668
- return;
669
- } catch (err) {
670
- this.logger.error(`Error evaluating function-based target: ${err}`);
671
- res.statusCode = 500;
672
- res.end('Internal Server Error: Failed to evaluate target functions');
673
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
674
- return;
675
- }
676
- }
677
-
678
- // If no route was found, return 404
679
- this.logger.warn(`No route configuration for host: ${req.headers.host}`);
680
- res.statusCode = 404;
681
- res.end('Not Found: No route configuration for this host');
682
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
683
- }
684
-
685
- /**
686
- * Handle HTTP/2 stream requests with function-based target support
687
- */
688
- public async handleHttp2(stream: plugins.http2.ServerHttp2Stream, headers: plugins.http2.IncomingHttpHeaders): Promise<void> {
689
- const startTime = Date.now();
690
-
691
- // Create a connection ID for this HTTP/2 stream
692
- const connectionId = `http2-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
693
-
694
- // Get client IP and server IP from the socket
695
- const socket = (stream.session as any)?.socket;
696
- const clientIp = socket?.remoteAddress?.replace('::ffff:', '') || '0.0.0.0';
697
- const serverIp = socket?.localAddress?.replace('::ffff:', '') || '0.0.0.0';
698
-
699
- // Create route context for function-based targets
700
- const routeContext = this.contextCreator.createHttp2RouteContext(stream, headers, {
701
- connectionId,
702
- clientIp,
703
- serverIp
704
- });
705
-
706
- // Try to find a matching route using RouteManager
707
- let matchingRoute: IRouteConfig | null = null;
708
- if (this.routeManager) {
709
- try {
710
- const matchResult = this.routeManager.findMatchingRoute(toBaseContext(routeContext));
711
- matchingRoute = matchResult?.route || null;
712
- } catch (err) {
713
- this.logger.error('Error finding matching route for HTTP/2 request', err);
714
- }
715
- }
716
-
717
- // If we found a matching route with forward action, select appropriate target
718
- if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
719
- this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`);
720
-
721
- // Select the appropriate target from the targets array
722
- const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
723
- port: routeContext.port,
724
- path: routeContext.path,
725
- headers: routeContext.headers,
726
- method: routeContext.method
727
- });
728
-
729
- if (!selectedTarget) {
730
- this.logger.error(`No matching target found for route ${matchingRoute.name}`);
731
- stream.respond({ ':status': 502 });
732
- stream.end();
733
- return;
734
- }
735
-
736
- // Extract target information, resolving functions if needed
737
- let targetHost: string | string[];
738
- let targetPort: number;
739
-
740
- try {
741
- // Check function cache for host and resolve or use cached value
742
- if (typeof selectedTarget.host === 'function') {
743
- // Generate a function ID for caching (use route name or ID if available)
744
- const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
745
-
746
- // Check if we have a cached result
747
- if (this.functionCache) {
748
- const cachedHost = this.functionCache.getCachedHost(routeContext, functionId);
749
- if (cachedHost !== undefined) {
750
- targetHost = cachedHost;
751
- this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`);
752
- } else {
753
- // Resolve the function and cache the result
754
- const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
755
- targetHost = resolvedHost;
756
-
757
- // Cache the result
758
- this.functionCache.cacheHost(routeContext, functionId, resolvedHost);
759
- this.logger.debug(`Resolved and cached HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
760
- }
761
- } else {
762
- // No cache available, just resolve
763
- const resolvedHost = selectedTarget.host(routeContext);
764
- targetHost = resolvedHost;
765
- this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
766
- }
767
- } else {
768
- targetHost = selectedTarget.host;
769
- }
770
-
771
- // Check function cache for port and resolve or use cached value
772
- if (typeof selectedTarget.port === 'function') {
773
- // Generate a function ID for caching
774
- const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
775
-
776
- // Check if we have a cached result
777
- if (this.functionCache) {
778
- const cachedPort = this.functionCache.getCachedPort(routeContext, functionId);
779
- if (cachedPort !== undefined) {
780
- targetPort = cachedPort;
781
- this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`);
782
- } else {
783
- // Resolve the function and cache the result
784
- const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
785
- targetPort = resolvedPort;
786
-
787
- // Cache the result
788
- this.functionCache.cachePort(routeContext, functionId, resolvedPort);
789
- this.logger.debug(`Resolved and cached HTTP/2 function-based port to: ${resolvedPort}`);
790
- }
791
- } else {
792
- // No cache available, just resolve
793
- const resolvedPort = selectedTarget.port(routeContext);
794
- targetPort = resolvedPort;
795
- this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`);
796
- }
797
- } else {
798
- targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
799
- }
800
-
801
- // Select a single host if an array was provided
802
- const selectedHost = Array.isArray(targetHost)
803
- ? targetHost[Math.floor(Math.random() * targetHost.length)]
804
- : targetHost;
805
-
806
- // Create a destination for forwarding
807
- const destination = {
808
- host: selectedHost,
809
- port: targetPort
810
- };
811
-
812
- // Handle HTTP/2 stream based on backend protocol
813
- const backendProtocol = matchingRoute.action.options?.backendProtocol || this.options.backendProtocol;
814
-
815
- if (backendProtocol === 'http2') {
816
- // Forward to HTTP/2 backend
817
- return Http2RequestHandler.handleHttp2WithHttp2Destination(
818
- stream,
819
- headers,
820
- destination,
821
- routeContext,
822
- this.h2Sessions,
823
- this.logger,
824
- this.metricsTracker
825
- );
826
- } else {
827
- // Forward to HTTP/1.1 backend
828
- return Http2RequestHandler.handleHttp2WithHttp1Destination(
829
- stream,
830
- headers,
831
- destination,
832
- routeContext,
833
- this.logger,
834
- this.metricsTracker
835
- );
836
- }
837
- } catch (err) {
838
- this.logger.error(`Error evaluating function-based target for HTTP/2: ${err}`);
839
- stream.respond({ ':status': 500 });
840
- stream.end('Internal Server Error: Failed to evaluate target functions');
841
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
842
- return;
843
- }
844
- }
845
-
846
- // Fall back to legacy routing if no matching route found
847
- const method = headers[':method'] || 'GET';
848
- const path = headers[':path'] || '/';
849
-
850
- // No route was found
851
- stream.respond({ ':status': 404 });
852
- stream.end('Not Found: No route configuration for this request');
853
- if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
854
- }
855
-
856
- /**
857
- * Cleanup resources and stop intervals
858
- */
859
- public destroy(): void {
860
- if (this.rateLimitCleanupInterval) {
861
- clearInterval(this.rateLimitCleanupInterval);
862
- this.rateLimitCleanupInterval = null;
863
- }
864
-
865
- // Close all HTTP/2 sessions
866
- for (const [key, session] of this.h2Sessions) {
867
- session.close();
868
- }
869
- this.h2Sessions.clear();
870
-
871
- // Clear function cache if it has a destroy method
872
- if (this.functionCache && typeof this.functionCache.destroy === 'function') {
873
- this.functionCache.destroy();
874
- }
875
-
876
- this.logger.debug('RequestHandler destroyed');
877
- }
878
- }