const logger = require('../utils/logger'); const config = require('config'); /** * Registration Handler - Equivalent to Kamailio's REGISTRAR route * Handles SIP REGISTER requests and manages user location database */ class RegistrationHandler { constructor(srf) { this.srf = srf; this.registry = new Map(); // In-memory registry (equivalent to Kamailio usrloc) this.config = config.get('registry'); } /** * Validate From header before processing (equivalent to kamailio.cfg:635-639) */ validateFromHeader(req, res) { const from = req.getParsedHeader('from'); if (!from || !from.uri || !from.uri.user || !from.uri.host) { logger.error('[REGISTER] Cannot register user - From header is invalid | Call-ID: %s', req.get('Call-ID')); res.send(400, 'Bad Request - Invalid From header'); return false; } return true; } /** * Get registration expiry from Contact header (equivalent to kamailio.cfg:647-657) */ getRegistrationExpiry(req) { let expires = req.get('Expires'); if (!expires || expires === '') { expires = this.config.defaultExpires.toString(); } // Validate expires is a number if (expires !== '0' && !/^[0-9]+$/.test(expires)) { expires = this.config.defaultExpires.toString(); } const expiresNum = parseInt(expires); // Validate expiry range if (expiresNum > this.config.maxExpires) { return this.config.maxExpires; } if (expiresNum < this.config.minExpires && expiresNum !== 0) { return this.config.minExpires; } return expiresNum; } /** * Save registration to registry (equivalent to kamailio.cfg:644) */ saveToRegistry(from, contact, expires, sourceIp, sourcePort) { const key = `${from.uri.user}@${from.uri.host}`; const registration = { aor: key, contact: contact, expires: expires, registeredAt: Date.now(), sourceIp: sourceIp, sourcePort: sourcePort, callId: from.params.tag || 'unknown' }; if (expires === 0) { // Remove registration (unregister) this.registry.delete(key); logger.info('[REGISTER] User %s unregistered | From: %s:%s', key, sourceIp, sourcePort); } else { // Add/update registration this.registry.set(key, registration); logger.info('[REGISTER] User %s registered | Contact: %s | Expires: %ds | From: %s:%s', key, contact, expires, sourceIp, sourcePort); } } /** * Handle REGISTER request (equivalent to kamailio.cfg:630-676) */ handleRegister(req, res) { const callId = req.get('Call-ID'); const sourceIp = req.source_address; const sourcePort = req.source_port; logger.info('[REGISTER] Registration request from %s:%s | Call-ID: %s', sourceIp, sourcePort, callId); // Validate From header if (!this.validateFromHeader(req, res)) { return; } // Get registration details const from = req.getParsedHeader('from'); const contact = req.get('Contact'); const expires = this.getRegistrationExpiry(req); // Save to registry this.saveToRegistry(from, contact, expires, sourceIp, sourcePort); // Send response if (expires === 0) { res.send(200, 'OK - Unregistered from Cyanet VoIP system.'); } else { res.send(200, 'OK - Welcome to Cyanet VoIP system.'); } } /** * Lookup user in registry (equivalent to Kamailio's lookup() function) */ lookup(aor) { return this.registry.get(aor); } /** * Get all registered users */ getAllRegistrations() { return Array.from(this.registry.values()); } /** * Clean expired registrations */ cleanExpiredRegistrations() { const now = Date.now(); const expiredKeys = []; for (const [key, registration] of this.registry.entries()) { const expiryTime = registration.registeredAt + (registration.expires * 1000); if (registration.expires > 0 && expiryTime <= now) { expiredKeys.push(key); } } for (const key of expiredKeys) { this.registry.delete(key); logger.info('[REGISTER] Expired registration removed: %s', key); } return expiredKeys.length; } /** * Start registration cleanup timer */ startCleanupTimer() { // Clean expired registrations every 60 seconds (equivalent to Kamailio timer_interval) setInterval(() => { const cleaned = this.cleanExpiredRegistrations(); if (cleaned > 0) { logger.info('[REGISTER] Cleaned %d expired registrations', cleaned); } }, 60000); } } module.exports = RegistrationHandler;