import * as plugins from './plugins.js'; import type { IPortProxySettings } from './classes.pp.interfaces.js'; /** * Handles security aspects like IP tracking, rate limiting, and authorization */ export class SecurityManager { private connectionsByIP: Map> = new Map(); private connectionRateByIP: Map = new Map(); constructor(private settings: IPortProxySettings) {} /** * Get connections count by IP */ public getConnectionCountByIP(ip: string): number { return this.connectionsByIP.get(ip)?.size || 0; } /** * Check and update connection rate for an IP * @returns true if within rate limit, false if exceeding limit */ public checkConnectionRate(ip: string): boolean { const now = Date.now(); const minute = 60 * 1000; if (!this.connectionRateByIP.has(ip)) { this.connectionRateByIP.set(ip, [now]); return true; } // Get timestamps and filter out entries older than 1 minute const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute); timestamps.push(now); this.connectionRateByIP.set(ip, timestamps); // Check if rate exceeds limit return timestamps.length <= this.settings.connectionRateLimitPerMinute!; } /** * Track connection by IP */ public trackConnectionByIP(ip: string, connectionId: string): void { if (!this.connectionsByIP.has(ip)) { this.connectionsByIP.set(ip, new Set()); } this.connectionsByIP.get(ip)!.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 if an IP is allowed using glob patterns */ 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 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 */ private isGlobIPMatch(ip: string, patterns: string[]): boolean { if (!ip || !patterns || patterns.length === 0) return false; const normalizeIP = (ip: string): string[] => { if (!ip) return []; if (ip.startsWith('::ffff:')) { const ipv4 = ip.slice(7); return [ip, ipv4]; } if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) { return [ip, `::ffff:${ip}`]; } return [ip]; }; const normalizedIPVariants = normalizeIP(ip); if (normalizedIPVariants.length === 0) return false; const expandedPatterns = patterns.flatMap(normalizeIP); return normalizedIPVariants.some((ipVariant) => expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern)) ); } /** * Check if IP should be allowed considering connection rate and max connections * @returns Object with result and reason */ public validateIP(ip: string): { allowed: boolean; reason?: string } { // Check connection count limit if ( this.settings.maxConnectionsPerIP && this.getConnectionCountByIP(ip) >= this.settings.maxConnectionsPerIP ) { return { allowed: false, reason: `Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded` }; } // Check connection rate limit if ( this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(ip) ) { return { allowed: false, reason: `Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded` }; } return { allowed: true }; } /** * Clears all IP tracking data (for shutdown) */ public clearIPTracking(): void { this.connectionsByIP.clear(); this.connectionRateByIP.clear(); } }