import * as plugins from '../../plugins.js'; import '../../core/models/socket-augmentation.js'; import type { IHttpRouteContext, IRouteContext } from '../../core/models/route-context.js'; import type { ILogger } from './models/types.js'; import type { IMetricsTracker } from './request-handler.js'; import type { IRouteConfig } from '../smart-proxy/models/route-types.js'; import { TemplateUtils } from '../../core/utils/template-utils.js'; /** * HTTP Request Handler Helper - handles requests with specific destinations * This is a helper class for the main RequestHandler */ export class HttpRequestHandler { /** * Handle HTTP request with a specific destination */ public static async handleHttpRequestWithDestination( req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, destination: { host: string, port: number }, routeContext: IHttpRouteContext, startTime: number, logger: ILogger, metricsTracker?: IMetricsTracker | null, route?: IRouteConfig ): Promise { try { // Apply URL rewriting if route config is provided if (route) { HttpRequestHandler.applyUrlRewriting(req, route, routeContext, logger); HttpRequestHandler.applyRouteHeaderModifications(route, req, res, logger); } // Create options for the proxy request const options: plugins.http.RequestOptions = { hostname: destination.host, port: destination.port, path: req.url, method: req.method, headers: { ...req.headers } }; // Optionally rewrite host header to match target if (options.headers && 'host' in options.headers) { // Only apply if host header rewrite is enabled or not explicitly disabled const shouldRewriteHost = route?.action.options?.rewriteHostHeader !== false; if (shouldRewriteHost) { // Safely cast to OutgoingHttpHeaders to access host property (options.headers as plugins.http.OutgoingHttpHeaders).host = `${destination.host}:${destination.port}`; } } logger.debug( `Proxying request to ${destination.host}:${destination.port}${req.url}`, { method: req.method } ); // Create proxy request const proxyReq = plugins.http.request(options, (proxyRes) => { // Copy status code res.statusCode = proxyRes.statusCode || 500; // Copy headers from proxy response to client response for (const [key, value] of Object.entries(proxyRes.headers)) { if (value !== undefined) { res.setHeader(key, value); } } // Apply response header modifications if route config is provided if (route && route.headers?.response) { HttpRequestHandler.applyResponseHeaderModifications(route, res, logger, routeContext); } // Pipe proxy response to client response proxyRes.pipe(res); // Increment served requests counter when the response finishes res.on('finish', () => { if (metricsTracker) { metricsTracker.incrementRequestsServed(); } // Log the completed request const duration = Date.now() - startTime; logger.debug( `Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`, { duration, statusCode: res.statusCode } ); }); }); // Handle proxy request errors proxyReq.on('error', (error) => { const duration = Date.now() - startTime; logger.error( `Proxy error for ${req.method} ${req.url}: ${error.message}`, { duration, error: error.message } ); // Increment failed requests counter if (metricsTracker) { metricsTracker.incrementFailedRequests(); } // Check if headers have already been sent if (!res.headersSent) { res.statusCode = 502; res.end(`Bad Gateway: ${error.message}`); } else { // If headers already sent, just close the connection res.end(); } }); // Pipe request body to proxy request and handle client-side errors req.pipe(proxyReq); // Handle client disconnection req.on('error', (error) => { logger.debug(`Client connection error: ${error.message}`); proxyReq.destroy(); // Increment failed requests counter on client errors if (metricsTracker) { metricsTracker.incrementFailedRequests(); } }); // Handle response errors res.on('error', (error) => { logger.debug(`Response error: ${error.message}`); proxyReq.destroy(); // Increment failed requests counter on response errors if (metricsTracker) { metricsTracker.incrementFailedRequests(); } }); } catch (error) { // Handle any unexpected errors logger.error( `Unexpected error handling request: ${error.message}`, { error: error.stack } ); // Increment failed requests counter if (metricsTracker) { metricsTracker.incrementFailedRequests(); } if (!res.headersSent) { res.statusCode = 500; res.end('Internal Server Error'); } else { res.end(); } } } /** * 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 * @param logger Logger for debugging information * @returns True if URL was rewritten, false otherwise */ private static applyUrlRewriting( req: plugins.http.IncomingMessage, route: IRouteConfig, routeContext: IHttpRouteContext, logger: ILogger ): 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 with optional flags 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); } logger.debug(`URL rewritten: ${originalUrl} -> ${req.url}`); return true; } catch (err) { logger.error(`Error in URL rewriting: ${err}`); return false; } } return false; } /** * Apply header modifications from route configuration to request headers * Implements Phase 5.1: Route-based header manipulation for requests */ private static applyRouteHeaderModifications( route: IRouteConfig, req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, logger: ILogger ): 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) { // Create routing context for template resolution const routeContext: IRouteContext = { domain: req.headers.host as string || '', path: req.url || '', clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '', serverIp: req.socket.localAddress?.replace('::ffff:', '') || '', port: parseInt(req.socket.localPort?.toString() || '0', 10), isTls: !!req.socket.encrypted, headers: req.headers as Record, timestamp: Date.now(), connectionId: `${Date.now()}-${Math.floor(Math.random() * 10000)}`, }; 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()]; logger.debug(`Deleted request header: ${key}`); continue; } // Handle forced override (!value) let finalValue: string; if (value.startsWith('!')) { // Keep the ! but resolve any templates in the rest const templateValue = value.substring(1); finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, routeContext); } else { // Resolve templates in the entire value finalValue = TemplateUtils.resolveTemplateVariables(value, routeContext); } // Set the header req.headers[key.toLowerCase()] = finalValue; logger.debug(`Modified request header: ${key}=${finalValue}`); } } } /** * Apply header modifications from route configuration to response headers * Implements Phase 5.1: Route-based header manipulation for responses */ private static applyResponseHeaderModifications( route: IRouteConfig, res: plugins.http.ServerResponse, logger: ILogger, routeContext?: IRouteContext ): void { // Check if route has response header modifications if (!route.headers?.response) { return; } // Apply response header modifications 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); 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 = routeContext ? '!' + TemplateUtils.resolveTemplateVariables(templateValue, routeContext) : '!' + templateValue; } else { // Resolve templates in the entire value finalValue = routeContext ? TemplateUtils.resolveTemplateVariables(value, routeContext) : value; } // Set the header res.setHeader(key, finalValue); logger.debug(`Modified response header: ${key}=${finalValue}`); } } // Template resolution is now handled by the TemplateUtils class }