add support for ms teams and bugfix for reinvite handling from network side

This commit is contained in:
Dave Horton
2020-05-22 16:44:19 -04:00
parent e213409dec
commit c66ba70500
8 changed files with 350 additions and 192 deletions

7
app.js
View File

@@ -18,14 +18,14 @@ const StatsCollector = require('jambonz-stats-collector');
const stats = srf.locals.stats = new StatsCollector(logger); const stats = srf.locals.stats = new StatsCollector(logger);
const {route, setLogger} = require('./lib/middleware'); const {route, setLogger} = require('./lib/middleware');
const CallSession = require('./lib/call-session'); const CallSession = require('./lib/call-session');
const {performLcr} = require('jambonz-db-helpers')({ const {performLcr, lookupAllTeamsFQDNs} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST, host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER, user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD, password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE, database: process.env.JAMBONES_MYSQL_DATABASE,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10 connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger); }, logger);
srf.locals.dbHelpers = {performLcr}; srf.locals.dbHelpers = {performLcr, lookupAllTeamsFQDNs};
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 emitter: srf.locals.stats
}); });
@@ -61,4 +61,7 @@ setInterval(() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound']); stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound']);
}, 5000); }, 5000);
const {pingMsTeamsGateways} = require('./lib/utils');
pingMsTeamsGateways(logger, srf);
module.exports = {srf}; module.exports = {srf};

View File

@@ -1,7 +1,25 @@
{ {
"transport-protocol": "UDP/TLS/RTP/SAVPF", "default": {
"ICE": "force", "transport-protocol": "UDP/TLS/RTP/SAVPF",
"SDES": "off", "ICE": "force",
"flags": ["generate mid", "SDES-no"], "SDES": "off",
"rtcp-mux": ["require"] "flags": ["generate mid", "SDES-no", "media handover"],
"rtcp-mux": ["require"]
},
"teams": {
"transport-protocol": "RTP/SAVP",
"ICE": "force",
"flags": ["generate mid", "media handover",
"SDES-no-AES_CM_128_HMAC_SHA1_32",
"SDES-no-F8_128_HMAC_SHA1_80",
"SDES-no-F8_128_HMAC_SHA1_32",
"SDES-no-NULL_HMAC_SHA1_80",
"SDES-no-NULL_HMAC_SHA1_32",
"SDES-no-AES_192_CM_HMAC_SHA1_80",
"SDES-no-AES_192_CM_HMAC_SHA1_32",
"SDES-no-AES_256_CM_HMAC_SHA1_80",
"SDES-no-AES_256_CM_HMAC_SHA1_32"
],
"rtcp-mux": ["require"]
}
} }

View File

