331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
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<void> {
|
|
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<string, string>,
|
|
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
|
|
} |