mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2025-12-19 04:27:45 +00:00
291 lines
11 KiB
JavaScript
291 lines
11 KiB
JavaScript
const debug = require('debug')('jambonz:sbc-outbound');
|
|
const parseUri = require('drachtio-srf').parseUri;
|
|
const Registrar = require('@jambonz/mw-registrar');
|
|
const { nudgeCallCounts} = require('./utils');
|
|
const FS_UUID_SET_NAME = 'fsUUIDs';
|
|
|
|
module.exports = (srf, logger, redisClient) => {
|
|
const {incrKey, decrKey, isMemberOfSet} = srf.locals.realtimeDbHelpers;
|
|
const {stats} = srf.locals;
|
|
const registrar = new Registrar(logger, redisClient);
|
|
const {
|
|
lookupAccountCapacitiesBySid,
|
|
lookupAccountBySid,
|
|
queryCallLimits
|
|
} = srf.locals.dbHelpers;
|
|
|
|
const initLocals = async(req, res, next) => {
|
|
req.locals = req.locals || {};
|
|
const callId = req.get('Call-ID');
|
|
req.locals.nudge = 0;
|
|
req.locals.callId = callId;
|
|
req.locals.account_sid = req.get('X-Account-Sid');
|
|
req.locals.application_sid = req.get('X-Application-Sid');
|
|
req.locals.record_all_calls = req.get('X-Record-All-Calls');
|
|
const traceId = req.locals.trace_id = req.get('X-Trace-ID');
|
|
req.locals.logger = logger.child({
|
|
callId,
|
|
traceId,
|
|
account_sid:
|
|
req.locals.account_sid});
|
|
|
|
if (!req.locals.account_sid) {
|
|
logger.info('missing X-Account-Sid on outbound call');
|
|
res.send(403, {
|
|
headers: {
|
|
'X-Reason': 'missing X-Account-Sid'
|
|
}
|
|
});
|
|
return req.srf.endSession(req);
|
|
}
|
|
|
|
/* must come from a valid FS */
|
|
if (!req.has('X-Jambonz-Routing')) {
|
|
logger.info({msg: req.msg}, 'missing X-Jambonz-Routing header');
|
|
res.send(403, {
|
|
headers: {
|
|
'X-Reason': 'missing required jambonz headers'
|
|
}
|
|
});
|
|
return req.srf.endSession(req);
|
|
}
|
|
if (process.env.K8S) {
|
|
/* for K8S we do not use JAMBONES_CIDR so we must validate the sender by uuid FS creates */
|
|
const fsUUID = req.get('X-Jambonz-FS-UUID');
|
|
try {
|
|
const exists = await isMemberOfSet(FS_UUID_SET_NAME, fsUUID);
|
|
if (!exists || !fsUUID) {
|
|
res.send(403, {
|
|
headers: {
|
|
'X-Reason': `missing or invalid FS-UUID ${fsUUID}`
|
|
}
|
|
});
|
|
return req.srf.endSession(req);
|
|
}
|
|
} catch {
|
|
res.send(500);
|
|
return req.srf.endSession(req);
|
|
}
|
|
}
|
|
|
|
stats.increment('sbc.invites', ['direction:outbound']);
|
|
|
|
req.on('cancel', () => {
|
|
req.locals.logger.info({callId}, 'caller hungup before connecting');
|
|
req.canceled = true;
|
|
const tags = ['canceled:yes', 'sipStatus:487'];
|
|
if (req.locals.originator) tags.push(`originator:${req.locals.originator}`);
|
|
stats.increment('sbc.origination', tags);
|
|
});
|
|
|
|
try {
|
|
const account = await lookupAccountBySid(req.locals.account_sid);
|
|
req.locals.account = account;
|
|
if (account.enable_debug_log) {
|
|
req.locals.logger.level = 'debug';
|
|
}
|
|
req.locals.service_provider_sid = req.locals.account.service_provider_sid;
|
|
} catch (err) {
|
|
req.locals.logger.error({err}, `Error looking up account sid ${req.locals.account_sid}`);
|
|
res.send(500);
|
|
return req.srf.endSession(req);
|
|
}
|
|
next();
|
|
};
|
|
|
|
const checkLimits = async(req, res, next) => {
|
|
const {logger, account_sid, service_provider_sid, application_sid} = req.locals;
|
|
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
|
|
process.env.JAMBONES_TRACK_SP_CALLS ||
|
|
process.env.JAMBONES_TRACK_APP_CALLS;
|
|
if (!process.env.JAMBONES_HOSTING && !trackingOn) {
|
|
logger.debug('tracking is off, skipping call limit checks');
|
|
return next(); // skip
|
|
}
|
|
|
|
const {writeCallCount, writeCallCountSP, writeCallCountApp, writeAlerts, AlertType} = req.srf.locals;
|
|
|
|
try {
|
|
/* decrement count if INVITE is later rejected */
|
|
res.once('end', async({status}) => {
|
|
if (status > 200) {
|
|
nudgeCallCounts(req, 'failure', {
|
|
service_provider_sid,
|
|
account_sid,
|
|
application_sid,
|
|
callId: req.locals.callId
|
|
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
|
|
.catch((err) => logger.error(err, 'Error decrementing call counts'));
|
|
const tags = ['accepted:no', `sipStatus:${status}`];
|
|
stats.increment('sbc.originations', tags);
|
|
}
|
|
else {
|
|
const tags = ['accepted:yes', 'sipStatus:200'];
|
|
stats.increment('sbc.originations', tags);
|
|
}
|
|
});
|
|
|
|
/* increment the call count */
|
|
const {callsSP, calls} = await nudgeCallCounts(req, 'init', {
|
|
service_provider_sid,
|
|
account_sid,
|
|
application_sid,
|
|
callId: req.locals.callId
|
|
}, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp});
|
|
|
|
/* compare to account's limit, though avoid db hit when call count is low */
|
|
const minLimit = process.env.MIN_CALL_LIMIT ?
|
|
parseInt(process.env.MIN_CALL_LIMIT) :
|
|
0;
|
|
if (calls <= minLimit) return next();
|
|
|
|
const capacities = await lookupAccountCapacitiesBySid(account_sid);
|
|
const limit = capacities.find((c) => c.category == 'voice_call_session');
|
|
if (limit) {
|
|
const limit_sessions = limit.quantity;
|
|
|
|
if (calls > limit_sessions) {
|
|
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
|
|
writeAlerts({
|
|
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
|
|
service_provider_sid,
|
|
account_sid,
|
|
count: limit_sessions
|
|
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
|
res.send(503, 'Maximum Calls In Progress');
|
|
return req.srf.endSession(req);
|
|
}
|
|
}
|
|
else if (trackingOn) {
|
|
const {account_limit, sp_limit} = await queryCallLimits(service_provider_sid, account_sid);
|
|
if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS && account_limit > 0 && calls > account_limit) {
|
|
logger.info({calls, account_limit}, 'checkLimits: account limits exceeded');
|
|
writeAlerts({
|
|
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
|
|
service_provider_sid: service_provider_sid,
|
|
account_sid,
|
|
count: calls
|
|
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
|
res.send(503, 'Max Account Calls In Progress', {
|
|
headers: {
|
|
'X-Account-Sid': account_sid,
|
|
'X-Call-Limit': account_limit
|
|
}
|
|
});
|
|
return req.srf.endSession(req);
|
|
}
|
|
if (!account_limit && !sp_limit && process.env.JAMBONES_HOSTING) {
|
|
logger.info(`checkLimits: no active subscription found for account ${account_sid}, rejecting call`);
|
|
res.send(503, 'No Active Subscription');
|
|
return req.srf.endSession(req);
|
|
}
|
|
if (process.env.JAMBONES_TRACK_SP_CALLS && sp_limit > 0 && callsSP > sp_limit) {
|
|
logger.info({callsSP, sp_limit}, 'checkLimits: service provider limits exceeded');
|
|
writeAlerts({
|
|
alert_type: AlertType.SP_CALL_LIMIT,
|
|
service_provider_sid: service_provider_sid,
|
|
count: callsSP
|
|
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
|
res.send(503, 'Max Service Provider Calls In Progress', {
|
|
headers: {
|
|
'X-Service-Provider-Sid': service_provider_sid,
|
|
'X-Call-Limit': sp_limit
|
|
}
|
|
});
|
|
return req.srf.endSession(req);
|
|
}
|
|
}
|
|
next();
|
|
} catch (err) {
|
|
logger.error({err}, 'error checking limits error for inbound call');
|
|
res.send(500);
|
|
}
|
|
};
|
|
|
|
const route = async(req, res, next) => {
|
|
const logger = req.locals.logger;
|
|
const {lookupAccountBySipRealm} = req.srf.locals.dbHelpers;
|
|
logger.info(`received outbound INVITE to ${req.uri} from server at ${req.server.hostport}`);
|
|
const uri = parseUri(req.uri);
|
|
const desiredRouting = req.get('X-Jambonz-Routing');
|
|
const validUri = uri && uri.user && uri.host;
|
|
if (['user', 'sip'].includes(desiredRouting) && !validUri) {
|
|
logger.info({uri: req.uri}, 'invalid request-uri on outbound call, rejecting');
|
|
res.send(400, {
|
|
headers: {
|
|
'X-Reason': 'invalid request-uri'
|
|
}
|
|
});
|
|
return req.srf.endSession(req);
|
|
}
|
|
debug(`received outbound INVITE to ${req.calledNumber} from server at ${req.server.hostport}`);
|
|
|
|
if ('teams' === desiredRouting) {
|
|
logger.debug('This is a call to ms teams');
|
|
req.locals.target = 'teams';
|
|
}
|
|
else if ('user' === desiredRouting) {
|
|
const aor = `${uri.user}@${uri.host}`;
|
|
const reg = await registrar.query(aor);
|
|
if (reg) {
|
|
// user is registered..find out which sbc is handling it
|
|
// us => we can put the call through
|
|
// other sbc => proxy the call there
|
|
logger.info({details: reg}, `sending call to registered user ${aor}`);
|
|
if (req.server.hostport !== reg.sbcAddress) {
|
|
/* redirect to the correct SBC where this user is connected */
|
|
const proxyAddress = reg.privateSbcAddress.split(':');
|
|
const redirectUri = `<sip:${proxyAddress[0]}>`;
|
|
logger.info({
|
|
myHostPort: req.server.hostport,
|
|
registeredHostPort: reg.sbcAddress,
|
|
}, `redirecting call to SBC at ${redirectUri}`);
|
|
return res.send(302, {headers: {Contact: redirectUri}});
|
|
}
|
|
req.locals.registration = reg;
|
|
req.locals.target = 'user';
|
|
}
|
|
else {
|
|
const account = await lookupAccountBySipRealm(uri.host);
|
|
if (account) {
|
|
logger.info({host: uri.host, account}, `returning 404 to unregistered user in valid domain: ${req.uri}`);
|
|
}
|
|
else {
|
|
logger.info({host: uri.host, account}, `returning 404 to user in invalid domain: ${req.uri}`);
|
|
}
|
|
res.send(404);
|
|
return req.srf.endSession(req);
|
|
}
|
|
}
|
|
else if ('sip' === desiredRouting) {
|
|
// call that needs to be forwarded to a sip endpoint
|
|
logger.info(`forwarding call to sip endpoint ${req.uri}`);
|
|
|
|
// check if the domain is one of ours
|
|
if (process.env.JAMBONES_LOCAL_SIP_DOMAINS) {
|
|
const allowedDomains = process.env.JAMBONES_LOCAL_SIP_DOMAINS.split(',');
|
|
const domain = uri.host;
|
|
const isLoop = allowedDomains.some((allowed) =>
|
|
domain === allowed.trim() || domain.endsWith(`.${allowed.trim()}`)
|
|
);
|
|
if (isLoop) {
|
|
logger.info({ host: domain }, `returning 482 Loop Detected for attempt to send to: ${req.uri}`);
|
|
res.send(482, 'Loop Detected');
|
|
return req.srf.endSession(req);
|
|
}
|
|
}
|
|
req.locals.target = 'forward';
|
|
} else if ('phone' === desiredRouting) {
|
|
debug('sending call to LCR');
|
|
req.locals.target = 'lcr';
|
|
}
|
|
next();
|
|
};
|
|
|
|
return {
|
|
initLocals,
|
|
checkLimits,
|
|
route
|
|
};
|
|
};
|