@@ -9,17 +9,20 @@ const debug = require('debug')('jambonz:sbc-outbound');
* this is to make sure the outgoing From has the number in the incoming From * this is to make sure the outgoing From has the number in the incoming From
* and not the incoming PAI * and not the incoming PAI
*/ */
const createBLegFromHeader = (req) => { const createBLegFromHeader = (req, teams) => {
const from = req.getParsedHeader('From'); const from = req.getParsedHeader('From');
const host = teams ? req.get('X-MS-Teams-Tenant-FQDN') : 'localhost';
const uri = parseUri(from.uri); const uri = parseUri(from.uri);
if (uri && uri.user) return `sip:${uri.user}@localhost`; //if (uri && uri.user) return `sip:${uri.user}@${host}`;
return 'sip:anonymous@localhost'; if (uri && uri.user) return `sip:+15085710838@${host}`;
return `sip:anonymous@${host}`;
}; };
const createBLegToHeader = (req) => { const createBLegToHeader = (req, teams) => {
const from = req.getParsedHeader('To'); const to = req.getParsedHeader('To');
const uri = parseUri(from.uri); const host = teams ? req.get('X-MS-Teams-Tenant-FQDN') : 'localhost';
if (uri && uri.user) return `sip:${uri.user}@localhost`; const uri = parseUri(to.uri);
return 'sip:localhost'; if (uri && uri.user) return `sip:${uri.user}@${host}`;
return `sip:anonymous@${host}`;
}; };
class CallSession extends Emitter { class CallSession extends Emitter {
@@ -36,6 +39,7 @@ class CallSession extends Emitter {
} }
async connect() { async connect() {
const teams = this.teams = this.req.locals.target === 'teams';
const engine = this.srf.locals.getRtpEngine(); const engine = this.srf.locals.getRtpEngine();
if (!engine) { if (!engine) {
this.logger.info('No available rtpengines, rejecting call!'); this.logger.info('No available rtpengines, rejecting call!');
@@ -49,13 +53,19 @@ class CallSession extends Emitter {
this.answer = answer; this.answer = answer;
this.del = del; this.del = del;
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss); this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, teams);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)}; this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
let proxy, uris; let proxy, uris;
try { try {
// determine where to send the call // determine where to send the call
debug(`connecting call: ${JSON.stringify(this.req.locals)}`); debug(`connecting call: ${JSON.stringify(this.req.locals)}`);
const headers = {
'From': createBLegFromHeader(this.req, teams),
'To': createBLegToHeader(this.req, teams),
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK'
};
if (this.req.locals.registration) { if (this.req.locals.registration) {
debug(`sending call to user ${JSON.stringify(this.req.locals.registration)}`); debug(`sending call to user ${JSON.stringify(this.req.locals.registration)}`);
const contact = this.req.locals.registration.contact; const contact = this.req.locals.registration.contact;
@@ -70,6 +80,17 @@ class CallSession extends Emitter {
else if (this.req.locals.target === 'forward') { else if (this.req.locals.target === 'forward') {
uris = [this.req.uri]; uris = [this.req.uri];
} }
else if (teams) {
const vmailParam = 'opaque=app:voicemail';
proxy = `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com:5061;transport=tls`;
if (this.req.uri.includes(vmailParam)) {
uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`];
}
else uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`];
Object.assign(headers, {
Contact: `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
});
}
else { else {
debug('calling lcr'); debug('calling lcr');
try { try {
@@ -114,17 +135,13 @@ class CallSession extends Emitter {
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, { const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
proxy, proxy,
passFailure: false, passFailure: false,
proxyRequestHeaders: ['all'], proxyRequestHeaders: ['all', '-X-MS-Teams-FQDN', '-X-MS-Teams-Tenant-FQDN', '-Allow'],
proxyResponseHeaders: ['all'], proxyResponseHeaders: ['all', '-Allow'],
headers: { headers,
'From': createBLegFromHeader(this.req),
'To': createBLegToHeader(this.req)
},
localSdpB: response.sdp, localSdpB: response.sdp,
localSdpA: async(sdp, res) => { localSdpA: async(sdp, res) => {
this.toTag = res.getParsedHeader('To').params.tag; this.toTag = res.getParsedHeader('To').params.tag;
const opts = Object.assign({sdp, 'to-tag': this.toTag}, const opts = Object.assign(this.rtpEngineOpts.answer, {sdp, 'to-tag': this.toTag});
this.rtpEngineOpts.answer);
const response = await this.answer(opts); const response = await this.answer(opts);
this.logger.debug({answer: opts, response}, 'rtpengine answer'); this.logger.debug({answer: opts, response}, 'rtpengine answer');
if ('ok' !== response.result) { if ('ok' !== response.result) {
@@ -202,19 +219,18 @@ class CallSession extends Emitter {
// default forwarding of other request types // default forwarding of other request types
forwardInDialogRequests(uac); forwardInDialogRequests(uac, ['info', 'notify', 'options', 'message']);
} }
async _onReinvite(dlg, req, res) { async _onReinvite(dlg, req, res) {
try { try {
let response = await this.offer(Object.assign({sdp: req.body}, this.rtpEngineOpts.offer)); let response = await this.offer(Object.assign(this.rtpEngineOpts.offer, {sdp: req.body}));
if ('ok' !== response.result) { if ('ok' !== response.result) {
res.send(488); res.send(488);
throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`); throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`);
} }
const sdp = await dlg.other.modify(response.sdp); const sdp = await dlg.other.modify(response.sdp);
const opts = Object.assign({sdp, 'to-tag': res.getParsedHeader('To').params.tag}, const opts = Object.assign(this.rtpEngineOpts.answer, {sdp, 'to-tag': res.getParsedHeader('To').params.tag});
this.rtpEngineOpts.answer);
response = await this.answer(opts); response = await this.answer(opts);
if ('ok' !== response.result) { if ('ok' !== response.result) {
res.send(488); res.send(488);
@@ -228,16 +244,27 @@ class CallSession extends Emitter {
async _onNetworkReinvite(dlg, req, res) { async _onNetworkReinvite(dlg, req, res) {
try { try {
const opts = Object.assign({sdp: req.body, 'to-tag': this.toTag}, this.rtpEngineOpts.answer); const newAnswerOpts = Object.assign({}, this.rtpEngineOpts.answer, {sdp: req.body});
const response = await this.answer(opts); let response = await this.answer(newAnswerOpts);
this.logger.debug({answer: opts, response}, '_onNetworkReinvite: rtpengine answer'); this.logger.debug({answer: newAnswerOpts, response}, '_onNetworkReinvite: answer to rtpengine');
if ('ok' !== response.result) { if ('ok' !== response.result) {
res.send(488); res.send(488);
throw new Error(`_onFeatureServerReinvite: rtpengine failed: ${JSON.stringify(response)}`); throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`);
} }
res.send(200, {body: dlg.local.sdp});
// reinvite feature server
const sdp = await dlg.other.modify(response.sdp);
const newOfferOpts = Object.assign({}, this.rtpEngineOpts.offer, {sdp, 'to-tag': this.toTag});
response = await this.offer(newOfferOpts);
this.logger.debug({answer: newOfferOpts, response}, '_onNetworkReinvite: offer to rtpengine');
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
res.send(200, {body: response.sdp});
} catch (err) { } catch (err) {
this.logger.error(err, 'Error handling reinvite from feature server'); this.logger.error(err, 'Error handling reinvite');
} }
} }
@@ -258,7 +285,7 @@ class CallSession extends Emitter {
if (req.has('X-Retain-Call-Sid')) { if (req.has('X-Retain-Call-Sid')) {
Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')}); Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')});
} }
const dlg = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, proxyRequestHeaders}); const dlg = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uas.destroy(); this.uas.destroy();
this.uas = dlg; this.uas = dlg;

