feat(core): Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.

This commit is contained in:
Philipp Kunz 2025-03-11 11:34:29 +00:00
parent d2ad659d37
commit e31c84493f
5 changed files with 526 additions and 413 deletions

View File

@ -1,5 +1,14 @@
# Changelog
## 2025-03-11 - 3.34.0 - feat(core)
Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.
- Added support for TLD wildcard matching (e.g., 'example.*') to improve domain routing.
- Implemented complex wildcard pattern matching (e.g., '*.lossless*') in the router.
- Enhanced NetworkProxy integration by initializing a single NetworkProxy instance and forwarding TLS connections accordingly.
- Refactored TLS renegotiation handling to terminate connections on SNI mismatch for stricter enforcement.
- Updated tests to cover the new wildcard matching scenarios.
## 2025-03-11 - 3.33.0 - feat(portproxy)
Add browser-friendly mode and SNI renegotiation configuration options to PortProxy

View File

@ -197,6 +197,52 @@ tap.test('should match wildcard subdomains', async () => {
expect(result).toEqual(wildcardConfig);
});
// Test TLD wildcards (example.*)
tap.test('should match TLD wildcards', async () => {
const tldWildcardConfig = createProxyConfig('example.*');
router.setNewProxyConfigs([tldWildcardConfig]);
// Test that example.com matches example.*
const req1 = createMockRequest('example.com');
const result1 = router.routeReq(req1);
expect(result1).toBeTruthy();
expect(result1).toEqual(tldWildcardConfig);
// Test that example.org matches example.*
const req2 = createMockRequest('example.org');
const result2 = router.routeReq(req2);
expect(result2).toBeTruthy();
expect(result2).toEqual(tldWildcardConfig);
// Test that subdomain.example.com doesn't match example.*
const req3 = createMockRequest('subdomain.example.com');
const result3 = router.routeReq(req3);
expect(result3).toBeUndefined();
});
// Test complex pattern matching (*.lossless*)
tap.test('should match complex wildcard patterns', async () => {
const complexWildcardConfig = createProxyConfig('*.lossless*');
router.setNewProxyConfigs([complexWildcardConfig]);
// Test that sub.lossless.com matches *.lossless*
const req1 = createMockRequest('sub.lossless.com');
const result1 = router.routeReq(req1);
expect(result1).toBeTruthy();
expect(result1).toEqual(complexWildcardConfig);
// Test that api.lossless.org matches *.lossless*
const req2 = createMockRequest('api.lossless.org');
const result2 = router.routeReq(req2);
expect(result2).toBeTruthy();
expect(result2).toEqual(complexWildcardConfig);
// Test that losslessapi.com matches *.lossless*
const req3 = createMockRequest('losslessapi.com');
const result3 = router.routeReq(req3);
expect(result3).toBeUndefined(); // Should not match as it doesn't have a subdomain
});
// Test default configuration fallback
tap.test('should fall back to default configuration', async () => {
const defaultConfig = createProxyConfig('*');

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.33.0',
version: '3.34.0',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
}

View File

@ -10,10 +10,6 @@ export interface IDomainConfig {
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
// Allow domain-specific timeout override
connectionTimeout?: number; // Connection timeout override (ms)
// New properties for NetworkProxy integration
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
}
/** Port proxy settings including global allowed port ranges */
@ -61,12 +57,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
// New property for NetworkProxy integration
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
// Browser optimization settings
browserFriendlyMode?: boolean; // Optimizes handling for browser connections
allowRenegotiationWithDifferentSNI?: boolean; // Allows SNI changes during renegotiation
relatedDomainPatterns?: string[][]; // Patterns for domains that should be allowed to share connections
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
}
/**
@ -102,11 +94,10 @@ interface IConnectionRecord {
incomingTerminationReason?: string | null; // Reason for incoming termination
outgoingTerminationReason?: string | null; // Reason for outgoing termination
// New field for NetworkProxy tracking
// NetworkProxy tracking
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
networkProxyIndex?: number; // Which NetworkProxy instance is being used
// New field for renegotiation handler
// Renegotiation handler
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
// Browser connection tracking
@ -301,35 +292,6 @@ function isClientHello(buffer: Buffer): boolean {
}
}
/**
* Checks if two domains are related based on configured patterns
* @param domain1 - First domain name
* @param domain2 - Second domain name
* @param relatedPatterns - Array of domain pattern groups where domains in the same group are considered related
* @returns true if domains are related, false otherwise
*/
function areDomainsRelated(
domain1: string,
domain2: string,
relatedPatterns?: string[][]
): boolean {
// Only exact same domains or empty domains are automatically related
if (!domain1 || !domain2 || domain1 === domain2) return true;
// Check against configured related domain patterns - the ONLY source of truth
if (relatedPatterns && relatedPatterns.length > 0) {
for (const patternGroup of relatedPatterns) {
const domain1Matches = patternGroup.some((pattern) => plugins.minimatch(domain1, pattern));
const domain2Matches = patternGroup.some((pattern) => plugins.minimatch(domain2, pattern));
if (domain1Matches && domain2Matches) return true;
}
}
// If no patterns match, domains are not related
return false;
}
// Helper: Check if a port falls within any of the given port ranges
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
return ranges.some((range) => port >= range.from && port <= range.to);
@ -413,8 +375,8 @@ export class PortProxy {
private connectionsByIP: Map<string, Set<string>> = new Map();
private connectionRateByIP: Map<string, number[]> = new Map();
// New property to store NetworkProxy instances
private networkProxies: NetworkProxy[] = [];
// NetworkProxy instance for TLS termination
private networkProxy: NetworkProxy | null = null;
constructor(settingsArg: IPortProxySettings) {
// Set reasonable defaults for all settings
@ -434,34 +396,49 @@ export class PortProxy {
// Socket optimization settings
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
// Feature flags
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
enableKeepAliveProbes:
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
? settingsArg.enableKeepAliveProbes : true,
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
// Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
// Enhanced keep-alive settings
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
// Browser optimization settings (new)
browserFriendlyMode: settingsArg.browserFriendlyMode || true, // On by default
allowRenegotiationWithDifferentSNI: settingsArg.allowRenegotiationWithDifferentSNI || false, // Off by default
relatedDomainPatterns: settingsArg.relatedDomainPatterns || [], // Empty by default
// NetworkProxy settings
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
};
// Store NetworkProxy instances if provided
this.networkProxies = settingsArg.networkProxies || [];
// Initialize NetworkProxy if enabled
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
this.initializeNetworkProxy();
}
}
/**
* Initialize NetworkProxy instance
*/
private initializeNetworkProxy(): void {
if (!this.networkProxy) {
this.networkProxy = new NetworkProxy({
port: this.settings.networkProxyPort!,
portProxyIntegration: true,
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
});
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
}
}
/**
@ -469,45 +446,36 @@ export class PortProxy {
* @param connectionId - Unique connection identifier
* @param socket - The incoming client socket
* @param record - The connection record
* @param domainConfig - The domain configuration
* @param initialData - Initial data chunk (TLS ClientHello)
* @param serverName - SNI hostname (if available)
*/
private forwardToNetworkProxy(
connectionId: string,
socket: plugins.net.Socket,
record: IConnectionRecord,
domainConfig: IDomainConfig,
initialData: Buffer,
serverName?: string
initialData: Buffer
): void {
// Determine which NetworkProxy to use
const proxyIndex =
domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
// Validate the NetworkProxy index
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
// Ensure NetworkProxy is initialized
if (!this.networkProxy) {
console.log(
`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
`[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
);
// Fall back to direct connection
return this.setupDirectConnection(
connectionId,
socket,
record,
domainConfig,
serverName,
undefined,
undefined,
initialData
);
}
const networkProxy = this.networkProxies[proxyIndex];
const proxyPort = networkProxy.getListeningPort();
const proxyPort = this.networkProxy.getListeningPort();
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
);
}
@ -521,7 +489,6 @@ export class PortProxy {
record.outgoing = proxySocket;
record.outgoingStartTime = Date.now();
record.usingNetworkProxy = true;
record.networkProxyIndex = proxyIndex;
// Set up error handlers
proxySocket.on('error', (err) => {
@ -565,7 +532,7 @@ export class PortProxy {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
);
}
});
@ -886,11 +853,11 @@ export class PortProxy {
record.pendingData = [];
record.pendingDataSize = 0;
// Add the renegotiation handler for SNI validation, with browser-friendly improvements
// Add the renegotiation handler for SNI validation with strict domain enforcement
if (serverName) {
// Define a handler for checking renegotiation with improved detection
const renegotiationHandler = (renegChunk: Buffer) => {
// Only process if this looks like a TLS ClientHello (more precise than just checking for type 22)
// Only process if this looks like a TLS ClientHello
if (isClientHello(renegChunk)) {
try {
// Extract SNI from ClientHello
@ -899,44 +866,14 @@ export class PortProxy {
// Skip if no SNI was found
if (!newSNI) return;
// Handle SNI change during renegotiation
// Handle SNI change during renegotiation - always terminate for domain switches
if (newSNI !== record.lockedDomain) {
// Track domain switches for browser connections
if (!record.domainSwitches) record.domainSwitches = 0;
record.domainSwitches++;
// Check if this is a normal behavior of browser connection reuse
const isRelatedDomain = areDomainsRelated(
newSNI,
record.lockedDomain || '',
this.settings.relatedDomainPatterns
);
// Decide how to handle the SNI change based on settings
if (this.settings.browserFriendlyMode && isRelatedDomain) {
console.log(
`[${connectionId}] Browser domain switch detected: ${record.lockedDomain} -> ${newSNI}. ` +
`Domains are related, allowing connection to continue (domain switch #${record.domainSwitches}).`
);
// Update the locked domain to the new one
record.lockedDomain = newSNI;
} else if (this.settings.allowRenegotiationWithDifferentSNI) {
// Log and terminate the connection for any SNI change
console.log(
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
`Allowing due to allowRenegotiationWithDifferentSNI setting.`
);
// Update the locked domain to the new one
record.lockedDomain = newSNI;
} else {
// Standard strict behavior - terminate connection on SNI mismatch
console.log(
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
`Terminating connection. Enable browserFriendlyMode to allow this.`
`Terminating connection - SNI domain switching is not allowed.`
);
this.initiateCleanupOnce(record, 'sni_mismatch');
}
} else if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
@ -1201,7 +1138,7 @@ export class PortProxy {
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
record.hasKeepAlive ? 'Yes' : 'No'
}` +
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` +
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
);
} else {
@ -1341,6 +1278,12 @@ export class PortProxy {
return;
}
// Start NetworkProxy if configured
if (this.networkProxy) {
await this.networkProxy.start();
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
}
// Define a unified connection handler for all listening ports.
const connectionHandler = (socket: plugins.net.Socket) => {
if (this.isShuttingDown) {
@ -1401,12 +1344,12 @@ export class PortProxy {
incomingTerminationReason: null,
outgoingTerminationReason: null,
// Initialize NetworkProxy tracking fields
// Initialize NetworkProxy tracking
usingNetworkProxy: false,
// Initialize browser connection tracking
isBrowserConnection: this.settings.browserFriendlyMode, // Assume browser if browserFriendlyMode is enabled
domainSwitches: 0, // Track domain switches
isBrowserConnection: false,
domainSwitches: 0,
};
// Apply keep-alive settings if enabled
@ -1443,7 +1386,6 @@ export class PortProxy {
console.log(
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
`Active connections: ${this.connectionRecords.size}`
);
} else {
@ -1452,8 +1394,63 @@ export class PortProxy {
);
}
// Check if this connection should be forwarded directly to NetworkProxy based on port
const shouldUseNetworkProxy = this.settings.useNetworkProxy &&
this.settings.useNetworkProxy.includes(localPort);
if (shouldUseNetworkProxy) {
// For NetworkProxy ports, we want to capture the TLS handshake and forward directly
let initialDataReceived = false;
// Set an initial timeout for handshake data
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
if (!initialDataReceived) {
console.log(
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'initial_timeout';
this.incrementTerminationStat('incoming', 'initial_timeout');
}
socket.end();
this.cleanupConnection(connectionRecord, 'initial_timeout');
}
}, this.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
initialTimeout.unref();
}
socket.on('error', this.handleError('incoming', connectionRecord));
// First data handler to capture initial TLS handshake for NetworkProxy
socket.once('data', (chunk: Buffer) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
connectionRecord.hasReceivedInitialData = true;
// Check if this looks like a TLS handshake
if (isTlsHandshake(chunk)) {
connectionRecord.isTLS = true;
// Forward directly to NetworkProxy without SNI processing
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
} else {
// If not TLS, use normal direct connection
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk);
}
});
} else {
// For non-NetworkProxy ports, proceed with normal processing
// Define helpers for rejecting connections
const rejectIncomingConnection = (reason: string, logMessage: string) => {
console.log(`[${connectionId}] ${logMessage}`);
@ -1465,6 +1462,8 @@ export class PortProxy {
this.cleanupConnection(connectionRecord, reason);
};
let initialDataReceived = false;
// Set an initial timeout for SNI data if needed
let initialTimeout: NodeJS.Timeout | null = null;
if (this.settings.sniEnabled) {
@ -1513,7 +1512,7 @@ export class PortProxy {
});
/**
* Sets up the connection to the target host or NetworkProxy.
* Sets up the connection to the target host.
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
* @param initialChunk - Optional initial data chunk.
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
@ -1582,23 +1581,6 @@ export class PortProxy {
)}`
);
}
// Check if we should forward this to a NetworkProxy
if (
isTlsHandshakeDetected &&
domainConfig.useNetworkProxy === true &&
initialChunk &&
this.networkProxies.length > 0
) {
return this.forwardToNetworkProxy(
connectionId,
socket,
connectionRecord,
domainConfig,
initialChunk,
serverName
);
}
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
if (
!isGlobIPAllowed(
@ -1614,12 +1596,12 @@ export class PortProxy {
}
}
// Save the initial SNI for browser connection management
// Save the initial SNI
if (serverName) {
connectionRecord.lockedDomain = serverName;
}
// If we didn't forward to NetworkProxy, proceed with direct connection
// Set up the direct connection
return this.setupDirectConnection(
connectionId,
socket,
@ -1764,6 +1746,7 @@ export class PortProxy {
setupConnection('');
}
}
};
// --- SETUP LISTENERS ---
@ -1788,12 +1771,11 @@ export class PortProxy {
console.log(`Server Error on port ${port}: ${err.message}`);
});
server.listen(port, () => {
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
console.log(
`PortProxy -> OK: Now listening on port ${port}${
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}${
this.settings.browserFriendlyMode ? ' (Browser-friendly mode enabled)' : ''
}`
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
);
});
this.netServers.push(server);
@ -1963,21 +1945,6 @@ export class PortProxy {
}
}
/**
* Add or replace NetworkProxy instances
*/
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
this.networkProxies = networkProxies;
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
}
/**
* Get a list of configured NetworkProxy instances
*/
public getNetworkProxies(): NetworkProxy[] {
return this.networkProxies;
}
/**
* Gracefully shut down the proxy
*/
@ -2069,6 +2036,16 @@ export class PortProxy {
}
}
// Stop NetworkProxy if it was started
if (this.networkProxy) {
try {
await this.networkProxy.stop();
console.log('NetworkProxy stopped successfully');
} catch (err) {
console.log(`Error stopping NetworkProxy: ${err}`);
}
}
// Clear all tracking maps
this.connectionRecords.clear();
this.domainTargetIndices.clear();

View File

@ -19,6 +19,21 @@ export interface IRouterResult {
pathRemainder?: string;
}
/**
* Router for HTTP reverse proxy requests
*
* Supports the following domain matching patterns:
* - Exact matches: "example.com"
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
* - Default fallback: "*" (matches any unmatched domain)
*
* Also supports path pattern matching for each domain:
* - Exact path: "/api/users"
* - Wildcard paths: "/api/*"
* - Path parameters: "/users/:id/profile"
*/
export class ProxyRouter {
// Store original configs for reference
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
@ -98,9 +113,11 @@ export class ProxyRouter {
return exactConfig;
}
// Try wildcard subdomain
// Try various wildcard patterns
if (hostWithoutPort.includes('.')) {
const domainParts = hostWithoutPort.split('.');
// Try wildcard subdomain (*.example.com)
if (domainParts.length > 2) {
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
@ -108,6 +125,23 @@ export class ProxyRouter {
return wildcardConfig;
}
}
// Try TLD wildcard (example.*)
const baseDomain = domainParts.slice(0, -1).join('.');
const tldWildcardDomain = `${baseDomain}.*`;
const tldWildcardConfig = this.findConfigForHost(tldWildcardDomain, urlPath);
if (tldWildcardConfig) {
return tldWildcardConfig;
}
// Try complex wildcard patterns
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
for (const pattern of wildcardPatterns) {
const wildcardConfig = this.findConfigForHost(pattern, urlPath);
if (wildcardConfig) {
return wildcardConfig;
}
}
}
// Fall back to default config if available
@ -120,6 +154,53 @@ export class ProxyRouter {
return undefined;
}
/**
* Find potential wildcard patterns that could match a given hostname
* Handles complex patterns like "*.lossless*" or other partial matches
* @param hostname The hostname to find wildcard matches for
* @returns Array of potential wildcard patterns that could match
*/
private findWildcardMatches(hostname: string): string[] {
const patterns: string[] = [];
const hostnameParts = hostname.split('.');
// Find all configured hostnames that contain wildcards
const wildcardConfigs = this.reverseProxyConfigs.filter(
config => config.hostName.includes('*')
);
// Extract unique wildcard patterns
const wildcardPatterns = [...new Set(
wildcardConfigs.map(config => config.hostName.toLowerCase())
)];
// For each wildcard pattern, check if it could match the hostname
// using simplified regex pattern matching
for (const pattern of wildcardPatterns) {
// Skip the default wildcard '*'
if (pattern === '*') continue;
// Skip already checked patterns (*.domain.com and domain.*)
if (pattern.startsWith('*.') && pattern.indexOf('*', 2) === -1) continue;
if (pattern.endsWith('.*') && pattern.indexOf('*') === pattern.length - 1) continue;
// Convert wildcard pattern to regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*'); // Convert * to .* for regex
// Create regex object with case insensitive flag
const regex = new RegExp(`^${regexPattern}$`, 'i');
// If hostname matches this complex pattern, add it to the list
if (regex.test(hostname)) {
patterns.push(pattern);
}
}
return patterns;
}
/**
* Find a config for a specific host and path
*/