feat(proxies): introduce nftables command executor and utilities, default certificate provider, expanded route/socket helper modules, and security improvements
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { SmartProxy } from './smart-proxy.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
||||
import { isIPAuthorized, normalizeIP } from '../../core/utils/security-utils.js';
|
||||
|
||||
/**
|
||||
* Handles security aspects like IP tracking, rate limiting, and authorization
|
||||
* for SmartProxy. This is a lightweight wrapper that uses shared utilities.
|
||||
*/
|
||||
export class SecurityManager {
|
||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||
@@ -15,14 +16,22 @@ export class SecurityManager {
|
||||
// Start periodic cleanup every 60 seconds
|
||||
this.startPeriodicCleanup();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get connections count by IP
|
||||
* Get connections count by IP (checks normalized variants)
|
||||
*/
|
||||
public getConnectionCountByIP(ip: string): number {
|
||||
return this.connectionsByIP.get(ip)?.size || 0;
|
||||
// Check all normalized variants of the IP
|
||||
const variants = normalizeIP(ip);
|
||||
for (const variant of variants) {
|
||||
const connections = this.connectionsByIP.get(variant);
|
||||
if (connections) {
|
||||
return connections.size;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check and update connection rate for an IP
|
||||
* @returns true if within rate limit, false if exceeding limit
|
||||
@@ -31,43 +40,73 @@ export class SecurityManager {
|
||||
const now = Date.now();
|
||||
const minute = 60 * 1000;
|
||||
|
||||
if (!this.connectionRateByIP.has(ip)) {
|
||||
this.connectionRateByIP.set(ip, [now]);
|
||||
// Find existing rate tracking (check normalized variants)
|
||||
const variants = normalizeIP(ip);
|
||||
let existingKey: string | null = null;
|
||||
for (const variant of variants) {
|
||||
if (this.connectionRateByIP.has(variant)) {
|
||||
existingKey = variant;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const key = existingKey || ip;
|
||||
|
||||
if (!this.connectionRateByIP.has(key)) {
|
||||
this.connectionRateByIP.set(key, [now]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get timestamps and filter out entries older than 1 minute
|
||||
const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
|
||||
const timestamps = this.connectionRateByIP.get(key)!.filter((time) => now - time < minute);
|
||||
timestamps.push(now);
|
||||
this.connectionRateByIP.set(ip, timestamps);
|
||||
this.connectionRateByIP.set(key, timestamps);
|
||||
|
||||
// Check if rate exceeds limit
|
||||
return timestamps.length <= this.smartProxy.settings.connectionRateLimitPerMinute!;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Track connection by IP
|
||||
*/
|
||||
public trackConnectionByIP(ip: string, connectionId: string): void {
|
||||
if (!this.connectionsByIP.has(ip)) {
|
||||
this.connectionsByIP.set(ip, new Set());
|
||||
// Check if any variant already exists
|
||||
const variants = normalizeIP(ip);
|
||||
let existingKey: string | null = null;
|
||||
|
||||
for (const variant of variants) {
|
||||
if (this.connectionsByIP.has(variant)) {
|
||||
existingKey = variant;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.connectionsByIP.get(ip)!.add(connectionId);
|
||||
|
||||
const key = existingKey || ip;
|
||||
if (!this.connectionsByIP.has(key)) {
|
||||
this.connectionsByIP.set(key, new Set());
|
||||
}
|
||||
this.connectionsByIP.get(key)!.add(connectionId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove connection tracking for an IP
|
||||
*/
|
||||
public removeConnectionByIP(ip: string, connectionId: string): void {
|
||||
if (this.connectionsByIP.has(ip)) {
|
||||
const connections = this.connectionsByIP.get(ip)!;
|
||||
connections.delete(connectionId);
|
||||
if (connections.size === 0) {
|
||||
this.connectionsByIP.delete(ip);
|
||||
// Check all variants to find where the connection is tracked
|
||||
const variants = normalizeIP(ip);
|
||||
|
||||
for (const variant of variants) {
|
||||
if (this.connectionsByIP.has(variant)) {
|
||||
const connections = this.connectionsByIP.get(variant)!;
|
||||
connections.delete(connectionId);
|
||||
if (connections.size === 0) {
|
||||
this.connectionsByIP.delete(variant);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if an IP is authorized using security rules
|
||||
*
|
||||
@@ -81,71 +120,7 @@ export class SecurityManager {
|
||||
* @returns true if IP is authorized, false if blocked
|
||||
*/
|
||||
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
||||
// Skip IP validation if allowedIPs is empty
|
||||
if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// First check if IP is blocked - blocked IPs take precedence
|
||||
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check if IP is allowed
|
||||
return this.isGlobIPMatch(ip, allowedIPs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the IP matches any of the glob patterns from security configuration
|
||||
*
|
||||
* This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
|
||||
* It's used to implement IP filtering based on the route.security configuration.
|
||||
*
|
||||
* @param ip - The IP address to check
|
||||
* @param patterns - Array of glob patterns from security.ipAllowList or ipBlockList
|
||||
* @returns true if IP matches any pattern, false otherwise
|
||||
*/
|
||||
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
|
||||
if (!ip || !patterns || patterns.length === 0) return false;
|
||||
|
||||
// Handle IPv4/IPv6 normalization for proper matching
|
||||
const normalizeIP = (ip: string): string[] => {
|
||||
if (!ip) return [];
|
||||
// Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
|
||||
if (ip.startsWith('::ffff:')) {
|
||||
const ipv4 = ip.slice(7);
|
||||
return [ip, ipv4];
|
||||
}
|
||||
// Handle IPv4 addresses by also checking IPv4-mapped form
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
||||
return [ip, `::ffff:${ip}`];
|
||||
}
|
||||
return [ip];
|
||||
};
|
||||
|
||||
// Normalize the IP being checked
|
||||
const normalizedIPVariants = normalizeIP(ip);
|
||||
if (normalizedIPVariants.length === 0) return false;
|
||||
|
||||
// Expand shorthand patterns and normalize IPs for consistent comparison
|
||||
const expandShorthand = (pattern: string): string => {
|
||||
// Expand shorthand IP patterns like '192.168.*' to '192.168.*.*'
|
||||
if (pattern.includes('*') && !pattern.includes(':')) {
|
||||
const parts = pattern.split('.');
|
||||
while (parts.length < 4) {
|
||||
parts.push('*');
|
||||
}
|
||||
return parts.join('.');
|
||||
}
|
||||
return pattern;
|
||||
};
|
||||
|
||||
const expandedPatterns = patterns.map(expandShorthand).flatMap(normalizeIP);
|
||||
|
||||
// Check for any match between normalized IP variants and patterns
|
||||
return normalizedIPVariants.some((ipVariant) =>
|
||||
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
||||
);
|
||||
return isIPAuthorized(ip, allowedIPs, blockedIPs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
144
ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts
Normal file
144
ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* API Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating API route configurations.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js';
|
||||
import { mergeRouteConfigs } from '../route-utils.js';
|
||||
import { createHttpRoute } from './http-helpers.js';
|
||||
import { createHttpsTerminateRoute } from './https-helpers.js';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
targets: [target]
|
||||
};
|
||||
|
||||
// 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 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);
|
||||
}
|
||||
124
ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts
Normal file
124
ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Dynamic Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating dynamic routes
|
||||
* with context-based host and port mapping.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange, IRouteContext } from '../../models/route-types.js';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
targets: [{
|
||||
host: options.targetHost,
|
||||
port: options.portMapper
|
||||
}]
|
||||
};
|
||||
|
||||
// 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',
|
||||
targets: [{
|
||||
host: options.targetHost,
|
||||
port: options.portMapper
|
||||
}]
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `Dynamic Route for ${options.domains || 'all domains'}`,
|
||||
priority: options.priority,
|
||||
...options
|
||||
};
|
||||
}
|
||||
40
ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts
Normal file
40
ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* HTTP Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating HTTP route configurations.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
targets: [target]
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `HTTP Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...options
|
||||
};
|
||||
}
|
||||
163
ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts
Normal file
163
ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* HTTPS Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating HTTPS route configurations
|
||||
* including TLS termination and passthrough routes.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js';
|
||||
import { SocketHandlers } from './socket-handlers.js';
|
||||
|
||||
/**
|
||||
* Create an HTTPS route with 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 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',
|
||||
targets: [target],
|
||||
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 = {
|
||||
type: 'socket-handler',
|
||||
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
|
||||
};
|
||||
|
||||
// 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',
|
||||
targets: [target],
|
||||
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];
|
||||
}
|
||||
62
ts/proxies/smart-proxy/utils/route-helpers/index.ts
Normal file
62
ts/proxies/smart-proxy/utils/route-helpers/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Route Helper Functions
|
||||
*
|
||||
* This module 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 barrel file re-exports all helper functions for backwards compatibility.
|
||||
*/
|
||||
|
||||
// HTTP helpers
|
||||
export { createHttpRoute } from './http-helpers.js';
|
||||
|
||||
// HTTPS helpers
|
||||
export {
|
||||
createHttpsTerminateRoute,
|
||||
createHttpToHttpsRedirect,
|
||||
createHttpsPassthroughRoute,
|
||||
createCompleteHttpsServer
|
||||
} from './https-helpers.js';
|
||||
|
||||
// WebSocket helpers
|
||||
export { createWebSocketRoute } from './websocket-helpers.js';
|
||||
|
||||
// Load balancer helpers
|
||||
export {
|
||||
createLoadBalancerRoute,
|
||||
createSmartLoadBalancer
|
||||
} from './load-balancer-helpers.js';
|
||||
|
||||
// NFTables helpers
|
||||
export {
|
||||
createNfTablesRoute,
|
||||
createNfTablesTerminateRoute,
|
||||
createCompleteNfTablesHttpsServer
|
||||
} from './nftables-helpers.js';
|
||||
|
||||
// Dynamic routing helpers
|
||||
export {
|
||||
createPortOffset,
|
||||
createPortMappingRoute,
|
||||
createOffsetPortMappingRoute,
|
||||
createDynamicRoute
|
||||
} from './dynamic-helpers.js';
|
||||
|
||||
// API helpers
|
||||
export {
|
||||
createApiRoute,
|
||||
createApiGatewayRoute
|
||||
} from './api-helpers.js';
|
||||
|
||||
// Security helpers
|
||||
export {
|
||||
addRateLimiting,
|
||||
addBasicAuth,
|
||||
addJwtAuth
|
||||
} from './security-helpers.js';
|
||||
|
||||
// Socket handlers
|
||||
export {
|
||||
SocketHandlers,
|
||||
createSocketHandlerRoute
|
||||
} from './socket-handlers.js';
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Load Balancer Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating load balancer route configurations.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../../models/route-types.js';
|
||||
|
||||
/**
|
||||
* Create a load balancer route (round-robin between multiple backend hosts)
|
||||
* @param domains Domain(s) to match
|
||||
* @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)
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createLoadBalancerRoute(
|
||||
domains: string | string[],
|
||||
backendsOrHosts: Array<{ host: string; port: number }> | string[],
|
||||
portOrOptions?: number | {
|
||||
tls?: {
|
||||
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
};
|
||||
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;
|
||||
};
|
||||
[key: string]: any;
|
||||
},
|
||||
options?: {
|
||||
tls?: {
|
||||
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
): IRouteConfig {
|
||||
// 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);
|
||||
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80),
|
||||
domains
|
||||
};
|
||||
|
||||
// Create route target
|
||||
const target: IRouteTarget = {
|
||||
host: hosts,
|
||||
port
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
targets: [target]
|
||||
};
|
||||
|
||||
// Add TLS configuration if provided
|
||||
if (finalOptions.tls || finalOptions.useTls) {
|
||||
action.tls = {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
...finalOptions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
targets: [{
|
||||
host: hostSelector,
|
||||
port: options.portMapper
|
||||
}]
|
||||
};
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: options.name || `Smart Load Balancer for ${domains.join(', ')}`,
|
||||
priority: options.priority,
|
||||
...options
|
||||
};
|
||||
}
|
||||
202
ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts
Normal file
202
ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* NFTables Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating NFTables-based route configurations
|
||||
* for high-performance packet forwarding at the kernel level.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange } from '../../models/route-types.js';
|
||||
import { createHttpToHttpsRedirect } from './https-helpers.js';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
targets: [{
|
||||
host: target.host,
|
||||
port: target.port
|
||||
}],
|
||||
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
|
||||
const routeConfig: IRouteConfig = {
|
||||
name,
|
||||
match,
|
||||
action
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Security Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for adding security features to routes.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig } from '../../models/route-types.js';
|
||||
import { mergeRouteConfigs } from '../route-utils.js';
|
||||
|
||||
/**
|
||||
* 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 || []
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
337
ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts
Normal file
337
ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Socket Handler Functions
|
||||
*
|
||||
* This module provides pre-built socket handlers for common use cases
|
||||
* like echoing, proxying, HTTP responses, and redirects.
|
||||
*/
|
||||
|
||||
import * as plugins from '../../../../plugins.js';
|
||||
import type { IRouteConfig, TPortRange, IRouteContext } from '../../models/route-types.js';
|
||||
import { ProtocolDetector } from '../../../../detection/index.js';
|
||||
import { createSocketTracker } from '../../../../core/utils/socket-tracker.js';
|
||||
|
||||
/**
|
||||
* Pre-built socket handlers for common use cases
|
||||
*/
|
||||
export const SocketHandlers = {
|
||||
/**
|
||||
* Simple echo server handler
|
||||
*/
|
||||
echo: (socket: plugins.net.Socket, context: IRouteContext) => {
|
||||
socket.write('ECHO SERVER READY\n');
|
||||
socket.on('data', data => socket.write(data));
|
||||
},
|
||||
|
||||
/**
|
||||
* TCP proxy handler
|
||||
*/
|
||||
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
||||
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
|
||||
*/
|
||||
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
||||
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)
|
||||
*/
|
||||
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
||||
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();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Uses the centralized detection module for HTTP parsing
|
||||
*/
|
||||
httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
|
||||
const tracker = createSocketTracker(socket);
|
||||
const connectionId = ProtocolDetector.createConnectionId({
|
||||
socketId: context.connectionId || `${Date.now()}-${Math.random()}`
|
||||
});
|
||||
|
||||
const handleData = async (data: Buffer) => {
|
||||
// Use detection module for parsing
|
||||
const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
|
||||
data,
|
||||
connectionId,
|
||||
{ extractFullHeaders: false } // We only need method and path
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
socket.end();
|
||||
// Clean up detection state
|
||||
ProtocolDetector.cleanupConnections();
|
||||
// Clean up all tracked resources
|
||||
tracker.cleanup();
|
||||
};
|
||||
|
||||
// Use tracker to manage the listener
|
||||
socket.once('data', handleData);
|
||||
|
||||
tracker.addListener('error', (err) => {
|
||||
tracker.safeDestroy(err);
|
||||
});
|
||||
|
||||
tracker.addListener('close', () => {
|
||||
tracker.cleanup();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP server handler for ACME challenges and other HTTP needs
|
||||
* Uses the centralized detection module for HTTP parsing
|
||||
*/
|
||||
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) => {
|
||||
const tracker = createSocketTracker(socket);
|
||||
let requestParsed = false;
|
||||
let responseTimer: NodeJS.Timeout | null = null;
|
||||
const connectionId = ProtocolDetector.createConnectionId({
|
||||
socketId: context.connectionId || `${Date.now()}-${Math.random()}`
|
||||
});
|
||||
|
||||
const processData = async (data: Buffer) => {
|
||||
if (requestParsed) return; // Only handle the first request
|
||||
|
||||
// Use HttpDetector for parsing
|
||||
const detectionResult = await ProtocolDetector.detectWithConnectionTracking(
|
||||
data,
|
||||
connectionId,
|
||||
{ extractFullHeaders: true }
|
||||
);
|
||||
|
||||
if (detectionResult.protocol !== 'http' || !detectionResult.isComplete) {
|
||||
// Not a complete HTTP request yet
|
||||
return;
|
||||
}
|
||||
|
||||
requestParsed = true;
|
||||
// Remove data listener after parsing request
|
||||
socket.removeListener('data', processData);
|
||||
const connInfo = detectionResult.connectionInfo;
|
||||
|
||||
// Create request object from detection result
|
||||
const req = {
|
||||
method: connInfo.method || 'GET',
|
||||
url: connInfo.path || '/',
|
||||
headers: connInfo.headers || {},
|
||||
body: detectionResult.remainingBuffer?.toString() || ''
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
// Clear response timer since we're sending now
|
||||
if (responseTimer) {
|
||||
clearTimeout(responseTimer);
|
||||
responseTimer = null;
|
||||
}
|
||||
|
||||
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()
|
||||
responseTimer = setTimeout(() => {
|
||||
if (!ended) {
|
||||
res.send('');
|
||||
}
|
||||
responseTimer = null;
|
||||
}, 1000);
|
||||
// Track and unref the timer
|
||||
tracker.addTimer(responseTimer);
|
||||
} catch (error) {
|
||||
if (!ended) {
|
||||
res.status(500);
|
||||
res.send('Internal Server Error');
|
||||
}
|
||||
// Use safeDestroy for error cases
|
||||
tracker.safeDestroy(error instanceof Error ? error : new Error('Handler error'));
|
||||
}
|
||||
};
|
||||
|
||||
// Use tracker to manage listeners
|
||||
tracker.addListener('data', processData);
|
||||
|
||||
tracker.addListener('error', (err) => {
|
||||
if (!requestParsed) {
|
||||
tracker.safeDestroy(err);
|
||||
}
|
||||
});
|
||||
|
||||
tracker.addListener('close', () => {
|
||||
// Clear any pending response timer
|
||||
if (responseTimer) {
|
||||
clearTimeout(responseTimer);
|
||||
responseTimer = null;
|
||||
}
|
||||
// Clean up detection state
|
||||
ProtocolDetector.cleanupConnections();
|
||||
// Clean up all tracked resources
|
||||
tracker.cleanup();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* WebSocket Route Helper Functions
|
||||
*
|
||||
* This module provides utility functions for creating WebSocket route configurations.
|
||||
*/
|
||||
|
||||
import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js';
|
||||
|
||||
/**
|
||||
* Create a WebSocket route configuration
|
||||
* @param domains Domain(s) to match
|
||||
* @param targetOrPath Target server OR WebSocket path (legacy)
|
||||
* @param targetOrOptions Target server (legacy) OR options
|
||||
* @param options Additional route options (legacy)
|
||||
* @returns Route configuration object
|
||||
*/
|
||||
export function createWebSocketRoute(
|
||||
domains: string | string[],
|
||||
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?: {
|
||||
useTls?: boolean;
|
||||
certificate?: 'auto' | { key: string; cert: string };
|
||||
httpPort?: number | number[];
|
||||
httpsPort?: number | number[];
|
||||
pingInterval?: number;
|
||||
pingTimeout?: number;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
): IRouteConfig {
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Normalize WebSocket path
|
||||
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
||||
|
||||
// Create route match
|
||||
const match: IRouteMatch = {
|
||||
ports: finalOptions.useTls
|
||||
? (finalOptions.httpsPort || 443)
|
||||
: (finalOptions.httpPort || 80),
|
||||
domains,
|
||||
path: normalizedPath
|
||||
};
|
||||
|
||||
// Create route action
|
||||
const action: IRouteAction = {
|
||||
type: 'forward',
|
||||
targets: [target],
|
||||
websocket: {
|
||||
enabled: true,
|
||||
pingInterval: finalOptions.pingInterval || 30000, // 30 seconds
|
||||
pingTimeout: finalOptions.pingTimeout || 5000 // 5 seconds
|
||||
}
|
||||
};
|
||||
|
||||
// Add TLS configuration if using HTTPS
|
||||
if (finalOptions.useTls) {
|
||||
action.tls = {
|
||||
mode: 'terminate',
|
||||
certificate: finalOptions.certificate || 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
// Create the route config
|
||||
return {
|
||||
match,
|
||||
action,
|
||||
name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||
priority: finalOptions.priority || 100, // Higher priority for WebSocket routes
|
||||
...finalOptions
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user