First commit merge

This commit is contained in:
kbe
2025-10-09 16:37:02 +02:00
parent 915ef14fc5
commit 5aae456ab3
20 changed files with 2584 additions and 2 deletions

248
routes/indialog.js Normal file
View File

@@ -0,0 +1,248 @@
const logger = require('../utils/logger');
/**
* In-Dialog Handler - Equivalent to Kamailio's WITHINDLG route
* Handles SIP requests within established dialogs (BYE, re-INVITE, etc.)
*/
class InDialogHandler {
constructor(srf) {
this.srf = srf;
}
/**
* Handle media teardown for BYE/CANCEL (equivalent to kamailio.cfg:494-497)
*/
handleMediaTeardown(req) {
if (req.method === 'BYE' || req.method === 'CANCEL') {
logger.info('[WITHINDLG] Media teardown for %s | Call-ID: %s', req.method, req.get('Call-ID'));
// RTP proxy cleanup would go here
}
}
/**
* Handle media setup for ACK (equivalent to kamailio.cfg:500-502)
*/
handleMediaSetup(req) {
if (req.method === 'ACK') {
logger.info('[WITHINDLG] Media setup for ACK | Call-ID: %s', req.get('Call-ID'));
// RTP proxy setup would go here
}
}
/**
* Process loose route headers (equivalent to kamailio.cfg:506-561)
*/
processLooseRoute(req, res) {
const routeHeaders = req.get('Route');
if (!routeHeaders) {
return false;
}
logger.info('[WITHINDLG] Route headers processed | Call-ID: %s', req.get('Call-ID'));
// Process Route headers to determine next hop
const routes = Array.isArray(routeHeaders) ? routeHeaders : [routeHeaders];
const nextRoute = routes[0];
if (nextRoute.includes(this.srf.localAddress)) {
// Route points back to us - check if user is registered locally
const aor = `${req.uri.user}@${req.uri.host}`;
const registration = this.srf.registrationHandler?.lookup(aor);
if (registration) {
logger.info('[WITHINDLG] Loose route resolved to local user, forwarding | Call-ID: %s', req.get('Call-ID'));
return this.forwardToUser(req, res, registration);
} else {
logger.warn('[WITHINDLG] Loose route points to us but user not found locally | Call-ID: %s', req.get('Call-ID'));
res.send(404, 'Not here');
return true;
}
} else {
// Route is to a different destination, process normally
logger.info('[WITHINDLG] Forwarding to different destination | Call-ID: %s', req.get('Call-ID'));
return this.forwardToDestination(req, res, nextRoute);
}
}
/**
* Forward request to registered user
*/
async forwardToUser(req, res, registration) {
try {
const dlg = await this.srf.createRequest(req.method, registration.contact, {
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('[WITHINDLG] Successfully forwarded to user | Call-ID: %s', req.get('Call-ID'));
// Send response back to originator
res.send(200, 'OK');
// Relay responses
this.relayResponses(dlg, req, res);
} catch (error) {
logger.error('[WITHINDLG] Failed to forward to user: %s | Call-ID: %s', error.message, req.get('Call-ID'));
res.send(500, 'Server Error');
}
}
/**
* Forward request to destination
*/
async forwardToDestination(req, res, destination) {
try {
const dlg = await this.srf.createRequest(req.method, destination, {
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('[WITHINDLG] Successfully forwarded to destination | Call-ID: %s', req.get('Call-ID'));
// Send response back to originator
res.send(200, 'OK');
// Relay responses
this.relayResponses(dlg, req, res);
} catch (error) {
logger.error('[WITHINDLG] Failed to forward to destination: %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 BYE request (equivalent to kamailio.cfg:508-511)
*/
handleBye(req, res) {
logger.info('[WITHINDLG] BYE request | Call-ID: %s', req.get('Call-ID'));
// Handle media teardown
this.handleMediaTeardown(req);
// Process routing
if (this.processLooseRoute(req, res)) {
return;
}
// If no loose route, try to find the target user
const aor = `${req.uri.user}@${req.uri.host}`;
const registration = this.srf.registrationHandler?.lookup(aor);
if (registration) {
logger.info('[WITHINDLG] BYE: Target user found locally, forwarding | Call-ID: %s', req.get('Call-ID'));
this.forwardToUser(req, res, registration);
} else {
logger.warn('[WITHINDLG] BYE: Target user not found locally | Call-ID: %s', req.get('Call-ID'));
res.send(404, 'Not here');
}
}
/**
* Handle ACK request (equivalent to kamailio.cfg:611-621)
*/
handleAck(req, res) {
logger.info('[WITHINDLG] ACK request | Call-ID: %s', req.get('Call-ID'));
// Handle media setup
this.handleMediaSetup(req);
// ACK is typically just absorbed
res.send(200, 'OK');
}
/**
* Handle in-dialog SUBSCRIBE (equivalent to kamailio.cfg:606-610)
*/
handleSubscribe(req, res) {
logger.info('[WITHINDLG] SUBSCRIBE request | Call-ID: %s', req.get('Call-ID'));
res.send(404, 'Not here');
}
/**
* Main in-dialog request handler (equivalent to kamailio.cfg:489-627)
*/
async handleInDialog(req, res) {
const method = req.method;
const callId = req.get('Call-ID');
logger.info('[WITHINDLG] %s request | Call-ID: %s', method, callId);
// Handle media-related operations
if (method === 'BYE' || method === 'CANCEL') {
this.handleMediaTeardown(req);
} else if (method === 'ACK') {
this.handleMediaSetup(req);
}
// Process based on method
switch (method) {
case 'BYE':
this.handleBye(req, res);
break;
case 'ACK':
this.handleAck(req, res);
break;
case 'SUBSCRIBE':
this.handleSubscribe(req, res);
break;
default:
// For other methods, try to process Route headers
if (!this.processLooseRoute(req, res)) {
// No route found, try direct forwarding
logger.info('[WITHINDLG] No route headers, forwarding to destination | Call-ID: %s', callId);
const aor = `${req.uri.user}@${req.uri.host}`;
const registration = this.srf.registrationHandler?.lookup(aor);
if (registration) {
this.forwardToUser(req, res, registration);
} else {
logger.warn('[WITHINDLG] No route found for %s | Call-ID: %s', method, callId);
res.send(404, 'Not here');
}
}
break;
}
}
}
module.exports = InDialogHandler;

188
routes/invite.js Normal file
View File

@@ -0,0 +1,188 @@
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: <sip:${req.source_address}:${req.source_port};lr>`;
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;

164
routes/registration.js Normal file
View File

@@ -0,0 +1,164 @@
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;