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