const logger = require('../utils/logger'); const config = require('config'); /** * INVITE Handler - Equivalent to Kamailio's INCOMING_INVITE route * Handles SIP INVITE requests and manages call routing */ class InviteHandler { constructor(srf) { this.srf = srf; this.specialNumbers = config.get('security.specialNumbers'); this.asteriskServers = config.get('dispatcher.asteriskServers'); this.currentServerIndex = 0; // For round-robin dispatching } /** * Record routing for dialog-forming requests (equivalent to kamailio.cfg:766-767) */ recordRoute(req) { // Add Record-Route header const recordRoute = `Record-Route: `; req.set('Record-Route', recordRoute); } /** * Enable RTP proxy for media relay (equivalent to kamailio.cfg:774) */ enableRtpProxy(req, res) { if (config.get('rtpProxy.enabled')) { logger.info('[INVITE] Enabling RTPProxy media relay | Call-ID: %s', req.get('Call-ID')); // RTP proxy integration will be handled in the SDP processing return true; } return false; } /** * Check if it's a special number (equivalent to kamailio.cfg:778-782) */ isSpecialNumber(number) { return this.specialNumbers.includes(number); } /** * Route to Asterisk servers (equivalent to kamailio.cfg:696-720) */ async routeToAsterisk(req, res) { logger.info('[INVITE] Routing to Asterisk servers | Call-ID: %s', req.get('Call-ID')); // Select Asterisk server using round-robin const server = this.asteriskServers[this.currentServerIndex]; this.currentServerIndex = (this.currentServerIndex + 1) % this.asteriskServers.length; if (!server) { logger.error('[INVITE] No available Asterisk servers | Call-ID: %s', req.get('Call-ID')); res.send(404, 'No destination'); return; } logger.info('[INVITE] Selected destination: %s:%s | Call-ID: %s', server.host, server.port, req.get('Call-ID')); try { // Create new INVITE to Asterisk const targetUri = `sip:${req.uri.user}@${server.host}:${server.port}`; const dlg = await this.srf.createUacInvite(targetUri, { headers: { 'From': req.get('From'), 'To': req.get('To'), 'Call-ID': req.get('Call-ID'), 'CSeq': req.get('CSeq'), 'Contact': req.get('Contact'), 'Content-Type': req.get('Content-Type'), 'Via': req.get('Via'), 'Route': req.get('Route'), 'Max-Forwards': req.get('Max-Forwards') }, body: req.body }); logger.info('[INVITE] Successfully routed to Asterisk: %s | Call-ID: %s', targetUri, req.get('Call-ID')); // Send 100 Trying response res.send(100, 'Trying'); // Relay responses between endpoints this.relayResponses(dlg, req, res); } catch (error) { logger.error('[INVITE] Failed to route to Asterisk: %s | Call-ID: %s', error.message, req.get('Call-ID')); res.send(500, 'Server Error'); } } /** * Route to local user (equivalent to kamailio.cfg:786-791) */ async routeToLocalUser(req, res, registration) { logger.info('[INVITE] User %s found locally - routing directly | Call-ID: %s', req.uri.user, req.get('Call-ID')); try { // Create INVITE to local user's contact const targetUri = registration.contact; const dlg = await this.srf.createUacInvite(targetUri, { headers: { 'From': req.get('From'), 'To': req.get('To'), 'Call-ID': req.get('Call-ID'), 'CSeq': req.get('CSeq'), 'Contact': req.get('Contact'), 'Content-Type': req.get('Content-Type'), 'Via': req.get('Via'), 'Route': req.get('Route'), 'Max-Forwards': req.get('Max-Forwards') }, body: req.body }); logger.info('[INVITE] Successfully routed to local user: %s | Call-ID: %s', targetUri, req.get('Call-ID')); // Send 100 Trying response res.send(100, 'Trying'); // Relay responses between endpoints this.relayResponses(dlg, req, res); } catch (error) { logger.error('[INVITE] Failed to route to local user: %s | Call-ID: %s', error.message, req.get('Call-ID')); res.send(500, 'Server Error'); } } /** * Relay responses between dialogs */ relayResponses(uacDialog, uasReq, uasRes) { // Relay UAC responses back to UAS uacDialog.on('response', (response) => { uasRes.send(response.status, response.reason, response.headers); }); // Handle UAS responses to UAC uasRes.on('response', (response) => { uacDialog.send(response.status, response.reason, response.headers); }); } /** * Handle INVITE request (equivalent to kamailio.cfg:762-856) */ async handleInvite(req, res) { const callId = req.get('Call-ID'); const fromUser = req.getParsedHeader('from').uri.user; const toUser = req.uri.user; logger.info('[INVITE] Call setup: %s -> %s | Call-ID: %s', fromUser, toUser, callId); // Record routing this.recordRoute(req); // Enable RTP proxy this.enableRtpProxy(req, res); // Step 1: Check if it's a special number if (this.isSpecialNumber(toUser)) { logger.info('[INVITE] Special service call to %s, routing to Asterisk | Call-ID: %s', toUser, callId); await this.routeToAsterisk(req, res); return; } // Step 2: Check if user is registered locally const aor = `${toUser}@${req.uri.host}`; const registration = this.srf.registrationHandler?.lookup(aor); if (registration) { await this.routeToLocalUser(req, res, registration); return; } // Step 3: No route found - send error logger.info('[INVITE] User %s not found locally, no route available | Call-ID: %s', toUser, callId); res.send(480, 'Temporarily Unavailable - User not found'); } } module.exports = InviteHandler;