const config = require('config'); const logger = require('../utils/logger'); /** * User-Agent Filtering Middleware - Equivalent to Kamailio UA_FILTER route * Provides security protection by filtering SIP requests based on User-Agent header */ class UserAgentFilter { constructor() { this.blockedPatterns = config.get('security.blockedUserAgents'); this.allowedPatterns = config.get('security.allowedUserAgents'); this.enabled = config.get('security.enableUAFilter'); } /** * Extract User-Agent header from SIP request */ getUserAgent(req) { return req.get('User-Agent') || ''; } /** * Block requests with no User-Agent header (equivalent to kamailio.cfg:425-429) */ checkEmptyUserAgent(userAgent, req, res) { if (!userAgent || userAgent.trim() === '') { logger.warn('[UA_FILTER] Blocked request with empty User-Agent from %s:%s | Method: %s | Call-ID: %s', req.source_address, req.source_port, req.method, req.get('Call-ID')); res.send(403, 'Forbidden - User-Agent header required'); return true; } return false; } /** * Block known malicious scanners (equivalent to kamailio.cfg:438-442) */ checkBlockedPatterns(userAgent, req, res) { const userAgentLower = userAgent.toLowerCase(); for (const pattern of this.blockedPatterns) { if (userAgentLower.includes(pattern.toLowerCase())) { logger.warn('[UA_FILTER] Blocked malicious scanner User-Agent: \'%s\' from %s:%s | Method: %s | Call-ID: %s', userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID')); res.send(403, 'Forbidden - Access denied'); return true; } } return false; } /** * Block User-Agents containing attack tool keywords (equivalent to kamailio.cfg:446-450) */ checkAttackToolPatterns(userAgent, req, res) { const attackPatterns = [ 'scanner', 'vicious', 'warvox', 'sipdic', 'sip-scan', 'brute', 'attack', 'exploit', 'flood', 'dos' ]; const userAgentLower = userAgent.toLowerCase(); for (const pattern of attackPatterns) { if (userAgentLower.includes(pattern)) { logger.warn('[UA_FILTER] Blocked attack tool User-Agent: \'%s\' from %s:%s | Method: %s | Call-ID: %s', userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID')); res.send(403, 'Forbidden - Access denied'); return true; } } return false; } /** * Block User-Agents with only numbers/special characters (equivalent to kamailio.cfg:461-465) */ checkNumericOnlyUserAgent(userAgent, req, res) { if (userAgent.length > 1 && /^[0-9\-\._\s]+$/.test(userAgent)) { logger.warn('[UA_FILTER] Blocked numeric/special-char-only User-Agent: \'%s\' from %s:%s | Method: %s | Call-ID: %s', userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID')); res.send(403, 'Forbidden - Invalid User-Agent'); return true; } return false; } /** * Check allowlist for legitimate SIP clients (equivalent to kamailio.cfg:469-472) */ checkAllowlist(userAgent, req, res) { const userAgentLower = userAgent.toLowerCase(); for (const pattern of this.allowedPatterns) { if (userAgentLower.includes(pattern.toLowerCase())) { logger.info('[UA_FILTER] Allowed legitimate SIP client: \'%s\' from %s:%s | Method: %s | Call-ID: %s', userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID')); return true; } } return false; } /** * Check for carrier/provider infrastructure (equivalent to kamailio.cfg:477-480) */ checkCarrierPatterns(userAgent, req, res) { const carrierPatterns = [ 'carrier', 'provider', 'operator', 'core', 'gateway', 'trunk', 'pbx', 'sbc', 'session-border', 'voip' ]; const userAgentLower = userAgent.toLowerCase(); for (const pattern of carrierPatterns) { if (userAgentLower.includes(pattern)) { logger.info('[UA_FILTER] Allowed carrier/provider system: \'%s\' from %s:%s | Method: %s | Call-ID: %s', userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID')); return true; } } return false; } /** * Log unknown but allowed User-Agents for monitoring (equivalent to kamailio.cfg:485) */ logUnknownUserAgent(userAgent, req) { logger.info('[UA_FILTER] Unknown User-Agent allowed (for monitoring): \'%s\' from %s:%s | Method: %s | Call-ID: %s', userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID')); } /** * Main filtering function that processes User-Agent checks */ filter(req, res, next) { if (!this.enabled) { return next(); } const userAgent = this.getUserAgent(req); // Check for empty User-Agent if (this.checkEmptyUserAgent(userAgent, req, res)) { return; } // Check blocked patterns if (this.checkBlockedPatterns(userAgent, req, res)) { return; } // Check attack tool patterns if (this.checkAttackToolPatterns(userAgent, req, res)) { return; } // Check numeric-only User-Agents if (this.checkNumericOnlyUserAgent(userAgent, req, res)) { return; } // Check allowlist if (this.checkAllowlist(userAgent, req, res)) { return next(); } // Check carrier patterns if (this.checkCarrierPatterns(userAgent, req, res)) { return next(); } // Unknown User-Agent - allow but log for monitoring this.logUnknownUserAgent(userAgent, req); next(); } /** * Middleware function */ middleware() { return (req, res, next) => { this.filter(req, res, next); }; } } module.exports = UserAgentFilter;