Compare commits

..
42 Commits
Author SHA1 Message Date
Dave Horton a7bf420dd6 bugfix: reinvite handling mixed up the public-private direction in rtpengine offer 2021-08-09 15:45:20 -04:00
Dave Horton 55b02c924f if we get re-invite with no SDP (looking at you, BT) just respond with current offer 2021-08-03 10:41:19 -04:00
Dave Horton df02837c82 brackets around From 2021-07-28 16:22:05 -04:00
Dave Horton e3963bf057 bugfix: re-invite handling from UAC side now handled properly 2021-04-27 08:16:36 -04:00
Dave Horton 238f3fe05a add test case for cidr matching of carrier inbound calls 2021-03-10 10:45:56 -05:00
Dave Horton 51e926629c update to db-helpers that supports sip gateways identified by cidr range 2021-03-09 19:00:35 -05:00
Dave Horton 3df94ced57 Merge pull request #8 from radicaldrew/master
Updated Dockerfile
2020-12-30 08:57:55 -05:00
Andrew 62063a7250 Updated Dockerfile
created multistage build and tested in compose
2020-12-30 15:56:58 +02:00
Dave Horton 3d58fdd242 dep change to @jambonz/http-authenticator 2020-12-15 14:26:09 -05:00
Dave Horton 12ba6b3968 Merge pull request #7 from jambonz/gh-actions
migrate ci to github actions
2020-12-14 14:50:04 -05:00
Dave Horton ae8469cfee jslint 2020-12-14 14:47:05 -05:00
Dave Horton bf7c09fc83 migrate ci to github actions 2020-12-14 14:44:41 -05:00
Dave Horton 4476c233ff update deps 2020-12-13 14:23:17 -05:00
Dave Horton 45883238e1 update to stats-collector that reconnects when socket dropped 2020-12-11 14:44:57 -05:00
Dave Horton 471f3dc0bb deps 2020-10-26 12:03:56 -04:00
Dave Horton 5ba9058216 update realtimedb-helpers 2020-10-26 10:04:30 -04:00
Dave Horton 6f82454be2 bump deps 2020-10-26 09:29:03 -04:00
Dave Horton 5f60580040 update stats 2020-10-26 09:26:43 -04:00
Dave Horton d0b0feafdc bump deps again 2020-08-18 14:53:41 -04:00
Dave Horton b3e38ab761 update deps 2020-08-18 14:50:56 -04:00
Dave Horton ee16cdd859 deps 2020-07-16 15:59:20 -04:00
Dave Horton 28cfe27b58 update deps 2020-07-09 12:57:50 -04:00
Dave Horton e1ea9dbdf2 update to latest rtpengine-utils 2020-07-09 12:39:08 -04:00
Dave Horton 3fdbeb64f8 update deps and use new rtpengine statistics command 2020-07-09 10:05:40 -04:00
Dave Horton 2204140291 update deps 2020-07-07 13:48:05 -04:00
Dave Horton f01d2140db dont pass Session-Expires header into feature server 2020-06-10 12:04:13 -04:00
Dave Horton ae9b4acf19 update to drachtio-srf@4.4.34 2020-06-08 14:26:59 -04:00
Dave Horton 09a1386283 bugfix: rtpengine now generates internal IP on sdp for internal-facing streams 2020-06-07 14:51:37 -04:00
Dave Horton 958c34efbc add support for ms teams 2020-05-22 19:16:11 -04:00
Dave Horton d7df23bfc1 pass X-Retain-Call-Sid on REFER from feature server 2020-05-11 09:12:33 -04:00
Dave Horton 2914c9f356 reinvite changes 2020-04-26 09:58:14 -04:00
Dave Horton de64af278d simplify handling of reinvites from feature server 2020-04-24 16:20:20 -04:00
Dave Horton 2c751a0b36 bugfix: regex error when feature server was removed 2020-04-21 08:14:15 -04:00
Dave Horton 3bb719c035 revert one change from prev checkin as it impacted testing 2020-04-20 16:52:27 -04:00
Dave Horton 203cd0d16b bugfix: setting public ip and handling options ping 2020-04-20 16:34:54 -04:00
Dave Horton c1e4324b51 add sbc public address to database on startup 2020-04-20 12:26:55 -04:00
Dave Horton b4766a4b88 save current list of active feature servers to redis, for use by api server 2020-04-20 09:46:38 -04:00
Dave Horton 0636b294b1 bugfix: checking feature server map 2020-04-20 08:20:11 -04:00
Dave Horton 1d1f3d5c10 attach X-Authenticated-User header for incoming calls from registered user 2020-04-19 10:27:58 -04:00
Dave Horton 48fe42098e update deps 2020-04-16 11:16:05 -04:00
Dave Horton efaf7d28eb minor stats fix 2020-04-03 11:11:46 -04:00
Dave Horton 1ad61dc7b2 bugfix: PAI header on incoming call was being put in From header of outbound call 2020-04-03 11:11:15 -04:00
22 changed files with 6970 additions and 3795 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"jsx": false,
"modules": false
},
"ecmaVersion": 2017
"ecmaVersion": 2018
},
"plugins": ["promise"],
"rules": {
+19
View File
@@ -0,0 +1,19 @@
name: CI
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm install
- run: npm run jslint
- run: npm test
-9
View File
@@ -1,9 +0,0 @@
sudo: required
language: node_js
node_js:
- "lts/*"
services:
- docker
- mysql
script:
- npm test
+10 -7
View File
@@ -1,13 +1,16 @@
FROM node:lts-alpine
FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
FROM node:alpine as app
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app
CMD [ "npm", "start" ]
+1 -1
View File
@@ -1,4 +1,4 @@
# sbc-inbound [![Build Status](https://secure.travis-ci.org/jambonz/sbc-inbound.png)](http://travis-ci.org/jambonz/sbc-inbound)
# sbc-inbound ![Build Status](https://github.com/jambonz/sbc-inbound/workflows/CI/badge.svg)
This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
+22 -7
View File
@@ -13,20 +13,21 @@ const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
const logger = require('pino')(opts);
const StatsCollector = require('jambonz-stats-collector');
const StatsCollector = require('@jambonz/stats-collector');
const stats = srf.locals.stats = new StatsCollector(logger);
srf.locals.getFeatureServer = require('./lib/fs-tracking')(srf, logger);
const {getRtpEngine} = require('jambonz-rtpengine-utils')(process.env.JAMBONES_RTPENGINES.split(','), logger, {
const {getRtpEngine} = require('@jambonz/rtpengine-utils')(process.env.JAMBONES_RTPENGINES.split(','), logger, {
emitter: srf.locals.stats
});
srf.locals.getRtpEngine = getRtpEngine;
const activeCallIds = srf.locals.activeCallIds = new Set();
const activeCallIds = srf.locals.activeCallIds = new Map();
logger.info('starting..');
const {
lookupAuthHook,
lookupSipGatewayBySignalingAddress
} = require('jambonz-db-helpers')({
lookupSipGatewayBySignalingAddress,
addSbcAddress
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD,
@@ -44,7 +45,10 @@ const CallSession = require('./lib/call-session');
if (process.env.DRACHTIO_HOST) {
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
srf.on('connect', (err, hp) => {
logger.info(`connected to drachtio listening on ${hp}`);
const last = hp.split(',').pop();
const arr = /^(.*)\/(.*):(\d+)$/.exec(last);
logger.info(`connected to drachtio listening on ${hp}: adding ${arr[2]} to sbc_addresses table`);
addSbcAddress(arr[2]);
});
}
else {
@@ -60,6 +64,17 @@ if (process.env.NODE_ENV === 'test') {
srf.use('invite', [initLocals, challengeDeviceCalls]);
srf.invite((req, res) => {
if (req.has('Replaces')) {
const arr = /^(.*);from/.exec(req.get('Replaces'));
if (arr) logger.info(`replacing call-id ${arr}`);
else logger.info(`failed parsing ${req.get('Replaces')}`);
const session = arr ? activeCallIds.get(arr[1]) : null;
if (!session) {
logger.info(`failed to find session in Replaces header: ${req.has('Replaces')}`);
return res.send(404);
}
return session.replaces(req, res);
}
const session = new CallSession(logger, req, res);
session.connect();
});
@@ -71,6 +86,6 @@ srf.use((req, res, next, err) => {
setInterval(() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:inbound']);
}, 5000);
}, 20000);
module.exports = {srf, logger};
+2 -1
View File
@@ -3,5 +3,6 @@
"DTLS": "off",
"SDES": "off",
"ICE": "remove",
"rtcp-mux": ["demux"]
"flags": ["media handover"],
"rtcp-mux": ["accept"]
}
+14 -5
View File
@@ -1,7 +1,16 @@
{
"transport-protocol": "UDP/TLS/RTP/SAVPF",
"ICE": "force",
"SDES": "off",
"flags": ["generate mid", "SDES-no"],
"rtcp-mux": ["require"]
"default": {
"transport-protocol": "UDP/TLS/RTP/SAVPF",
"ICE": "force",
"SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover"],
"rtcp-mux": ["require"]
},
"teams": {
"transport-protocol": "RTP/SAVP",
"ICE": "force",
"SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover"],
"rtcp-mux": ["accept"]
}
}
+276 -23
View File
@@ -1,8 +1,21 @@
const Emitter = require('events');
const {isWSS, makeRtpEngineOpts} = require('./utils');
const {makeRtpEngineOpts, SdpWantsSrtp} = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {parseUri, SipError} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-inbound');
const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy';
const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
/**
* this is to make sure the outgoing From has the number in the incoming From
* and not the incoming PAI
*/
const createBLegFromHeader = (req) => {
const from = req.getParsedHeader('From');
const uri = parseUri(from.uri);
if (uri && uri.user) return `<sip:${uri.user}@localhost>`;
return '<sip:anonymous@localhost>';
};
class CallSession extends Emitter {
constructor(logger, req, res) {
@@ -16,7 +29,10 @@ class CallSession extends Emitter {
this.getFeatureServer = req.srf.locals.getFeatureServer;
this.stats = this.srf.locals.stats;
this.activeCallIds = this.srf.locals.activeCallIds;
}
get isFromMSTeams() {
return !!this.req.locals.msTeamsTenantFqdn;
}
async connect() {
@@ -43,37 +59,58 @@ class CallSession extends Emitter {
}
debug(`using feature server ${featureServer}`);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, isWSS(this.req), false);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, SdpWantsSrtp(this.req.body), false, this.isFromMSTeams);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
const obj = parseUri(this.req.uri);
let proxy, host, uri;
// replace host part of uri if its an ipv4 address, leave it otherwise
if (/\d{1-3}\.\d{1-3}\.\d{1-3}\.\d{1-3}/.test(obj.host)) {
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(obj.host)) {
debug(`replacing host: was ${obj.host} is ${featureServer}`);
host = featureServer;
}
else {
debug(`not replacing host: "${obj.host}"`);
host = obj.host;
proxy = `sip:${featureServer}`;
}
else {
host = featureServer;
}
if (obj.user) uri = `${obj.scheme}:${obj.user}@${host}`;
else uri = `${obj.scheme}:${host}`;
debug(`uri will be: ${uri}, proxy ${proxy}`);
this.logger.info(`uri will be: ${uri}, proxy ${proxy}`);
try {
const response = await this.offer(this.rtpEngineOpts.offer);
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
const opts = {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['public', 'private'],
sdp: this.req.body
};
const response = await this.offer(opts);
this.logger.debug({opts, response}, 'response from rtpengine to offer');
if ('ok' !== response.result) {
this.logger.error(`rtpengine offer failed with ${JSON.stringify(response)}`);
this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer');
}
// now send the INVITE in towards the feature servers
const headers = {
'From': createBLegFromHeader(this.req),
'To': this.req.get('To'),
'X-CID': this.req.get('Call-ID'),
'X-Forwarded-For': `${this.req.source_address}:${this.req.source_port}`
};
const responseHeaders = {};
if (this.req.locals.carrier) Object.assign(headers, {'X-Originating-Carrier': this.req.locals.carrier});
if (this.req.locals.msTeamsTenantFqdn) {
Object.assign(headers, {'X-MS-Teams-Tenant-FQDN': this.req.locals.msTeamsTenantFqdn});
// for Microsoft Teams the Contact header must include the tenant FQDN
Object.assign(responseHeaders, {
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
Contact: `sip:${this.req.locals.msTeamsTenantFqdn}`
});
}
if (this.req.locals.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid});
}
@@ -81,7 +118,7 @@ class CallSession extends Emitter {
if (this.req.authorization.grant && this.req.authorization.grant.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid});
}
else if (this.req.authorization.challengeResponse) {
if (this.req.authorization.challengeResponse) {
const {username, realm} = this.req.authorization.challengeResponse;
Object.assign(headers, {'X-Authenticated-User': `${username}@${realm}`});
}
@@ -93,12 +130,19 @@ class CallSession extends Emitter {
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
proxy,
headers,
proxyRequestHeaders: ['all', '-Authorization', '-Max-Forwards'],
responseHeaders,
proxyRequestHeaders: ['all', '-Authorization', '-Max-Forwards', '-Record-Route', '-Session-Expires', 'Min-SE'],
proxyResponseHeaders: ['all'],
localSdpB: response.sdp,
localSdpA: async(sdp, res) => {
const opts = Object.assign({sdp, 'to-tag': res.getParsedHeader('To').params.tag},
this.rtpEngineOpts.answer);
this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
const opts = {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uas.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
sdp
};
const response = await this.answer(opts);
if ('ok' !== response.result) {
this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`);
@@ -128,11 +172,23 @@ class CallSession extends Emitter {
}
}
_setDlgHandlers(dlg) {
this.activeCallIds.set(this.req.get('Call-ID'), this);
dlg.on('destroy', () => {
this.logger.info('call ended with normal termination');
this.rtpEngineResource.destroy().catch((err) => {});
this.activeCallIds.delete(this.req.get('Call-ID'));
if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {});
});
//re-invite
dlg.on('modify', this._onReinvite.bind(this, dlg));
}
_setHandlers({uas, uac}) {
this.emit('connected');
const tags = ['accepted:yes', 'sipStatus:200', `originator:${this.req.locals.originator}`];
this.stats.increment('sbc.terminations', tags);
this.activeCallIds.add(this.req.get('Call-ID'));
this.activeCallIds.set(this.req.get('Call-ID'), this);
this.uas = uas;
this.uac = uac;
@@ -140,28 +196,108 @@ class CallSession extends Emitter {
//hangup
dlg.on('destroy', () => {
this.logger.info('call ended with normal termination');
this.rtpEngineResource.destroy();
this.rtpEngineResource.destroy().catch((err) => {});
this.activeCallIds.delete(this.req.get('Call-ID'));
dlg.other.destroy().catch((e) => {});
});
//re-invite
dlg.on('modify', this._onReinvite.bind(this, dlg));
});
uas.on('modify', this._onReinvite.bind(this, uas));
uac.on('modify', this._onReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uas.on('refer', this._onRefer.bind(this, uas));
// default forwarding of other request types
forwardInDialogRequests(uas);
forwardInDialogRequests(uas, ['info', 'notify', 'options', 'message']);
}
/**
* handle INVITE with Replaces header from uas side (this will never come from the feature server)
* @param {*} req incoming request
* @param {*} res incoming response
*/
async replaces(req, res) {
try {
let opts = Object.assign(this.rtpEngineOpts.offer, {sdp: req.body});
let response = await this.offer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`replaces: rtpengine failed: offer: ${JSON.stringify(response)}`);
}
this.logger.info({opts, response}, 'sent offer for reinvite to rtpengine');
const sdp = await this.uac.modify(response.sdp);
opts = Object.assign(this.rtpEngineOpts.answer, {sdp, 'to-tag': this.toTag});
Object.assign(this.rtpEngineOpts.offer, {'to-tag': this.toTag});
response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`replaces: rtpengine failed: ${JSON.stringify(response)}`);
}
this.logger.info({opts, response}, 'sent answer for reinvite to rtpengine');
const headers = {};
if (this.req.locals.msTeamsTenantFqdn) {
Object.assign(headers, {'X-MS-Teams-Tenant-FQDN': this.req.locals.msTeamsTenantFqdn});
// for Microsoft Teams the Contact header must include the tenant FQDN
Object.assign(headers, {
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY',
Contact: `sip:${this.req.locals.msTeamsTenantFqdn}`
});
}
const uas = await this.srf.createUAS(req, res, {
localSdp: response.sdp,
headers
});
this.logger.info('successfully connected new INVITE w/replaces, hanging up leg being replaced');
this.uas.destroy();
this.req = req;
this.uas = uas;
this.uas.other = this.uac;
this.uac.other = this.uas;
this.activeCallIds.delete(this.req.get('Call-ID'));
this._setDlgHandlers(uas);
} catch (err) {
this.logger.error(err, 'Error handling invite with replaces');
res.send(err.status || 500);
}
}
async _onReinvite(dlg, req, res) {
try {
let response = await this.offer(Object.assign({sdp: req.body}, this.rtpEngineOpts.offer));
/* check for re-invite with no SDP -- seen that from BT when they provide UUI info */
if (!req.body) {
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
res.send(200, {body: dlg.local.sdp});
return;
}
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
'from-tag': fromTag,
'to-tag': toTag,
direction,
sdp: req.body,
};
let response = await this.offer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`);
}
const sdp = await dlg.other.modify(response.sdp);
const opts = Object.assign({sdp, 'to-tag': res.getParsedHeader('To').params.tag},
this.rtpEngineOpts.answer);
opts = {
...this.rtpEngineOpts.common,
...answerMedia,
'from-tag': fromTag,
'to-tag': toTag,
sdp
};
response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
@@ -173,6 +309,123 @@ class CallSession extends Emitter {
}
}
async _onFeatureServerTransfer(dlg, req, res) {
try {
const referTo = req.getParsedHeader('Refer-To');
const uri = parseUri(referTo.uri);
this.logger.info({uri, referTo}, 'received REFER from feature server');
const arr = /context-(.*)/.exec(uri.user);
if (!arr) {
this.logger.info(`invalid Refer-To header: ${referTo.uri}`);
return res.send(501);
}
res.send(202);
// invite to new fs
const headers = {};
if (req.has('X-Retain-Call-Sid')) {
Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')});
}
const uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uac = uac;
uac.other = this.uas;
this.uas.other = uac;
uac.on('modify', this._onFeatureServerReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uac.on('destroy', () => {
this.logger.info('call ended with normal termination');
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
uac.other.destroy();
});
// now we can destroy the old dialog
dlg.destroy().catch(() => {});
// modify rtpengine to stream to new feature server
const opts = Object.assign({sdp: uac.remote.sdp, 'to-tag': res.getParsedHeader('To').params.tag},
this.rtpEngineOpts.answer);
const response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onFeatureServerReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
this.logger.info('successfully moved call to new feature server');
} catch (err) {
this.logger.error(err, 'Error handling refer from feature server');
}
}
async _onRefer(dlg, req, res) {
const ua = req.get('User-Agent');
const referTo = req.get('Refer-To');
const rt = req.getParsedHeader('Refer-To');
const uri = parseUri(rt.uri);
this.logger.info({referTo, ua, rt, uri}, 'got a REFER');
/**
* send NOTIFY of INVITE status, return true if call answered
*/
const sendNotify = (dlg, body) => {
const arr = /SIP\/2.0\s+(\d+).*$/.exec(body);
const status = arr ? parseInt(arr[1]) : null;
dlg.request({
method: 'NOTIFY',
headers: {
'Content-Type': 'message/sipfrag;version=2.0',
'Contact': `sip:${this.req.locals.msTeamsTenantFqdn}`
},
body
});
this.logger.info(`sent NOTIFY for REFER with status ${status}`);
return status === 200;
};
if (this.isFromMSTeams && ua.startsWith(MS_TEAMS_USER_AGENT) &&
referTo.startsWith(`<sip:${MS_TEAMS_SIP_ENDPOINT}`) &&
!uri.user) {
// the Refer-To endpoint is within Teams itself, so we can handle
res.send(202);
try {
const dlg = await this.srf.createUAC(rt.uri, {
localSdp: this.uas.local.sdp.replace(/a=inactive/g, 'a=sendrecv'),
headers: {
'From': `sip:${this.req.callingNumber}@${this.req.locals.msTeamsTenantFqdn}`,
'Contact': `sip:${this.req.callingNumber}@${this.req.locals.msTeamsTenantFqdn}`
}
},
{
cbRequest: (err, inviteSent) => {
if (err) return sendNotify(this.uas, `SIP/2.0 ${err.status || '500'}`);
sendNotify(this.uas, '100 Trying ');
this.referInvite = inviteSent;
},
cbProvisional: (prov) => {
sendNotify(this.uas, `${prov.status} ${prov.reason}`);
}
});
// successfully connected
this.logger.info('successfully connected new call leg for REFER');
this.referInvite = null;
sendNotify(this.uas, '200 OK');
this.uas.destroy();
this.uas = dlg;
this.uas.other = this.uac;
this.activeCallIds.delete(this.req.get('Call-ID'));
this._setDlgHandlers(dlg);
} catch (err) {
this.logger.error({err}, 'Error creating new call leg for REFER');
sendNotify(this.uas, `${err.status || 500} ${err.reason || ''}`);
}
}
else {
// TODO: forward on to feature server
res.send(501);
}
}
}
module.exports = CallSession;
+27 -8
View File
@@ -7,6 +7,12 @@ module.exports = (srf, logger) => {
let dynamic = true;
let idx = 0;
const stats = srf.locals.stats;
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {createSet} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
srf.options((req, res) => {
res.send(200);
@@ -15,24 +21,37 @@ module.exports = (srf, logger) => {
const status = req.get('X-FS-Status');
const calls = req.has('X-FS-Calls') ? parseInt(req.get('X-FS-Calls')) : 0;
if (status === 'open') {
if (!contacts.has(uri)) {
logger.info(`adding feature server at ${uri}`);
stats.gauge('sbc.featureservers.count', contacts.size + 1);
}
const adding = !contacts.has(uri);
debug(`Feature server at ${uri} has ${calls} calls`);
contacts.set(uri, {pingTime: new Date(), calls: calls});
if (adding) {
logger.info(`adding feature server at ${uri}: ${[...contacts.keys()]}`);
stats.gauge('sbc.featureservers.count', contacts.size);
const featureServerIps = [...contacts.keys()].map((u) => {
const arr = /^(.*):\d+$/.exec(u);
return arr[1];
});
createSet(setName, new Set(featureServerIps));
}
}
else {
if (contacts.includes(uri)) {
logger.info(`removing feature server at ${uri}`);
if (contacts.has(uri)) {
contacts.delete(uri);
stats.gauge('sbc.featureservers.count', contacts.size + 1);
logger.info(`removing feature server at ${uri}, leaving ${[...contacts.keys()]}`);
stats.gauge('sbc.featureservers.count', contacts.size);
const featureServerIps = [...contacts.keys()].map((u) => {
const arr = /^(.*):\d+$/.exec(u);
return arr[1];
});
createSet(setName, new Set(featureServerIps));
}
}
}
});
if (process.env.JAMBONES_FEATURE_SERVERS) {
// NB: this should only be used for test suite at this point
// in actual scenario OPTIONS pings from feature servers are used
if (process.env.NODE_ENV === 'test' && process.env.JAMBONES_FEATURE_SERVERS) {
dynamic = false;
process.env.JAMBONES_FEATURE_SERVERS
.split(',')
+21 -10
View File
@@ -1,5 +1,9 @@
const debug = require('debug')('jambonz:sbc-inbound');
const Emitter = require('events');
const parseUri = require('drachtio-srf').parseUri;
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ?
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
[];
class AuthOutcomeReporter extends Emitter {
constructor(stats) {
@@ -13,7 +17,7 @@ class AuthOutcomeReporter extends Emitter {
module.exports = function(srf, logger) {
const {lookupSipGatewayBySignalingAddress, lookupAuthHook} = srf.locals.dbHelpers;
const {stats} = srf.locals;
const authenticator = require('jambonz-http-authenticator')(lookupAuthHook, logger, {
const authenticator = require('@jambonz/http-authenticator')(lookupAuthHook, logger, {
blacklistUnknownRealms: true,
emitter: new AuthOutcomeReporter(stats)
});
@@ -35,16 +39,23 @@ module.exports = function(srf, logger) {
async function challengeDeviceCalls(req, res, next) {
try {
const gateway = await lookupSipGatewayBySignalingAddress(req.source_address, req.source_port);
if (!gateway) {
// TODO: if uri.host is not a domain, just reject
req.locals.originator = 'device';
return authenticator(req, res, next);
if (gateway) {
debug(`challengeDeviceCalls: call came from gateway: ${JSON.stringify(gateway)}`);
req.locals.originator = 'trunk';
req.locals.carrier = gateway.name;
if (gateway.application_sid) req.locals.application_sid = gateway.application_sid;
return next();
}
debug(`challengeDeviceCalls: call came from gateway: ${JSON.stringify(gateway)}`);
req.locals.originator = 'trunk';
req.locals.carrier = gateway.name;
if (gateway.application_sid) req.locals.application_sid = gateway.application_sid;
next();
if (msProxyIps.includes(req.source_address)) {
logger.debug({source_address: req.source_address}, 'challengeDeviceCalls: incoming call from Microsoft Teams');
const uri = parseUri(req.uri);
req.locals.originator = 'teams';
req.locals.carrier = 'Microsoft Teams';
req.locals.msTeamsTenantFqdn = uri.host;
return next();
}
req.locals.originator = 'device';
return authenticator(req, res, next);
} catch (err) {
stats.increment('sbc.terminations', ['sipStatus:500']);
logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`);
+21 -5
View File
@@ -11,19 +11,35 @@ function getAppserver(srf) {
return srf.locals.featureServers[ idx++ % len];
}
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp) {
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const from = req.getParsedHeader('from');
const common = {'call-id': req.get('Call-ID'), 'from-tag': from.params.tag};
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const srctOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const common = {
'call-id': req.get('Call-ID'),
'replace': ['origin', 'session-connection']
};
return {
common,
offer: Object.assign({'sdp': req.body, 'replace': ['origin', 'session-connection']}, common,
dstIsUsingSrtp ? srtpCharacteristics : rtpCharacteristics),
answer: Object.assign({}, common, srcIsUsingSrtp ? srtpCharacteristics : rtpCharacteristics)
uas: {
tag: from.params.tag,
mediaOpts: srctOpts
},
uac: {
tag: null,
mediaOpts: dstOpts
}
};
}
function SdpWantsSrtp(sdp) {
return /m=audio.*SAVP/.test(sdp);
}
module.exports = {
isWSS,
SdpWantsSrtp,
getAppserver,
makeRtpEngineOpts
};
+6310 -3536
View File
File diff suppressed because it is too large Load Diff
+15 -17
View File
@@ -1,6 +1,6 @@
{
"name": "sbc-inbound",
"version": "0.3.0",
"version": "0.3.6",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -20,30 +20,28 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test JAMBONES_MYSQL_HOST=localhost JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 JAMBONES_FEATURE_SERVERS=172.38.0.11 node test/ | ./node_modules/.bin/tap-spec",
"test": "NODE_ENV=test 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_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 JAMBONES_FEATURE_SERVERS=172.38.0.11 node test/ | ./node_modules/.bin/tap-spec",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
},
"dependencies": {
"debug": "^4.1.1",
"@jambonz/db-helpers": "^0.5.12",
"@jambonz/http-authenticator": "^0.1.6",
"@jambonz/realtimedb-helpers": "^0.2.20",
"@jambonz/rtpengine-utils": "^0.1.7",
"@jambonz/stats-collector": "^0.1.4",
"debug": "^4.3.1",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.4.28",
"jambonz-db-helpers": "^0.3.3",
"jambonz-http-authenticator": "0.1.5",
"jambonz-rtpengine-utils": "0.1.1",
"jambonz-stats-collector": "0.0.3",
"pino": "^5.14.0",
"rtpengine-client": "^0.0.8"
"drachtio-srf": "^4.4.44",
"pino": "^6.8.0",
"rtpengine-client": "^0.1.1"
},
"devDependencies": {
"blue-tape": "^1.0.0",
"clear-module": "^4.0.0",
"eslint": "^6.7.2",
"clear-module": "^4.1.1",
"eslint": "^7.15.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^14.1.1",
"tap": "^14.10.2",
"tap-dot": "^2.0.0",
"tap-spec": "^5.0.0",
"tape": "^4.11.0"
"nyc": "^15.1.0",
"tap-spec": "^5.0.0"
}
}
+3 -4
View File
@@ -1,9 +1,8 @@
const test = require('tape').test ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
test('creating jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('database successfully created');
t.end();
@@ -11,7 +10,7 @@ test('creating jambones_test database', (t) => {
});
test('creating schema', (t) => {
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/db/jambones-sql.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/jambones-sql.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('schema successfully created');
t.end();
@@ -19,7 +18,7 @@ test('creating schema', (t) => {
});
test('populating test case data', (t) => {
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/db/populate-test-data.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/populate-test-data.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('test data set created');
t.end();
+2 -2
View File
@@ -1,3 +1,3 @@
create database jambones_test;
create user jambones_test@localhost IDENTIFIED WITH mysql_native_password by 'jambones_test';
grant all on jambones_test.* to jambones_test@localhost;
create user jambones_test@'%' IDENTIFIED WITH mysql_native_password by 'jambones_test';
grant all on jambones_test.* to jambones_test@'%';
+201 -139
View File
@@ -1,204 +1,266 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS = 0;
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `call_routes`;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS `lcr_carrier_set_entry`;
DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS `lcr_routes`;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS `api_keys`;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS `phone_numbers`;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS `sip_gateways`;
DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS `voip_carriers`;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS `accounts`;
DROP TABLE IF EXISTS phone_numbers;
DROP TABLE IF EXISTS `applications`;
DROP TABLE IF EXISTS sip_gateways;
DROP TABLE IF EXISTS `service_providers`;
DROP TABLE IF EXISTS voip_carriers;
DROP TABLE IF EXISTS `webhooks`;
DROP TABLE IF EXISTS accounts;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS applications;
CREATE TABLE IF NOT EXISTS `call_routes`
DROP TABLE IF EXISTS service_providers;
DROP TABLE IF EXISTS webhooks;
CREATE TABLE call_routes
(
`call_route_sid` CHAR(36) NOT NULL UNIQUE ,
`priority` INTEGER NOT NULL,
`account_sid` CHAR(36) NOT NULL,
`regex` VARCHAR(255) NOT NULL,
`application_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`call_route_sid`)
call_route_sid CHAR(36) NOT NULL UNIQUE ,
priority INTEGER NOT NULL,
account_sid CHAR(36) NOT NULL,
regex VARCHAR(255) NOT NULL,
application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid)
) ENGINE=InnoDB COMMENT='a regex-based pattern match for call routing';
CREATE TABLE IF NOT EXISTS `lcr_routes`
CREATE TABLE lcr_routes
(
`lcr_route_sid` CHAR(36),
`regex` VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
`description` VARCHAR(1024),
`priority` INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (`lcr_route_sid`)
lcr_route_sid CHAR(36),
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
description VARCHAR(1024),
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
CREATE TABLE IF NOT EXISTS `api_keys`
CREATE TABLE api_keys
(
`api_key_sid` CHAR(36) NOT NULL UNIQUE ,
`token` CHAR(36) NOT NULL UNIQUE ,
`account_sid` CHAR(36),
`service_provider_sid` CHAR(36),
PRIMARY KEY (`api_key_sid`)
api_key_sid CHAR(36) NOT NULL UNIQUE ,
token CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36),
service_provider_sid CHAR(36),
expires_at TIMESTAMP NULL DEFAULT NULL,
last_used TIMESTAMP NULL DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (api_key_sid)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
CREATE TABLE IF NOT EXISTS `voip_carriers`
CREATE TABLE ms_teams_tenants
(
`voip_carrier_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL UNIQUE ,
`description` VARCHAR(255),
`account_sid` CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
`application_sid` CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
PRIMARY KEY (`voip_carrier_sid`)
ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
account_sid CHAR(36) NOT NULL,
application_sid CHAR(36),
tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE sbc_addresses
(
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
service_provider_sid CHAR(36),
PRIMARY KEY (sbc_address_sid)
);
CREATE TABLE users
(
user_sid CHAR(36) NOT NULL UNIQUE ,
name CHAR(36) NOT NULL UNIQUE ,
hashed_password VARCHAR(1024) NOT NULL,
salt CHAR(16) NOT NULL,
force_change BOOLEAN NOT NULL DEFAULT TRUE,
PRIMARY KEY (user_sid)
);
CREATE TABLE voip_carriers
(
voip_carrier_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL UNIQUE ,
description VARCHAR(255),
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
application_sid CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
e164_leading_plus BOOLEAN NOT NULL DEFAULT false,
requires_register BOOLEAN NOT NULL DEFAULT false,
register_username VARCHAR(64),
register_sip_realm VARCHAR(64),
register_password VARCHAR(64),
PRIMARY KEY (voip_carrier_sid)
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE IF NOT EXISTS `phone_numbers`
CREATE TABLE phone_numbers
(
`phone_number_sid` CHAR(36) UNIQUE ,
`number` VARCHAR(32) NOT NULL UNIQUE ,
`voip_carrier_sid` CHAR(36) NOT NULL,
`account_sid` CHAR(36),
`application_sid` CHAR(36),
PRIMARY KEY (`phone_number_sid`)
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36) NOT NULL,
account_sid CHAR(36),
application_sid CHAR(36),
PRIMARY KEY (phone_number_sid)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
CREATE TABLE IF NOT EXISTS `webhooks`
CREATE TABLE webhooks
(
`webhook_sid` CHAR(36) NOT NULL UNIQUE ,
`url` VARCHAR(1024) NOT NULL,
`method` ENUM("GET","POST") NOT NULL DEFAULT 'POST',
`username` VARCHAR(255),
`password` VARCHAR(255),
PRIMARY KEY (`webhook_sid`)
webhook_sid CHAR(36) NOT NULL UNIQUE ,
url VARCHAR(1024) NOT NULL,
method ENUM("GET","POST") NOT NULL DEFAULT 'POST',
username VARCHAR(255),
password VARCHAR(255),
PRIMARY KEY (webhook_sid)
) COMMENT='An HTTP callback';
CREATE TABLE IF NOT EXISTS `lcr_carrier_set_entry`
CREATE TABLE sip_gateways
(
`lcr_carrier_set_entry_sid` CHAR(36),
`workload` INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
`lcr_route_sid` CHAR(36) NOT NULL,
`voip_carrier_sid` CHAR(36) NOT NULL,
`priority` INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (`lcr_carrier_set_entry_sid`)
) COMMENT='An entry in the LCR routing list';
CREATE TABLE IF NOT EXISTS `sip_gateways`
(
`sip_gateway_sid` CHAR(36),
`ipv4` VARCHAR(32) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
`port` INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
`inbound` BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
`outbound` BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
`voip_carrier_sid` CHAR(36) NOT NULL,
`is_active` BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (`sip_gateway_sid`)
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
CREATE TABLE IF NOT EXISTS `applications`
CREATE TABLE lcr_carrier_set_entry
(
`application_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL,
`account_sid` CHAR(36) NOT NULL COMMENT 'account that this application belongs to',
`call_hook_sid` CHAR(36) COMMENT 'webhook to call for inbound calls to phone numbers owned by this account',
`call_status_hook_sid` CHAR(36) COMMENT 'webhook to call for call status events',
`speech_synthesis_vendor` VARCHAR(64) NOT NULL DEFAULT 'google',
`speech_synthesis_voice` VARCHAR(64) NOT NULL DEFAULT 'en-US-Wavenet-C',
`speech_recognizer_vendor` VARCHAR(64) NOT NULL DEFAULT 'google',
`speech_recognizer_language` VARCHAR(64) NOT NULL DEFAULT 'en-US',
PRIMARY KEY (`application_sid`)
lcr_carrier_set_entry_sid CHAR(36),
workload INTEGER NOT NULL DEFAULT 1 COMMENT 'represents a proportion of traffic to send through the associated carrier; can be used for load balancing traffic across carriers with a common priority for a destination',
lcr_route_sid CHAR(36) NOT NULL,
voip_carrier_sid CHAR(36) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (lcr_carrier_set_entry_sid)
) COMMENT='An entry in the LCR routing list';
CREATE TABLE applications
(
application_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
account_sid CHAR(36) NOT NULL COMMENT 'account that this application belongs to',
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
PRIMARY KEY (application_sid)
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls ';
CREATE TABLE IF NOT EXISTS `service_providers`
CREATE TABLE service_providers
(
`service_provider_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL UNIQUE ,
`description` VARCHAR(255),
`root_domain` VARCHAR(128) UNIQUE ,
`registration_hook_sid` CHAR(36),
PRIMARY KEY (`service_provider_sid`)
service_provider_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL UNIQUE ,
description VARCHAR(255),
root_domain VARCHAR(128) UNIQUE ,
registration_hook_sid CHAR(36),
ms_teams_fqdn VARCHAR(255),
PRIMARY KEY (service_provider_sid)
) ENGINE=InnoDB COMMENT='A partition of the platform used by one service provider';
CREATE TABLE IF NOT EXISTS `accounts`
CREATE TABLE accounts
(
`account_sid` CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL,
`sip_realm` VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account',
`service_provider_sid` CHAR(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account',
`registration_hook_sid` CHAR(36) COMMENT 'webhook to call when devices underr this account attempt to register',
`device_calling_application_sid` CHAR(36) COMMENT 'application to use for outbound calling from an account',
`is_active` BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`account_sid`)
account_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
sip_realm VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account',
service_provider_sid CHAR(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account',
registration_hook_sid CHAR(36) COMMENT 'webhook to call when devices underr this account attempt to register',
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (account_sid)
) ENGINE=InnoDB COMMENT='An enterprise that uses the platform for comm services';
CREATE INDEX `call_routes_call_route_sid_idx` ON `call_routes` (`call_route_sid`);
ALTER TABLE `call_routes` ADD FOREIGN KEY account_sid_idxfk (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE `call_routes` ADD FOREIGN KEY application_sid_idxfk (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX `api_keys_api_key_sid_idx` ON `api_keys` (`api_key_sid`);
CREATE INDEX `api_keys_account_sid_idx` ON `api_keys` (`account_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY account_sid_idxfk_1 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
CREATE INDEX account_sid_idx ON api_keys (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX `api_keys_service_provider_sid_idx` ON `api_keys` (`service_provider_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY service_provider_sid_idxfk (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX `voip_carriers_voip_carrier_sid_idx` ON `voip_carriers` (`voip_carrier_sid`);
CREATE INDEX `voip_carriers_name_idx` ON `voip_carriers` (`name`);
ALTER TABLE `voip_carriers` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE `voip_carriers` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
CREATE INDEX `phone_numbers_voip_carrier_sid_idx` ON `phone_numbers` (`voip_carrier_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);
CREATE INDEX sbc_address_sid_idx ON sbc_addresses (sbc_address_sid);
CREATE INDEX service_provider_sid_idx ON sbc_addresses (service_provider_sid);
ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX `webhooks_webhook_sid_idx` ON `webhooks` (`webhook_sid`);
ALTER TABLE `lcr_carrier_set_entry` ADD FOREIGN KEY lcr_route_sid_idxfk (`lcr_route_sid`) REFERENCES `lcr_routes` (`lcr_route_sid`);
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX name_idx ON users (name);
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
CREATE INDEX name_idx ON voip_carriers (name);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE `lcr_carrier_set_entry` ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
CREATE UNIQUE INDEX `sip_gateways_sip_gateway_idx_hostport` ON `sip_gateways` (`ipv4`,`port`);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE `sip_gateways` ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE UNIQUE INDEX `applications_idx_name` ON `applications` (`account_sid`,`name`);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX `applications_application_sid_idx` ON `applications` (`application_sid`);
CREATE INDEX `applications_name_idx` ON `applications` (`name`);
CREATE INDEX `applications_account_sid_idx` ON `applications` (`account_sid`);
ALTER TABLE `applications` ADD FOREIGN KEY account_sid_idxfk_4 (`account_sid`) REFERENCES `accounts` (`account_sid`);
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
ALTER TABLE `applications` ADD FOREIGN KEY call_hook_sid_idxfk (`call_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE `applications` ADD FOREIGN KEY call_status_hook_sid_idxfk (`call_status_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid);
CREATE INDEX `service_providers_service_provider_sid_idx` ON `service_providers` (`service_provider_sid`);
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
CREATE INDEX `service_providers_root_domain_idx` ON `service_providers` (`root_domain`);
ALTER TABLE `service_providers` ADD FOREIGN KEY registration_hook_sid_idxfk (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE INDEX `accounts_account_sid_idx` ON `accounts` (`account_sid`);
CREATE INDEX `accounts_name_idx` ON `accounts` (`name`);
CREATE INDEX `accounts_sip_realm_idx` ON `accounts` (`sip_realm`);
CREATE INDEX `accounts_service_provider_sid_idx` ON `accounts` (`service_provider_sid`);
ALTER TABLE `accounts` ADD FOREIGN KEY service_provider_sid_idxfk_1 (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
ALTER TABLE `accounts` ADD FOREIGN KEY registration_hook_sid_idxfk_1 (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`);
CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX account_sid_idx ON applications (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE `accounts` ADD FOREIGN KEY device_calling_application_sid_idxfk (`device_calling_application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE applications ADD FOREIGN KEY messaging_hook_sid_idxfk (messaging_hook_sid) REFERENCES webhooks (webhook_sid);
CREATE INDEX service_provider_sid_idx ON service_providers (service_provider_sid);
CREATE INDEX name_idx ON service_providers (name);
CREATE INDEX root_domain_idx ON service_providers (root_domain);
ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (registration_hook_sid) REFERENCES webhooks (webhook_sid);
CREATE INDEX account_sid_idx ON accounts (account_sid);
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
+1 -1
View File
@@ -7,7 +7,7 @@ values ('ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3
insert into voip_carriers (voip_carrier_sid, name) values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.20', true, true);
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.20/32', true, true);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound)
values ('efbc4830-57cd-4c78-a56f-d64fdf210fe8', '287c1452-620d-4195-9f19-c9814ef90d78', '3.3.3.3', 5062, false, true);
+18 -1
View File
@@ -7,7 +7,20 @@ networks:
config:
- subnet: 172.38.0.0/16
services:
services:
mysql:
image: mysql:5.7
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "--protocol", "tcp"]
timeout: 5s
retries: 10
networks:
sbc-inbound:
ipv4_address: 172.38.0.2
sbc:
image: drachtio/drachtio-server:latest
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
@@ -16,6 +29,9 @@ services:
networks:
sbc-inbound:
ipv4_address: 172.38.0.10
depends_on:
mysql:
condition: service_healthy
appserver:
image: drachtio/sipp:latest
@@ -47,6 +63,7 @@ services:
rtpengine:
image: drachtio/rtpengine:latest
command: rtpengine --interface private/172.38.0.14 --interface public/172.38.0.14!172.38.0.14
ports:
- "12222:22222/udp"
networks:
-1
View File
@@ -1,5 +1,4 @@
require('./docker_start');
require('./create-test-db');
require('./sip-tests');
require('./remove-test-db');
require('./docker_stop');
-11
View File
@@ -1,11 +0,0 @@
const test = require('tape').test ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
test('dropping jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('database successfully dropped');
t.end();
});
});
+6 -6
View File
@@ -1,5 +1,5 @@
const { spawn } = require('child_process');
const debug = require('debug')('test:sipp');
const debug = require('debug')('jambonz:ci');
let network;
const obj = {};
let output = '';
@@ -27,7 +27,7 @@ obj.output = () => {
obj.sippUac = (file, bindAddress) => {
const cmd = 'docker';
const args = [
'run', '-ti', '--rm', '--net', `${network}`,
'run', '--rm', '--net', `${network}`,
'-v', `${__dirname}/scenarios:/tmp/scenarios`,
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
'-m', '1',
@@ -37,7 +37,7 @@ obj.sippUac = (file, bindAddress) => {
'sbc'
];
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
if (bindAddress) args.splice(4, 0, '--ip', bindAddress);
//console.log(args.join(' '));
clearOutput();
@@ -57,11 +57,11 @@ obj.sippUac = (file, bindAddress) => {
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
debug(`stderr: ${data}`);
addOutput(data.toString());
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
child_process.stderr.on('data', (data) => {
debug(`stderr: ${data}`);
addOutput(data.toString());
});
});