Compare commits

..
Author SHA1 Message Date
Dave Horton 76625c7596 Feature/recent calls enhancement with sp (#50)
* write cdrs with service_provider_sid

* write call counts at the SP level
2022-09-16 13:45:33 +02:00
Paulo Tellesandp.souza 3b0f7ff6eb change node image (#48)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-09-07 13:20:13 +02:00
Dave Horton 33c75acc9e bump version to 0.7.6 2022-08-26 20:09:43 +02:00
xquanluu bae9ef7638 feat: update time-series 0.11.12 (#47) 2022-08-19 16:21:11 +02:00
Dave Horton 4ed4b38301 update time-series 2022-08-19 09:57:48 +02:00
Dave Horton 6b6f89264f minor logging change 2022-08-17 16:11:10 +02:00
Dave Horton f0e0fba2f1 make rtpengine transcode if non-preferred codec is selected by far end 2022-08-17 14:14:45 +02:00
Dave Horton 290723f234 initial changes for sip info dtmf (#46) 2022-08-11 15:13:07 +02:00
6 changed files with 131 additions and 64 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.6.0-alpine as base
FROM --platform=linux/amd64 node:18.8.0-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
+70 -10
View File
@@ -1,6 +1,7 @@
const Emitter = require('events');
const sdpTransform = require('sdp-transform');
const SrsClient = require('@jambonz/siprec-client-utils');
const {makeRtpEngineOpts, makeCallCountKey} = require('./utils');
const {makeRtpEngineOpts, makeAccountCallCountKey, makeSPCallCountKey} = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {SipError, stringifyUri, parseUri} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-outbound');
@@ -49,6 +50,15 @@ const initCdr = (srf, req) => {
};
};
const updateRtpEngineFlags = (sdp, opts) => {
try {
const parsed = sdpTransform.parse(sdp);
const codec = parsed.media[0].rtp[0].codec;
if (['PCMU', 'PCMA'].includes(codec)) opts.flags.push(`codec-accept-${codec}`);
} catch (err) {}
return opts;
};
class CallSession extends Emitter {
constructor(logger, req, res) {
super();
@@ -63,12 +73,15 @@ class CallSession extends Emitter {
this.incrKey = req.srf.locals.realtimeDbHelpers.incrKey;
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.callCountKey = makeCallCountKey(req.locals.account_sid);
this.callCountKey = makeAccountCallCountKey(req.locals.account_sid);
this.callCountKeySP = makeSPCallCountKey(req.locals.service_provider_sid);
const {performLcr, lookupCarrierBySid, lookupSipGatewaysByCarrier} = this.srf.locals.dbHelpers;
this.performLcr = performLcr;
this.lookupCarrierBySid = lookupCarrierBySid;
this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier;
this._mediaReleased = false;
}
get account_sid() {
@@ -79,6 +92,10 @@ class CallSession extends Emitter {
return this.srf.locals.privateSipAddress;
}
get isMediaReleased() {
return this._mediaReleased;
}
async connect() {
const teams = this.teams = this.req.locals.target === 'teams';
const engine = this.srf.locals.getRtpEngine();
@@ -95,6 +112,7 @@ class CallSession extends Emitter {
unblockMedia,
blockDTMF,
unblockDTMF,
playDTMF,
subscribeDTMF,
unsubscribeDTMF,
subscribeRequest,
@@ -109,6 +127,7 @@ class CallSession extends Emitter {
this.unblockMedia = unblockMedia;
this.blockDTMF = blockDTMF;
this.unblockDTMF = unblockDTMF;
this.playDTMF = playDTMF;
this.subscribeDTMF = subscribeDTMF;
this.unsubscribeDTMF = unsubscribeDTMF;
this.subscribeRequest = subscribeRequest;
@@ -228,13 +247,13 @@ class CallSession extends Emitter {
}
// rtpengine 'offer'
const opts = {
const opts = updateRtpEngineFlags(this.req.body, {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['private', 'public'],
sdp: this.req.body
};
});
const response = await this.offer(opts);
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
@@ -343,6 +362,7 @@ class CallSession extends Emitter {
if (!this.req.locals.account.disable_cdrs) {
this.req.locals.cdr = {
...initCdr(this.req.srf, inv),
service_provider_sid: this.req.locals.service_provider_sid,
account_sid: this.req.locals.account_sid,
trunk
};
@@ -437,13 +457,17 @@ class CallSession extends Emitter {
await other.destroy();
} catch (err) {}
this.decrKey(this.callCountKey)
.then((count) => {
this.logger.debug(`after hangup there are ${count} active calls for this account`);
debug(`after hangup there are ${count} active calls for this account`);
return;
Promise.all([
this.decrKey(this.callCountKey),
this.decrKey(this.callCountKeySP)
])
.then(([calls, callsSP]) => {
this.logger.info({calls, callsSP}, 'decremented call counts after call completion');
return [calls, callsSP];
})
.catch((err) => this.logger.error({err}, 'Error decrementing call count'));
.catch((err) => {
this.logger.error({err}, 'error decrementing call counts after call completion');
});
/* write cdr for connected call */
if (this.req.locals.cdr) {
@@ -560,6 +584,7 @@ Duration=${payload.duration} `
this.logger.info(`got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp;
answerMedia.flags = ['asymmetric', 'port latching'];
this._mediaReleased = 'release-media' === reason;
}
else {
sdp = await dlg.other.modify(response.sdp);
@@ -586,6 +611,7 @@ Duration=${payload.duration} `
async _onInfo(dlg, req, res) {
try {
const contentType = req.get('Content-Type');
if (dlg.type === 'uas' && req.has('X-Reason')) {
const toTag = this.rtpEngineOpts.uac.tag;
const reason = req.get('X-Reason');
@@ -688,6 +714,40 @@ Duration=${payload.duration} `
res.send(succeeded ? 200 : 503);
}
}
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
const arr = /Signal=\s*([1-9#*])/.exec(req.body);
if (!arr) {
this.logger.info({body: req.body}, '_onInfo: invalid INFO dtmf request');
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
}
const code = arr[1];
const arr2 = /Duration=\s*(\d+)/.exec(req.body);
const duration = arr2 ? arr2[1] : 250;
if (this.isMediaReleased) {
/* just relay on to the feature server */
this.logger.info({code, duration}, 'got SIP INFO DTMF from caller, relaying to feature server');
this._onDTMF(dlg.other, {event: code, duration})
.catch((err) => this.logger.info({err}, 'Error relaying DTMF to feature server'));
res.send(200);
}
else {
/* else convert SIP INFO to RFC 2833 telephony events */
this.logger.info({code, duration}, 'got SIP INFO DTMF from caller, converting to RFC 2833');
const opts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uac.tag,
code,
duration
};
const response = await this.playDTMF(opts);
if ('ok' !== response.result) {
this.logger.info({response}, `rtpengine playDTMF failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer');
}
res.send(200);
}
}
else {
const response = await dlg.other.request({
method: 'INFO',
+26 -31
View File
@@ -1,7 +1,7 @@
const debug = require('debug')('jambonz:sbc-outbound');
const parseUri = require('drachtio-srf').parseUri;
const Registrar = require('@jambonz/mw-registrar');
const {selectHostPort, makeCallCountKey} = require('./utils');
const {selectHostPort, makeAccountCallCountKey, makeSPCallCountKey} = require('./utils');
const FS_UUID_SET_NAME = 'fsUUIDs';
module.exports = (srf, logger, opts) => {
@@ -63,7 +63,6 @@ module.exports = (srf, logger, opts) => {
}
}
stats.increment('sbc.invites', ['direction:outbound']);
req.on('cancel', () => {
@@ -76,6 +75,7 @@ module.exports = (srf, logger, opts) => {
try {
req.locals.account = await lookupAccountBySid(req.locals.account_sid);
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);
@@ -85,26 +85,21 @@ module.exports = (srf, logger, opts) => {
};
const checkLimits = async(req, res, next) => {
const {logger, account_sid} = req.locals;
const {logger, account_sid, service_provider_sid} = req.locals;
const {writeAlerts, AlertType} = req.srf.locals;
const key = makeCallCountKey(account_sid);
const keyAccount = makeAccountCallCountKey(account_sid);
const keySP = makeSPCallCountKey(service_provider_sid);
try {
/* increment the call count */
const calls = await incrKey(key);
debug(`checkLimits: call count is now ${calls}`);
const [calls, callsSP] = await Promise.all([incrKey(keyAccount), incrKey(keySP)]);
logger.info({calls, callsSP}, 'incremented call counts');
/* decrement count if INVITE is later rejected */
res.once('end', ({status}) => {
res.once('end', async({status}) => {
if (status > 200) {
debug('checkLimits: decrementing call count due to rejection');
decrKey(key)
.then((count) => {
logger.debug({key}, `after rejection there are ${count} active calls for this account`);
debug({key}, `after rejection there are ${count} active calls for this account`);
return;
})
.catch((err) => logger.error({err}, 'checkLimits: decrKey err'));
const [calls, callsSP] = await Promise.all([decrKey(keyAccount), decrKey(keySP)]);
logger.info({calls, callsSP}, `decremented call counts after ${status} response`);
const tags = ['accepted:no', `sipStatus:${status}`];
stats.increment('sbc.originations', tags);
}
@@ -122,24 +117,24 @@ module.exports = (srf, logger, opts) => {
const capacities = await lookupAccountCapacitiesBySid(account_sid);
const limit = capacities.find((c) => c.category == 'voice_call_session');
if (!limit) {
logger.debug('checkLimits: no call limits specified');
return next();
}
const limit_sessions = limit.quantity;
if (limit) {
const limit_sessions = limit.quantity;
if (calls > limit_sessions) {
debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`);
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
writeAlerts({
alert_type: AlertType.CALL_LIMIT,
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);
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);
}
}
//TODO: check SP limits
next();
} catch (err) {
logger.error({err}, 'error checking limits error for inbound call');
+10 -2
View File
@@ -7,6 +7,12 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
dstOpts.flags.push('inject DTMF');
srcOpts.flags.push('inject DTMF');
}
const common = {
'call-id': req.get('Call-ID'),
'replace': ['origin', 'session-connection']
@@ -71,7 +77,8 @@ const pingMsTeamsGateways = (logger, srf) => {
});
};
const makeCallCountKey = (sid) => `${sid}:outcalls`;
const makeAccountCallCountKey = (sid) => `${sid}:outcalls:account`;
const makeSPCallCountKey = (sid) => `${sid}:outcalls:sp`;
const equalsIgnoreOrder = (a, b) => {
if (a.length !== b.length) return false;
@@ -108,7 +115,8 @@ module.exports = {
makeRtpEngineOpts,
selectHostPort,
pingMsTeamsGateways,
makeCallCountKey,
makeAccountCallCountKey,
makeSPCallCountKey,
equalsIgnoreOrder,
systemHealth,
createHealthCheckApp
+19 -16
View File
@@ -1,12 +1,12 @@
{
"name": "sbc-outbound",
"version": "v0.7.5",
"version": "v0.7.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "sbc-outbound",
"version": "v0.7.5",
"version": "v0.7.6",
"license": "MIT",
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
@@ -16,13 +16,14 @@
"@jambonz/rtpengine-utils": "^0.3.1",
"@jambonz/siprec-client-utils": "^0.1.4",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.9",
"@jambonz/time-series": "^0.2.1",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"pino": "^7.11.0"
"pino": "^7.11.0",
"sdp-transform": "^2.14.1"
},
"devDependencies": {
"bent": "^7.3.12",
@@ -643,12 +644,12 @@
}
},
"node_modules/@jambonz/time-series": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.9.tgz",
"integrity": "sha512-FLD7mLGEMToG7s6LGWr/GtNp61753RBxyYuKSxCVI6G14bm6ydgcBdG85h8EGTVQlIp9dnwbn5ebSVsGb8a46w==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.1.tgz",
"integrity": "sha512-uAoeZ3ibS7kEOGdT+vaY8BB8hOV4q38eEaF+d5OvLQaHCrPonNiwB8tWhhXDwtYdDompfqVRUy/plNA9fyS7Vw==",
"dependencies": {
"debug": "^4.3.1",
"influx": "^5.8.0"
"influx": "^5.9.3"
}
},
"node_modules/@protobufjs/aspromise": {
@@ -2643,8 +2644,9 @@
}
},
"node_modules/influx": {
"version": "5.9.2",
"integrity": "sha512-N2YRIQiwCO60BzmzEzTxyWj/hDNsce2CLSmki8S1qbRwwuKecfvC6n0OwJrFV70lk5CpU/sLsOZFIy6PP01nOA=="
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/influx/-/influx-5.9.3.tgz",
"integrity": "sha512-QQU9CgwnaEV6zMrK8+vhVItsdoKFqDioXJrjJhRQaff9utvT3N0jcrQJT9qnxFLktqgJ5ngbDY68Zh4eo4uD/w=="
},
"node_modules/inherits": {
"version": "2.0.4",
@@ -5596,12 +5598,12 @@
}
},
"@jambonz/time-series": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.9.tgz",
"integrity": "sha512-FLD7mLGEMToG7s6LGWr/GtNp61753RBxyYuKSxCVI6G14bm6ydgcBdG85h8EGTVQlIp9dnwbn5ebSVsGb8a46w==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.1.tgz",
"integrity": "sha512-uAoeZ3ibS7kEOGdT+vaY8BB8hOV4q38eEaF+d5OvLQaHCrPonNiwB8tWhhXDwtYdDompfqVRUy/plNA9fyS7Vw==",
"requires": {
"debug": "^4.3.1",
"influx": "^5.8.0"
"influx": "^5.9.3"
}
},
"@protobufjs/aspromise": {
@@ -7131,8 +7133,9 @@
}
},
"influx": {
"version": "5.9.2",
"integrity": "sha512-N2YRIQiwCO60BzmzEzTxyWj/hDNsce2CLSmki8S1qbRwwuKecfvC6n0OwJrFV70lk5CpU/sLsOZFIy6PP01nOA=="
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/influx/-/influx-5.9.3.tgz",
"integrity": "sha512-QQU9CgwnaEV6zMrK8+vhVItsdoKFqDioXJrjJhRQaff9utvT3N0jcrQJT9qnxFLktqgJ5ngbDY68Zh4eo4uD/w=="
},
"inherits": {
"version": "2.0.4",
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "sbc-outbound",
"version": "v0.7.5",
"version": "v0.7.6",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
@@ -22,7 +22,7 @@
"description": "jambonz session border controller application for outbound calls",
"scripts": {
"start": "node app",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONZ_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONZ_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=info DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
},
@@ -34,13 +34,14 @@
"@jambonz/rtpengine-utils": "^0.3.1",
"@jambonz/siprec-client-utils": "^0.1.4",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.9",
"@jambonz/time-series": "^0.2.1",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"pino": "^7.11.0"
"pino": "^7.11.0",
"sdp-transform": "^2.14.1"
},
"devDependencies": {
"bent": "^7.3.12",