import * as plugins from '../../plugins.js'; import '../../core/models/socket-augmentation.js'; import { type IHttpProxyOptions, type ILogger, createLogger, type IReverseProxyConfig, RouteManager } from './models/types.js'; import { ConnectionPool } from './connection-pool.js'; import { ProxyRouter } from '../../routing/router/index.js'; import { ContextCreator } from './context-creator.js'; import { HttpRequestHandler } from './http-request-handler.js'; import { Http2RequestHandler } from './http2-request-handler.js'; import type { IRouteConfig } from '../smart-proxy/models/route-types.js'; import type { IRouteContext, IHttpRouteContext } from '../../core/models/route-context.js'; import { toBaseContext } from '../../core/models/route-context.js'; import { TemplateUtils } from '../../core/utils/template-utils.js'; import { SecurityManager } from './security-manager.js'; /** * Interface for tracking metrics */ export interface IMetricsTracker { incrementRequestsServed(): void; incrementFailedRequests(): void; } // Backward compatibility export type MetricsTracker = IMetricsTracker; /** * Handles HTTP request processing and proxying */ export class RequestHandler { private defaultHeaders: { [key: string]: string } = {}; private logger: ILogger; private metricsTracker: IMetricsTracker | null = null; // HTTP/2 client sessions for backend proxying private h2Sessions: Map = new Map(); // Context creator for route contexts private contextCreator: ContextCreator = new ContextCreator(); // Security manager for IP filtering, rate limiting, etc. public securityManager: SecurityManager; constructor( private options: IHttpProxyOptions, private connectionPool: ConnectionPool, private legacyRouter: ProxyRouter, // Legacy router for backward compatibility private routeManager?: RouteManager, private functionCache?: any, // FunctionCache - using any to avoid circular dependency private router?: any // RouteRouter - using any to avoid circular dependency ) { this.logger = createLogger(options.logLevel || 'info'); this.securityManager = new SecurityManager(this.logger); // Schedule rate limit cleanup every minute setInterval(() => { this.securityManager.cleanupExpiredRateLimits(); }, 60000); } /** * Set the route manager instance */ public setRouteManager(routeManager: RouteManager): void { this.routeManager = routeManager; } /** * Set the metrics tracker instance */ public setMetricsTracker(tracker: IMetricsTracker): void { this.metricsTracker = tracker; } /** * Set default headers to be included in all responses */ public setDefaultHeaders(headers: { [key: string]: string }): void { this.defaultHeaders = { ...this.defaultHeaders, ...headers }; this.logger.info('Updated default response headers'); } /** * Get all default headers */ public getDefaultHeaders(): { [key: string]: string } { return { ...this.defaultHeaders }; } /** * Apply CORS headers to response if configured * Implements Phase 5.5: Context-aware CORS handling * * @param res The server response to apply headers to * @param req The incoming request * @param route Optional route config with CORS settings */ private applyCorsHeaders( res: plugins.http.ServerResponse, req: plugins.http.IncomingMessage, route?: IRouteConfig ): void { // Use route-specific CORS config if available, otherwise use global config let corsConfig: any = null; // Route CORS config takes precedence if enabled if (route?.headers?.cors?.enabled) { corsConfig = route.headers.cors; this.logger.debug(`Using route-specific CORS config for ${route.name || 'unnamed route'}`); } // Fall back to global CORS config if available else if (this.options.cors) { corsConfig = this.options.cors; this.logger.debug('Using global CORS config'); } // If no CORS config available, skip if (!corsConfig) { return; } // Get origin from request const origin = req.headers.origin; // Apply Allow-Origin (with dynamic validation if needed) if (corsConfig.allowOrigin) { // Handle multiple origins in array format if (Array.isArray(corsConfig.allowOrigin)) { if (origin && corsConfig.allowOrigin.includes(origin)) { // Match found, set specific origin res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); // Important for caching } else if (corsConfig.allowOrigin.includes('*')) { // Wildcard match res.setHeader('Access-Control-Allow-Origin', '*'); } } // Handle single origin or wildcard else if (corsConfig.allowOrigin === '*') { res.setHeader('Access-Control-Allow-Origin', '*'); } // Match single origin against request else if (origin && corsConfig.allowOrigin === origin) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); } // Use template variables if present else if (origin && corsConfig.allowOrigin.includes('{')) { const resolvedOrigin = TemplateUtils.resolveTemplateVariables( corsConfig.allowOrigin, { domain: req.headers.host } as any ); if (resolvedOrigin === origin || resolvedOrigin === '*') { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); } } } // Apply other CORS headers if (corsConfig.allowMethods) { res.setHeader('Access-Control-Allow-Methods', corsConfig.allowMethods); } if (corsConfig.allowHeaders) { res.setHeader('Access-Control-Allow-Headers', corsConfig.allowHeaders); } if (corsConfig.allowCredentials) { res.setHeader('Access-Control-Allow-Credentials', 'true'); } if (corsConfig.exposeHeaders) { res.setHeader('Access-Control-Expose-Headers', corsConfig.exposeHeaders); } if (corsConfig.maxAge) { res.setHeader('Access-Control-Max-Age', corsConfig.maxAge.toString()); } // Handle CORS preflight requests if enabled (default: true) if (req.method === 'OPTIONS' && corsConfig.preflight !== false) { res.statusCode = 204; // No content res.end(); return; } } // First implementation of applyRouteHeaderModifications moved to the second implementation below /** * Apply default headers to response */ private applyDefaultHeaders(res: plugins.http.ServerResponse): void { // Apply default headers for (const [key, value] of Object.entries(this.defaultHeaders)) { if (!res.hasHeader(key)) { res.setHeader(key, value); } } // Add server identifier if not already set if (!res.hasHeader('Server')) { res.setHeader('Server', 'NetworkProxy'); } } /** * Apply URL rewriting based on route configuration * Implements Phase 5.2: URL rewriting using route context * * @param req The request with the URL to rewrite * @param route The route configuration containing rewrite rules * @param routeContext Context for template variable resolution * @returns True if URL was rewritten, false otherwise */ private applyUrlRewriting( req: plugins.http.IncomingMessage, route: IRouteConfig, routeContext: IHttpRouteContext ): boolean { // Check if route has URL rewriting configuration if (!route.action.advanced?.urlRewrite) { return false; } const rewriteConfig = route.action.advanced.urlRewrite; // Store original URL for logging const originalUrl = req.url; if (rewriteConfig.pattern && rewriteConfig.target) { try { // Create a RegExp from the pattern const regex = new RegExp(rewriteConfig.pattern, rewriteConfig.flags || ''); // Apply rewriting with template variable resolution let target = rewriteConfig.target; // Replace template variables in target with values from context target = TemplateUtils.resolveTemplateVariables(target, routeContext); // If onlyRewritePath is set, split URL into path and query parts if (rewriteConfig.onlyRewritePath && req.url) { const [path, query] = req.url.split('?'); const rewrittenPath = path.replace(regex, target); req.url = query ? `${rewrittenPath}?${query}` : rewrittenPath; } else { // Perform the replacement on the entire URL req.url = req.url?.replace(regex, target); } this.logger.debug(`URL rewritten: ${originalUrl} -> ${req.url}`); return true; } catch (err) { this.logger.error(`Error in URL rewriting: ${err}`); return false; } } return false; } /** * Apply header modifications from route configuration * Implements Phase 5.1: Route-based header manipulation */ private applyRouteHeaderModifications( route: IRouteConfig, req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse ): void { // Check if route has header modifications if (!route.headers) { return; } // Apply request header modifications (these will be sent to the backend) if (route.headers.request && req.headers) { for (const [key, value] of Object.entries(route.headers.request)) { // Skip if header already exists and we're not overriding if (req.headers[key.toLowerCase()] && !value.startsWith('!')) { continue; } // Handle special delete directive (!delete) if (value === '!delete') { delete req.headers[key.toLowerCase()]; this.logger.debug(`Deleted request header: ${key}`); continue; } // Handle forced override (!value) let finalValue: string; if (value.startsWith('!') && value !== '!delete') { // Keep the ! but resolve any templates in the rest const templateValue = value.substring(1); finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {} as IRouteContext); } else { // Resolve templates in the entire value finalValue = TemplateUtils.resolveTemplateVariables(value, {} as IRouteContext); } // Set the header req.headers[key.toLowerCase()] = finalValue; this.logger.debug(`Modified request header: ${key}=${finalValue}`); } } // Apply response header modifications (these will be stored for later use) if (route.headers.response) { for (const [key, value] of Object.entries(route.headers.response)) { // Skip if header already exists and we're not overriding if (res.hasHeader(key) && !value.startsWith('!')) { continue; } // Handle special delete directive (!delete) if (value === '!delete') { res.removeHeader(key); this.logger.debug(`Deleted response header: ${key}`); continue; } // Handle forced override (!value) let finalValue: string; if (value.startsWith('!') && value !== '!delete') { // Keep the ! but resolve any templates in the rest const templateValue = value.substring(1); finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {} as IRouteContext); } else { // Resolve templates in the entire value finalValue = TemplateUtils.resolveTemplateVariables(value, {} as IRouteContext); } // Set the header res.setHeader(key, finalValue); this.logger.debug(`Modified response header: ${key}=${finalValue}`); } } } /** * Handle an HTTP request */ public async handleRequest( req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse ): Promise { // Record start time for logging const startTime = Date.now(); // Get route before applying CORS (we might need its settings) // Try to find a matching route using RouteManager let matchingRoute: IRouteConfig | null = null; if (this.routeManager) { try { // Create a connection ID for this request const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // Create route context for function-based targets const routeContext = this.contextCreator.createHttpRouteContext(req, { connectionId, clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0', serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0', tlsVersion: req.socket.getTLSVersion?.() || undefined }); matchingRoute = this.routeManager.findMatchingRoute(toBaseContext(routeContext)); } catch (err) { this.logger.error('Error finding matching route', err); } } // Apply CORS headers with route-specific settings if available this.applyCorsHeaders(res, req, matchingRoute); // If this is an OPTIONS request, the response has already been ended in applyCorsHeaders // so we should return early to avoid trying to set more headers if (req.method === 'OPTIONS') { // Increment metrics for OPTIONS requests too if (this.metricsTracker) { this.metricsTracker.incrementRequestsServed(); } return; } // Apply default headers this.applyDefaultHeaders(res); // We already have the connection ID and routeContext from CORS handling const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // Create route context for function-based targets (if we don't already have one) const routeContext = this.contextCreator.createHttpRouteContext(req, { connectionId, clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0', serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0', tlsVersion: req.socket.getTLSVersion?.() || undefined }); // Check security restrictions if we have a matching route if (matchingRoute) { // Check IP filtering and rate limiting if (!this.securityManager.isAllowed(matchingRoute, routeContext)) { this.logger.warn(`Access denied for ${routeContext.clientIp} to ${matchingRoute.name || 'unnamed'}`); res.statusCode = 403; res.end('Forbidden: Access denied by security policy'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Check basic auth if (matchingRoute.security?.basicAuth?.enabled) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Basic ')) { // No auth header provided - send 401 with WWW-Authenticate header res.statusCode = 401; const realm = matchingRoute.security.basicAuth.realm || 'Protected Area'; res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`); res.end('Authentication Required'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Verify credentials try { const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8'); const [username, password] = credentials.split(':'); if (!this.securityManager.checkBasicAuth(matchingRoute, username, password)) { res.statusCode = 401; const realm = matchingRoute.security.basicAuth.realm || 'Protected Area'; res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`); res.end('Invalid Credentials'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } catch (err) { this.logger.error(`Error verifying basic auth: ${err}`); res.statusCode = 401; res.end('Authentication Error'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } // Check JWT auth if (matchingRoute.security?.jwtAuth?.enabled) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { // No auth header provided - send 401 res.statusCode = 401; res.end('Authentication Required: JWT token missing'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Verify token const token = authHeader.substring(7); if (!this.securityManager.verifyJwtToken(matchingRoute, token)) { res.statusCode = 401; res.end('Invalid or Expired JWT'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } } // If we found a matching route with function-based targets, use it if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.target) { this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`); // Extract target information, resolving functions if needed let targetHost: string | string[]; let targetPort: number; try { // Check function cache for host and resolve or use cached value if (typeof matchingRoute.action.target.host === 'function') { // Generate a function ID for caching (use route name or ID if available) const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedHost = this.functionCache.getCachedHost(routeContext, functionId); if (cachedHost !== undefined) { targetHost = cachedHost; this.logger.debug(`Using cached host value for ${functionId}`); } else { // Resolve the function and cache the result const resolvedHost = matchingRoute.action.target.host(toBaseContext(routeContext)); targetHost = resolvedHost; // Cache the result this.functionCache.cacheHost(routeContext, functionId, resolvedHost); this.logger.debug(`Resolved and cached function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { // No cache available, just resolve const resolvedHost = matchingRoute.action.target.host(routeContext); targetHost = resolvedHost; this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { targetHost = matchingRoute.action.target.host; } // Check function cache for port and resolve or use cached value if (typeof matchingRoute.action.target.port === 'function') { // Generate a function ID for caching const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedPort = this.functionCache.getCachedPort(routeContext, functionId); if (cachedPort !== undefined) { targetPort = cachedPort; this.logger.debug(`Using cached port value for ${functionId}`); } else { // Resolve the function and cache the result const resolvedPort = matchingRoute.action.target.port(toBaseContext(routeContext)); targetPort = resolvedPort; // Cache the result this.functionCache.cachePort(routeContext, functionId, resolvedPort); this.logger.debug(`Resolved and cached function-based port to: ${resolvedPort}`); } } else { // No cache available, just resolve const resolvedPort = matchingRoute.action.target.port(routeContext); targetPort = resolvedPort; this.logger.debug(`Resolved function-based port to: ${resolvedPort}`); } } else { targetPort = matchingRoute.action.target.port === 'preserve' ? routeContext.port : matchingRoute.action.target.port as number; } // Select a single host if an array was provided const selectedHost = Array.isArray(targetHost) ? targetHost[Math.floor(Math.random() * targetHost.length)] : targetHost; // Create a destination for the connection pool const destination = { host: selectedHost, port: targetPort }; // Apply URL rewriting if configured this.applyUrlRewriting(req, matchingRoute, routeContext); // Apply header modifications if configured this.applyRouteHeaderModifications(matchingRoute, req, res); // Continue with handling using the resolved destination HttpRequestHandler.handleHttpRequestWithDestination( req, res, destination, routeContext, startTime, this.logger, this.metricsTracker, matchingRoute // Pass the route config for additional processing ); return; } catch (err) { this.logger.error(`Error evaluating function-based target: ${err}`); res.statusCode = 500; res.end('Internal Server Error: Failed to evaluate target functions'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } // Try modern router first, then fall back to legacy routing if needed if (this.router) { try { // Try to find a matching route using the modern router const route = this.router.routeReq(req); if (route && route.action.type === 'forward' && route.action.target) { // Handle this route similarly to RouteManager logic this.logger.debug(`Found matching route via modern router: ${route.name || 'unnamed'}`); // No need to do anything here, we'll continue with legacy routing // The routeManager would have already found this route if applicable } } catch (err) { this.logger.error('Error using modern router', err); // Continue with legacy routing } } // Fall back to legacy routing if no matching route found via RouteManager let proxyConfig: IReverseProxyConfig | undefined; try { proxyConfig = this.legacyRouter.routeReq(req); } catch (err) { this.logger.error('Error routing request with legacy router', err); res.statusCode = 500; res.end('Internal Server Error'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } if (!proxyConfig) { this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); res.statusCode = 404; res.end('Not Found: No proxy configuration for this host'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Determine protocol to backend (per-domain override or global) const backendProto = proxyConfig.backendProtocol || this.options.backendProtocol; if (backendProto === 'http2') { const destination = this.connectionPool.getNextTarget( proxyConfig.destinationIps, proxyConfig.destinationPorts[0] ); const key = `${destination.host}:${destination.port}`; let session = this.h2Sessions.get(key); if (!session || session.closed || (session as any).destroyed) { session = plugins.http2.connect(`http://${destination.host}:${destination.port}`); this.h2Sessions.set(key, session); session.on('error', () => this.h2Sessions.delete(key)); session.on('close', () => this.h2Sessions.delete(key)); } // Build headers for HTTP/2 request const hdrs: Record = { ':method': req.method, ':path': req.url, ':authority': `${destination.host}:${destination.port}` }; for (const [hk, hv] of Object.entries(req.headers)) { if (typeof hv === 'string') hdrs[hk] = hv; } const h2Stream = session.request(hdrs); req.pipe(h2Stream); h2Stream.on('response', (hdrs2: any) => { const status = (hdrs2[':status'] as number) || 502; res.statusCode = status; // Copy headers from HTTP/2 response to HTTP/1 response for (const [hk, hv] of Object.entries(hdrs2)) { if (!hk.startsWith(':') && hv != null) { res.setHeader(hk, hv as string | string[]); } } h2Stream.pipe(res); }); h2Stream.on('error', (err) => { res.statusCode = 502; res.end(`Bad Gateway: ${err.message}`); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); }); return; } } /** * Handle HTTP/2 stream requests with function-based target support */ public async handleHttp2(stream: plugins.http2.ServerHttp2Stream, headers: plugins.http2.IncomingHttpHeaders): Promise { const startTime = Date.now(); // Create a connection ID for this HTTP/2 stream const connectionId = `http2-${Date.now()}-${Math.floor(Math.random() * 10000)}`; // Get client IP and server IP from the socket const socket = (stream.session as any)?.socket; const clientIp = socket?.remoteAddress?.replace('::ffff:', '') || '0.0.0.0'; const serverIp = socket?.localAddress?.replace('::ffff:', '') || '0.0.0.0'; // Create route context for function-based targets const routeContext = this.contextCreator.createHttp2RouteContext(stream, headers, { connectionId, clientIp, serverIp }); // Try to find a matching route using RouteManager let matchingRoute: IRouteConfig | null = null; if (this.routeManager) { try { matchingRoute = this.routeManager.findMatchingRoute(toBaseContext(routeContext)); } catch (err) { this.logger.error('Error finding matching route for HTTP/2 request', err); } } // If we found a matching route with function-based targets, use it if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.target) { this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`); // Extract target information, resolving functions if needed let targetHost: string | string[]; let targetPort: number; try { // Check function cache for host and resolve or use cached value if (typeof matchingRoute.action.target.host === 'function') { // Generate a function ID for caching (use route name or ID if available) const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedHost = this.functionCache.getCachedHost(routeContext, functionId); if (cachedHost !== undefined) { targetHost = cachedHost; this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`); } else { // Resolve the function and cache the result const resolvedHost = matchingRoute.action.target.host(toBaseContext(routeContext)); targetHost = resolvedHost; // Cache the result this.functionCache.cacheHost(routeContext, functionId, resolvedHost); this.logger.debug(`Resolved and cached HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { // No cache available, just resolve const resolvedHost = matchingRoute.action.target.host(routeContext); targetHost = resolvedHost; this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`); } } else { targetHost = matchingRoute.action.target.host; } // Check function cache for port and resolve or use cached value if (typeof matchingRoute.action.target.port === 'function') { // Generate a function ID for caching const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`; // Check if we have a cached result if (this.functionCache) { const cachedPort = this.functionCache.getCachedPort(routeContext, functionId); if (cachedPort !== undefined) { targetPort = cachedPort; this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`); } else { // Resolve the function and cache the result const resolvedPort = matchingRoute.action.target.port(toBaseContext(routeContext)); targetPort = resolvedPort; // Cache the result this.functionCache.cachePort(routeContext, functionId, resolvedPort); this.logger.debug(`Resolved and cached HTTP/2 function-based port to: ${resolvedPort}`); } } else { // No cache available, just resolve const resolvedPort = matchingRoute.action.target.port(routeContext); targetPort = resolvedPort; this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`); } } else { targetPort = matchingRoute.action.target.port === 'preserve' ? routeContext.port : matchingRoute.action.target.port as number; } // Select a single host if an array was provided const selectedHost = Array.isArray(targetHost) ? targetHost[Math.floor(Math.random() * targetHost.length)] : targetHost; // Create a destination for forwarding const destination = { host: selectedHost, port: targetPort }; // Handle HTTP/2 stream based on backend protocol const backendProtocol = matchingRoute.action.options?.backendProtocol || this.options.backendProtocol; if (backendProtocol === 'http2') { // Forward to HTTP/2 backend return Http2RequestHandler.handleHttp2WithHttp2Destination( stream, headers, destination, routeContext, this.h2Sessions, this.logger, this.metricsTracker ); } else { // Forward to HTTP/1.1 backend return Http2RequestHandler.handleHttp2WithHttp1Destination( stream, headers, destination, routeContext, this.logger, this.metricsTracker ); } } catch (err) { this.logger.error(`Error evaluating function-based target for HTTP/2: ${err}`); stream.respond({ ':status': 500 }); stream.end('Internal Server Error: Failed to evaluate target functions'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } } // Fall back to legacy routing if no matching route found const method = headers[':method'] || 'GET'; const path = headers[':path'] || '/'; // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions if (this.options.backendProtocol === 'http2') { const authority = headers[':authority'] as string || ''; const host = authority.split(':')[0]; const fakeReq: any = { headers: { host }, method: headers[':method'], url: headers[':path'], socket: (stream.session as any).socket }; // Try modern router first if available let route; if (this.router) { try { route = this.router.routeReq(fakeReq); if (route && route.action.type === 'forward' && route.action.target) { this.logger.debug(`Found matching HTTP/2 route via modern router: ${route.name || 'unnamed'}`); // The routeManager would have already found this route if applicable } } catch (err) { this.logger.error('Error using modern router for HTTP/2', err); } } // Fall back to legacy routing const proxyConfig = this.legacyRouter.routeReq(fakeReq); if (!proxyConfig) { stream.respond({ ':status': 404 }); stream.end('Not Found'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } const destination = this.connectionPool.getNextTarget(proxyConfig.destinationIps, proxyConfig.destinationPorts[0]); // Use the helper for HTTP/2 to HTTP/2 routing return Http2RequestHandler.handleHttp2WithHttp2Destination( stream, headers, destination, routeContext, this.h2Sessions, this.logger, this.metricsTracker ); } try { // Determine host for routing const authority = headers[':authority'] as string || ''; const host = authority.split(':')[0]; // Fake request object for routing const fakeReq: any = { headers: { host }, method, url: path, socket: (stream.session as any).socket }; // Try modern router first if available if (this.router) { try { const route = this.router.routeReq(fakeReq); if (route && route.action.type === 'forward' && route.action.target) { this.logger.debug(`Found matching HTTP/2 route via modern router: ${route.name || 'unnamed'}`); // The routeManager would have already found this route if applicable } } catch (err) { this.logger.error('Error using modern router for HTTP/2', err); } } // Fall back to legacy routing const proxyConfig = this.legacyRouter.routeReq(fakeReq as any); if (!proxyConfig) { stream.respond({ ':status': 404 }); stream.end('Not Found'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); return; } // Select backend target const destination = this.connectionPool.getNextTarget( proxyConfig.destinationIps, proxyConfig.destinationPorts[0] ); // Use the helper for HTTP/2 to HTTP/1 routing return Http2RequestHandler.handleHttp2WithHttp1Destination( stream, headers, destination, routeContext, this.logger, this.metricsTracker ); } catch (err: any) { stream.respond({ ':status': 500 }); stream.end('Internal Server Error'); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); } } }