UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.

297 lines 21.2 kB
import * as plugins from '../../plugins.js'; import { isIPAuthorized, checkMaxConnections, checkConnectionRate, trackConnection, removeConnection, cleanupExpiredRateLimits, parseBasicAuthHeader, normalizeIP } from './security-utils.js'; /** * Shared SecurityManager for use across proxy components * Handles IP tracking, rate limiting, and authentication */ export class SharedSecurityManager { /** * Create a new SharedSecurityManager * * @param options - Configuration options * @param logger - Logger instance */ constructor(options, logger) { this.logger = logger; // IP connection tracking this.connectionsByIP = new Map(); // Route-specific rate limiting this.rateLimits = new Map(); // Cache IP filtering results to avoid constant regex matching this.ipFilterCache = new Map(); // Cache cleanup interval this.cleanupInterval = null; 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 */ getConnectionCountByIP(ip) { // Check all normalized variants of the IP const variants = normalizeIP(ip); for (const variant of variants) { const info = this.connectionsByIP.get(variant); if (info) { return info.connections.size; } } return 0; } /** * Track connection by IP * * @param ip - The IP address to track * @param connectionId - The connection ID to associate */ trackConnectionByIP(ip, connectionId) { // Check if any variant already exists const variants = normalizeIP(ip); let existingKey = null; for (const variant of variants) { if (this.connectionsByIP.has(variant)) { existingKey = variant; break; } } // Use existing key or the original IP trackConnection(existingKey || ip, connectionId, this.connectionsByIP); } /** * Remove connection tracking for an IP * * @param ip - The IP address to update * @param connectionId - The connection ID to remove */ removeConnectionByIP(ip, connectionId) { // Check all variants to find where the connection is tracked const variants = normalizeIP(ip); for (const variant of variants) { if (this.connectionsByIP.has(variant)) { removeConnection(variant, connectionId, this.connectionsByIP); break; } } } /** * 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 */ isIPAuthorized(ip, allowedIPs = ['*'], blockedIPs = []) { 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 */ validateIP(ip) { // 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 * @param routeConnectionCount - Current connection count for this route (optional) * @returns Whether access is allowed */ isAllowed(route, context, routeConnectionCount) { 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; } // --- Route-level connection limit --- if (route.security.maxConnections !== undefined && routeConnectionCount !== undefined) { if (routeConnectionCount >= route.security.maxConnections) { this.logger?.debug?.(`Route connection limit (${route.security.maxConnections}) exceeded 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 */ isClientIpAllowed(route, clientIp) { 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; const ipBlockList = route.security.ipBlockList; 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 */ isWithinRateLimit(route, context) { 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 */ validateBasicAuth(route, authHeader) { // 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 */ cleanupCaches() { // Clean up rate limits cleanupExpiredRateLimits(this.rateLimits, this.logger); // Clean up IP connection tracking let cleanedIPs = 0; for (const [ip, info] of this.connectionsByIP.entries()) { // Remove IPs with no active connections and no recent timestamps if (info.connections.size === 0 && info.timestamps.length === 0) { this.connectionsByIP.delete(ip); cleanedIPs++; } } if (cleanedIPs > 0 && this.logger?.debug) { this.logger.debug(`Cleaned up ${cleanedIPs} IPs with no active connections`); } // IP filter cache doesn't need cleanup (tied to routes) } /** * Clear all IP tracking data (for shutdown) */ clearIPTracking() { 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 */ setRoutes(routes) { // Only clear the IP filter cache - route-specific this.ipFilterCache.clear(); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2hhcmVkLXNlY3VyaXR5LW1hbmFnZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9jb3JlL3V0aWxzL3NoYXJlZC1zZWN1cml0eS1tYW5hZ2VyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFRNUMsT0FBTyxFQUNMLGNBQWMsRUFDZCxtQkFBbUIsRUFDbkIsbUJBQW1CLEVBQ25CLGVBQWUsRUFDZixnQkFBZ0IsRUFDaEIsd0JBQXdCLEVBQ3hCLG9CQUFvQixFQUNwQixXQUFXLEVBQ1osTUFBTSxxQkFBcUIsQ0FBQztBQUU3Qjs7O0dBR0c7QUFDSCxNQUFNLE9BQU8scUJBQXFCO0lBaUJoQzs7Ozs7T0FLRztJQUNILFlBQVksT0FLWCxFQUFVLE1BQXdCO1FBQXhCLFdBQU0sR0FBTixNQUFNLENBQWtCO1FBM0JuQyx5QkFBeUI7UUFDakIsb0JBQWUsR0FBbUMsSUFBSSxHQUFHLEVBQUUsQ0FBQztRQUVwRSwrQkFBK0I7UUFDdkIsZUFBVSxHQUE2QyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBRXpFLDhEQUE4RDtRQUN0RCxrQkFBYSxHQUFzQyxJQUFJLEdBQUcsRUFBRSxDQUFDO1FBTXJFLHlCQUF5QjtRQUNqQixvQkFBZSxHQUEwQixJQUFJLENBQUM7UUFjcEQsSUFBSSxDQUFDLG1CQUFtQixHQUFHLE9BQU8sQ0FBQyxtQkFBbUIsSUFBSSxHQUFHLENBQUM7UUFDOUQsSUFBSSxDQUFDLDRCQUE0QixHQUFHLE9BQU8sQ0FBQyw0QkFBNEIsSUFBSSxHQUFHLENBQUM7UUFFaEYsOENBQThDO1FBQzlDLElBQUksQ0FBQyxNQUFNLEdBQUcsTUFBTSxJQUFJO1lBQ3RCLElBQUksRUFBRSxPQUFPLENBQUMsR0FBRztZQUNqQixJQUFJLEVBQUUsT0FBTyxDQUFDLElBQUk7WUFDbEIsS0FBSyxFQUFFLE9BQU8sQ0FBQyxLQUFLO1NBQ3JCLENBQUM7UUFFRixnQ0FBZ0M7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