mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
19
Commits
v0.7.2
...
v0.7.5-rc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ccfa91b4f | ||
|
|
0c8967acdb | ||
|
|
1dfeed3ac8 | ||
|
|
d10bda2926 | ||
|
|
3938773738 | ||
|
|
f881002943 | ||
|
|
2266b80e73 | ||
|
|
4c3d6ddf0c | ||
|
|
3d1bcb23f4 | ||
|
|
98ecfa20aa | ||
|
|
56efe50aec | ||
|
|
891d0ff38b | ||
|
|
78d6cb5f22 | ||
|
|
23255a71db | ||
|
|
4cb34ad784 | ||
|
|
f096a31a95 | ||
|
|
f671ef56bb | ||
|
|
3a2fcf5976 | ||
|
|
dd85ec762d |
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run jslint
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:17.4-slim
|
||||
FROM node:17-slim
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
+48
-10
@@ -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 {
|
||||
@@ -519,6 +555,7 @@ Duration=${payload.duration} `
|
||||
}
|
||||
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 +569,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
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
Generated
+1940
-326
File diff suppressed because it is too large
Load Diff
+8
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "v0.7.2",
|
||||
"version": "v0.7.5",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -22,22 +22,23 @@
|
||||
"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.24",
|
||||
"@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",
|
||||
"drachtio-fn-b2b-sugar": "^0.0.12",
|
||||
"drachtio-srf": "^4.4.59",
|
||||
"husky": "^7.0.4",
|
||||
"pino": "^7.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+7
-1
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user