smartproxy/ts/proxies/network-proxy/request-handler.ts

915 lines
34 KiB
TypeScript

import * as plugins from '../../plugins.js';
import '../../core/models/socket-augmentation.js';
import {
type INetworkProxyOptions,
type ILogger,
createLogger,
type IReverseProxyConfig,
RouteManager
} from './models/types.js';
import { ConnectionPool } from './connection-pool.js';
import { ProxyRouter } from '../../http/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<string, plugins.http2.ClientHttp2Session> = 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: INetworkProxyOptions,
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<void> {
// 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;
}
// 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<string, any> = {
':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<void> {
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;
}
// 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();
}
}
}