2025-05-10 13:59:34 +00:00
|
|
|
/**
|
|
|
|
* Route Helper Functions
|
|
|
|
*
|
|
|
|
* This file provides utility functions for creating route configurations for common scenarios.
|
|
|
|
* These functions aim to simplify the creation of route configurations for typical use cases.
|
|
|
|
*
|
|
|
|
* This module includes helper functions for creating:
|
|
|
|
* - HTTP routes (createHttpRoute)
|
|
|
|
* - HTTPS routes with TLS termination (createHttpsTerminateRoute)
|
|
|
|
* - HTTP to HTTPS redirects (createHttpToHttpsRedirect)
|
|
|
|
* - HTTPS passthrough routes (createHttpsPassthroughRoute)
|
|
|
|
* - Complete HTTPS servers with redirects (createCompleteHttpsServer)
|
|
|
|
* - Load balancer routes (createLoadBalancerRoute)
|
|
|
|
* - API routes (createApiRoute)
|
|
|
|
* - WebSocket routes (createWebSocketRoute)
|
2025-05-13 12:48:41 +00:00
|
|
|
* - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute)
|
|
|
|
* - Dynamic routing (createDynamicRoute, createSmartLoadBalancer)
|
2025-05-15 14:35:01 +00:00
|
|
|
* - NFTables routes (createNfTablesRoute, createNfTablesTerminateRoute)
|
2025-05-10 13:59:34 +00:00
|
|
|
*/
|
|
|
|
|
2025-05-29 00:24:57 +00:00
|
|
|
import * as plugins from '../../../plugins.js';
|
2025-05-13 12:48:41 +00:00
|
|
|
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
|
2025-07-21 18:44:59 +00:00
|
|
|
import { mergeRouteConfigs } from './route-utils.js';
|
2025-07-21 19:40:01 +00:00
|
|
|
import { ProtocolDetector, HttpDetector } from '../../../detection/index.js';
|
2025-05-10 13:59:34 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an HTTP-only route configuration
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createHttpRoute(
|
|
|
|
domains: string | string[],
|
|
|
|
target: { host: string | string[]; port: number },
|
|
|
|
options: Partial<IRouteConfig> = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.match?.ports || 80,
|
|
|
|
domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [target]
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `HTTP Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an HTTPS route with TLS termination (including HTTP redirect to HTTPS)
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createHttpsTerminateRoute(
|
|
|
|
domains: string | string[],
|
|
|
|
target: { host: string | string[]; port: number },
|
|
|
|
options: {
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
httpPort?: number | number[];
|
|
|
|
httpsPort?: number | number[];
|
|
|
|
reencrypt?: boolean;
|
|
|
|
name?: string;
|
|
|
|
[key: string]: any;
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.httpsPort || 443,
|
|
|
|
domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [target],
|
2025-05-10 13:59:34 +00:00
|
|
|
tls: {
|
|
|
|
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
|
|
|
|
certificate: options.certificate || 'auto'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `HTTPS Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an HTTP to HTTPS redirect route
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param httpsPort HTTPS port to redirect to (default: 443)
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createHttpToHttpsRedirect(
|
|
|
|
domains: string | string[],
|
|
|
|
httpsPort: number = 443,
|
|
|
|
options: Partial<IRouteConfig> = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.match?.ports || 80,
|
|
|
|
domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
2025-05-29 01:00:20 +00:00
|
|
|
type: 'socket-handler',
|
|
|
|
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an HTTPS passthrough route (SNI-based forwarding without TLS termination)
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createHttpsPassthroughRoute(
|
|
|
|
domains: string | string[],
|
|
|
|
target: { host: string | string[]; port: number },
|
|
|
|
options: Partial<IRouteConfig> = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.match?.ports || 443,
|
|
|
|
domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [target],
|
2025-05-10 13:59:34 +00:00
|
|
|
tls: {
|
|
|
|
mode: 'passthrough'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `HTTPS Passthrough for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a complete HTTPS server with HTTP to HTTPS redirects
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional configuration options
|
|
|
|
* @returns Array of two route configurations (HTTPS and HTTP redirect)
|
|
|
|
*/
|
|
|
|
export function createCompleteHttpsServer(
|
|
|
|
domains: string | string[],
|
|
|
|
target: { host: string | string[]; port: number },
|
|
|
|
options: {
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
httpPort?: number | number[];
|
|
|
|
httpsPort?: number | number[];
|
|
|
|
reencrypt?: boolean;
|
|
|
|
name?: string;
|
|
|
|
[key: string]: any;
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig[] {
|
|
|
|
// Create the HTTPS route
|
|
|
|
const httpsRoute = createHttpsTerminateRoute(domains, target, options);
|
|
|
|
|
|
|
|
// Create the HTTP redirect route
|
|
|
|
const httpRedirectRoute = createHttpToHttpsRedirect(
|
|
|
|
domains,
|
|
|
|
// Extract the HTTPS port from the HTTPS route - ensure it's a number
|
|
|
|
typeof options.httpsPort === 'number' ? options.httpsPort :
|
|
|
|
Array.isArray(options.httpsPort) ? options.httpsPort[0] : 443,
|
|
|
|
{
|
|
|
|
// Set the HTTP port
|
|
|
|
match: {
|
|
|
|
ports: options.httpPort || 80,
|
|
|
|
domains
|
|
|
|
},
|
|
|
|
name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return [httpsRoute, httpRedirectRoute];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a load balancer route (round-robin between multiple backend hosts)
|
|
|
|
* @param domains Domain(s) to match
|
2025-07-21 18:44:59 +00:00
|
|
|
* @param backendsOrHosts Array of backend servers OR array of host strings (legacy)
|
|
|
|
* @param portOrOptions Port number (legacy) OR options object
|
|
|
|
* @param options Additional route options (legacy)
|
2025-05-10 13:59:34 +00:00
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createLoadBalancerRoute(
|
|
|
|
domains: string | string[],
|
2025-07-21 18:44:59 +00:00
|
|
|
backendsOrHosts: Array<{ host: string; port: number }> | string[],
|
|
|
|
portOrOptions?: number | {
|
2025-05-10 13:59:34 +00:00
|
|
|
tls?: {
|
|
|
|
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
};
|
2025-07-21 18:44:59 +00:00
|
|
|
useTls?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
algorithm?: 'round-robin' | 'least-connections' | 'ip-hash';
|
|
|
|
healthCheck?: {
|
|
|
|
path: string;
|
|
|
|
interval: number;
|
|
|
|
timeout: number;
|
|
|
|
unhealthyThreshold: number;
|
|
|
|
healthyThreshold: number;
|
|
|
|
};
|
2025-05-10 13:59:34 +00:00
|
|
|
[key: string]: any;
|
2025-07-21 18:44:59 +00:00
|
|
|
},
|
|
|
|
options?: {
|
|
|
|
tls?: {
|
|
|
|
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
};
|
|
|
|
[key: string]: any;
|
|
|
|
}
|
2025-05-10 13:59:34 +00:00
|
|
|
): IRouteConfig {
|
2025-07-21 18:44:59 +00:00
|
|
|
// Handle legacy signature: (domains, hosts[], port, options)
|
|
|
|
let backends: Array<{ host: string; port: number }>;
|
|
|
|
let finalOptions: any;
|
|
|
|
|
|
|
|
if (Array.isArray(backendsOrHosts) && backendsOrHosts.length > 0 && typeof backendsOrHosts[0] === 'string') {
|
|
|
|
// Legacy signature
|
|
|
|
const hosts = backendsOrHosts as string[];
|
|
|
|
const port = portOrOptions as number;
|
|
|
|
backends = hosts.map(host => ({ host, port }));
|
|
|
|
finalOptions = options || {};
|
|
|
|
} else {
|
|
|
|
// New signature
|
|
|
|
backends = backendsOrHosts as Array<{ host: string; port: number }>;
|
|
|
|
finalOptions = (portOrOptions as any) || {};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract hosts and ensure all backends use the same port
|
|
|
|
const port = backends[0].port;
|
|
|
|
const hosts = backends.map(backend => backend.host);
|
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
2025-07-21 18:44:59 +00:00
|
|
|
ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80),
|
2025-05-10 13:59:34 +00:00
|
|
|
domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route target
|
|
|
|
const target: IRouteTarget = {
|
|
|
|
host: hosts,
|
|
|
|
port
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [target]
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Add TLS configuration if provided
|
2025-07-21 18:44:59 +00:00
|
|
|
if (finalOptions.tls || finalOptions.useTls) {
|
2025-05-10 13:59:34 +00:00
|
|
|
action.tls = {
|
2025-07-21 18:44:59 +00:00
|
|
|
mode: finalOptions.tls?.mode || 'terminate',
|
|
|
|
certificate: finalOptions.tls?.certificate || finalOptions.certificate || 'auto'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add load balancing options
|
|
|
|
if (finalOptions.algorithm || finalOptions.healthCheck) {
|
|
|
|
action.loadBalancing = {
|
|
|
|
algorithm: finalOptions.algorithm || 'round-robin',
|
|
|
|
healthCheck: finalOptions.healthCheck
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
2025-07-21 18:44:59 +00:00
|
|
|
name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
...finalOptions
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an API route configuration
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param apiPath API base path (e.g., "/api")
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createApiRoute(
|
|
|
|
domains: string | string[],
|
|
|
|
apiPath: string,
|
|
|
|
target: { host: string | string[]; port: number },
|
|
|
|
options: {
|
|
|
|
useTls?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
addCorsHeaders?: boolean;
|
|
|
|
httpPort?: number | number[];
|
|
|
|
httpsPort?: number | number[];
|
|
|
|
name?: string;
|
|
|
|
[key: string]: any;
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Normalize API path
|
|
|
|
const normalizedPath = apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
|
|
const pathWithWildcard = normalizedPath.endsWith('/')
|
|
|
|
? `${normalizedPath}*`
|
|
|
|
: `${normalizedPath}/*`;
|
|
|
|
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.useTls
|
|
|
|
? (options.httpsPort || 443)
|
|
|
|
: (options.httpPort || 80),
|
|
|
|
domains,
|
|
|
|
path: pathWithWildcard
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [target]
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Add TLS configuration if using HTTPS
|
|
|
|
if (options.useTls) {
|
|
|
|
action.tls = {
|
|
|
|
mode: 'terminate',
|
|
|
|
certificate: options.certificate || 'auto'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add CORS headers if requested
|
|
|
|
const headers: Record<string, Record<string, string>> = {};
|
|
|
|
if (options.addCorsHeaders) {
|
|
|
|
headers.response = {
|
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
|
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
|
|
'Access-Control-Max-Age': '86400'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
|
|
name: options.name || `API Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
priority: options.priority || 100, // Higher priority for specific path matches
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a WebSocket route configuration
|
|
|
|
* @param domains Domain(s) to match
|
2025-07-21 18:44:59 +00:00
|
|
|
* @param targetOrPath Target server OR WebSocket path (legacy)
|
|
|
|
* @param targetOrOptions Target server (legacy) OR options
|
|
|
|
* @param options Additional route options (legacy)
|
2025-05-10 13:59:34 +00:00
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createWebSocketRoute(
|
|
|
|
domains: string | string[],
|
2025-07-21 18:44:59 +00:00
|
|
|
targetOrPath: { host: string | string[]; port: number } | string,
|
|
|
|
targetOrOptions?: { host: string | string[]; port: number } | {
|
|
|
|
useTls?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
path?: string;
|
|
|
|
httpPort?: number | number[];
|
|
|
|
httpsPort?: number | number[];
|
|
|
|
pingInterval?: number;
|
|
|
|
pingTimeout?: number;
|
|
|
|
name?: string;
|
|
|
|
[key: string]: any;
|
|
|
|
},
|
|
|
|
options?: {
|
2025-05-10 13:59:34 +00:00
|
|
|
useTls?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
httpPort?: number | number[];
|
|
|
|
httpsPort?: number | number[];
|
|
|
|
pingInterval?: number;
|
|
|
|
pingTimeout?: number;
|
|
|
|
name?: string;
|
|
|
|
[key: string]: any;
|
2025-07-21 18:44:59 +00:00
|
|
|
}
|
2025-05-10 13:59:34 +00:00
|
|
|
): IRouteConfig {
|
2025-07-21 18:44:59 +00:00
|
|
|
// Handle different signatures
|
|
|
|
let target: { host: string | string[]; port: number };
|
|
|
|
let wsPath: string;
|
|
|
|
let finalOptions: any;
|
|
|
|
|
|
|
|
if (typeof targetOrPath === 'string') {
|
|
|
|
// Legacy signature: (domains, path, target, options)
|
|
|
|
wsPath = targetOrPath;
|
|
|
|
target = targetOrOptions as { host: string | string[]; port: number };
|
|
|
|
finalOptions = options || {};
|
|
|
|
} else {
|
|
|
|
// New signature: (domains, target, options)
|
|
|
|
target = targetOrPath;
|
|
|
|
finalOptions = (targetOrOptions as any) || {};
|
|
|
|
wsPath = finalOptions.path || '/ws';
|
|
|
|
}
|
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Normalize WebSocket path
|
|
|
|
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
|
|
|
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
2025-07-21 18:44:59 +00:00
|
|
|
ports: finalOptions.useTls
|
|
|
|
? (finalOptions.httpsPort || 443)
|
|
|
|
: (finalOptions.httpPort || 80),
|
2025-05-10 13:59:34 +00:00
|
|
|
domains,
|
|
|
|
path: normalizedPath
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [target],
|
2025-05-10 13:59:34 +00:00
|
|
|
websocket: {
|
|
|
|
enabled: true,
|
2025-07-21 18:44:59 +00:00
|
|
|
pingInterval: finalOptions.pingInterval || 30000, // 30 seconds
|
|
|
|
pingTimeout: finalOptions.pingTimeout || 5000 // 5 seconds
|
2025-05-10 13:59:34 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add TLS configuration if using HTTPS
|
2025-07-21 18:44:59 +00:00
|
|
|
if (finalOptions.useTls) {
|
2025-05-10 13:59:34 +00:00
|
|
|
action.tls = {
|
|
|
|
mode: 'terminate',
|
2025-07-21 18:44:59 +00:00
|
|
|
certificate: finalOptions.certificate || 'auto'
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
2025-07-21 18:44:59 +00:00
|
|
|
name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
|
|
priority: finalOptions.priority || 100, // Higher priority for WebSocket routes
|
|
|
|
...finalOptions
|
2025-05-10 13:59:34 +00:00
|
|
|
};
|
2025-05-13 12:48:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a helper function that applies a port offset
|
|
|
|
* @param offset The offset to apply to the matched port
|
|
|
|
* @returns A function that adds the offset to the matched port
|
|
|
|
*/
|
|
|
|
export function createPortOffset(offset: number): (context: IRouteContext) => number {
|
|
|
|
return (context: IRouteContext) => context.port + offset;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a port mapping route with context-based port function
|
|
|
|
* @param options Port mapping route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createPortMappingRoute(options: {
|
|
|
|
sourcePortRange: TPortRange;
|
|
|
|
targetHost: string | string[] | ((context: IRouteContext) => string | string[]);
|
|
|
|
portMapper: (context: IRouteContext) => number;
|
|
|
|
name?: string;
|
|
|
|
domains?: string | string[];
|
|
|
|
priority?: number;
|
|
|
|
[key: string]: any;
|
|
|
|
}): IRouteConfig {
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.sourcePortRange,
|
|
|
|
domains: options.domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [{
|
2025-05-13 12:48:41 +00:00
|
|
|
host: options.targetHost,
|
|
|
|
port: options.portMapper
|
2025-07-17 15:34:58 +00:00
|
|
|
}]
|
2025-05-13 12:48:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `Port Mapping Route for ${options.domains || 'all domains'}`,
|
|
|
|
priority: options.priority,
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a simple offset port mapping route
|
|
|
|
* @param options Offset port mapping route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createOffsetPortMappingRoute(options: {
|
|
|
|
ports: TPortRange;
|
|
|
|
targetHost: string | string[];
|
|
|
|
offset: number;
|
|
|
|
name?: string;
|
|
|
|
domains?: string | string[];
|
|
|
|
priority?: number;
|
|
|
|
[key: string]: any;
|
|
|
|
}): IRouteConfig {
|
|
|
|
return createPortMappingRoute({
|
|
|
|
sourcePortRange: options.ports,
|
|
|
|
targetHost: options.targetHost,
|
|
|
|
portMapper: (context) => context.port + options.offset,
|
|
|
|
name: options.name || `Offset Mapping (${options.offset > 0 ? '+' : ''}${options.offset}) for ${options.domains || 'all domains'}`,
|
|
|
|
domains: options.domains,
|
|
|
|
priority: options.priority,
|
|
|
|
...options
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a dynamic route with context-based host and port mapping
|
|
|
|
* @param options Dynamic route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createDynamicRoute(options: {
|
|
|
|
ports: TPortRange;
|
|
|
|
targetHost: (context: IRouteContext) => string | string[];
|
|
|
|
portMapper: (context: IRouteContext) => number;
|
|
|
|
name?: string;
|
|
|
|
domains?: string | string[];
|
|
|
|
path?: string;
|
|
|
|
clientIp?: string[];
|
|
|
|
priority?: number;
|
|
|
|
[key: string]: any;
|
|
|
|
}): IRouteConfig {
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.ports,
|
|
|
|
domains: options.domains,
|
|
|
|
path: options.path,
|
|
|
|
clientIp: options.clientIp
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [{
|
2025-05-13 12:48:41 +00:00
|
|
|
host: options.targetHost,
|
|
|
|
port: options.portMapper
|
2025-07-17 15:34:58 +00:00
|
|
|
}]
|
2025-05-13 12:48:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `Dynamic Route for ${options.domains || 'all domains'}`,
|
|
|
|
priority: options.priority,
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a smart load balancer with dynamic domain-based backend selection
|
|
|
|
* @param options Smart load balancer options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createSmartLoadBalancer(options: {
|
|
|
|
ports: TPortRange;
|
|
|
|
domainTargets: Record<string, string | string[]>;
|
|
|
|
portMapper: (context: IRouteContext) => number;
|
|
|
|
name?: string;
|
|
|
|
defaultTarget?: string | string[];
|
|
|
|
priority?: number;
|
|
|
|
[key: string]: any;
|
|
|
|
}): IRouteConfig {
|
|
|
|
// Extract all domain keys to create the match criteria
|
|
|
|
const domains = Object.keys(options.domainTargets);
|
|
|
|
|
|
|
|
// Create the smart host selector function
|
|
|
|
const hostSelector = (context: IRouteContext) => {
|
|
|
|
const domain = context.domain || '';
|
|
|
|
return options.domainTargets[domain] || options.defaultTarget || 'localhost';
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
ports: options.ports,
|
|
|
|
domains
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [{
|
2025-05-13 12:48:41 +00:00
|
|
|
host: hostSelector,
|
|
|
|
port: options.portMapper
|
2025-07-17 15:34:58 +00:00
|
|
|
}]
|
2025-05-13 12:48:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create the route config
|
|
|
|
return {
|
|
|
|
match,
|
|
|
|
action,
|
|
|
|
name: options.name || `Smart Load Balancer for ${domains.join(', ')}`,
|
|
|
|
priority: options.priority,
|
|
|
|
...options
|
|
|
|
};
|
2025-05-15 14:35:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an NFTables-based route for high-performance packet forwarding
|
|
|
|
* @param nameOrDomains Name or domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createNfTablesRoute(
|
|
|
|
nameOrDomains: string | string[],
|
|
|
|
target: { host: string; port: number | 'preserve' },
|
|
|
|
options: {
|
|
|
|
ports?: TPortRange;
|
|
|
|
protocol?: 'tcp' | 'udp' | 'all';
|
|
|
|
preserveSourceIP?: boolean;
|
|
|
|
ipAllowList?: string[];
|
|
|
|
ipBlockList?: string[];
|
|
|
|
maxRate?: string;
|
|
|
|
priority?: number;
|
|
|
|
useTls?: boolean;
|
|
|
|
tableName?: string;
|
|
|
|
useIPSets?: boolean;
|
|
|
|
useAdvancedNAT?: boolean;
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Determine if this is a name or domain
|
|
|
|
let name: string;
|
|
|
|
let domains: string | string[] | undefined;
|
|
|
|
|
|
|
|
if (Array.isArray(nameOrDomains) || (typeof nameOrDomains === 'string' && nameOrDomains.includes('.'))) {
|
|
|
|
domains = nameOrDomains;
|
|
|
|
name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains;
|
|
|
|
} else {
|
|
|
|
name = nameOrDomains;
|
|
|
|
domains = undefined; // No domains
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create route match
|
|
|
|
const match: IRouteMatch = {
|
|
|
|
domains,
|
|
|
|
ports: options.ports || 80
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create route action
|
|
|
|
const action: IRouteAction = {
|
|
|
|
type: 'forward',
|
2025-07-17 15:34:58 +00:00
|
|
|
targets: [{
|
2025-05-15 14:35:01 +00:00
|
|
|
host: target.host,
|
|
|
|
port: target.port
|
2025-07-17 15:34:58 +00:00
|
|
|
}],
|
2025-05-15 14:35:01 +00:00
|
|
|
forwardingEngine: 'nftables',
|
|
|
|
nftables: {
|
|
|
|
protocol: options.protocol || 'tcp',
|
|
|
|
preserveSourceIP: options.preserveSourceIP,
|
|
|
|
maxRate: options.maxRate,
|
|
|
|
priority: options.priority,
|
|
|
|
tableName: options.tableName,
|
|
|
|
useIPSets: options.useIPSets,
|
|
|
|
useAdvancedNAT: options.useAdvancedNAT
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add TLS options if needed
|
|
|
|
if (options.useTls) {
|
|
|
|
action.tls = {
|
|
|
|
mode: 'passthrough'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the route config
|
2025-05-29 12:15:53 +00:00
|
|
|
const routeConfig: IRouteConfig = {
|
2025-05-15 14:35:01 +00:00
|
|
|
name,
|
|
|
|
match,
|
|
|
|
action
|
|
|
|
};
|
2025-05-29 12:15:53 +00:00
|
|
|
|
|
|
|
// Add security if allowed or blocked IPs are specified
|
|
|
|
if (options.ipAllowList?.length || options.ipBlockList?.length) {
|
|
|
|
routeConfig.security = {
|
|
|
|
ipAllowList: options.ipAllowList,
|
|
|
|
ipBlockList: options.ipBlockList
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return routeConfig;
|
2025-05-15 14:35:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an NFTables-based TLS termination route
|
|
|
|
* @param nameOrDomains Name or domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createNfTablesTerminateRoute(
|
|
|
|
nameOrDomains: string | string[],
|
|
|
|
target: { host: string; port: number | 'preserve' },
|
|
|
|
options: {
|
|
|
|
ports?: TPortRange;
|
|
|
|
protocol?: 'tcp' | 'udp' | 'all';
|
|
|
|
preserveSourceIP?: boolean;
|
|
|
|
ipAllowList?: string[];
|
|
|
|
ipBlockList?: string[];
|
|
|
|
maxRate?: string;
|
|
|
|
priority?: number;
|
|
|
|
tableName?: string;
|
|
|
|
useIPSets?: boolean;
|
|
|
|
useAdvancedNAT?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Create basic NFTables route
|
|
|
|
const route = createNfTablesRoute(
|
|
|
|
nameOrDomains,
|
|
|
|
target,
|
|
|
|
{
|
|
|
|
...options,
|
|
|
|
ports: options.ports || 443,
|
|
|
|
useTls: false
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// Set TLS termination
|
|
|
|
route.action.tls = {
|
|
|
|
mode: 'terminate',
|
|
|
|
certificate: options.certificate || 'auto'
|
|
|
|
};
|
|
|
|
|
|
|
|
return route;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a complete NFTables-based HTTPS setup with HTTP redirect
|
|
|
|
* @param nameOrDomains Name or domain(s) to match
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Array of two route configurations (HTTPS and HTTP redirect)
|
|
|
|
*/
|
|
|
|
export function createCompleteNfTablesHttpsServer(
|
|
|
|
nameOrDomains: string | string[],
|
|
|
|
target: { host: string; port: number | 'preserve' },
|
|
|
|
options: {
|
|
|
|
httpPort?: TPortRange;
|
|
|
|
httpsPort?: TPortRange;
|
|
|
|
protocol?: 'tcp' | 'udp' | 'all';
|
|
|
|
preserveSourceIP?: boolean;
|
|
|
|
ipAllowList?: string[];
|
|
|
|
ipBlockList?: string[];
|
|
|
|
maxRate?: string;
|
|
|
|
priority?: number;
|
|
|
|
tableName?: string;
|
|
|
|
useIPSets?: boolean;
|
|
|
|
useAdvancedNAT?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig[] {
|
|
|
|
// Create the HTTPS route using NFTables
|
|
|
|
const httpsRoute = createNfTablesTerminateRoute(
|
|
|
|
nameOrDomains,
|
|
|
|
target,
|
|
|
|
{
|
|
|
|
...options,
|
|
|
|
ports: options.httpsPort || 443
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// Determine the domain(s) for HTTP redirect
|
|
|
|
const domains = typeof nameOrDomains === 'string' && !nameOrDomains.includes('.')
|
|
|
|
? undefined
|
|
|
|
: nameOrDomains;
|
|
|
|
|
|
|
|
// Extract the HTTPS port for the redirect destination
|
|
|
|
const httpsPort = typeof options.httpsPort === 'number'
|
|
|
|
? options.httpsPort
|
|
|
|
: Array.isArray(options.httpsPort) && typeof options.httpsPort[0] === 'number'
|
|
|
|
? options.httpsPort[0]
|
|
|
|
: 443;
|
|
|
|
|
|
|
|
// Create the HTTP redirect route (this uses standard forwarding, not NFTables)
|
|
|
|
const httpRedirectRoute = createHttpToHttpsRedirect(
|
|
|
|
domains as any, // Type cast needed since domains can be undefined now
|
|
|
|
httpsPort,
|
|
|
|
{
|
|
|
|
match: {
|
|
|
|
ports: options.httpPort || 80,
|
|
|
|
domains: domains as any // Type cast needed since domains can be undefined now
|
|
|
|
},
|
|
|
|
name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains || 'all domains'}`
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return [httpsRoute, httpRedirectRoute];
|
2025-05-29 00:24:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a socket handler route configuration
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param ports Port(s) to listen on
|
|
|
|
* @param handler Socket handler function
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns Route configuration object
|
|
|
|
*/
|
|
|
|
export function createSocketHandlerRoute(
|
|
|
|
domains: string | string[],
|
|
|
|
ports: TPortRange,
|
|
|
|
handler: (socket: plugins.net.Socket) => void | Promise<void>,
|
|
|
|
options: {
|
|
|
|
name?: string;
|
|
|
|
priority?: number;
|
|
|
|
path?: string;
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
return {
|
|
|
|
name: options.name || 'socket-handler-route',
|
|
|
|
priority: options.priority !== undefined ? options.priority : 50,
|
|
|
|
match: {
|
|
|
|
domains,
|
|
|
|
ports,
|
|
|
|
...(options.path && { path: options.path })
|
|
|
|
},
|
|
|
|
action: {
|
|
|
|
type: 'socket-handler',
|
|
|
|
socketHandler: handler
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pre-built socket handlers for common use cases
|
|
|
|
*/
|
|
|
|
export const SocketHandlers = {
|
|
|
|
/**
|
|
|
|
* Simple echo server handler
|
|
|
|
*/
|
2025-05-29 01:00:20 +00:00
|
|
|
echo: (socket: plugins.net.Socket, context: IRouteContext) => {
|
2025-05-29 00:24:57 +00:00
|
|
|
socket.write('ECHO SERVER READY\n');
|
|
|
|
socket.on('data', data => socket.write(data));
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* TCP proxy handler
|
|
|
|
*/
|
2025-05-29 01:00:20 +00:00
|
|
|
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
2025-05-29 00:24:57 +00:00
|
|
|
const target = plugins.net.connect(targetPort, targetHost);
|
|
|
|
socket.pipe(target);
|
|
|
|
target.pipe(socket);
|
|
|
|
socket.on('close', () => target.destroy());
|
|
|
|
target.on('close', () => socket.destroy());
|
|
|
|
target.on('error', (err) => {
|
|
|
|
console.error('Proxy target error:', err);
|
|
|
|
socket.destroy();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Line-based protocol handler
|
|
|
|
*/
|
2025-05-29 01:00:20 +00:00
|
|
|
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
2025-05-29 00:24:57 +00:00
|
|
|
let buffer = '';
|
|
|
|
socket.on('data', (data) => {
|
|
|
|
buffer += data.toString();
|
|
|
|
const lines = buffer.split('\n');
|
|
|
|
buffer = lines.pop() || '';
|
|
|
|
lines.forEach(line => {
|
|
|
|
if (line.trim()) {
|
|
|
|
handler(line.trim(), socket);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple HTTP response handler (for testing)
|
|
|
|
*/
|
2025-05-29 01:00:20 +00:00
|
|
|
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
2025-05-29 00:24:57 +00:00
|
|
|
const response = [
|
|
|
|
`HTTP/1.1 ${statusCode} ${statusCode === 200 ? 'OK' : 'Error'}`,
|
|
|
|
'Content-Type: text/plain',
|
|
|
|
`Content-Length: ${body.length}`,
|
|
|
|
'Connection: close',
|
|
|
|
'',
|
|
|
|
body
|
|
|
|
].join('\r\n');
|
|
|
|
|
|
|
|
socket.write(response);
|
|
|
|
socket.end();
|
2025-05-29 01:00:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Block connection immediately
|
|
|
|
*/
|
|
|
|
block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
|
|
const finalMessage = message || `Connection blocked from ${context.clientIp}`;
|
|
|
|
if (finalMessage) {
|
|
|
|
socket.write(finalMessage);
|
|
|
|
}
|
|
|
|
socket.end();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HTTP block response
|
|
|
|
*/
|
|
|
|
httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
|
|
const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`;
|
|
|
|
const finalMessage = message || defaultMessage;
|
|
|
|
|
|
|
|
const response = [
|
|
|
|
`HTTP/1.1 ${statusCode} ${finalMessage}`,
|
|
|
|
'Content-Type: text/plain',
|
|
|
|
`Content-Length: ${finalMessage.length}`,
|
|
|
|
'Connection: close',
|
|
|
|
'',
|
|
|
|
finalMessage
|
|
|
|
].join('\r\n');
|
|
|
|
|
|
|
|
socket.write(response);
|
|
|
|
socket.end();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HTTP redirect handler
|
2025-07-21 19:40:01 +00:00
|
|
|
* Now uses the centralized detection module for HTTP parsing
|
2025-05-29 01:00:20 +00:00
|
|
|
*/
|
|
|
|
httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
2025-07-21 19:40:01 +00:00
|
|
|
const connectionId = ProtocolDetector.createConnectionId({
|
|
|
|
socketId: context.connectionId || `${Date.now()}-${Math.random()}`
|
|
|
|
});
|
2025-05-29 01:00:20 +00:00
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
socket.once('data', async (data) => {
|
|
|
|
// Use detection module for parsing
|
|
|
|
const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
|
|
|
|
data,
|
|
|
|
connectionId,
|
|
|
|
{ extractFullHeaders: false } // We only need method and path
|
|
|
|
);
|
2025-05-29 01:00:20 +00:00
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
if (detectionResult.protocol === 'http' && detectionResult.connectionInfo.path) {
|
|
|
|
const method = detectionResult.connectionInfo.method || 'GET';
|
|
|
|
const path = detectionResult.connectionInfo.path || '/';
|
|
|
|
|
|
|
|
const domain = context.domain || 'localhost';
|
|
|
|
const port = context.port;
|
|
|
|
|
|
|
|
let finalLocation = locationTemplate
|
|
|
|
.replace('{domain}', domain)
|
|
|
|
.replace('{port}', String(port))
|
|
|
|
.replace('{path}', path)
|
|
|
|
.replace('{clientIp}', context.clientIp);
|
|
|
|
|
|
|
|
const message = `Redirecting to ${finalLocation}`;
|
|
|
|
const response = [
|
|
|
|
`HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`,
|
|
|
|
`Location: ${finalLocation}`,
|
|
|
|
'Content-Type: text/plain',
|
|
|
|
`Content-Length: ${message.length}`,
|
|
|
|
'Connection: close',
|
|
|
|
'',
|
|
|
|
message
|
|
|
|
].join('\r\n');
|
|
|
|
|
|
|
|
socket.write(response);
|
|
|
|
} else {
|
|
|
|
// Not a valid HTTP request, close connection
|
|
|
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
|
|
}
|
2025-05-29 01:00:20 +00:00
|
|
|
|
|
|
|
socket.end();
|
2025-07-21 19:40:01 +00:00
|
|
|
// Clean up detection state
|
|
|
|
ProtocolDetector.cleanupConnections();
|
2025-05-29 01:00:20 +00:00
|
|
|
});
|
2025-05-29 01:07:39 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HTTP server handler for ACME challenges and other HTTP needs
|
2025-07-21 19:40:01 +00:00
|
|
|
* Now uses the centralized detection module for HTTP parsing
|
2025-05-29 01:07:39 +00:00
|
|
|
*/
|
|
|
|
httpServer: (handler: (req: { method: string; url: string; headers: Record<string, string>; body?: string }, res: { status: (code: number) => void; header: (name: string, value: string) => void; send: (data: string) => void; end: () => void }) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
|
|
|
let requestParsed = false;
|
2025-07-21 19:40:01 +00:00
|
|
|
const connectionId = ProtocolDetector.createConnectionId({
|
|
|
|
socketId: context.connectionId || `${Date.now()}-${Math.random()}`
|
|
|
|
});
|
2025-05-29 01:07:39 +00:00
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
const processData = async (data: Buffer) => {
|
2025-05-29 01:07:39 +00:00
|
|
|
if (requestParsed) return; // Only handle the first request
|
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
// Use HttpDetector for parsing
|
|
|
|
const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
|
|
|
|
data,
|
|
|
|
connectionId,
|
|
|
|
{ extractFullHeaders: true }
|
|
|
|
);
|
2025-05-29 01:07:39 +00:00
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
if (detectionResult.protocol !== 'http' || !detectionResult.isComplete) {
|
|
|
|
// Not a complete HTTP request yet
|
|
|
|
return;
|
|
|
|
}
|
2025-05-29 01:07:39 +00:00
|
|
|
|
|
|
|
requestParsed = true;
|
2025-07-21 19:40:01 +00:00
|
|
|
const connInfo = detectionResult.connectionInfo;
|
2025-05-29 01:07:39 +00:00
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
// Create request object from detection result
|
2025-05-29 01:07:39 +00:00
|
|
|
const req = {
|
2025-07-21 19:40:01 +00:00
|
|
|
method: connInfo.method || 'GET',
|
|
|
|
url: connInfo.path || '/',
|
|
|
|
headers: connInfo.headers || {},
|
|
|
|
body: detectionResult.remainingBuffer?.toString() || ''
|
2025-05-29 01:07:39 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create response object
|
|
|
|
let statusCode = 200;
|
|
|
|
const responseHeaders: Record<string, string> = {};
|
|
|
|
let ended = false;
|
|
|
|
|
|
|
|
const res = {
|
|
|
|
status: (code: number) => {
|
|
|
|
statusCode = code;
|
|
|
|
},
|
|
|
|
header: (name: string, value: string) => {
|
|
|
|
responseHeaders[name] = value;
|
|
|
|
},
|
|
|
|
send: (data: string) => {
|
|
|
|
if (ended) return;
|
|
|
|
ended = true;
|
|
|
|
|
|
|
|
if (!responseHeaders['content-type']) {
|
|
|
|
responseHeaders['content-type'] = 'text/plain';
|
|
|
|
}
|
|
|
|
responseHeaders['content-length'] = String(data.length);
|
|
|
|
responseHeaders['connection'] = 'close';
|
|
|
|
|
|
|
|
const statusText = statusCode === 200 ? 'OK' :
|
|
|
|
statusCode === 404 ? 'Not Found' :
|
|
|
|
statusCode === 500 ? 'Internal Server Error' : 'Response';
|
|
|
|
|
|
|
|
let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`;
|
|
|
|
for (const [name, value] of Object.entries(responseHeaders)) {
|
|
|
|
response += `${name}: ${value}\r\n`;
|
|
|
|
}
|
|
|
|
response += '\r\n';
|
|
|
|
response += data;
|
|
|
|
|
|
|
|
socket.write(response);
|
|
|
|
socket.end();
|
|
|
|
},
|
|
|
|
end: () => {
|
|
|
|
if (ended) return;
|
|
|
|
ended = true;
|
|
|
|
socket.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n');
|
|
|
|
socket.end();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
handler(req, res);
|
|
|
|
// Ensure response is sent even if handler doesn't call send()
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!ended) {
|
|
|
|
res.send('');
|
|
|
|
}
|
|
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
|
|
if (!ended) {
|
|
|
|
res.status(500);
|
|
|
|
res.send('Internal Server Error');
|
|
|
|
}
|
|
|
|
}
|
2025-07-21 19:40:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
socket.on('data', processData);
|
2025-05-29 01:07:39 +00:00
|
|
|
|
|
|
|
socket.on('error', () => {
|
|
|
|
if (!requestParsed) {
|
|
|
|
socket.end();
|
|
|
|
}
|
|
|
|
});
|
2025-07-21 19:40:01 +00:00
|
|
|
|
|
|
|
socket.on('close', () => {
|
|
|
|
// Clean up detection state
|
|
|
|
ProtocolDetector.cleanupConnections();
|
|
|
|
});
|
2025-05-29 00:24:57 +00:00
|
|
|
}
|
|
|
|
};
|
2025-07-21 18:44:59 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an API Gateway route pattern
|
|
|
|
* @param domains Domain(s) to match
|
|
|
|
* @param apiBasePath Base path for API endpoints (e.g., '/api')
|
|
|
|
* @param target Target host and port
|
|
|
|
* @param options Additional route options
|
|
|
|
* @returns API route configuration
|
|
|
|
*/
|
|
|
|
export function createApiGatewayRoute(
|
|
|
|
domains: string | string[],
|
|
|
|
apiBasePath: string,
|
|
|
|
target: { host: string | string[]; port: number },
|
|
|
|
options: {
|
|
|
|
useTls?: boolean;
|
|
|
|
certificate?: 'auto' | { key: string; cert: string };
|
|
|
|
addCorsHeaders?: boolean;
|
|
|
|
[key: string]: any;
|
|
|
|
} = {}
|
|
|
|
): IRouteConfig {
|
|
|
|
// Normalize apiBasePath to ensure it starts with / and doesn't end with /
|
|
|
|
const normalizedPath = apiBasePath.startsWith('/')
|
|
|
|
? apiBasePath
|
|
|
|
: `/${apiBasePath}`;
|
|
|
|
|
|
|
|
// Add wildcard to path to match all API endpoints
|
|
|
|
const apiPath = normalizedPath.endsWith('/')
|
|
|
|
? `${normalizedPath}*`
|
|
|
|
: `${normalizedPath}/*`;
|
|
|
|
|
|
|
|
// Create base route
|
|
|
|
const baseRoute = options.useTls
|
|
|
|
? createHttpsTerminateRoute(domains, target, {
|
|
|
|
certificate: options.certificate || 'auto'
|
|
|
|
})
|
|
|
|
: createHttpRoute(domains, target);
|
|
|
|
|
|
|
|
// Add API-specific configurations
|
|
|
|
const apiRoute: Partial<IRouteConfig> = {
|
|
|
|
match: {
|
|
|
|
...baseRoute.match,
|
|
|
|
path: apiPath
|
|
|
|
},
|
|
|
|
name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
|
|
|
|
priority: options.priority || 100 // Higher priority for specific path matching
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add CORS headers if requested
|
|
|
|
if (options.addCorsHeaders) {
|
|
|
|
apiRoute.headers = {
|
|
|
|
response: {
|
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
|
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
|
|
'Access-Control-Max-Age': '86400'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return mergeRouteConfigs(baseRoute, apiRoute);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a rate limiting route pattern
|
|
|
|
* @param baseRoute Base route to add rate limiting to
|
|
|
|
* @param rateLimit Rate limiting configuration
|
|
|
|
* @returns Route with rate limiting
|
|
|
|
*/
|
|
|
|
export function addRateLimiting(
|
|
|
|
baseRoute: IRouteConfig,
|
|
|
|
rateLimit: {
|
|
|
|
maxRequests: number;
|
|
|
|
window: number; // Time window in seconds
|
|
|
|
keyBy?: 'ip' | 'path' | 'header';
|
|
|
|
headerName?: string; // Required if keyBy is 'header'
|
|
|
|
errorMessage?: string;
|
|
|
|
}
|
|
|
|
): IRouteConfig {
|
|
|
|
return mergeRouteConfigs(baseRoute, {
|
|
|
|
security: {
|
|
|
|
rateLimit: {
|
|
|
|
enabled: true,
|
|
|
|
maxRequests: rateLimit.maxRequests,
|
|
|
|
window: rateLimit.window,
|
|
|
|
keyBy: rateLimit.keyBy || 'ip',
|
|
|
|
headerName: rateLimit.headerName,
|
|
|
|
errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a basic authentication route pattern
|
|
|
|
* @param baseRoute Base route to add authentication to
|
|
|
|
* @param auth Authentication configuration
|
|
|
|
* @returns Route with basic authentication
|
|
|
|
*/
|
|
|
|
export function addBasicAuth(
|
|
|
|
baseRoute: IRouteConfig,
|
|
|
|
auth: {
|
|
|
|
users: Array<{ username: string; password: string }>;
|
|
|
|
realm?: string;
|
|
|
|
excludePaths?: string[];
|
|
|
|
}
|
|
|
|
): IRouteConfig {
|
|
|
|
return mergeRouteConfigs(baseRoute, {
|
|
|
|
security: {
|
|
|
|
basicAuth: {
|
|
|
|
enabled: true,
|
|
|
|
users: auth.users,
|
|
|
|
realm: auth.realm || 'Restricted Area',
|
|
|
|
excludePaths: auth.excludePaths || []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a JWT authentication route pattern
|
|
|
|
* @param baseRoute Base route to add JWT authentication to
|
|
|
|
* @param jwt JWT authentication configuration
|
|
|
|
* @returns Route with JWT authentication
|
|
|
|
*/
|
|
|
|
export function addJwtAuth(
|
|
|
|
baseRoute: IRouteConfig,
|
|
|
|
jwt: {
|
|
|
|
secret: string;
|
|
|
|
algorithm?: string;
|
|
|
|
issuer?: string;
|
|
|
|
audience?: string;
|
|
|
|
expiresIn?: number; // Time in seconds
|
|
|
|
excludePaths?: string[];
|
|
|
|
}
|
|
|
|
): IRouteConfig {
|
|
|
|
return mergeRouteConfigs(baseRoute, {
|
|
|
|
security: {
|
|
|
|
jwtAuth: {
|
|
|
|
enabled: true,
|
|
|
|
secret: jwt.secret,
|
|
|
|
algorithm: jwt.algorithm || 'HS256',
|
|
|
|
issuer: jwt.issuer,
|
|
|
|
audience: jwt.audience,
|
|
|
|
expiresIn: jwt.expiresIn,
|
|
|
|
excludePaths: jwt.excludePaths || []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|