import * as plugins from '../../plugins.js'; import type { IRouteConfig, IRouteContext } from '../../proxies/smart-proxy/models/route-types.js'; import type { IIpValidationResult, IIpConnectionInfo, ISecurityLogger, IRateLimitInfo } from './security-utils.js'; import { isIPAuthorized, checkMaxConnections, checkConnectionRate, trackConnection, removeConnection, cleanupExpiredRateLimits, parseBasicAuthHeader } from './security-utils.js'; /** * Shared SecurityManager for use across proxy components * Handles IP tracking, rate limiting, and authentication */ export class SharedSecurityManager { // IP connection tracking private connectionsByIP: Map = new Map(); // Route-specific rate limiting private rateLimits: Map> = new Map(); // Cache IP filtering results to avoid constant regex matching private ipFilterCache: Map> = new Map(); // Default limits private maxConnectionsPerIP: number; private connectionRateLimitPerMinute: number; // Cache cleanup interval private cleanupInterval: NodeJS.Timeout | null = null; /** * Create a new SharedSecurityManager * * @param options - Configuration options * @param logger - Logger instance */ constructor(options: { maxConnectionsPerIP?: number; connectionRateLimitPerMinute?: number; cleanupIntervalMs?: number; routes?: IRouteConfig[]; }, private logger?: ISecurityLogger) { this.maxConnectionsPerIP = options.maxConnectionsPerIP || 100; this.connectionRateLimitPerMinute = options.connectionRateLimitPerMinute || 300; // Set up logger with defaults if not provided this.logger = logger || { info: console.log, warn: console.warn, error: console.error }; // Set up cache cleanup interval const cleanupInterval = options.cleanupIntervalMs || 60000; // Default: 1 minute this.cleanupInterval = setInterval(() => { this.cleanupCaches(); }, cleanupInterval); // Don't keep the process alive just for cleanup if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } /** * Get connections count by IP * * @param ip - The IP address to check * @returns Number of connections from this IP */ public getConnectionCountByIP(ip: string): number { return this.connectionsByIP.get(ip)?.connections.size || 0; } /** * Track connection by IP * * @param ip - The IP address to track * @param connectionId - The connection ID to associate */ public trackConnectionByIP(ip: string, connectionId: string): void { trackConnection(ip, connectionId, this.connectionsByIP); } /** * Remove connection tracking for an IP * * @param ip - The IP address to update * @param connectionId - The connection ID to remove */ public removeConnectionByIP(ip: string, connectionId: string): void { removeConnection(ip, connectionId, this.connectionsByIP); } /** * Check if IP is authorized based on route security settings * * @param ip - The IP address to check * @param allowedIPs - List of allowed IP patterns * @param blockedIPs - List of blocked IP patterns * @returns Whether the IP is authorized */ public isIPAuthorized( ip: string, allowedIPs: string[] = ['*'], blockedIPs: string[] = [] ): boolean { return isIPAuthorized(ip, allowedIPs, blockedIPs); } /** * Validate IP against rate limits and connection limits * * @param ip - The IP address to validate * @returns Result with allowed status and reason if blocked */ public validateIP(ip: string): IIpValidationResult { // Check connection count limit const connectionResult = checkMaxConnections( ip, this.connectionsByIP, this.maxConnectionsPerIP ); if (!connectionResult.allowed) { return connectionResult; } // Check connection rate limit const rateResult = checkConnectionRate( ip, this.connectionsByIP, this.connectionRateLimitPerMinute ); if (!rateResult.allowed) { return rateResult; } return { allowed: true }; } /** * Check if a client is allowed to access a specific route * * @param route - The route to check * @param context - The request context * @returns Whether access is allowed */ public isAllowed(route: IRouteConfig, context: IRouteContext): boolean { if (!route.security) { return true; // No security restrictions } // --- IP filtering --- if (!this.isClientIpAllowed(route, context.clientIp)) { this.logger?.debug?.(`IP ${context.clientIp} is blocked for route ${route.name || 'unnamed'}`); return false; } // --- Rate limiting --- if (route.security.rateLimit?.enabled && !this.isWithinRateLimit(route, context)) { this.logger?.debug?.(`Rate limit exceeded for route ${route.name || 'unnamed'}`); return false; } return true; } /** * Check if a client IP is allowed for a route * * @param route - The route to check * @param clientIp - The client IP * @returns Whether the IP is allowed */ private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean { if (!route.security) { return true; // No security restrictions } const routeId = route.id || route.name || 'unnamed'; // Check cache first if (!this.ipFilterCache.has(routeId)) { this.ipFilterCache.set(routeId, new Map()); } const routeCache = this.ipFilterCache.get(routeId)!; if (routeCache.has(clientIp)) { return routeCache.get(clientIp)!; } // Check IP against route security settings const ipAllowList = route.security.ipAllowList || route.security.allowedIps; const ipBlockList = route.security.ipBlockList || route.security.blockedIps; const allowed = this.isIPAuthorized(clientIp, ipAllowList, ipBlockList); // Cache the result routeCache.set(clientIp, allowed); return allowed; } /** * Check if request is within rate limit * * @param route - The route to check * @param context - The request context * @returns Whether the request is within rate limit */ private isWithinRateLimit(route: IRouteConfig, context: IRouteContext): boolean { if (!route.security?.rateLimit?.enabled) { return true; } const rateLimit = route.security.rateLimit; const routeId = route.id || route.name || 'unnamed'; // Determine rate limit key (by IP, path, or header) let key = context.clientIp; // Default to IP if (rateLimit.keyBy === 'path' && context.path) { key = `${context.clientIp}:${context.path}`; } else if (rateLimit.keyBy === 'header' && rateLimit.headerName && context.headers) { const headerValue = context.headers[rateLimit.headerName.toLowerCase()]; if (headerValue) { key = `${context.clientIp}:${headerValue}`; } } // Get or create rate limit tracking for this route if (!this.rateLimits.has(routeId)) { this.rateLimits.set(routeId, new Map()); } const routeLimits = this.rateLimits.get(routeId)!; const now = Date.now(); // Get or create rate limit tracking for this key let limit = routeLimits.get(key); if (!limit || limit.expiry < now) { // Create new rate limit or reset expired one limit = { count: 1, expiry: now + (rateLimit.window * 1000) }; routeLimits.set(key, limit); return true; } // Increment the counter limit.count++; // Check if rate limit is exceeded return limit.count <= rateLimit.maxRequests; } /** * Validate HTTP Basic Authentication * * @param route - The route to check * @param authHeader - The Authorization header * @returns Whether authentication is valid */ public validateBasicAuth(route: IRouteConfig, authHeader?: string): boolean { // Skip if basic auth not enabled for route if (!route.security?.basicAuth?.enabled) { return true; } // No auth header means auth failed if (!authHeader) { return false; } // Parse auth header const credentials = parseBasicAuthHeader(authHeader); if (!credentials) { return false; } // Check credentials against configured users const { username, password } = credentials; const users = route.security.basicAuth.users; return users.some(user => user.username === username && user.password === password ); } /** * Clean up caches to prevent memory leaks */ private cleanupCaches(): void { // Clean up rate limits cleanupExpiredRateLimits(this.rateLimits, this.logger); // IP filter cache doesn't need cleanup (tied to routes) } /** * Clear all IP tracking data (for shutdown) */ public clearIPTracking(): void { this.connectionsByIP.clear(); this.rateLimits.clear(); this.ipFilterCache.clear(); if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Update routes for security checking * * @param routes - New routes to use */ public setRoutes(routes: IRouteConfig[]): void { // Only clear the IP filter cache - route-specific this.ipFilterCache.clear(); } }