feat(smartproxy): Update documentation and route helper functions; add createPortRange, createSecurityConfig, createStaticFileRoute, and createTestRoute helpers to the readme and tests. Refactor test examples to use the new helper API and remove legacy connection handling files (including the old connection handler and PortRangeManager) to fully embrace the unified route‐based configuration.

This commit is contained in:
2025-05-10 07:34:35 +00:00
parent 36bea96ac7
commit b4a0e4be6b
14 changed files with 458 additions and 193 deletions

View File

@ -0,0 +1,441 @@
import * as plugins from '../../plugins.js';
import type { IDomainConfig, ISmartProxyOptions } from './models/interfaces.js';
import type { TForwardingType, IForwardConfig } from '../../forwarding/config/forwarding-types.js';
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
import { ForwardingHandlerFactory } from '../../forwarding/factory/forwarding-factory.js';
import type { IRouteConfig } from './models/route-types.js';
import { RouteManager } from './route-manager.js';
/**
* Manages domain configurations and target selection
*/
export class DomainConfigManager {
// Track round-robin indices for domain configs
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
// Cache forwarding handlers for each domain config
private forwardingHandlers: Map<IDomainConfig, ForwardingHandler> = new Map();
// Store derived domain configs from routes
private derivedDomainConfigs: IDomainConfig[] = [];
// Reference to RouteManager for route-based configuration
private routeManager?: RouteManager;
constructor(private settings: ISmartProxyOptions) {
// Initialize with derived domain configs if using route-based configuration
if (settings.routes && !settings.domainConfigs) {
this.generateDomainConfigsFromRoutes();
}
}
/**
* Set the route manager reference for route-based queries
*/
public setRouteManager(routeManager: RouteManager): void {
this.routeManager = routeManager;
// Regenerate domain configs from routes if needed
if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) {
this.generateDomainConfigsFromRoutes();
}
}
/**
* Generate domain configs from routes
*/
public generateDomainConfigsFromRoutes(): void {
this.derivedDomainConfigs = [];
if (!this.settings.routes) return;
for (const route of this.settings.routes) {
if (route.action.type !== 'forward' || !route.match.domains) continue;
// Convert route to domain config
const domainConfig = this.routeToDomainConfig(route);
if (domainConfig) {
this.derivedDomainConfigs.push(domainConfig);
}
}
}
/**
* Convert a route to a domain config
*/
private routeToDomainConfig(route: IRouteConfig): IDomainConfig | null {
if (route.action.type !== 'forward' || !route.action.target) return null;
// Get domains from route
const domains = Array.isArray(route.match.domains) ?
route.match.domains :
(route.match.domains ? [route.match.domains] : []);
if (domains.length === 0) return null;
// Determine forwarding type based on TLS mode
let forwardingType: TForwardingType = 'http-only';
if (route.action.tls) {
switch (route.action.tls.mode) {
case 'passthrough':
forwardingType = 'https-passthrough';
break;
case 'terminate':
forwardingType = 'https-terminate-to-http';
break;
case 'terminate-and-reencrypt':
forwardingType = 'https-terminate-to-https';
break;
}
}
// Create domain config
return {
domains,
forwarding: {
type: forwardingType,
target: {
host: route.action.target.host,
port: route.action.target.port
},
security: route.action.security ? {
allowedIps: route.action.security.allowedIps,
blockedIps: route.action.security.blockedIps,
maxConnections: route.action.security.maxConnections
} : undefined,
https: route.action.tls && route.action.tls.certificate !== 'auto' ? {
customCert: route.action.tls.certificate
} : undefined,
advanced: route.action.advanced
}
};
}
/**
* Updates the domain configurations
*/
public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void {
// If we're using domainConfigs property, update it
if (this.settings.domainConfigs) {
this.settings.domainConfigs = newDomainConfigs;
} else {
// Otherwise update our derived configs
this.derivedDomainConfigs = newDomainConfigs;
}
// Reset target indices for removed configs
const currentConfigSet = new Set(newDomainConfigs);
for (const [config] of this.domainTargetIndices) {
if (!currentConfigSet.has(config)) {
this.domainTargetIndices.delete(config);
}
}
// Clear handlers for removed configs and create handlers for new configs
const handlersToRemove: IDomainConfig[] = [];
for (const [config] of this.forwardingHandlers) {
if (!currentConfigSet.has(config)) {
handlersToRemove.push(config);
}
}
// Remove handlers that are no longer needed
for (const config of handlersToRemove) {
this.forwardingHandlers.delete(config);
}
// Create handlers for new configs
for (const config of newDomainConfigs) {
if (!this.forwardingHandlers.has(config)) {
try {
const handler = this.createForwardingHandler(config);
this.forwardingHandlers.set(config, handler);
} catch (err) {
console.log(`Error creating forwarding handler for domain ${config.domains.join(', ')}: ${err}`);
}
}
}
}
/**
* Get all domain configurations
*/
public getDomainConfigs(): IDomainConfig[] {
// Use domainConfigs from settings if available, otherwise use derived configs
return this.settings.domainConfigs || this.derivedDomainConfigs;
}
/**
* Find domain config matching a server name
*/
public findDomainConfig(serverName: string): IDomainConfig | undefined {
if (!serverName) return undefined;
// Get domain configs from the appropriate source
const domainConfigs = this.getDomainConfigs();
// Check for direct match
for (const config of domainConfigs) {
if (config.domains.some(d => plugins.minimatch(serverName, d))) {
return config;
}
}
// No match found
return undefined;
}
/**
* Find domain config for a specific port
*/
public findDomainConfigForPort(port: number): IDomainConfig | undefined {
// Get domain configs from the appropriate source
const domainConfigs = this.getDomainConfigs();
// Check if any domain config has a matching port range
for (const domain of domainConfigs) {
const portRanges = domain.forwarding?.advanced?.portRanges;
if (portRanges && portRanges.length > 0 && this.isPortInRanges(port, portRanges)) {
return domain;
}
}
// If we're in route-based mode, also check routes for this port
if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) {
const routesForPort = this.settings.routes.filter(route => {
// Check if this port is in the route's ports
if (typeof route.match.ports === 'number') {
return route.match.ports === port;
} else if (Array.isArray(route.match.ports)) {
return route.match.ports.some(p => {
if (typeof p === 'number') {
return p === port;
} else if (p.from && p.to) {
return port >= p.from && port <= p.to;
}
return false;
});
}
return false;
});
// If we found any routes for this port, convert the first one to a domain config
if (routesForPort.length > 0 && routesForPort[0].action.type === 'forward') {
const domainConfig = this.routeToDomainConfig(routesForPort[0]);
if (domainConfig) {
return domainConfig;
}
}
}
return undefined;
}
/**
* Check if a port is within any of the given ranges
*/
public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean {
return ranges.some((range) => port >= range.from && port <= range.to);
}
/**
* Get target IP with round-robin support
*/
public getTargetIP(domainConfig: IDomainConfig): string {
const targetHosts = Array.isArray(domainConfig.forwarding.target.host)
? domainConfig.forwarding.target.host
: [domainConfig.forwarding.target.host];
if (targetHosts.length > 0) {
const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
const ip = targetHosts[currentIndex % targetHosts.length];
this.domainTargetIndices.set(domainConfig, currentIndex + 1);
return ip;
}
return this.settings.targetIP || 'localhost';
}
/**
* Get target host with round-robin support (for tests)
* This is just an alias for getTargetIP for easier test compatibility
*/
public getTargetHost(domainConfig: IDomainConfig): string {
return this.getTargetIP(domainConfig);
}
/**
* Get target port from domain config
*/
public getTargetPort(domainConfig: IDomainConfig, defaultPort: number): number {
return domainConfig.forwarding.target.port || defaultPort;
}
/**
* Checks if a domain should use NetworkProxy
*/
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
const forwardingType = this.getForwardingType(domainConfig);
return forwardingType === 'https-terminate-to-http' ||
forwardingType === 'https-terminate-to-https';
}
/**
* Gets the NetworkProxy port for a domain
*/
public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined {
// First check if we should use NetworkProxy at all
if (!this.shouldUseNetworkProxy(domainConfig)) {
return undefined;
}
return domainConfig.forwarding.advanced?.networkProxyPort || this.settings.networkProxyPort;
}
/**
* Get effective allowed and blocked IPs for a domain
*
* This method combines domain-specific security rules from the forwarding configuration
* with global security defaults when necessary.
*/
public getEffectiveIPRules(domainConfig: IDomainConfig): {
allowedIPs: string[],
blockedIPs: string[]
} {
// Start with empty arrays
const allowedIPs: string[] = [];
const blockedIPs: string[] = [];
// Add IPs from forwarding security settings if available
if (domainConfig.forwarding?.security?.allowedIps) {
allowedIPs.push(...domainConfig.forwarding.security.allowedIps);
} else {
// If no allowed IPs are specified in forwarding config and global defaults exist, use them
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
allowedIPs.push(...this.settings.defaultAllowedIPs);
} else {
// Default to allow all if no specific rules
allowedIPs.push('*');
}
}
// Add blocked IPs from forwarding security settings if available
if (domainConfig.forwarding?.security?.blockedIps) {
blockedIPs.push(...domainConfig.forwarding.security.blockedIps);
}
// Always add global blocked IPs, even if domain has its own rules
// This ensures that global blocks take precedence
if (this.settings.defaultBlockedIPs && this.settings.defaultBlockedIPs.length > 0) {
// Add only unique IPs that aren't already in the list
for (const ip of this.settings.defaultBlockedIPs) {
if (!blockedIPs.includes(ip)) {
blockedIPs.push(ip);
}
}
}
return {
allowedIPs,
blockedIPs
};
}
/**
* Get connection timeout for a domain
*/
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
if (domainConfig?.forwarding.advanced?.timeout) {
return domainConfig.forwarding.advanced.timeout;
}
return this.settings.maxConnectionLifetime || 86400000; // 24 hours default
}
/**
* Creates a forwarding handler for a domain configuration
*/
private createForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler {
// Create a new handler using the factory
const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding);
// Initialize the handler
handler.initialize().catch(err => {
console.log(`Error initializing forwarding handler for ${domainConfig.domains.join(', ')}: ${err}`);
});
return handler;
}
/**
* Gets a forwarding handler for a domain config
* If no handler exists, creates one
*/
public getForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler {
// If we already have a handler, return it
if (this.forwardingHandlers.has(domainConfig)) {
return this.forwardingHandlers.get(domainConfig)!;
}
// Otherwise create a new handler
const handler = this.createForwardingHandler(domainConfig);
this.forwardingHandlers.set(domainConfig, handler);
return handler;
}
/**
* Gets the forwarding type for a domain config
*/
public getForwardingType(domainConfig?: IDomainConfig): TForwardingType | undefined {
if (!domainConfig?.forwarding) return undefined;
return domainConfig.forwarding.type;
}
/**
* Checks if the forwarding type requires TLS termination
*/
public requiresTlsTermination(domainConfig?: IDomainConfig): boolean {
if (!domainConfig) return false;
const forwardingType = this.getForwardingType(domainConfig);
return forwardingType === 'https-terminate-to-http' ||
forwardingType === 'https-terminate-to-https';
}
/**
* Checks if the forwarding type supports HTTP
*/
public supportsHttp(domainConfig?: IDomainConfig): boolean {
if (!domainConfig) return false;
const forwardingType = this.getForwardingType(domainConfig);
// HTTP-only always supports HTTP
if (forwardingType === 'http-only') return true;
// For termination types, check the HTTP settings
if (forwardingType === 'https-terminate-to-http' ||
forwardingType === 'https-terminate-to-https') {
// HTTP is supported by default for termination types
return domainConfig.forwarding?.http?.enabled !== false;
}
// HTTPS-passthrough doesn't support HTTP
return false;
}
/**
* Checks if HTTP requests should be redirected to HTTPS
*/
public shouldRedirectToHttps(domainConfig?: IDomainConfig): boolean {
if (!domainConfig?.forwarding) return false;
// Only check for redirect if HTTP is enabled
if (this.supportsHttp(domainConfig)) {
return !!domainConfig.forwarding.http?.redirectToHttps;
}
return false;
}
}