This commit is contained in:
Juergen Kunz
2025-07-17 15:13:09 +00:00
parent a625675922
commit 82df9a6f52
9 changed files with 581 additions and 370 deletions

View File

@@ -3,7 +3,7 @@ import '../../core/models/socket-augmentation.js';
import { type IHttpProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger } from './models/types.js';
import { ConnectionPool } from './connection-pool.js';
import { HttpRouter } from '../../routing/router/index.js';
import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
import type { IRouteContext } from '../../core/models/route-context.js';
import { toBaseContext } from '../../core/models/route-context.js';
import { ContextCreator } from './context-creator.js';
@@ -53,6 +53,80 @@ export class WebSocketHandler {
this.securityManager.setRoutes(routes);
}
/**
* Select the appropriate target from the targets array based on sub-matching criteria
*/
private selectTarget(
targets: IRouteTarget[],
context: {
port: number;
path?: string;
headers?: Record<string, string>;
method?: string;
}
): IRouteTarget | null {
// Sort targets by priority (higher first)
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
// Find the first matching target
for (const target of sortedTargets) {
if (!target.match) {
// No match criteria means this is a default/fallback target
return target;
}
// Check port match
if (target.match.ports && !target.match.ports.includes(context.port)) {
continue;
}
// Check path match (supports wildcards)
if (target.match.path && context.path) {
const pathPattern = target.match.path.replace(/\*/g, '.*');
const pathRegex = new RegExp(`^${pathPattern}$`);
if (!pathRegex.test(context.path)) {
continue;
}
}
// Check method match
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
continue;
}
// Check headers match
if (target.match.headers && context.headers) {
let headersMatch = true;
for (const [key, pattern] of Object.entries(target.match.headers)) {
const headerValue = context.headers[key.toLowerCase()];
if (!headerValue) {
headersMatch = false;
break;
}
if (pattern instanceof RegExp) {
if (!pattern.test(headerValue)) {
headersMatch = false;
break;
}
} else if (headerValue !== pattern) {
headersMatch = false;
break;
}
}
if (!headersMatch) {
continue;
}
}
// All criteria matched
return target;
}
// No matching target found, return the first target without match criteria (default)
return sortedTargets.find(t => !t.match) || null;
}
/**
* Initialize WebSocket server on an existing HTTPS server
*/
@@ -146,9 +220,23 @@ export class WebSocketHandler {
let destination: { host: string; port: number };
// If we found a route with the modern router, use it
if (route && route.action.type === 'forward' && route.action.target) {
if (route && route.action.type === 'forward' && route.action.targets && route.action.targets.length > 0) {
this.logger.debug(`Found matching WebSocket route: ${route.name || 'unnamed'}`);
// Select the appropriate target from the targets array
const selectedTarget = this.selectTarget(route.action.targets, {
port: routeContext.port,
path: routeContext.path,
headers: routeContext.headers,
method: routeContext.method
});
if (!selectedTarget) {
this.logger.error(`No matching target found for route ${route.name}`);
wsIncoming.close(1003, 'No matching target');
return;
}
// Check if WebSockets are enabled for this route
if (route.action.websocket?.enabled === false) {
this.logger.debug(`WebSockets are disabled for route: ${route.name || 'unnamed'}`);
@@ -192,20 +280,20 @@ export class WebSocketHandler {
try {
// Resolve host if it's a function
if (typeof route.action.target.host === 'function') {
const resolvedHost = route.action.target.host(toBaseContext(routeContext));
if (typeof selectedTarget.host === 'function') {
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
targetHost = resolvedHost;
this.logger.debug(`Resolved function-based host for WebSocket: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
} else {
targetHost = route.action.target.host;
targetHost = selectedTarget.host;
}
// Resolve port if it's a function
if (typeof route.action.target.port === 'function') {
targetPort = route.action.target.port(toBaseContext(routeContext));
if (typeof selectedTarget.port === 'function') {
targetPort = selectedTarget.port(toBaseContext(routeContext));
this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`);
} else {
targetPort = route.action.target.port === 'preserve' ? routeContext.port : route.action.target.port as number;
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
}
// Select a single host if an array was provided