Compare commits

...
25 Commits
Author SHA1 Message Date
Dave Horton 7d2125788f when releasing media, use asymetric flag so that rtpengine does react to a spurious final packet from freeswitch by incorrectly sending rtp there 2022-07-26 12:25:58 +01:00
Snyk bot 3b83c1bda8 fix: upgrade drachtio-srf from 4.5.0 to 4.5.1 (#42)
Snyk has created this PR to upgrade drachtio-srf from 4.5.0 to 4.5.1.

See this package in npm:
https://www.npmjs.com/package/drachtio-srf

See this project in Snyk:
https://app.snyk.io/org/davehorton/project/282b1881-3e13-4fad-85bd-3b1c662c2134?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-07-13 09:16:37 +02:00
Paulo Tellesandp.souza f352bf885c improve dockerfile to fix snyk security issues (#41)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-07-07 15:19:14 +02:00
Dave Horton 08a4f5defb update Dockerfile 2022-06-23 09:57:38 -04:00
Dave Horton 87df38110b update to azure 1.22.0 2022-06-11 16:23:08 -04:00
Dave Horton 06e370fa59 update deps 2022-06-11 11:55:21 -04:00
Dave Horton e4ed2cea26 healthcheck improvements (#38) 2022-04-12 15:45:33 -04:00
Dave Horton 0c8967acdb bump version 2022-04-06 08:18:22 -04:00
Dave Horton 1dfeed3ac8 bugfix: mem leak 2022-04-05 19:57:03 -04:00
Dave Horton d10bda2926 update deps 2022-04-01 13:46:16 -04:00
Dave Horton 3938773738 mem leak fix 2022-03-28 09:09:14 -04:00
Dave Horton f881002943 write otel trace_id to call history 2022-03-23 09:26:04 -04:00
Dave Horton 2266b80e73 release drachtio connection on final outbound failure 2022-03-18 14:34:30 -04:00
Snyk bot 4c3d6ddf0c fix: Dockerfile to reduce vulnerabilities (#35)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-DEBIAN11-GNUTLS28-2419151
- https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2388380
- https://snyk.io/vuln/SNYK-DEBIAN11-OPENSSL-2426309
- https://snyk.io/vuln/SNYK-DEBIAN11-UTILLINUX-2401081
- https://snyk.io/vuln/SNYK-DEBIAN11-UTILLINUX-2401081
2022-03-18 07:55:10 -04:00
Dave Horton 3d1bcb23f4 bump version 2022-03-08 20:16:19 -05:00
Dave Horton 98ecfa20aa add support for redis auth 2022-03-08 09:30:44 -05:00
Dave Horton 56efe50aec forward incoming REFER to FS (#34) 2022-03-05 15:21:49 -05:00
Dave Horton 891d0ff38b use registered contact as uri when sending to user (#32) 2022-02-23 15:09:18 -05:00
Dave Horton 78d6cb5f22 use registered contact as uri when sending to user (#31) 2022-02-17 21:35:19 -05:00
Dave Horton 23255a71db added pre-commit hook for linting 2022-02-14 13:18:46 -05:00
Dave Horton 4cb34ad784 regression handling 302 redirect 2022-02-14 12:11:32 -05:00
Dave Horton f096a31a95 bump version 2022-02-09 15:43:50 -05:00
Dave Horton f671ef56bb update to latest @jambonz/realtimedb-helpers with support for redis username / password auth 2022-02-09 15:16:03 -05:00
Dave Horton 3a2fcf5976 update deps 2022-02-09 08:24:43 -05:00
Dave Horton dd85ec762d merge support for X-Override-To feature back into main branch 2022-02-01 12:30:34 -05:00
10 changed files with 1134 additions and 5321 deletions
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run jslint
+19 -6
View File
@@ -1,10 +1,23 @@
FROM node:17.4-slim
FROM --platform=linux/amd64 node:16.15.1-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
COPY . /opt/app
FROM base as build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "npm", "start" ]
CMD [ "node", "app.js" ]
+27 -5
View File
@@ -11,7 +11,7 @@ assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONE
const Srf = require('drachtio-srf');
const srf = new Srf('sbc-outbound');
const CIDRMatcher = require('cidr-matcher');
const {pingMsTeamsGateways, equalsIgnoreOrder} = require('./lib/utils');
const {equalsIgnoreOrder, pingMsTeamsGateways, createHealthCheckApp, systemHealth} = require('./lib/utils');
const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
@@ -32,6 +32,7 @@ const CallSession = require('./lib/call-session');
const setNameRtp = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-rtp`;
const rtpServers = [];
const {
ping,
performLcr,
lookupAllTeamsFQDNs,
lookupAccountBySipRealm,
@@ -47,6 +48,7 @@ const {
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger);
const {
client: redisClient,
createHash,
retrieveHash,
incrKey,
@@ -68,6 +70,7 @@ srf.locals = {...srf.locals,
queryCdrs,
activeCallIds,
dbHelpers: {
ping,
performLcr,
lookupAllTeamsFQDNs,
lookupAccountBySipRealm,
@@ -137,13 +140,32 @@ srf.invite((req, res) => {
session.connect();
});
if (process.env.K8S) {
if (process.env.K8S || process.env.HTTP_PORT) {
const PORT = process.env.HTTP_PORT || 3000;
const getCount = () => activeCallIds.size;
const healthCheck = require('@jambonz/http-health-check');
healthCheck({port: PORT, logger, path: '/', fn: getCount});
}
const getCount = () => srf.locals.activeCallIds.size;
createHealthCheckApp(PORT, logger)
.then((app) => {
healthCheck({
app,
logger,
path: '/',
fn: getCount
});
healthCheck({
app,
logger,
path: '/system-health',
fn: systemHealth.bind(null, redisClient, ping, getCount)
});
return;
})
.catch((err) => {
logger.error({err}, 'Error creating health check server');
});
}
if ('test' !== process.env.NODE_ENV) {
/* update call stats periodically */
setInterval(() => {
+49 -10
View File
@@ -43,7 +43,8 @@ const initCdr = (srf, req) => {
attempted_at: Date.now(),
direction: 'outbound',
host: srf.locals.sipAddress,
remote_host: uri.host
remote_host: uri.host,
trace_id: req.get('X-Trace-ID') || '00000000000000000000000000000000'
};
};
@@ -124,14 +125,19 @@ class CallSession extends Emitter {
};
if (this.req.locals.registration) {
debug(`sending call to user ${JSON.stringify(this.req.locals.registration)}`);
debug(`sending call to registered user ${JSON.stringify(this.req.locals.registration)}`);
const contact = this.req.locals.registration.contact;
if (contact.includes('transport=ws')) {
uris = [contact];
let destUri = contact;
if (this.req.has('X-Override-To')) {
const dest = this.req.get('X-Override-To');
const uri = parseUri(contact);
uri.user = dest;
destUri = stringifyUri(uri);
this.logger.info(`overriding destination user with ${dest}, so final uri is ${destUri}`);
}
else {
uris = [destUri];
if (!contact.includes('transport=ws')) {
proxy = this.req.locals.registration.proxy;
uris = [this.req.uri];
}
}
else if (this.req.locals.target === 'forward') {
@@ -281,6 +287,7 @@ class CallSession extends Emitter {
'all',
'-X-MS-Teams-FQDN',
'-X-MS-Teams-Tenant-FQDN',
'-X-Trace-ID',
'X-CID',
'-Allow',
'-Session-Expires',
@@ -374,6 +381,7 @@ class CallSession extends Emitter {
this.emit('failed');
this.rtpEngineResource.destroy()
.catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure'));
this.srf.endSession(this.req);
const tags = ['accepted:no', `sipStatus:${status}`];
this.stats.increment('sbc.originations', tags);
@@ -381,7 +389,7 @@ class CallSession extends Emitter {
this.writeCdrs({...this.req.locals.cdr,
terminated_at: Date.now(),
termination_reason: 487 === status ? 'caller abandoned' : 'failed',
sip_status: status,
sip_status: status
}).catch((err) => this.logger.error({err}, 'Error writing cdr for call failure'));
}
}
@@ -413,12 +421,14 @@ class CallSession extends Emitter {
this.uas = uas;
this.uac = uac;
[uas, uac].forEach((dlg) => {
dlg.on('destroy', () => {
this.logger.info('call ended');
dlg.on('destroy', async() => {
const other = dlg.other;
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uac.tag);
dlg.other.destroy();
try {
await other.destroy();
} catch (err) {}
this.decrKey(this.callCountKey)
.then((count) => {
@@ -439,6 +449,14 @@ class CallSession extends Emitter {
duration: Math.floor((now - callStart) / 1000)
}).catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
}
/* de-link the 2 Dialogs for GC */
dlg.removeAllListeners();
other.removeAllListeners();
dlg.other = null;
other.other = null;
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
this.srf.endSession(this.req);
});
});
@@ -450,6 +468,7 @@ class CallSession extends Emitter {
uac.on('modify', this._onReinvite.bind(this, uac));
uas.on('refer', this._onFeatureServerTransfer.bind(this, uas));
uac.on('refer', this._onRefer.bind(this, uac));
uas.on('info', this._onInfo.bind(this, uas));
uac.on('info', this._onInfo.bind(this, uac));
@@ -458,6 +477,23 @@ class CallSession extends Emitter {
forwardInDialogRequests(uac, ['notify', 'options', 'message']);
}
async _onRefer(dlg, req, res) {
/* REFER coming in from a sip device, forward to feature server */
try {
const response = await dlg.other.request({
method: 'REFER',
headers: {
'Refer-To': req.get('Refer-To'),
'Referred-By': req.get('Referred-By'),
'User-Agent': req.get('User-Agent')
}
});
res.send(response.status, response.reason);
} catch (err) {
this.logger.error({err}, 'CallSession:_onRefer: error handling incoming REFER');
}
}
async _onDTMF(dlg, payload) {
this.logger.info({payload}, '_onDTMF');
try {
@@ -516,9 +552,11 @@ Duration=${payload.duration} `
if (reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason)) {
this.logger.info(`got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp;
answerMedia.flags = ['asymmetric', 'port latching'];
}
else {
sdp = await dlg.other.modify(response.sdp);
this.logger.info({sdp}, 'CallSession:_onReinvite: got sdp from 200 OK to invite we sent');
}
opts = {
...this.rtpEngineOpts.common,
@@ -532,6 +570,7 @@ Duration=${payload.duration} `
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
this.logger.info({sdp: response.sdp}, 'CallSession:_onReinvite: sending back upstream');
res.send(200, {body: response.sdp});
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
+6 -1
View File
@@ -17,7 +17,12 @@ module.exports = (srf, logger, opts) => {
req.locals = req.locals || {};
const callId = req.get('Call-ID');
req.locals.account_sid = req.get('X-Account-Sid');
req.locals.logger = logger.child({callId, account_sid: req.locals.account_sid});
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');
+23 -1
View File
@@ -84,10 +84,32 @@ const equalsIgnoreOrder = (a, b) => {
return true;
};
const systemHealth = async(redisClient, ping, getCount) => {
await Promise.all([redisClient.ping(), ping()]);
return getCount();
};
const createHealthCheckApp = (port, logger) => {
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
return new Promise((resolve) => {
app.listen(port, () => {
logger.info(`Health check server started at http://localhost:${port}`);
resolve(app);
});
});
};
module.exports = {
makeRtpEngineOpts,
selectHostPort,
pingMsTeamsGateways,
makeCallCountKey,
equalsIgnoreOrder
equalsIgnoreOrder,
systemHealth,
createHealthCheckApp
};
+981 -5285
View File
File diff suppressed because it is too large Load Diff
+14 -12
View File
@@ -1,6 +1,6 @@
{
"name": "sbc-outbound",
"version": "v0.7.2",
"version": "v0.7.5",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
@@ -22,28 +22,30 @@
"description": "jambonz session border controller application for outbound calls",
"scripts": {
"start": "node app",
"test": "NODE_ENV=test 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=error 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"
},
"dependencies": {
"@jambonz/db-helpers": "^0.6.16",
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "0.2.1",
"@jambonz/realtimedb-helpers": "^0.4.9",
"@jambonz/rtpengine-utils": "^0.2.2",
"@jambonz/mw-registrar": "0.2.2",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/rtpengine-utils": "^0.3.1",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.6",
"@jambonz/time-series": "^0.1.9",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.3",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.4.59",
"pino": "^7.4.1"
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"pino": "^7.11.0"
},
"devDependencies": {
"bent": "^7.3.12",
"eslint": "^7.32.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-promise": "^5.2.0",
"nyc": "^15.1.0",
"tape": "^5.3.2"
"tape": "^5.5.3"
}
}
+4
View File
@@ -5,3 +5,7 @@ DRACHTIO_SECRET=cymru
JAMBONES_REDIS_HOST=172.39.0.11
JAMBONES_REDIS_PORT=6379
JAMBONES_LOGLEVEL=info
JAMBONES_MYSQL_HOST=172.39.0.2
JAMBONES_MYSQL_USER=jambones_test
JAMBONES_MYSQL_PASSWORD=jambones_test
JAMBONES_MYSQL_DATABASE=jambones_test
+7 -1
View File
@@ -2,7 +2,8 @@ const test = require('tape');
const { output, sippUac } = require('./sipp')('test_sbc-outbound');
const {execSync} = require('child_process');
const debug = require('debug')('jambonz:sbc-outbound');
const consoleLogger = {error: console.error, info: console.log, debug: console.log};
const bent = require('bent');
const getJSON = bent('json');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -29,6 +30,11 @@ test('sbc-outbound tests', async(t) => {
try {
await connect(srf);
let obj = await getJSON('http://127.0.0.1:3050/');
t.ok(obj.calls === 0, 'HTTP GET / works (current call count)')
obj = await getJSON('http://127.0.0.1:3050/system-health');
t.ok(obj.calls === 0, 'HTTP GET /system-health works (health check)')
/* call to unregistered user */
debug('successfully connected to drachtio server');
await sippUac('uac-pcap-device-404.xml');