View File

@@ -43,7 +43,12 @@ function route(opts) {
let reg; let reg;
const dotDecimalHost = /^[0-9\.]+$/.test(uri.host); const dotDecimalHost = /^[0-9\.]+$/.test(uri.host);
if (!dotDecimalHost) { if (req.has('X-MS-Teams-FQDN') && req.has('X-MS-Teams-Tenant-FQDN')) {
logger.debug('This is a call to ms teams');
req.locals.target = 'teams';
return next();
}
else if (!dotDecimalHost) {
// uri host is not a dot-decimal address, so try to look up user // uri host is not a dot-decimal address, so try to look up user
debug(`searching for registered user ${aor}`); debug(`searching for registered user ${aor}`);
reg = await registrar.query(aor); reg = await registrar.query(aor);

View File

@@ -2,14 +2,20 @@ const rtpCharacteristics = require('../data/rtp-transcoding');
const srtpCharacteristics = require('../data/srtp-transcoding'); const srtpCharacteristics = require('../data/srtp-transcoding');
const debug = require('debug')('jambonz:sbc-outbound'); const debug = require('debug')('jambonz:sbc-outbound');
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp) { function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const from = req.getParsedHeader('from'); const from = req.getParsedHeader('from');
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
const common = {'call-id': req.get('Call-ID'), 'from-tag': from.params.tag}; const common = {'call-id': req.get('Call-ID'), 'from-tag': from.params.tag};
return { return {
common, common,
offer: Object.assign({'sdp': req.body, 'replace': ['origin', 'session-connection']}, common, offer: Object.assign(
dstIsUsingSrtp ? srtpCharacteristics : rtpCharacteristics), {'sdp': req.body, 'replace': ['origin', 'session-connection']},
answer: Object.assign({}, common, srcIsUsingSrtp ? srtpCharacteristics : rtpCharacteristics) common,
dstIsUsingSrtp ? srtpOpts : rtpCharacteristics),
answer: Object.assign(
{'replace': ['origin', 'session-connection']},
common,
srcIsUsingSrtp ? srtpOpts : rtpCharacteristics)
}; };
} }
@@ -27,7 +33,40 @@ function selectHostPort(hostport, protocol) {
return sel[0]; return sel[0];
} }
function pingMs(logger, srf, gateway, fqdns) {
const uri = `sip:${gateway}`;
const proxy = `sip:${gateway}:5061;transport=tls`;
fqdns.forEach((fqdn) => {
const contact = `<sip:${fqdn}:5061;transport=tls>`;
srf.request(uri, {
method: 'OPTIONS',
proxy,
headers: {
'Contact': contact,
'From': contact,
}
}).catch((err) => logger.error(err, `Error pinging MS Teams at ${gateway}`));
});
}
function pingMsTeamsGateways(logger, srf) {
const {lookupAllTeamsFQDNs} = srf.locals.dbHelpers;
lookupAllTeamsFQDNs()
.then((fqdns) => {
if (fqdns.length > 0) {
['sip.pstnhub.microsoft.com', 'sip2.pstnhub.microsoft.com', 'sip3.pstnhub.microsoft.com']
.forEach((gw) => {
setInterval(pingMs.bind(this, logger, srf, gw, fqdns), 60000);
});
}
return;
})
.catch((err) => {
logger.error(err, 'Error looking up all ms teams fqdns');
});
}
module.exports = { module.exports = {
makeRtpEngineOpts, makeRtpEngineOpts,
selectHostPort selectHostPort,
pingMsTeamsGateways
}; };

38
package-lock.json generated
View File

@@ -257,6 +257,23 @@
"integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==",
"dev": true "dev": true
}, },
"@jambonz/db-helpers": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.3.8.tgz",
"integrity": "sha512-w2eiWThEsjL4xERqkV7Ot4BU7HWxZxyarcoCubY0QekyGruUamnEys3KsvZ26oGKYfYpnJEj5jI9WRYcu5QqmQ==",
"requires": {
"debug": "^4.1.1",
"mysql2": "^2.0.2",
"uuid": "^7.0.3"
},
"dependencies": {
"uuid": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="
}
}
},
"@types/color-name": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@@ -744,9 +761,9 @@
} }
}, },
"drachtio-srf": { "drachtio-srf": {
"version": "4.4.28", "version": "4.4.33",
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.4.28.tgz", "resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.4.33.tgz",
"integrity": "sha512-gY/wmH6JFmeEv2/jhwFbky1NYUmDwgIJzjNeGlDiAkQEA6GgcI/CFi5RRHAUzghpczTwXQNDXpARPB8QDWX1JA==", "integrity": "sha512-2bVOObbP9m9ASZ+XXgnaeEk9v1a2Vn8d4Oaz6anrRCuRUzirUOJ/c5nJh9VQvbmXqUx+VubPmXa+AGJuFyYNow==",
"requires": { "requires": {
"async": "^1.4.2", "async": "^1.4.2",
"debug": "^3.1.0", "debug": "^3.1.0",
@@ -757,6 +774,7 @@
"lodash": "^4.17.13", "lodash": "^4.17.13",
"node-noop": "0.0.1", "node-noop": "0.0.1",
"only": "0.0.2", "only": "0.0.2",
"sdp-transform": "^2.14.0",
"sip-methods": "^0.3.0", "sip-methods": "^0.3.0",
"utils-merge": "1.0.0", "utils-merge": "1.0.0",
"uuid": "^3.0.0" "uuid": "^3.0.0"
@@ -1665,15 +1683,6 @@
"istanbul-lib-report": "^3.0.0" "istanbul-lib-report": "^3.0.0"
} }
}, },
"jambonz-db-helpers": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/jambonz-db-helpers/-/jambonz-db-helpers-0.3.2.tgz",
"integrity": "sha512-j7AEgts+Bj1CFPiM0estFmWmdDTZKWbkeIPY1QT3BR0cLClzjqo9fmdCzLoDtk/NWMy7IPNEQpVHzEejxFHq9g==",
"requires": {
"debug": "^4.1.1",
"mysql2": "^2.0.2"
}
},
"jambonz-mw-registrar": { "jambonz-mw-registrar": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/jambonz-mw-registrar/-/jambonz-mw-registrar-0.1.2.tgz", "resolved": "https://registry.npmjs.org/jambonz-mw-registrar/-/jambonz-mw-registrar-0.1.2.tgz",
@@ -2406,6 +2415,11 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sdp-transform": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.0.tgz",
"integrity": "sha512-8ZYOau/o9PzRhY0aMuRzvmiM6/YVQR8yjnBScvZHSdBnywK5oZzAJK+412ZKkDq29naBmR3bRw8MFu0C01Gehg=="
},
"semver": { "semver": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",

View File

@@ -29,9 +29,9 @@
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
"drachtio-fn-b2b-sugar": "^0.0.12", "drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.4.28", "drachtio-srf": "^4.4.33",
"jambonz-rtpengine-utils": "0.1.1", "jambonz-rtpengine-utils": "0.1.1",
"jambonz-db-helpers": "^0.3.2", "@jambonz/db-helpers": "^0.3.8",
"jambonz-mw-registrar": "^0.1.2", "jambonz-mw-registrar": "^0.1.2",
"jambonz-stats-collector": "0.0.3", "jambonz-stats-collector": "0.0.3",
"pino": "^5.14.0", "pino": "^5.14.0",

View File

@@ -2,203 +2,255 @@
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 ms_teams_tenants;
DROP TABLE IF EXISTS `phone_numbers`; DROP TABLE IF EXISTS api_keys;
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 applications;
DROP TABLE IF EXISTS accounts;
DROP TABLE IF EXISTS service_providers;
DROP TABLE IF EXISTS webhooks;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE IF NOT EXISTS `call_routes` CREATE TABLE call_routes
( (
`call_route_sid` CHAR(36) NOT NULL UNIQUE , call_route_sid CHAR(36) NOT NULL UNIQUE ,
`priority` INTEGER NOT NULL, priority INTEGER NOT NULL,
`account_sid` CHAR(36) NOT NULL, account_sid CHAR(36) NOT NULL,
`regex` VARCHAR(255) NOT NULL, regex VARCHAR(255) NOT NULL,
`application_sid` CHAR(36) NOT NULL, application_sid CHAR(36) NOT NULL,
PRIMARY KEY (`call_route_sid`) PRIMARY KEY (call_route_sid)
) ENGINE=InnoDB COMMENT='a regex-based pattern match for call routing'; ) 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), 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', regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
`description` VARCHAR(1024), description VARCHAR(1024),
`priority` INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first', priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (`lcr_route_sid`) PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table'; ) COMMENT='Least cost routing table';
CREATE TABLE IF NOT EXISTS `api_keys` CREATE TABLE ms_teams_tenants
( (
`api_key_sid` CHAR(36) NOT NULL UNIQUE , ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE ,
`token` CHAR(36) NOT NULL UNIQUE , service_provider_sid CHAR(36) NOT NULL,
`account_sid` CHAR(36), account_sid CHAR(36),
`service_provider_sid` CHAR(36), application_sid CHAR(36),
PRIMARY KEY (`api_key_sid`) tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
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),
expires_at TIMESTAMP,
PRIMARY KEY (api_key_sid)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api'; ) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
CREATE TABLE IF NOT EXISTS `voip_carriers` CREATE TABLE sbc_addresses
( (
`voip_carrier_sid` CHAR(36) NOT NULL UNIQUE , sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL UNIQUE , ipv4 VARCHAR(255) NOT NULL,
`description` VARCHAR(255), port INTEGER NOT NULL DEFAULT 5060,
`account_sid` CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account', service_provider_sid CHAR(36),
`application_sid` CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application', PRIMARY KEY (sbc_address_sid)
PRIMARY KEY (`voip_carrier_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',
PRIMARY KEY (voip_carrier_sid)
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls'; ) 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 , phone_number_sid CHAR(36) UNIQUE ,
`number` VARCHAR(32) NOT NULL UNIQUE , number VARCHAR(32) NOT NULL UNIQUE ,
`voip_carrier_sid` CHAR(36) NOT NULL, voip_carrier_sid CHAR(36) NOT NULL,
`account_sid` CHAR(36), account_sid CHAR(36),
`application_sid` CHAR(36), application_sid CHAR(36),
PRIMARY KEY (`phone_number_sid`) PRIMARY KEY (phone_number_sid)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account'; ) 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 , webhook_sid CHAR(36) NOT NULL UNIQUE ,
`url` VARCHAR(1024) NOT NULL, url VARCHAR(1024) NOT NULL,
`method` ENUM("GET","POST") NOT NULL DEFAULT 'POST', method ENUM("GET","POST") NOT NULL DEFAULT 'POST',
`username` VARCHAR(255), username VARCHAR(255),
`password` VARCHAR(255), password VARCHAR(255),
PRIMARY KEY (`webhook_sid`) PRIMARY KEY (webhook_sid)
) COMMENT='An HTTP callback'; ) COMMENT='An HTTP callback';
CREATE TABLE IF NOT EXISTS `lcr_carrier_set_entry` CREATE TABLE sip_gateways
( (
`lcr_carrier_set_entry_sid` CHAR(36), sip_gateway_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', ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
`lcr_route_sid` CHAR(36) NOT NULL, port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
`voip_carrier_sid` CHAR(36) NOT NULL, inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
`priority` INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first', outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
PRIMARY KEY (`lcr_carrier_set_entry_sid`) voip_carrier_sid CHAR(36) NOT NULL,
) COMMENT='An entry in the LCR routing list'; is_active BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (sip_gateway_sid)
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`)
) COMMENT='A whitelisted sip gateway used for origination/termination'; ) 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 , lcr_carrier_set_entry_sid CHAR(36),
`name` VARCHAR(64) NOT NULL, 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',
`account_sid` CHAR(36) NOT NULL COMMENT 'account that this application belongs to', lcr_route_sid CHAR(36) NOT NULL,
`call_hook_sid` CHAR(36) COMMENT 'webhook to call for inbound calls to phone numbers owned by this account', voip_carrier_sid CHAR(36) NOT NULL,
`call_status_hook_sid` CHAR(36) COMMENT 'webhook to call for call status events', priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempted first',
`speech_synthesis_vendor` VARCHAR(64) NOT NULL DEFAULT 'google', PRIMARY KEY (lcr_carrier_set_entry_sid)
`speech_synthesis_voice` VARCHAR(64) NOT NULL DEFAULT 'en-US-Wavenet-C', ) COMMENT='An entry in the LCR routing list';
`speech_recognizer_vendor` VARCHAR(64) NOT NULL DEFAULT 'google',
`speech_recognizer_language` VARCHAR(64) NOT NULL DEFAULT 'en-US', CREATE TABLE applications
PRIMARY KEY (`application_sid`) (
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_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 '; ) 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 , service_provider_sid CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL UNIQUE , name VARCHAR(64) NOT NULL UNIQUE ,
`description` VARCHAR(255), description VARCHAR(255),
`root_domain` VARCHAR(128) UNIQUE , root_domain VARCHAR(128) UNIQUE ,
`registration_hook_sid` CHAR(36), registration_hook_sid CHAR(36),
PRIMARY KEY (`service_provider_sid`) ms_teams_fqdn VARCHAR(255),
PRIMARY KEY (service_provider_sid)
) ENGINE=InnoDB COMMENT='A partition of the platform used by one service provider'; ) 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 , account_sid CHAR(36) NOT NULL UNIQUE ,
`name` VARCHAR(64) NOT NULL, name VARCHAR(64) NOT NULL,
`sip_realm` VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account', 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', 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', 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', device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
`is_active` BOOLEAN NOT NULL DEFAULT true, is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`account_sid`) PRIMARY KEY (account_sid)
) ENGINE=InnoDB COMMENT='An enterprise that uses the platform for comm services'; ) 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`); 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 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 ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
CREATE INDEX `api_keys_account_sid_idx` ON `api_keys` (`account_sid`); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_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 ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_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`); ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_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`);
ALTER TABLE `voip_carriers` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`); CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
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_2 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`); CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
CREATE INDEX `phone_numbers_voip_carrier_sid_idx` ON `phone_numbers` (`voip_carrier_sid`); ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`); 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`); CREATE INDEX user_sid_idx ON users (user_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 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 webhook_sid_idx ON webhooks (webhook_sid);
CREATE INDEX `applications_name_idx` ON `applications` (`name`); CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
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`);
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`); 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 `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`);
CREATE INDEX `accounts_account_sid_idx` ON `accounts` (`account_sid`); CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
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`);
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);
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);