merge features from hosted branch (#32)

major merge of features from the hosted branch that was created temporarily during the initial launch of jambonz.org
This commit is contained in:
Dave Horton
2021-06-17 16:25:50 -04:00
committed by GitHub
parent 473a34ec9f
commit 9b59d08dcf
68 changed files with 3436 additions and 1066 deletions

View File

@@ -8,7 +8,7 @@
"jsx": false,
"modules": false
},
"ecmaVersion": 2018
"ecmaVersion": 2018
},
"plugins": ["promise"],
"rules": {

View File

@@ -1,6 +1,3 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: CI
on:
@@ -8,12 +5,18 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- run: npm ci
- run: npm run jslint
- run: docker pull drachtio/sipp
- run: npm test
env:
GCP_JSON_KEY: ${{ secrets.GCP_JSON_KEY }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}

2
.gitignore vendored
View File

@@ -39,3 +39,5 @@ examples/*
ecosystem.config.js
.vscode
test/credentials/*.json
run-tests.sh

29
app.js
View File

@@ -12,9 +12,10 @@ assert.ok(process.env.JAMBONES_NETWORK_CIDR, 'missing JAMBONES_SUBNET env var');
const Srf = require('drachtio-srf');
const srf = new Srf();
const PORT = process.env.HTTP_PORT || 3000;
const opts = Object.assign({
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
const opts = {
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;},
level: process.env.JAMBONES_LOGLEVEL || 'info'
};
const logger = require('pino')(opts);
const {LifeCycleEvents} = require('./lib/utils/constants');
const installSrfLocals = require('./lib/utils/install-srf-locals');
@@ -22,6 +23,7 @@ installSrfLocals(srf, logger);
const {
initLocals,
getAccountDetails,
normalizeNumbers,
retrieveApplication,
invokeWebCallback
@@ -57,7 +59,13 @@ if (process.env.NODE_ENV === 'test') {
});
}
srf.use('invite', [initLocals, normalizeNumbers, retrieveApplication, invokeWebCallback]);
srf.use('invite', [
initLocals,
getAccountDetails,
normalizeNumbers,
retrieveApplication,
invokeWebCallback
]);
srf.invite((req, res) => {
const session = new InboundCallSession(req, res);
@@ -72,7 +80,7 @@ app.use((err, req, res, next) => {
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
app.listen(PORT);
const httpServer = app.listen(PORT);
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
@@ -88,4 +96,13 @@ setInterval(() => {
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
}, 5000);
module.exports = {srf, logger};
const disconnect = () => {
return new Promise ((resolve) => {
httpServer.on('close', resolve);
httpServer.close();
srf.disconnect();
srf.locals.mediaservers.forEach((ms) => ms.disconnect());
});
};
module.exports = {srf, logger, disconnect};

View File

@@ -3,9 +3,11 @@ const makeTask = require('../../tasks/make_task');
const RestCallSession = require('../../session/rest-call-session');
const CallInfo = require('../../session/call-info');
const {CallDirection, CallStatus} = require('../../utils/constants');
const { v4: uuidv4 } = require('uuid');
const SipError = require('drachtio-srf').SipError;
const sysError = require('./error');
const Requestor = require('../../utils/requestor');
const dbUtils = require('../../utils/db-utils');
router.post('/', async(req, res) => {
const {logger} = req.app.locals;
@@ -15,6 +17,7 @@ router.post('/', async(req, res) => {
let uri, cs, to;
const restDial = makeTask(logger, {'rest:dial': req.body});
const {srf} = require('../../..');
const {lookupAccountDetails} = dbUtils(logger, srf);
const {getSBC, getFreeswitch} = srf.locals;
const sbcAddress = getSBC();
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
@@ -24,13 +27,24 @@ router.post('/', async(req, res) => {
headers: req.body.headers || {}
};
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
const account = await lookupAccountBySid(req.body.account_sid);
const accountInfo = await lookupAccountDetails(req.body.account_sid);
const callSid = uuidv4();
opts.headers = {
...opts.headers,
'X-Call-Sid': callSid,
'X-Account-Sid': req.body.account_sid
};
switch (target.type) {
case 'phone':
case 'teams':
uri = `sip:${target.number}@${sbcAddress}`;
to = target.number;
if ('teams' === target.type) {
const {lookupTeamsByAccount} = srf.locals.dbHelpers;
const obj = await lookupTeamsByAccount(req.body.account_sid);
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
Object.assign(opts.headers, {
@@ -83,8 +97,10 @@ router.post('/', async(req, res) => {
* attach our requestor and notifier objects
* these will be used for all http requests we make during this call
*/
app.requestor = new Requestor(logger, app.call_hook);
if (app.call_status_hook) app.notifier = new Requestor(logger, app.call_status_hook);
app.requestor = new Requestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
if (app.call_status_hook) {
app.notifier = new Requestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
}
else app.notifier = {request: () => {}};
/* now launch the outdial */
@@ -104,10 +120,11 @@ router.post('/', async(req, res) => {
req: inviteReq,
to,
tag: app.tag,
callSid,
accountSid: req.body.account_sid,
applicationSid: app.application_sid
});
cs = new RestCallSession({logger, application: app, srf, req: inviteReq, ep, tasks, callInfo});
cs = new RestCallSession({logger, application: app, srf, req: inviteReq, ep, tasks, callInfo, accountInfo});
cs.exec(req);
res.status(201).json({sid: cs.callSid});

View File

@@ -8,12 +8,15 @@ const makeTask = require('../../tasks/make_task');
router.post('/:sid', async(req, res) => {
const {logger} = req.app.locals;
const {srf} = req.app.locals;
const {messageSid} = req.body;
const {message_sid, account_sid} = req.body;
logger.debug({body: req.body}, 'got createMessage request');
const data = [Object.assign({verb: 'message'}, req.body)];
delete data[0].messageSid;
const data = [{
verb: 'message',
...req.body
}];
delete data[0].message_sid;
try {
const tasks = normalizeJambones(logger, data)
@@ -21,8 +24,8 @@ router.post('/:sid', async(req, res) => {
const callInfo = new CallInfo({
direction: CallDirection.None,
messageSid,
accountSid: req.params.sid,
messageSid: message_sid,
accountSid: account_sid,
res
});
const cs = new SmsSession({logger, srf, tasks, callInfo});

View File

@@ -13,11 +13,14 @@ router.post('/:partner', async(req, res) => {
logger.debug({body: req.body}, `got incomingSms request from partner ${req.params.partner}`);
let tasks;
const {srf} = require('../../..');
const {lookupAccountBySid} = srf.locals.dbHelpers;
const app = req.body.app;
const account = await lookupAccountBySid(app.accountSid);
const hook = app.messaging_hook;
const requestor = new Requestor(logger, hook);
const requestor = new Requestor(logger, account.account_sid, hook, account.webhook_secret);
const payload = {
provider: req.params.partner,
carrier: req.params.partner,
messageSid: app.messageSid,
accountSid: app.accountSid,
applicationSid: app.applicationSid,
@@ -38,7 +41,7 @@ router.post('/:partner', async(req, res) => {
}
// process any versb in response
// process any verbs in response
if (Array.isArray(tasks) && tasks.length) {
const {srf} = req.app.locals;

View File

@@ -32,7 +32,10 @@ function retrieveCallSession(callSid, opts) {
return cs;
}
const updateCall = async(req, res) => {
/**
* update a call
*/
router.post('/:callSid', async(req, res) => {
const logger = req.app.locals.logger;
const callSid = req.params.callSid;
logger.debug({body: req.body}, 'got upateCall request');
@@ -42,23 +45,11 @@ const updateCall = async(req, res) => {
logger.info(`updateCall: callSid not found ${callSid}`);
return res.sendStatus(404);
}
res.sendStatus(204);
res.sendStatus(202);
cs.updateCall(req.body, callSid);
} catch (err) {
sysError(logger, res, err);
}
};
/**
* update a call
*/
/* leaving in for legacy; should have been (and now is) a PUT */
router.post('/:callSid', async(req, res) => {
await updateCall(req, res);
});
router.put('/:callSid', async(req, res) => {
await updateCall(req, res);
});
module.exports = router;

View File

@@ -5,10 +5,11 @@ const Requestor = require('./utils/requestor');
const makeTask = require('./tasks/make_task');
const parseUri = require('drachtio-srf').parseUri;
const normalizeJambones = require('./utils/normalize-jambones');
const dbUtils = require('./utils/db-utils');
module.exports = function(srf, logger) {
const {lookupAppByPhoneNumber, lookupAppBySid, lookupAppByRealm, lookupAppByTeamsTenant} = srf.locals.dbHelpers;
const {lookupAccountDetails} = dbUtils(logger, srf);
function initLocals(req, res, next) {
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
req.locals = {
@@ -26,6 +27,32 @@ module.exports = function(srf, logger) {
next();
}
/**
* retrieve account information for the incoming call
*/
async function getAccountDetails(req, res, next) {
if (!req.has('X-Account-Sid')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
return res.send(500);
}
const account_sid = req.locals.account_sid = req.get('X-Account-Sid');
try {
req.locals.accountInfo = await lookupAccountDetails(account_sid);
if (!req.locals.accountInfo.account.is_active) {
logger.info(`Account is inactive or suspended ${account_sid}`);
// TODO: alert
return res.send(503, {headers: {'X-Reason': 'Account exists but is inactive'}});
}
logger.debug({accountInfo: req.locals.accountInfo}, `retrieved account info for ${account_sid}`);
next();
} catch (err) {
logger.info({err}, `Error retrieving account details for account ${account_sid}`);
res.send(503, {headers: {'X-Reason': `No Account exists for sid ${account_sid}`}});
}
}
/**
* Within the system, we deal with E.164 numbers _without_ the leading '+
*/
@@ -52,6 +79,7 @@ module.exports = function(srf, logger) {
*/
async function retrieveApplication(req, res, next) {
const logger = req.locals.logger;
const {accountInfo, account_sid} = req.locals;
try {
let app;
if (req.locals.application_sid) app = await lookupAppBySid(req.locals.application_sid);
@@ -100,8 +128,9 @@ module.exports = function(srf, logger) {
* create a requestor that we will use for all http requests we make during the call.
* also create a notifier for call status events (if not needed, its a no-op).
*/
app.requestor = new Requestor(logger, app.call_hook);
if (app.call_status_hook) app.notifier = new Requestor(logger, app.call_status_hook);
app.requestor = new Requestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
if (app.call_status_hook) app.notifier = new Requestor(logger, account_sid, app.call_status_hook,
accountInfo.account.webhook_secret);
else app.notifier = {request: () => {}};
req.locals.application = app;
@@ -145,6 +174,7 @@ module.exports = function(srf, logger) {
return {
initLocals,
getAccountDetails,
normalizeNumbers,
retrieveApplication,
invokeWebCallback

View File

@@ -54,8 +54,8 @@ class CallInfo {
}
else {
// outbound call triggered by REST
const {req, accountSid, applicationSid, to, tag} = opts;
this.callSid = uuidv4();
const {req, callSid, accountSid, applicationSid, to, tag} = opts;
this.callSid = callSid;
this.accountSid = accountSid;
this.applicationSid = applicationSid;
this.callStatus = CallStatus.Trying,

View File

@@ -27,12 +27,13 @@ class CallSession extends Emitter {
* @param {array} opts.tasks - tasks we are to execute
* @param {callInfo} opts.callInfo - information about the call
*/
constructor({logger, application, srf, tasks, callInfo}) {
constructor({logger, application, srf, tasks, callInfo, accountInfo}) {
super();
this.logger = logger;
this.application = application;
this.srf = srf;
this.callInfo = callInfo;
this.accountInfo = accountInfo;
this.tasks = tasks;
this.taskIdx = 0;
this.stackIdx = 0;
@@ -167,6 +168,7 @@ class CallSession extends Emitter {
get isAdultingCallSession() {
return this.constructor.name === 'AdultingCallSession';
}
/**
* returns true if this session is a ConfirmCallSession
*/
@@ -181,6 +183,55 @@ class CallSession extends Emitter {
return this.constructor.name === 'SmsCallSession';
}
/**
* Check for speech credentials for the specified vendor
* @param {*} vendor - google or aws
*/
getSpeechCredentials(vendor, type) {
const {writeAlerts, AlertType} = this.srf.locals;
this.logger.debug({vendor, type, speech: this.accountInfo.speech}, `searching for speech for vendor ${vendor}`);
if (this.accountInfo.speech && this.accountInfo.speech.length > 0) {
const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
if (credential && (
(type === 'tts' && credential.use_for_tts) ||
(type === 'stt' && credential.use_for_stt)
)) {
if ('google' === vendor) {
try {
const cred = JSON.parse(credential.service_key.replace(/\n/g, '\\n'));
return {
speech_credential_sid: credential.speech_credential_sid,
credentials: cred
};
} catch (err) {
const sid = this.accountInfo.account.account_sid;
this.logger.info({err}, `malformed google service_key provisioned for account ${sid}`);
writeAlerts({
alert_type: AlertType.TTS_FAILURE,
account_sid: this.accountSid,
vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
}
}
else if (['aws', 'polly'].includes(vendor)) {
return {
speech_credential_sid: credential.speech_credential_sid,
accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key,
region: process.env.AWS_REGION || credential.aws_region
};
}
}
else {
writeAlerts({
alert_type: AlertType.STT_NOT_PROVISIONED,
account_sid: this.accountSid,
vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
}
}
}
/**
* execute the tasks in the CallSession. The tasks are executed in sequence until
* they complete, or the caller hangs up.
@@ -537,10 +588,10 @@ class CallSession extends Emitter {
park_after_bridge: true
}).catch((err) => this.logger.error({err}, 'Error setting park_after_bridge'));
this.logger.debug(`allocated endpoint ${this.ep.uuid}`);
this.logger.debug(`allocated endpoint ${ep.uuid}`);
this.ep.on('destroy', () => {
this.logger.info(`endpoint was destroyed!! ${this.ep.uuid}`);
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
});
if (this.direction === CallDirection.Inbound) {

View File

@@ -15,6 +15,7 @@ class InboundCallSession extends CallSession {
srf: req.srf,
application: req.locals.application,
callInfo: req.locals.callInfo,
accountInfo: req.locals.accountInfo,
tasks: req.locals.application.tasks
});
this.req = req;

View File

@@ -8,14 +8,15 @@ const moment = require('moment');
* @extends CallSession
*/
class RestCallSession extends CallSession {
constructor({logger, application, srf, req, ep, tasks, callInfo}) {
constructor({logger, application, srf, req, ep, tasks, callInfo, accountInfo}) {
super({
logger,
application,
srf,
callSid: callInfo.callSid,
tasks,
callInfo
callInfo,
accountInfo
});
this.req = req;
this.ep = ep;

View File

@@ -135,13 +135,11 @@ class TaskDial extends Task {
this.epOther.play(this.dialMusic).catch((err) => {});
}
}
this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
if (this.epOther) this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
await this._attemptCalls(cs);
await this.awaitTaskDone();
this.logger.debug({callSid: this.cs.callSid}, 'Dial:exec task is done, sending actionHook if any');
await this.performAction(this.results);
this._removeDtmfDetection(cs, this.epOther);
if (this.epOther) this._removeDtmfDetection(cs, this.epOther);
this._removeDtmfDetection(cs, this.ep);
} catch (err) {
this.logger.error({err}, 'TaskDial:exec terminating with error');
@@ -151,10 +149,8 @@ class TaskDial extends Task {
async kill(cs) {
super.kill(cs);
this._removeDtmfDetection(this.cs, this.epOther);
if (this.epOther) this._removeDtmfDetection(this.cs, this.epOther);
this._removeDtmfDetection(this.cs, this.ep);
this.logger.debug({callSid: this.cs.callSid}, 'Dial:kill removed dtmf listeners');
this._killOutdials();
if (this.sd) {
this.sd.kill();
@@ -183,7 +179,7 @@ class TaskDial extends Task {
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
}
this.logger.debug('Dial:whisper tasks complete');
if (!cs.callGone) this.epOther.bridge(this.ep);
if (!cs.callGone && this.epOther) this.epOther.bridge(this.ep);
} catch (err) {
this.logger.error(err, 'Dial:whisper error');
}
@@ -221,7 +217,6 @@ class TaskDial extends Task {
_removeDtmfDetection(cs, ep) {
if (ep) {
delete ep.dtmfDetector;
this.logger.debug(`Dial:_removeDtmfDetection endpoint ${ep.uuid}`);
ep.removeAllListeners('dtmf');
}
}
@@ -238,7 +233,7 @@ class TaskDial extends Task {
this.logger.info(`Dial:_onDtmf got digits on B leg after adulting: ${evt.dtmf}`);
}
else {
requestor.request(this.dtmfHook, Object.assign({dtmf: match}, cs.callInfo))
requestor.request(this.dtmfHook, {dtmf: match, ...cs.callInfo})
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
}
}
@@ -250,6 +245,9 @@ class TaskDial extends Task {
this.epOther = ep;
debug(`Dial:__initializeInbound allocated ep for incoming call: ${ep.uuid}`);
/* send outbound legs back to the same SBC (to support static IP feature) */
if (!this.proxy) this.proxy = `${cs.req.source_address}:${cs.req.source_port};transport=tcp`;
if (this.dialMusic) {
// play dial music to caller while we outdial
ep.play(this.dialMusic).catch((err) => {
@@ -272,7 +270,10 @@ class TaskDial extends Task {
proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber
};
Object.assign(opts.headers, {'X-Account-Sid': cs.accountSid});
opts.headers = {
...opts.headers,
'X-Account-Sid': cs.accountSid
};
const t = this.target.find((t) => t.type === 'teams');
if (t) {
@@ -387,8 +388,10 @@ class TaskDial extends Task {
_connectSingleDial(cs, sd) {
if (!this.bridged) {
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
this.epOther.api('uuid_break', this.epOther.uuid);
this.epOther.bridge(sd.ep);
if (this.epOther) {
this.epOther.api('uuid_break', this.epOther.uuid);
this.epOther.bridge(sd.ep);
}
this.bridged = true;
}

View File

@@ -107,6 +107,8 @@ class Dialogflow extends Task {
this.language = cs.speechSynthesisLanguage;
this.voice = cs.speechSynthesisVoice;
}
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::transcription', this._onTranscription.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::audio_provided', this._onAudioProvided.bind(this, ep, cs));
@@ -211,11 +213,13 @@ class Dialogflow extends Task {
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid
salt: cs.callSid,
credentials: this.ttsCredentials
};
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
const {filePath} = await synthAudio(obj);
const {filePath, servedFromCache} = await synthAudio(obj);
if (filePath) cs.trackTmpFile(filePath);
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
if (this.playInProgress) {
await ep.api('uuid_break', ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));

View File

@@ -51,9 +51,23 @@ class TaskGather extends Task {
async exec(cs, ep) {
await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
this.ep = ep;
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (!this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but not creds supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
}
try {
if (this.sayTask) {
@@ -71,8 +85,10 @@ class TaskGather extends Task {
else this._startTimer();
if (this.input.includes('speech')) {
await this._initSpeech(ep);
await this._initSpeech(cs, ep);
this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */});
}
if (this.input.includes('digits')) {
@@ -105,10 +121,11 @@ class TaskGather extends Task {
this._killAudio();
}
async _initSpeech(ep) {
async _initSpeech(cs, ep) {
const opts = {};
if ('google' === this.vendor) {
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
Object.assign(opts, {
GOOGLE_SPEECH_USE_ENHANCED: true,
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
@@ -121,6 +138,8 @@ class TaskGather extends Task {
if (this.profanityFilter === true) {
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
}
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
}
else {
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
@@ -129,18 +148,16 @@ class TaskGather extends Task {
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
Object.assign(opts, {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_REGION: process.env.AWS_REGION
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
}
this.logger.debug(`setting freeswitch vars ${JSON.stringify(opts)}`);
this.logger.debug({vars: opts}, 'setting freeswitch vars');
await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, ep));
}
_startTranscribing(ep) {
@@ -148,7 +165,16 @@ class TaskGather extends Task {
vendor: this.vendor,
locale: this.language,
interim: this.partialResultCallback ? true : false,
}).catch((err) => this.logger.error(err, 'TaskGather:_startTranscribing error'));
}).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error');
writeAlerts({
account_sid: this.cs.accountSid,
alert_type: AlertType.STT_FAILURE,
vendor: this.vendor,
detail: err.message
});
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
}
_startTimer() {
@@ -175,19 +201,23 @@ class TaskGather extends Task {
}
}
_onTranscription(ep, evt) {
_onTranscription(cs, ep, evt) {
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
this.logger.debug(evt, 'TaskGather:_onTranscription');
const final = evt.is_final;
if (final) this._resolve('speech', evt);
if (final) {
this._resolve('speech', evt);
}
else if (this.partialResultHook) {
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
}
}
_onEndOfUtterance(ep, evt) {
this.logger.info(evt, 'TaskGather:_onEndOfUtterance');
this._startTranscribing(ep);
_onEndOfUtterance(cs, ep) {
this.logger.info('TaskGather:_onEndOfUtterance');
if (!this.resolved && !this.killed) {
this._startTranscribing(ep);
}
}
async _resolve(reason, evt) {
@@ -202,13 +232,10 @@ class TaskGather extends Task {
this._clearTimer();
if (reason.startsWith('dtmf')) {
await this.performAction({reason: 'dtmfDetected', digits: this.digitBuffer});
await this.performAction({digits: this.digitBuffer});
}
else if (reason.startsWith('speech')) {
await this.performAction({reason: 'speechDetected', speech: evt});
}
else if (reason.startsWith('timeout')) {
await this.performAction({reason: 'inputTimeout'});
await this.performAction({speech: evt});
}
this.notifyTaskDone();
}

View File

@@ -103,6 +103,8 @@ class Lex extends Task {
this.language = cs.speechSynthesisLanguage;
this.voice = cs.speechSynthesisVoice;
}
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
this.ep.addCustomEventListener('lex::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::transcription', this._onTranscription.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::audio_provided', this._onAudioProvided.bind(this, ep, cs));
@@ -184,20 +186,23 @@ class Lex extends Task {
try {
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
const {filepath} = await synthAudio({
// eslint-disable-next-line no-unused-vars
const {filePath, servedFromCache} = await synthAudio({
text: msg,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid
salt: cs.callSid,
credentials: this.ttsCredentials
});
if (filepath) cs.trackTmpFile(filepath);
if (filePath) cs.trackTmpFile(filePath);
if (this.events.includes('start-play')) {
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: filepath}});
this._performHook(cs, this.eventHook, {event: 'start-play', data: {path: filePath}});
}
await ep.play(filepath);
await ep.play(filePath);
if (this.events.includes('stop-play')) {
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: filepath}});
this._performHook(cs, this.eventHook, {event: 'stop-play', data: {path: filePath}});
}
this.logger.debug(`finished tts, sending play_done ${this.vendor} ${this.voice}`);
this.ep.api('aws_lex_play_done', this.ep.uuid)

View File

@@ -8,6 +8,7 @@ class TaskMessage extends Task {
this.preconditions = TaskPreconditions.None;
this.payload = {
message_sid: this.data.message_sid,
provider: this.data.provider,
to: this.data.to,
from: this.data.from,
@@ -23,21 +24,50 @@ class TaskMessage extends Task {
/**
* Send outbound SMS
*/
async exec(cs, dlg) {
const {srf} = cs;
async exec(cs) {
const {srf, accountSid} = cs;
const {res} = cs.callInfo;
let payload = this.payload;
await super.exec(cs);
try {
const {getSBC} = srf.locals;
const sbcAddress = getSBC();
if (sbcAddress) {
const url = `http://${sbcAddress}:3000/`;
const post = bent(url, 'POST', 'json', 200);
this.logger.info({payload: this.payload, sbcAddress}, 'Message:exec sending outbound SMS');
const response = await post('v1/outboundSMS', this.payload);
const {getSBC, getSmpp, dbHelpers} = srf.locals;
const {lookupSmppGateways} = dbHelpers;
this.logger.info(`looking up gateways for account_sid: ${accountSid}`);
const r = await lookupSmppGateways(accountSid);
let gw, url, relativeUrl;
if (r.length > 0) {
if (this.payload.provider) gw = r.find((o) => o.vc.name === this.payload.provider);
else gw = r[0];
}
if (gw) {
this.logger.info({gw, accountSid}, 'Message:exec - using smpp to send message');
url = getSmpp();
relativeUrl = '/sms';
payload = {
...payload,
...gw.sg,
...gw.vc
};
}
else {
this.logger.info({gw, accountSid, provider: this.payload.provider},
'Message:exec - no smpp gateways found to send message');
relativeUrl = 'v1/outboundSMS';
const sbcAddress = getSBC();
if (sbcAddress) url = `http://${sbcAddress}:3000/`;
//TMP: smpp only at the moment, need to add http back in
return res.sendStatus(404);
}
if (url) {
const post = bent(url, 'POST', 'json', 201);
this.logger.info({payload, url}, 'Message:exec sending outbound SMS');
const response = await post(relativeUrl, payload);
this.logger.info({response}, 'Successfully sent SMS');
if (cs.callInfo.res) {
this.logger.info('Message:exec sending 200 OK response to HTTP POST from api server');
cs.callInfo.res.status(200).json({
res.status(200).json({
sid: cs.callInfo.messageSid,
providerResponse: response
});
@@ -46,10 +76,12 @@ class TaskMessage extends Task {
// TODO: action Hook
}
else {
this.logger.info('Message:exec - unable to send SMS as there are no available SBCs');
this.logger.info('Message:exec - unable to send SMS as there are no available SMS gateways');
res.status(422).json({message: 'no configured SMS gateways'});
}
} catch (err) {
this.logger.error(err, 'TaskMessage:exec - Error sending SMS');
res.status(422).json({message: 'no configured SMS gateways'});
}
}
}

View File

@@ -15,34 +15,64 @@ class TaskSay extends Task {
get name() { return TaskName.Say; }
async exec(cs, ep) {
const {srf} = cs;
const {synthAudio} = srf.locals.dbHelpers;
await super.exec(cs);
const {srf} = cs;
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
const {writeAlerts, AlertType} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers;
const vendor = this.synthesizer.vendor || cs.speechSynthesisVendor;
const language = this.synthesizer.language || cs.speechSynthesisLanguage;
const voice = this.synthesizer.voice || cs.speechSynthesisVoice;
const salt = cs.callSid;
const credentials = cs.getSpeechCredentials(vendor, 'tts');
this.ep = ep;
try {
if (!credentials) {
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
throw new Error('no provisioned speech credentials for TTS');
}
// synthesize all of the text elements
const files = (await Promise.all(this.text.map(async(text) => {
const {filePath} = await synthAudio({
let lastUpdated = false;
const filepath = (await Promise.all(this.text.map(async(text) => {
const {filePath, servedFromCache} = await synthAudio({
text,
vendor: this.synthesizer.vendor || cs.speechSynthesisVendor,
language: this.synthesizer.language || cs.speechSynthesisLanguage,
voice: this.synthesizer.voice || cs.speechSynthesisVoice,
salt: cs.callSid
}).catch((err) => this.logger.error(err, 'Error synthesizing text'));
vendor,
language,
voice,
salt,
credentials
}).catch((err) => {
this.logger.info(err, 'Error synthesizing tts');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor,
detail: err.message
});
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath);
if (!servedFromCache && !lastUpdated) {
lastUpdated = true;
updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
.catch(() => {/*already logged error */});
}
return filePath;
})))
.filter((fp) => fp && fp.length);
}))).filter((fp) => fp && fp.length);
this.logger.debug({files, loop: this.loop}, 'synthesized files for tts');
if (!this.ep.connected) this.logger.debug('say: endpoint is not connected!');
this.logger.debug({filepath}, 'synthesized files for tts');
while (!this.killed && this.loop-- && this.ep.connected) {
let segment = 0;
do {
this.logger.debug(`playing file ${files[segment]}`);
await ep.play(files[segment]);
} while (!this.killed && ++segment < files.length);
await ep.play(filepath[segment]);
} while (!this.killed && ++segment < filepath.length);
}
} catch (err) {
this.logger.info(err, 'TaskSay:exec error');

View File

@@ -166,10 +166,6 @@
"passDtmf": "boolean",
"actionHook": "object|string",
"eventHook": "object|string",
"prompt": {
"type": "string",
"enum": ["lex", "tts"]
},
"noInputTimeout": "number",
"tts": "#synthesizer"
},
@@ -177,7 +173,7 @@
"botId",
"botAlias",
"region",
"prompt"
"credentials"
]
},
"listen": {
@@ -206,7 +202,9 @@
},
"message": {
"properties": {
"provider": "string",
"carrier": "string",
"account_sid": "string",
"message_sid": "string",
"to": "string",
"from": "string",
"text": "string",
@@ -292,8 +290,7 @@
"sipUri": "string",
"auth": "#auth",
"vmail": "boolean",
"tenant": "string",
"overrideTo": "string"
"tenant": "string"
},
"required": [
"type"

View File

@@ -200,6 +200,7 @@ class Task extends Emitter {
}
if (required.length > 0) throw new Error(`${name}: missing value for ${required}`);
}
}
module.exports = Task;

View File

@@ -44,11 +44,22 @@ class TaskTranscribe extends Task {
async exec(cs, ep, parentTask) {
super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
this.ep = ep;
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
try {
await this._startTranscribing(ep);
if (!this.sttCredentials) {
// TODO: generate alert (actually should be done by cs.getSpeechCredentials)
throw new Error('no provisioned speech credentials for TTS');
}
await this._startTranscribing(cs, ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */});
await this.awaitTaskDone();
} catch (err) {
this.logger.info(err, 'TaskTranscribe:exec - error');
@@ -74,19 +85,20 @@ class TaskTranscribe extends Task {
await this.awaitTaskDone();
}
async _startTranscribing(ep) {
async _startTranscribing(cs, ep) {
const opts = {};
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, ep));
this._onMaxDurationExceeded.bind(this, cs, ep));
if (this.vendor === 'google') {
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
[
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
@@ -134,11 +146,20 @@ class TaskTranscribe extends Task {
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
}
Object.assign(opts, {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_REGION: process.env.AWS_REGION
});
if (this.sttCredentials) {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
AWS_REGION: this.sttCredentials.region
});
}
else {
Object.assign(opts, {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
AWS_REGION: process.env.AWS_REGION
});
}
await ep.set(opts)
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
@@ -155,8 +176,10 @@ class TaskTranscribe extends Task {
});
}
_onTranscription(ep, evt) {
_onTranscription(cs, ep, evt) {
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
if (this.killed) {
@@ -166,12 +189,12 @@ class TaskTranscribe extends Task {
}
}
_onNoAudio(ep) {
_onNoAudio(cs, ep) {
this.logger.debug('TaskTranscribe:_onNoAudio restarting transcription');
this._transcribe(ep);
}
_onMaxDurationExceeded(ep) {
_onMaxDurationExceeded(cs, ep) {
this.logger.debug('TaskTranscribe:_onMaxDurationExceeded restarting transcription');
this._transcribe(ep);
}

74
lib/utils/db-utils.js Normal file
View File

@@ -0,0 +1,74 @@
const {decrypt} = require('./encrypt-decrypt');
const sqlAccountDetails = `SELECT *
FROM accounts account
WHERE account.account_sid = ?`;
const sqlSpeechCredentials = `SELECT *
FROM speech_credentials
WHERE account_sid = ? `;
const sqlSpeechCredentialsForSP = `SELECT *
FROM speech_credentials
WHERE service_provider_sid =
(SELECT service_provider_sid from accounts where account_sid = ?)`;
const speechMapper = (cred) => {
const {credential, ...obj} = cred;
if ('google' === obj.vendor) {
obj.service_key = decrypt(credential);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key;
}
return obj;
};
module.exports = (logger, srf) => {
const {pool} = srf.locals.dbHelpers;
const pp = pool.promise();
const lookupAccountDetails = async(account_sid) => {
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, account_sid);
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
const speech = r2.map(speechMapper);
/* search at the service provider level if we don't find it at the account level */
const haveGoogle = speech.find((s) => s.vendor === 'google');
const haveAws = speech.find((s) => s.vendor === 'aws');
if (!haveGoogle || !haveAws) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
if (!haveGoogle) {
const google = r3.find((s) => s.vendor === 'google');
if (google) speech.push(speechMapper(google));
}
if (!haveAws) {
const aws = r3.find((s) => s.vendor === 'aws');
if (aws) speech.push(speechMapper(aws));
}
}
}
return {
...r[0],
speech
};
};
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
const pp = pool.promise();
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
try {
await pp.execute(sql, [speech_credential_sid]);
} catch (err) {
logger.error({err}, `Error updating last_used for speech_credential_sid ${speech_credential_sid}`);
}
};
return {
lookupAccountDetails,
updateSpeechCredentialLastUsed
};
};

View File

@@ -0,0 +1,35 @@
const crypto = require('crypto');
const algorithm = 'aes-256-ctr';
const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256')
.update(String(process.env.JWT_SECRET))
.digest('base64')
.substr(0, 32);
const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
const data = {
iv: iv.toString('hex'),
content: encrypted.toString('hex')
};
return JSON.stringify(data);
};
const decrypt = (data) => {
let hash;
try {
hash = JSON.parse(data);
} catch (err) {
console.log(`failed to parse json string ${data}`);
throw err;
}
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrpyted.toString();
};
module.exports = {
encrypt,
decrypt
};

View File

@@ -38,9 +38,12 @@ function installSrfLocals(srf, logger) {
const fsInventory = process.env.JAMBONES_FREESWITCH
.split(',')
.map((fs) => {
const arr = /^(.*):(.*):(.*)/.exec(fs);
const arr = /^([^:]*):([^:]*):([^:]*)(?::([^:]*))?/.exec(fs);
assert.ok(arr, `Invalid syntax JAMBONES_FREESWITCH: ${process.env.JAMBONES_FREESWITCH}`);
return {address: arr[1], port: arr[2], secret: arr[3]};
const opts = {address: arr[1], port: arr[2], secret: arr[3]};
if (arr.length > 4) opts.advertisedAddress = arr[4];
if (process.env.NODE_ENV === 'test') opts.listenAddress = '0.0.0.0';
return opts;
});
logger.info({fsInventory}, 'freeswitch inventory');
@@ -72,6 +75,7 @@ function installSrfLocals(srf, logger) {
// if we have a single freeswitch (as is typical) report stats periodically
if (mediaservers.length === 1) {
srf.locals.mediaservers = [mediaservers[0].ms];
setInterval(() => {
try {
if (mediaservers[0].ms && mediaservers[0].active) {
@@ -99,20 +103,25 @@ function installSrfLocals(srf, logger) {
}
const {
pool,
lookupAppByPhoneNumber,
lookupAppBySid,
lookupAppByRealm,
lookupAppByTeamsTenant,
lookupTeamsByAccount,
lookupAccountBySid
lookupAccountBySid,
lookupAccountCapacitiesBySid,
lookupSmppGateways
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger);
const {
client,
updateCallStatus,
retrieveCall,
listCalls,
@@ -135,15 +144,27 @@ function installSrfLocals(srf, logger) {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const {
writeAlerts,
AlertType
} = require('@jambonz/time-series')(logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
Object.assign(srf.locals, {
srf.locals = {...srf.locals,
dbHelpers: {
client,
pool,
lookupAppByPhoneNumber,
lookupAppBySid,
lookupAppByRealm,
lookupAppByTeamsTenant,
lookupTeamsByAccount,
lookupAccountBySid,
lookupAccountCapacitiesBySid,
lookupSmppGateways,
updateCallStatus,
retrieveCall,
listCalls,
@@ -167,10 +188,15 @@ function installSrfLocals(srf, logger) {
ipv4: localIp,
serviceUrl: `http://${localIp}:${PORT}`,
getSBC,
getSmpp: () => {
return process.env.SMPP_URL;
},
lifecycleEmitter,
getFreeswitch,
stats: stats
});
stats: stats,
writeAlerts,
AlertType
};
}
module.exports = installSrfLocals;

View File

@@ -61,6 +61,7 @@ class SingleDialer extends Emitter {
async exec(srf, ms, opts) {
opts = opts || {};
opts.headers = opts.headers || {};
opts.headers = {...opts.headers, 'X-Call-Sid': this.callSid};
let uri, to;
try {
switch (this.target.type) {
@@ -71,10 +72,10 @@ class SingleDialer extends Emitter {
to = this.target.number;
if ('teams' === this.target.type) {
assert(this.target.teamsInfo);
Object.assign(opts.headers, {
opts.headers = {...opts.headers,
'X-MS-Teams-FQDN': this.target.teamsInfo.ms_teams_fqdn,
'X-MS-Teams-Tenant-FQDN': this.target.teamsInfo.tenant_fqdn
});
};
if (this.target.vmail === true) uri = `${uri};opaque=app:voicemail`;
}
break;
@@ -84,12 +85,6 @@ class SingleDialer extends Emitter {
uri = `sip:${this.target.name}`;
to = this.target.name;
if (this.target.overrideTo) {
Object.assign(opts.headers, {
'X-Override-To': this.target.overrideTo
});
}
// need to send to the SBC registered on
const reg = await registrar.query(aor);
if (reg) {
@@ -267,7 +262,7 @@ class SingleDialer extends Emitter {
// now execute it in a new ConfirmCallSession
this.logger.debug(`SingleDialer:_executeApp: executing ${tasks.length} tasks`);
const cs = new ConfirmCallSession({
logger: this.baseLogger,
logger: this.logger,
application: this.application,
dlg: this.dlg,
ep: this.ep,

View File

@@ -2,8 +2,30 @@ const bent = require('bent');
const parseUrl = require('parse-url');
const assert = require('assert');
const snakeCaseKeys = require('./snakecase-keys');
const crypto = require('crypto');
const timeSeries = require('@jambonz/time-series');
let alerter ;
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
function computeSignature(payload, timestamp, secret) {
assert(secret);
const data = `${timestamp}.${JSON.stringify(payload)}`;
return crypto
.createHmac('sha256', secret)
.update(data, 'utf8')
.digest('hex');
}
function generateSigHeader(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signature = computeSignature(payload, timestamp, secret);
const scheme = 'v1';
return {
'Jambonz-Signature': `t=${timestamp},${scheme}=${signature}`
};
}
function basicAuth(username, password) {
if (!username || !password) return {};
const creds = `${username}:${password || ''}`;
@@ -21,7 +43,7 @@ function isAbsoluteUrl(u) {
}
class Requestor {
constructor(logger, hook) {
constructor(logger, account_sid, hook, secret) {
assert(typeof hook === 'object');
this.logger = logger;
@@ -38,12 +60,22 @@ class Requestor {
this.username = hook.username;
this.password = hook.password;
this.secret = secret;
this.account_sid = account_sid;
assert(isAbsoluteUrl(this.url));
assert(['GET', 'POST'].includes(this.method));
const {stats} = require('../../').srf.locals;
this.stats = stats;
if (!alerter) {
alerter = timeSeries(logger, {
host: process.env.JAMBONES_TIME_SERIES_HOST,
commitSize: 50,
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
});
}
}
get baseUrl() {
@@ -65,7 +97,6 @@ class Requestor {
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
const url = hook.url || hook;
const method = hook.method || 'POST';
const {username, password} = typeof hook === 'object' ? hook : {};
assert.ok(url, 'Requestor:request url was not provided');
assert.ok, (['GET', 'POST'].includes(method), `Requestor:request method must be 'GET' or 'POST' not ${method}`);
@@ -75,12 +106,27 @@ class Requestor {
let buf;
try {
const sigHeader = generateSigHeader(payload, this.secret);
const headers = {...sigHeader, ...this.authHeader};
this.logger.info({url, headers}, 'send webhook');
buf = isRelativeUrl(url) ?
await this.post(url, payload, this.authHeader) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, basicAuth(username, password));
await this.post(url, payload, headers) :
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
} catch (err) {
this.logger.info({baseUrl: this.baseUrl, url, statusCode: err.statusCode},
this.logger.error({err, secret: this.secret, baseUrl: this.baseUrl, url, statusCode: err.statusCode},
`web callback returned unexpected error code ${err.statusCode}`);
let opts = {account_sid: this.account_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_STATUS_FAILURE, url, status: err.statusCode};
}
else {
opts = {...opts, alert_type: alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url, detail: err.message};
}
alerter.writeAlerts(opts).catch((err) => this.logger.info({err, opts}, 'Error writing alert'));
throw err;
}
const diff = process.hrtime(startAt);

View File

@@ -68,6 +68,8 @@ module.exports = (logger) => {
// send OPTIONS pings to SBCs
async function pingProxies(srf) {
if (process.env.NODE_ENV === 'test') return;
for (const sbc of sbcs) {
try {
const ms = srf.locals.getFreeswitch();

865
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,17 +21,17 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test JAMBONES_NETWORK_CIDR=127.0.0.1/32 node test/ | ./node_modules/.bin/tap-spec",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.5.20",
"@jambonz/mw-registrar": "^0.1.9",
"@jambonz/db-helpers": "^0.6.4",
"@jambonz/mw-registrar": "^0.2.1",
"@jambonz/realtimedb-helpers": "^0.4.1",
"@jambonz/stats-collector": "^0.1.5",
"@jambonz/time-series": "^0.1.5",
"aws-sdk": "^2.848.0",
"aws-sdk": "^2.846.0",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.1",
@@ -42,7 +42,7 @@
"ip": "^1.1.5",
"moment": "^2.29.1",
"parse-url": "^5.0.2",
"pino": "^6.11.1",
"pino": "^6.11.2",
"to-snake-case": "^1.0.0",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.6",
@@ -50,12 +50,10 @@
},
"devDependencies": {
"async": "^3.2.0",
"blue-tape": "^1.0.0",
"clear-module": "^4.1.1",
"eslint": "^7.20.0",
"eslint-plugin-promise": "^4.3.1",
"nyc": "^15.1.0",
"tap-spec": "^5.0.0",
"tape": "^5.2.0"
"tape": "^5.2.2"
}
}

View File

@@ -0,0 +1,34 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('account validation tests', async(t) => {
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-expect-500.xml', '172.38.0.10');
t.pass('rejected INVITE without X-Account-Sid header');
await sippUac('uac-invalid-account-expect-503.xml', '172.38.0.10');
t.pass('rejected INVITE with invalid X-Account-Sid header');
await sippUac('uac-inactive-account-expect-503.xml', '172.38.0.10');
t.pass('rejected INVITE from inactive account');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,9 +1,12 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
const fs = require('fs');
const {encrypt} = require('../lib/utils/encrypt-decrypt');
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 --port=3360 < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr)
if (err) return t.end(err);
t.pass('database successfully created');
t.end();
@@ -11,17 +14,35 @@ 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 --port=3360 -D jambones_test < ${__dirname}/db/create-and-populate-schema.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('schema successfully created');
t.end();
t.pass('schema and test data successfully created');
if (process.env.GCP_JSON_KEY && process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
const google_credential = encrypt(process.env.GCP_JSON_KEY);
const aws_credential = encrypt(JSON.stringify({
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY
}));
const cmd = `
UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';
UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';
`;
const path = `${__dirname}/.creds.sql`;
fs.writeFileSync(path, cmd);
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${path}`, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
if (err) return t.end(err);
fs.unlinkSync(path)
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
t.pass('set account-level speech credentials');
t.end();
});
}
else {
t.end();
}
});
});
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) => {
if (err) return t.end(err);
t.pass('test data set created');
t.end();
});
});

0
test/credentials/.keep Normal file
View File

View File

@@ -0,0 +1,750 @@
-- MySQL dump 10.13 Distrib 8.0.18, for macos10.14 (x86_64)
--
-- Host: 127.0.0.1 Database: jambones_test
-- ------------------------------------------------------
-- Server version 5.7.33
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `account_products`
--
DROP TABLE IF EXISTS `account_products`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account_products` (
`account_product_sid` char(36) NOT NULL,
`account_subscription_sid` char(36) NOT NULL,
`product_sid` char(36) NOT NULL,
`quantity` int(11) NOT NULL,
PRIMARY KEY (`account_product_sid`),
UNIQUE KEY `account_product_sid` (`account_product_sid`),
KEY `account_product_sid_idx` (`account_product_sid`),
KEY `account_subscription_sid_idx` (`account_subscription_sid`),
KEY `product_sid_idxfk` (`product_sid`),
CONSTRAINT `account_subscription_sid_idxfk` FOREIGN KEY (`account_subscription_sid`) REFERENCES `account_subscriptions` (`account_subscription_sid`),
CONSTRAINT `product_sid_idxfk` FOREIGN KEY (`product_sid`) REFERENCES `products` (`product_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account_products`
--
LOCK TABLES `account_products` WRITE;
/*!40000 ALTER TABLE `account_products` DISABLE KEYS */;
INSERT INTO `account_products` VALUES ('bb0e8a44-0e59-4103-a44c-f7ff950319fb','02639178-e073-4f8e-9b7e-48b1d36f4b7a','35a9fb10-233d-4eb9-aada-78de5814d680',10),('e2cd5148-07ad-4cdc-b395-22e4b4e23d7e','02639178-e073-4f8e-9b7e-48b1d36f4b7a','2c815913-5c26-4004-b748-183b459329df',10),('f9b320aa-c287-438b-a4c0-e4383b4f0256','02639178-e073-4f8e-9b7e-48b1d36f4b7a','c4403cdb-8e75-4b27-9726-7d8315e3216d',10);
/*!40000 ALTER TABLE `account_products` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `account_static_ips`
--
DROP TABLE IF EXISTS `account_static_ips`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account_static_ips` (
`account_static_ip_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`ipv4` varchar(16) NOT NULL,
`sbc_address_sid` char(36) NOT NULL,
PRIMARY KEY (`account_static_ip_sid`),
UNIQUE KEY `account_static_ip_sid` (`account_static_ip_sid`),
UNIQUE KEY `ipv4` (`ipv4`),
KEY `account_static_ip_sid_idx` (`account_static_ip_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `sbc_address_sid_idxfk` (`sbc_address_sid`),
CONSTRAINT `account_sid_idxfk_3` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `sbc_address_sid_idxfk` FOREIGN KEY (`sbc_address_sid`) REFERENCES `sbc_addresses` (`sbc_address_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account_static_ips`
--
LOCK TABLES `account_static_ips` WRITE;
/*!40000 ALTER TABLE `account_static_ips` DISABLE KEYS */;
/*!40000 ALTER TABLE `account_static_ips` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `account_subscriptions`
--
DROP TABLE IF EXISTS `account_subscriptions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account_subscriptions` (
`account_subscription_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`pending` tinyint(1) NOT NULL DEFAULT '0',
`effective_start_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`effective_end_date` datetime DEFAULT NULL,
`change_reason` varchar(255) DEFAULT NULL,
`stripe_subscription_id` varchar(56) DEFAULT NULL,
`stripe_payment_method_id` varchar(56) DEFAULT NULL,
`stripe_statement_descriptor` varchar(255) DEFAULT NULL,
`last4` char(4) DEFAULT NULL,
`exp_month` int(11) DEFAULT NULL,
`exp_year` int(11) DEFAULT NULL,
`card_type` varchar(16) DEFAULT NULL,
`pending_reason` varbinary(52) DEFAULT NULL,
PRIMARY KEY (`account_subscription_sid`),
UNIQUE KEY `account_subscription_sid` (`account_subscription_sid`),
KEY `account_subscription_sid_idx` (`account_subscription_sid`),
KEY `account_sid_idx` (`account_sid`),
CONSTRAINT `account_sid_idxfk` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account_subscriptions`
--
LOCK TABLES `account_subscriptions` WRITE;
/*!40000 ALTER TABLE `account_subscriptions` DISABLE KEYS */;
INSERT INTO `account_subscriptions` VALUES ('02639178-e073-4f8e-9b7e-48b1d36f4b7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f',0,'2021-04-03 15:41:03',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `account_subscriptions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `accounts`
--
DROP TABLE IF EXISTS `accounts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `accounts` (
`account_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`sip_realm` varchar(132) DEFAULT NULL 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) DEFAULT NULL COMMENT 'webhook to call when devices underr this account attempt to register',
`device_calling_application_sid` char(36) DEFAULT NULL COMMENT 'application to use for outbound calling from an account',
`is_active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`plan_type` enum('trial','free','paid') NOT NULL DEFAULT 'trial',
`stripe_customer_id` varchar(56) DEFAULT NULL,
`webhook_secret` varchar(36) NOT NULL,
`disable_cdrs` tinyint(1) NOT NULL DEFAULT '0',
`trial_end_date` datetime DEFAULT NULL,
`deactivated_reason` varchar(255) DEFAULT NULL,
PRIMARY KEY (`account_sid`),
UNIQUE KEY `account_sid` (`account_sid`),
UNIQUE KEY `sip_realm` (`sip_realm`),
KEY `account_sid_idx` (`account_sid`),
KEY `sip_realm_idx` (`sip_realm`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `registration_hook_sid_idxfk_1` (`registration_hook_sid`),
KEY `device_calling_application_sid_idxfk` (`device_calling_application_sid`),
CONSTRAINT `device_calling_application_sid_idxfk` FOREIGN KEY (`device_calling_application_sid`) REFERENCES `applications` (`application_sid`),
CONSTRAINT `registration_hook_sid_idxfk_1` FOREIGN KEY (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `service_provider_sid_idxfk_6` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An enterprise that uses the platform for comm services';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `accounts`
--
LOCK TABLES `accounts` WRITE;
/*!40000 ALTER TABLE `accounts` DISABLE KEYS */;
INSERT INTO `accounts` VALUES ('bb845d4b-83a9-4cde-a6e9-50f3743bab3f','Joe User','test.yakeeda.com','2708b1b3-2736-40ea-b502-c53d8396247f',NULL,NULL,1,'2021-04-03 15:41:03','trial',NULL,'wh_secret_ehV2dVyzNBs5kHxeJcatRQ',0,NULL,NULL);
INSERT INTO `accounts` VALUES ('622f62e4-303a-49f2-bbe0-eb1e1714e37a','Dave Horton','delta.yakeeda.com','2708b1b3-2736-40ea-b502-c53d8396247f',NULL,NULL,0,'2021-04-03 15:41:03','trial',NULL,'wh_secret_ehV2dVyzNBs5kHxeJcatRQ',0,NULL,NULL);
/*!40000 ALTER TABLE `accounts` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `api_keys`
--
DROP TABLE IF EXISTS `api_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `api_keys` (
`api_key_sid` char(36) NOT NULL,
`token` char(36) NOT NULL,
`account_sid` char(36) DEFAULT NULL,
`service_provider_sid` char(36) DEFAULT NULL,
`expires_at` timestamp NULL DEFAULT NULL,
`last_used` timestamp NULL DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`api_key_sid`),
UNIQUE KEY `api_key_sid` (`api_key_sid`),
UNIQUE KEY `token` (`token`),
KEY `api_key_sid_idx` (`api_key_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
CONSTRAINT `account_sid_idxfk_4` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `service_provider_sid_idxfk` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An authorization token that is used to access the REST api';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `api_keys`
--
LOCK TABLES `api_keys` WRITE;
/*!40000 ALTER TABLE `api_keys` DISABLE KEYS */;
INSERT INTO `api_keys` VALUES ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0','38700987-c7a4-4685-a5bb-af378f9734de',NULL,NULL,NULL,NULL,'2021-04-03 15:40:37'),('b00b1025-2b65-453b-a243-599b75be7d0a','52c2eb45-9f72-4545-9c60-9639e3f4eaf7','bb845d4b-83a9-4cde-a6e9-50f3743bab3f',NULL,NULL,NULL,'2021-04-03 15:42:40');
/*!40000 ALTER TABLE `api_keys` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `applications`
--
DROP TABLE IF EXISTS `applications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `applications` (
`application_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`service_provider_sid` char(36) DEFAULT NULL COMMENT 'if non-null, this application is a test application that can be used by any account under the associated service provider',
`account_sid` char(36) DEFAULT NULL COMMENT 'account that this application belongs to (if null, this is a service provider test application)',
`call_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for inbound calls ',
`call_status_hook_sid` char(36) DEFAULT NULL COMMENT 'webhook to call for call status events',
`messaging_hook_sid` char(36) DEFAULT NULL 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) DEFAULT NULL,
`speech_recognizer_vendor` varchar(64) NOT NULL DEFAULT 'google',
`speech_recognizer_language` varchar(64) NOT NULL DEFAULT 'en-US',
PRIMARY KEY (`application_sid`),
UNIQUE KEY `application_sid` (`application_sid`),
UNIQUE KEY `applications_idx_name` (`account_sid`,`name`),
KEY `application_sid_idx` (`application_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `call_hook_sid_idxfk` (`call_hook_sid`),
KEY `call_status_hook_sid_idxfk` (`call_status_hook_sid`),
KEY `messaging_hook_sid_idxfk` (`messaging_hook_sid`),
CONSTRAINT `account_sid_idxfk_10` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `call_hook_sid_idxfk` FOREIGN KEY (`call_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `call_status_hook_sid_idxfk` FOREIGN KEY (`call_status_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `messaging_hook_sid_idxfk` FOREIGN KEY (`messaging_hook_sid`) REFERENCES `webhooks` (`webhook_sid`),
CONSTRAINT `service_provider_sid_idxfk_5` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A defined set of behaviors to be applied to phone calls ';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `applications`
--
LOCK TABLES `applications` WRITE;
/*!40000 ALTER TABLE `applications` DISABLE KEYS */;
INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78b','decline call',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('308b4f41-1a18-4052-b89a-c054e75ce242','say',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('24d0f6af-e976-44dd-a2e8-41c7b55abe33','say account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('17461c69-56b5-4dab-ad83-1c43a0f93a3d','gather',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','10692465-a511-4277-9807-b7157e4f81e1','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('baf9213b-5556-4c20-870c-586392ed246f','transcribe',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
INSERT INTO `applications` VALUES ('ae026ab5-3029-47b4-9d7c-236e3a4b4ebe','transcribe account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'google','en-US','en-US-Standard-C','google','en-US');
/*!40000 ALTER TABLE `applications` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `call_routes`
--
DROP TABLE IF EXISTS `call_routes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `call_routes` (
`call_route_sid` char(36) NOT NULL,
`priority` int(11) NOT NULL,
`account_sid` char(36) NOT NULL,
`regex` varchar(255) NOT NULL,
`application_sid` char(36) NOT NULL,
PRIMARY KEY (`call_route_sid`),
UNIQUE KEY `call_route_sid` (`call_route_sid`),
KEY `call_route_sid_idx` (`call_route_sid`),
KEY `account_sid_idxfk_1` (`account_sid`),
KEY `application_sid_idxfk` (`application_sid`),
CONSTRAINT `account_sid_idxfk_1` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='a regex-based pattern match for call routing';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `call_routes`
--
LOCK TABLES `call_routes` WRITE;
/*!40000 ALTER TABLE `call_routes` DISABLE KEYS */;
/*!40000 ALTER TABLE `call_routes` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `dns_records`
--
DROP TABLE IF EXISTS `dns_records`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `dns_records` (
`dns_record_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`record_type` varchar(6) NOT NULL,
`record_id` int(11) NOT NULL,
PRIMARY KEY (`dns_record_sid`),
UNIQUE KEY `dns_record_sid` (`dns_record_sid`),
KEY `dns_record_sid_idx` (`dns_record_sid`),
KEY `account_sid_idxfk_2` (`account_sid`),
CONSTRAINT `account_sid_idxfk_2` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `dns_records`
--
LOCK TABLES `dns_records` WRITE;
/*!40000 ALTER TABLE `dns_records` DISABLE KEYS */;
/*!40000 ALTER TABLE `dns_records` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `lcr_carrier_set_entry`
--
DROP TABLE IF EXISTS `lcr_carrier_set_entry`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `lcr_carrier_set_entry` (
`lcr_carrier_set_entry_sid` char(36) NOT NULL,
`workload` int(11) 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` int(11) NOT NULL DEFAULT '0' COMMENT 'lower priority carriers are attempted first',
PRIMARY KEY (`lcr_carrier_set_entry_sid`),
KEY `lcr_route_sid_idxfk` (`lcr_route_sid`),
KEY `voip_carrier_sid_idxfk_2` (`voip_carrier_sid`),
CONSTRAINT `lcr_route_sid_idxfk` FOREIGN KEY (`lcr_route_sid`) REFERENCES `lcr_routes` (`lcr_route_sid`),
CONSTRAINT `voip_carrier_sid_idxfk_2` FOREIGN KEY (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An entry in the LCR routing list';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `lcr_carrier_set_entry`
--
LOCK TABLES `lcr_carrier_set_entry` WRITE;
/*!40000 ALTER TABLE `lcr_carrier_set_entry` DISABLE KEYS */;
/*!40000 ALTER TABLE `lcr_carrier_set_entry` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `lcr_routes`
--
DROP TABLE IF EXISTS `lcr_routes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `lcr_routes` (
`lcr_route_sid` char(36) NOT NULL,
`regex` varchar(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
`description` varchar(1024) DEFAULT NULL,
`priority` int(11) NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (`lcr_route_sid`),
UNIQUE KEY `priority` (`priority`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Least cost routing table';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `lcr_routes`
--
LOCK TABLES `lcr_routes` WRITE;
/*!40000 ALTER TABLE `lcr_routes` DISABLE KEYS */;
/*!40000 ALTER TABLE `lcr_routes` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `ms_teams_tenants`
--
DROP TABLE IF EXISTS `ms_teams_tenants`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `ms_teams_tenants` (
`ms_teams_tenant_sid` char(36) NOT NULL,
`service_provider_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`application_sid` char(36) DEFAULT NULL,
`tenant_fqdn` varchar(255) NOT NULL,
PRIMARY KEY (`ms_teams_tenant_sid`),
UNIQUE KEY `ms_teams_tenant_sid` (`ms_teams_tenant_sid`),
UNIQUE KEY `tenant_fqdn` (`tenant_fqdn`),
KEY `ms_teams_tenant_sid_idx` (`ms_teams_tenant_sid`),
KEY `service_provider_sid_idxfk_1` (`service_provider_sid`),
KEY `account_sid_idxfk_5` (`account_sid`),
KEY `application_sid_idxfk_1` (`application_sid`),
KEY `tenant_fqdn_idx` (`tenant_fqdn`),
CONSTRAINT `account_sid_idxfk_5` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk_1` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`),
CONSTRAINT `service_provider_sid_idxfk_1` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A Microsoft Teams customer tenant';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `ms_teams_tenants`
--
LOCK TABLES `ms_teams_tenants` WRITE;
/*!40000 ALTER TABLE `ms_teams_tenants` DISABLE KEYS */;
/*!40000 ALTER TABLE `ms_teams_tenants` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `phone_numbers`
--
DROP TABLE IF EXISTS `phone_numbers`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `phone_numbers` (
`phone_number_sid` char(36) NOT NULL,
`number` varchar(32) NOT NULL,
`voip_carrier_sid` char(36) DEFAULT NULL,
`account_sid` char(36) DEFAULT NULL,
`application_sid` char(36) DEFAULT NULL,
`service_provider_sid` char(36) DEFAULT NULL COMMENT 'if not null, this number is a test number for the associated service provider',
PRIMARY KEY (`phone_number_sid`),
UNIQUE KEY `number` (`number`),
UNIQUE KEY `phone_number_sid` (`phone_number_sid`),
KEY `phone_number_sid_idx` (`phone_number_sid`),
KEY `number_idx` (`number`),
KEY `voip_carrier_sid_idx` (`voip_carrier_sid`),
KEY `account_sid_idxfk_9` (`account_sid`),
KEY `application_sid_idxfk_3` (`application_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
CONSTRAINT `account_sid_idxfk_9` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk_3` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`),
CONSTRAINT `service_provider_sid_idxfk_4` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`),
CONSTRAINT `voip_carrier_sid_idxfk` FOREIGN KEY (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A phone number that has been assigned to an account';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `phone_numbers`
--
LOCK TABLES `phone_numbers` WRITE;
/*!40000 ALTER TABLE `phone_numbers` DISABLE KEYS */;
INSERT INTO `phone_numbers` VALUES ('4b439355-debc-40c7-9cfa-5be58c2bed6b','16174000000','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78b', NULL);
INSERT INTO `phone_numbers` VALUES ('9cc9e7fc-b7b0-4101-8f3c-9fe13ce5df0a','16174000001','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','308b4f41-1a18-4052-b89a-c054e75ce242', NULL);
INSERT INTO `phone_numbers` VALUES ('e686a320-0725-418f-be65-532159bdc3ed','16174000002','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','24d0f6af-e976-44dd-a2e8-41c7b55abe33', NULL);
INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','16174000003','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','17461c69-56b5-4dab-ad83-1c43a0f93a3d', NULL);
INSERT INTO `phone_numbers` VALUES ('f3c53863-b629-4cf6-9dcb-c7fb7072314b','16174000004','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','baf9213b-5556-4c20-870c-586392ed246f', NULL);
INSERT INTO `phone_numbers` VALUES ('f6416c17-829a-4f11-9c32-f0d00e4a9ae9','16174000005','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','ae026ab5-3029-47b4-9d7c-236e3a4b4ebe', NULL);
/*!40000 ALTER TABLE `phone_numbers` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `products`
--
DROP TABLE IF EXISTS `products`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `products` (
`product_sid` char(36) NOT NULL,
`name` varchar(32) NOT NULL,
`category` enum('api_rate','voice_call_session','device') NOT NULL,
PRIMARY KEY (`product_sid`),
UNIQUE KEY `product_sid` (`product_sid`),
KEY `product_sid_idx` (`product_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `products`
--
LOCK TABLES `products` WRITE;
/*!40000 ALTER TABLE `products` DISABLE KEYS */;
INSERT INTO `products` VALUES ('2c815913-5c26-4004-b748-183b459329df','registered device','device'),('35a9fb10-233d-4eb9-aada-78de5814d680','api call','api_rate'),('c4403cdb-8e75-4b27-9726-7d8315e3216d','concurrent call session','voice_call_session');
/*!40000 ALTER TABLE `products` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `sbc_addresses`
--
DROP TABLE IF EXISTS `sbc_addresses`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sbc_addresses` (
`sbc_address_sid` char(36) NOT NULL,
`ipv4` varchar(255) NOT NULL,
`port` int(11) NOT NULL DEFAULT '5060',
`service_provider_sid` char(36) DEFAULT NULL,
PRIMARY KEY (`sbc_address_sid`),
UNIQUE KEY `sbc_address_sid` (`sbc_address_sid`),
KEY `sbc_addresses_idx_host_port` (`ipv4`,`port`),
KEY `sbc_address_sid_idx` (`sbc_address_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
CONSTRAINT `service_provider_sid_idxfk_2` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `sbc_addresses`
--
LOCK TABLES `sbc_addresses` WRITE;
/*!40000 ALTER TABLE `sbc_addresses` DISABLE KEYS */;
INSERT INTO `sbc_addresses` VALUES ('8d6d0fda-4550-41ab-8e2f-60761d81fe7d','3.39.45.30',5060,NULL);
/*!40000 ALTER TABLE `sbc_addresses` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `service_providers`
--
DROP TABLE IF EXISTS `service_providers`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `service_providers` (
`service_provider_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`root_domain` varchar(128) DEFAULT NULL,
`registration_hook_sid` char(36) DEFAULT NULL,
`ms_teams_fqdn` varchar(255) DEFAULT NULL,
PRIMARY KEY (`service_provider_sid`),
UNIQUE KEY `service_provider_sid` (`service_provider_sid`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `root_domain` (`root_domain`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `name_idx` (`name`),
KEY `root_domain_idx` (`root_domain`),
KEY `registration_hook_sid_idxfk` (`registration_hook_sid`),
CONSTRAINT `registration_hook_sid_idxfk` FOREIGN KEY (`registration_hook_sid`) REFERENCES `webhooks` (`webhook_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A partition of the platform used by one service provider';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `service_providers`
--
LOCK TABLES `service_providers` WRITE;
/*!40000 ALTER TABLE `service_providers` DISABLE KEYS */;
INSERT INTO `service_providers` VALUES ('2708b1b3-2736-40ea-b502-c53d8396247f','jambonz.us','jambonz.us service provider','yakeeda.com',NULL,NULL);
/*!40000 ALTER TABLE `service_providers` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `sip_gateways`
--
DROP TABLE IF EXISTS `sip_gateways`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `sip_gateways` (
`sip_gateway_sid` char(36) NOT NULL,
`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` int(11) NOT NULL DEFAULT '5060' COMMENT 'sip signaling port',
`inbound` tinyint(1) NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
`outbound` tinyint(1) 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` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`sip_gateway_sid`),
KEY `sip_gateway_idx_hostport` (`ipv4`,`port`),
KEY `voip_carrier_sid_idx` (`voip_carrier_sid`),
CONSTRAINT `voip_carrier_sid_idxfk_1` FOREIGN KEY (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A whitelisted sip gateway used for origination/termination';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `sip_gateways`
--
LOCK TABLES `sip_gateways` WRITE;
/*!40000 ALTER TABLE `sip_gateways` DISABLE KEYS */;
INSERT INTO `sip_gateways` VALUES ('46b727eb-c7dc-44fa-b063-96e48d408e4a','3.3.3.3',5060,1,1,'5145b436-2f38-4029-8d4c-fd8c67831c7a',1),('81629182-6904-4588-8c72-a78d70053fb9','54.172.60.1',5060,1,1,'df0aefbf-ca7b-4d48-9fbf-3c66fef72060',1);
/*!40000 ALTER TABLE `sip_gateways` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `speech_credentials`
--
DROP TABLE IF EXISTS `speech_credentials`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `speech_credentials` (
`speech_credential_sid` char(36) NOT NULL,
`account_sid` char(36) NOT NULL,
`vendor` varchar(255) NOT NULL,
`credential` VARCHAR(8192) NOT NULL,
`use_for_tts` tinyint(1) DEFAULT '1',
`use_for_stt` tinyint(1) DEFAULT '1',
`last_used` datetime DEFAULT NULL,
`last_tested` datetime DEFAULT NULL,
`tts_tested_ok` tinyint(1) DEFAULT NULL,
`stt_tested_ok` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`speech_credential_sid`),
UNIQUE KEY `speech_credential_sid` (`speech_credential_sid`),
UNIQUE KEY `speech_credentials_idx_1` (`vendor`,`account_sid`),
KEY `speech_credential_sid_idx` (`speech_credential_sid`),
KEY `account_sid_idx` (`account_sid`),
CONSTRAINT `account_sid_idxfk_6` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `speech_credentials`
--
LOCK TABLES `speech_credentials` WRITE;
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`user_sid` char(36) NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`pending_email` varchar(255) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
`hashed_password` varchar(1024) DEFAULT NULL,
`salt` char(16) DEFAULT NULL,
`account_sid` char(36) DEFAULT NULL,
`service_provider_sid` char(36) DEFAULT NULL,
`force_change` tinyint(1) NOT NULL DEFAULT '0',
`provider` varchar(255) NOT NULL,
`provider_userid` varchar(255) DEFAULT NULL,
`scope` varchar(16) NOT NULL DEFAULT 'read-write',
`phone_activation_code` varchar(16) DEFAULT NULL,
`email_activation_code` varchar(16) DEFAULT NULL,
`email_validated` tinyint(1) NOT NULL DEFAULT '0',
`phone_validated` tinyint(1) NOT NULL DEFAULT '0',
`email_content_opt_out` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`user_sid`),
UNIQUE KEY `user_sid` (`user_sid`),
UNIQUE KEY `phone` (`phone`),
KEY `user_sid_idx` (`user_sid`),
KEY `email_idx` (`email`),
KEY `phone_idx` (`phone`),
KEY `account_sid_idx` (`account_sid`),
KEY `service_provider_sid_idx` (`service_provider_sid`),
KEY `email_activation_code_idx` (`email_activation_code`),
CONSTRAINT `account_sid_idxfk_7` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `service_provider_sid_idxfk_3` FOREIGN KEY (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES ('d9cdf199-78d1-4f92-b717-5f9dbdf56565','Dave Horton','daveh@drachtio.org',NULL,NULL,NULL,NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f',NULL,0,'github','davehorton','read-write',NULL,NULL,1,0,0);
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `voip_carriers`
--
DROP TABLE IF EXISTS `voip_carriers`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `voip_carriers` (
`voip_carrier_sid` char(36) NOT NULL,
`name` varchar(64) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`account_sid` char(36) DEFAULT NULL COMMENT 'if provided, indicates this entity represents a sip trunk that is associated with a specific account',
`application_sid` char(36) DEFAULT NULL COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
`e164_leading_plus` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
`requires_register` tinyint(1) NOT NULL DEFAULT '0',
`register_username` varchar(64) DEFAULT NULL,
`register_sip_realm` varchar(64) DEFAULT NULL,
`register_password` varchar(64) DEFAULT NULL,
`tech_prefix` varchar(16) DEFAULT NULL COMMENT 'tech prefix to prepend to outbound calls to this carrier',
PRIMARY KEY (`voip_carrier_sid`),
UNIQUE KEY `voip_carrier_sid` (`voip_carrier_sid`),
KEY `voip_carrier_sid_idx` (`voip_carrier_sid`),
KEY `account_sid_idx` (`account_sid`),
KEY `application_sid_idxfk_2` (`application_sid`),
CONSTRAINT `account_sid_idxfk_8` FOREIGN KEY (`account_sid`) REFERENCES `accounts` (`account_sid`),
CONSTRAINT `application_sid_idxfk_2` FOREIGN KEY (`application_sid`) REFERENCES `applications` (`application_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='A Carrier or customer PBX that can send or receive calls';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `voip_carriers`
--
LOCK TABLES `voip_carriers` WRITE;
/*!40000 ALTER TABLE `voip_carriers` DISABLE KEYS */;
INSERT INTO `voip_carriers` VALUES ('5145b436-2f38-4029-8d4c-fd8c67831c7a','my test carrier',NULL,NULL,NULL,0,0,NULL,NULL,NULL,NULL),('df0aefbf-ca7b-4d48-9fbf-3c66fef72060','my test carrier',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f',NULL,0,0,NULL,NULL,NULL,NULL);
/*!40000 ALTER TABLE `voip_carriers` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `webhooks`
--
DROP TABLE IF EXISTS `webhooks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `webhooks` (
`webhook_sid` char(36) NOT NULL,
`url` varchar(1024) NOT NULL,
`method` enum('GET','POST') NOT NULL DEFAULT 'POST',
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`webhook_sid`),
UNIQUE KEY `webhook_sid` (`webhook_sid`),
KEY `webhook_sid_idx` (`webhook_sid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='An HTTP callback';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `webhooks`
--
LOCK TABLES `webhooks` WRITE;
/*!40000 ALTER TABLE `webhooks` DISABLE KEYS */;
INSERT INTO `webhooks` VALUES ('6ac36aeb-6bd0-428a-80a1-aed95640a296','https://flows.jambonz.us/callStatus','POST',NULL,NULL),('d9c205c6-a129-443e-a9c0-d1bb437d4bb7','https://flows.jambonz.us/testCall','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('293904c1-351b-4bca-8d58-1a29b853c7db','http://127.0.0.1:3100/callStatus','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('c71e79db-24f2-4866-a3ee-febb0f97b341','http://127.0.0.1:3100/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL);
INSERT INTO `webhooks` VALUES ('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL);
/*!40000 ALTER TABLE `webhooks` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2021-04-03 11:50:25

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@'%';

View File

@@ -2,20 +2,46 @@
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS account_subscriptions;
DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS predefined_carriers;
DROP TABLE IF EXISTS account_offers;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS smpp_gateways;
DROP TABLE IF EXISTS phone_numbers;
DROP TABLE IF EXISTS sip_gateways;
@@ -30,6 +56,41 @@ DROP TABLE IF EXISTS service_providers;
DROP TABLE IF EXISTS webhooks;
CREATE TABLE account_static_ips
(
account_static_ip_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
public_ipv4 VARCHAR(16) NOT NULL UNIQUE ,
private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
PRIMARY KEY (account_static_ip_sid)
);
CREATE TABLE account_subscriptions
(
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
pending BOOLEAN NOT NULL DEFAULT false,
effective_start_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
effective_end_date DATETIME,
change_reason VARCHAR(255),
stripe_subscription_id VARCHAR(56),
stripe_payment_method_id VARCHAR(56),
stripe_statement_descriptor VARCHAR(255),
last4 VARCHAR(512),
exp_month INTEGER,
exp_year INTEGER,
card_type VARCHAR(16),
pending_reason VARBINARY(52),
PRIMARY KEY (account_subscription_sid)
);
CREATE TABLE beta_invite_codes
(
invite_code CHAR(6) NOT NULL UNIQUE ,
in_use BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (invite_code)
);
CREATE TABLE call_routes
(
call_route_sid CHAR(36) NOT NULL UNIQUE ,
@@ -38,7 +99,16 @@ 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';
) COMMENT='a regex-based pattern match for call routing';
CREATE TABLE dns_records
(
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
record_type VARCHAR(6) NOT NULL,
record_id INTEGER NOT NULL,
PRIMARY KEY (dns_record_sid)
);
CREATE TABLE lcr_routes
(
@@ -49,15 +119,81 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table';
CREATE TABLE predefined_carriers
(
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
requires_static_ip BOOLEAN NOT NULL DEFAULT false,
e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
requires_register BOOLEAN NOT NULL DEFAULT false,
register_username VARCHAR(64),
register_sip_realm VARCHAR(64),
register_password VARCHAR(64),
tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier',
inbound_auth_username VARCHAR(64),
inbound_auth_password VARCHAR(64),
diversion VARCHAR(32),
PRIMARY KEY (predefined_carrier_sid)
);
CREATE TABLE predefined_sip_gateways
(
predefined_sip_gateway_sid CHAR(36) NOT NULL UNIQUE ,
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',
netmask INTEGER NOT NULL DEFAULT 32,
predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_sip_gateway_sid)
);
CREATE TABLE products
(
product_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(32) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
PRIMARY KEY (product_sid)
);
CREATE TABLE account_products
(
account_product_sid CHAR(36) NOT NULL UNIQUE ,
account_subscription_sid CHAR(36) NOT NULL,
product_sid CHAR(36) NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_product_sid)
);
CREATE TABLE account_offers
(
account_offer_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
product_sid CHAR(36) NOT NULL,
stripe_product_id VARCHAR(56) NOT NULL,
PRIMARY KEY (account_offer_sid)
);
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,
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';
) COMMENT='An authorization token that is used to access the REST api';
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 ms_teams_tenants
(
@@ -69,60 +205,121 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant';
CREATE TABLE sbc_addresses
CREATE TABLE signup_history
(
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (email)
);
CREATE TABLE smpp_addresses
(
smpp_address_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(255) NOT NULL,
port INTEGER NOT NULL DEFAULT 5060,
use_tls BOOLEAN NOT NULL DEFAULT 0,
is_primary BOOLEAN NOT NULL DEFAULT 1,
service_provider_sid CHAR(36),
PRIMARY KEY (sbc_address_sid)
PRIMARY KEY (smpp_address_sid)
);
CREATE TABLE speech_credentials
(
speech_credential_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36),
account_sid CHAR(36),
vendor VARCHAR(32) NOT NULL,
credential VARCHAR(8192) NOT NULL,
use_for_tts BOOLEAN DEFAULT true,
use_for_stt BOOLEAN DEFAULT true,
last_used DATETIME,
last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (speech_credential_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,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
pending_email VARCHAR(255),
phone VARCHAR(20) UNIQUE ,
hashed_password VARCHAR(1024),
account_sid CHAR(36),
service_provider_sid CHAR(36),
force_change BOOLEAN NOT NULL DEFAULT FALSE,
provider VARCHAR(255) NOT NULL,
provider_userid VARCHAR(255),
scope VARCHAR(16) NOT NULL DEFAULT 'read-write',
phone_activation_code VARCHAR(16),
email_activation_code VARCHAR(16),
email_validated BOOLEAN NOT NULL DEFAULT false,
phone_validated BOOLEAN NOT NULL DEFAULT false,
email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (user_sid)
);
CREATE TABLE voip_carriers
(
voip_carrier_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
description VARCHAR(255),
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a sip trunk 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',
e164_leading_plus BOOLEAN NOT NULL DEFAULT false,
e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers',
requires_register BOOLEAN NOT NULL DEFAULT false,
register_username VARCHAR(64),
register_sip_realm VARCHAR(64),
register_password VARCHAR(64),
tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier',
inbound_auth_username VARCHAR(64),
inbound_auth_password VARCHAR(64),
diversion VARCHAR(32),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
smpp_system_id VARCHAR(255),
smpp_password VARCHAR(64),
smpp_enquire_link_interval INTEGER DEFAULT 0,
smpp_inbound_system_id VARCHAR(255),
smpp_inbound_password VARCHAR(64),
PRIMARY KEY (voip_carrier_sid)
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls';
) COMMENT='A Carrier or customer PBX that can send or receive calls';
CREATE TABLE smpp_gateways
(
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(128) NOT NULL,
port INTEGER NOT NULL DEFAULT 2775,
netmask INTEGER NOT NULL DEFAULT 32,
is_primary BOOLEAN NOT NULL DEFAULT 1,
inbound BOOLEAN NOT NULL DEFAULT 0 COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL DEFAULT 1 COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
use_tls BOOLEAN DEFAULT 0,
voip_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (smpp_gateway_sid)
);
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(32) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36) NOT NULL,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
service_provider_sid CHAR(36) COMMENT 'if not null, this number is a test number for the associated service provider',
PRIMARY KEY (phone_number_sid)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
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)
) COMMENT='An HTTP callback';
) COMMENT='A phone number that has been assigned to an account';
CREATE TABLE sip_gateways
(
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.',
netmask INTEGER NOT NULL DEFAULT 32,
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',
@@ -141,20 +338,33 @@ priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempt
PRIMARY KEY (lcr_carrier_set_entry_sid)
) COMMENT='An entry in the LCR routing list';
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)
) COMMENT='An HTTP callback';
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 to phone numbers owned by this account',
service_provider_sid CHAR(36) COMMENT 'if non-null, this application is a test application that can be used by any account under the associated service provider',
account_sid CHAR(36) COMMENT 'account that this application belongs to (if null, this is a service provider test application)',
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',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (application_sid)
) ENGINE=InnoDB COMMENT='A defined set of behaviors to be applied to phone calls ';
) COMMENT='A defined set of behaviors to be applied to phone calls ';
CREATE TABLE service_providers
(
@@ -165,7 +375,7 @@ 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';
) COMMENT='A partition of the platform used by one service provider';
CREATE TABLE accounts
(
@@ -174,72 +384,151 @@ 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',
queue_event_hook_sid CHAR(36),
device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
plan_type ENUM('trial','free','paid') NOT NULL DEFAULT 'trial',
stripe_customer_id VARCHAR(56),
webhook_secret VARCHAR(36) NOT NULL,
disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
trial_end_date DATETIME,
deactivated_reason VARCHAR(255),
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
PRIMARY KEY (account_sid)
) ENGINE=InnoDB COMMENT='An enterprise that uses the platform for comm services';
) COMMENT='An enterprise that uses the platform for comm services';
CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_sid);
CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
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_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX product_sid_idx ON products (product_sid);
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
ALTER TABLE account_products ADD FOREIGN KEY account_subscription_sid_idxfk (account_subscription_sid) REFERENCES account_subscriptions (account_subscription_sid);
ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REFERENCES products (product_sid);
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
CREATE INDEX account_sid_idx ON account_offers (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX product_sid_idx ON account_offers (product_sid);
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_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);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_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 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 ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants 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 sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port);
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);
ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_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_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants 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 email_idx ON signup_history (email);
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX name_idx ON users (name);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
CREATE INDEX account_sid_idx ON users (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX email_activation_code_idx ON users (email_activation_code);
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);
CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
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);
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways 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_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (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 lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid);
CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_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 applications ADD FOREIGN KEY account_sid_idxfk_11 (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 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);
@@ -248,10 +537,12 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
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 service_provider_sid_idxfk_9 (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 queue_event_hook_sid_idxfk (queue_event_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;

View File

@@ -1,3 +1,3 @@
DROP DATABASE jambones_test;
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'jambones_test'@'localhost';
DROP USER 'jambones_test'@'localhost';
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'jambones_test'@'%';
DROP USER 'jambones_test'@'%';

View File

@@ -1,55 +1,131 @@
version: '3'
version: '3.9'
networks:
sbc-inbound:
fs:
driver: bridge
ipam:
config:
- subnet: 172.38.0.0/16
services:
sbc:
image: drachtio/drachtio-server:latest
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
mysql:
image: mysql:5.7
ports:
- "9060:9022/tcp"
- "3360:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
timeout: 5s
retries: 10
networks:
sbc-inbound:
ipv4_address: 172.38.0.10
appserver:
image: drachtio/sipp:latest
command: sipp -sf /tmp/uas.xml
volumes:
- ./scenarios:/tmp
tty: true
networks:
sbc-inbound:
ipv4_address: 172.38.0.11
auth-server:
image: jambonz/customer-auth-server:latest
command: npm start
ports:
- "4000:4000/tcp"
env_file: docker.env
networks:
sbc-inbound:
ipv4_address: 172.38.0.12
fs:
ipv4_address: 172.38.0.5
redis:
image: redis:5-alpine
ports:
- "16379:6379/tcp"
depends_on:
- mysql
networks:
sbc-inbound:
ipv4_address: 172.38.0.13
fs:
ipv4_address: 172.38.0.6
rtpengine:
image: drachtio/rtpengine:latest
docker-host:
image: qoomon/docker-host
cap_add: [ 'NET_ADMIN', 'NET_RAW' ]
mem_limit: 8M
restart: on-failure
networks:
fs:
ipv4_address: 172.38.0.7
drachtio:
image: drachtio/drachtio-server:latest
restart: always
command: drachtio --contact "sip:*;transport=udp,tcp" --address 0.0.0.0 --port 9022
ports:
- "12222:22222/udp"
- "9060:9022/tcp"
networks:
sbc-inbound:
ipv4_address: 172.38.0.14
fs:
ipv4_address: 172.38.0.50
depends_on:
mysql:
condition: service_healthy
freeswitch:
condition: service_healthy
freeswitch:
image: drachtio/drachtio-freeswitch-mrf:v1.10.1-full
restart: always
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
environment:
GOOGLE_APPLICATION_CREDENTIALS: /opt/credentials/gcp.json
ports:
- "8022:8021/tcp"
volumes:
- /tmp:/tmp
- ./credentials:/opt/credentials
healthcheck:
test: ['CMD', 'fs_cli' ,'-x', '"sofia status"']
timeout: 5s
retries: 15
networks:
fs:
ipv4_address: 172.38.0.51
webhook-decline:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/decline.json
ports:
- "3100:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.60
webhook-say:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/say.json
ports:
- "3101:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.61
webhook-gather:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/gather.json
ports:
- "3102:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.62
webhook-transcribe:
image: jambonz/webhook-test-scaffold:latest
environment:
APP_PATH: /tmp/transcribe.json
ports:
- "3103:3000/tcp"
volumes:
- ./test-apps:/tmp
networks:
fs:
ipv4_address: 172.38.0.63
influxdb:
image: influxdb:1.8-alpine
ports:
- "8086:8086"
networks:
fs:
ipv4_address: 172.38.0.90

View File

@@ -1,9 +1,10 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
const async = require('async');
test('starting docker network..', (t) => {
test('starting docker network..takes a bit for mysql and freeswitch to come up..patience..', (t) => {
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
t.pass('docker network is up');
t.end(err);
});

View File

@@ -1,4 +1,4 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
test('stopping docker network..', (t) => {

36
test/gather-tests.js Normal file
View File

@@ -0,0 +1,36 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const bent = require('bent');
const getJSON = bent('json')
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'gather\' and \'transcribe\' tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10');
let obj = await getJSON('http://127.0.0.1:3102/actionHook');
t.ok(obj.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
'gather: succeeds when using account credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,8 +1,9 @@
require('./unit-tests');
/*
require('./docker_start');
require('./create-test-db');
require('./sip-tests');
require('./account-validation-tests');
require('./webhooks-tests');
require('./say-tests');
require('./gather-tests');
require('./remove-test-db');
require('./docker_stop');
*/

View File

@@ -1,11 +1,11 @@
const test = require('tape').test ;
const test = require('tape') ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
const fs = require('fs');
test('dropping jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('database successfully dropped');
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
t.end();
});
});

32
test/say-tests.js Normal file
View File

@@ -0,0 +1,32 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'say\' tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-say-account-creds-success.xml', '172.38.0.10');
t.pass('say: succeeds when using using account credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-expect-500
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="500">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-expect-500
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-expect-603
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="603">
</recv>
<send>
<![CDATA[
ACK sip:16174000000@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000000@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-expect-603
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000003@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-gather-account-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000003@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000003 <sip:16174000003@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-gather-account-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000001@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-inactive-account-expect-503
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="503">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000001@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-inactive-account-expect-503
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<send retrans="500">
<![CDATA[
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: deadbeef
Subject: uac-invalid-account-expect-503
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="503">
</recv>
<send>
<![CDATA[
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-invalid-account-expect-503
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000001@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000001@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000001 <sip:16174000001@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000002@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000002 <sip:16174000002@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000002@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000002@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000002 <sip:16174000002@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000004@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000004@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-transcribe-account-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000004@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000004 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-transcribe-account-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<pause milliseconds="10000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:16174000004@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000004 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Subject: uac-transcribe-account-creds-success
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000005@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-transcribe-our-creds-fail-low-balance
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000005 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-transcribe-our-creds-fail-low-balance
Content-Length: 0
]]>
</send>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</scenario>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:16174000005@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: 2a14b9ef-2d01-4b50-8123-fa79f56ab684
Subject: uac-transcribe-our-creds-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000005 <sip:16174000004@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-transcribe-our-creds-success
Content-Length: 0
]]>
</send>
<nop>
<action>
<exec rtp_stream="/tmp/scenarios/wav/speak-to-customer-support.wav,1,0"/>
</action>
</nop>
<pause milliseconds="10000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:16174000005@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: 16174000005 <sip:16174000005@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Subject: uac-transcribe-our-creds-success
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>

Binary file not shown.

View File

@@ -27,19 +27,19 @@ obj.output = () => {
obj.sippUac = (file, bindAddress) => {
const cmd = 'docker';
const args = [
'run', '-ti', '--rm', '--net', `${network}`,
'run', '-t', '--rm', '--net', `${network}`,
'-v', `${__dirname}/scenarios:/tmp/scenarios`,
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
'-m', '1',
'-sleep', '250ms',
'-nostdin',
'-cid_str', `%u-%p@%s-${idx++}`,
'sbc'
'172.38.0.50'
];
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
//console.log(args.join(' '));
console.log(args.join(' '));
clearOutput();
return new Promise((resolve, reject) => {
@@ -57,11 +57,11 @@ obj.sippUac = (file, bindAddress) => {
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
//console.log(`stdout: ${data}`);
addOutput(data.toString());
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
//console.log(`stdout: ${data}`);
addOutput(data.toString());
});
});

View File

@@ -0,0 +1,6 @@
[
{
"verb": "sip:decline",
"status": 603
}
]

View File

@@ -0,0 +1,12 @@
[
{
"verb": "gather",
"input": ["speech"],
"recognizer": {
"vendor": "google",
"hints": ["customer support", "sales", "human resources", "HR"]
},
"timeout": 10,
"actionHook": "/actionHook"
}
]

6
test/test-apps/say.json Normal file
View File

@@ -0,0 +1,6 @@
[
{
"verb": "say",
"text": "hello"
}
]

View File

@@ -0,0 +1,9 @@
[
{
"verb": "transcribe",
"recognizer": {
"vendor": "google"
},
"transcriptionHook": "/actionHook"
}
]

View File

@@ -9,7 +9,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('app payload parsing tests', (t) => {
test('unit tests', (t) => {
let task = makeTask(logger, require('./data/good/sip-decline'));
t.ok(task.name === 'sip:decline', 'parsed sip:decline');
@@ -53,7 +53,6 @@ test('app payload parsing tests', (t) => {
t.pass('alternate syntax works');
t.end();
process.exit(0);
});

15
test/webhook/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
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
FROM node:alpine as webapp
RUN apk add curl
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

49
test/webhook/app.js Normal file
View File

@@ -0,0 +1,49 @@
const assert = require('assert');
const fs = require('fs');
const express = require('express');
const app = express();
const listenPort = process.env.HTTP_PORT || 3000;
let lastAction, lastEvent;
assert.ok(process.env.APP_PATH, 'env var APP_PATH is required');
app.listen(listenPort, () => {
console.log(`sample jambones app server listening on ${listenPort}`);
});
const applicationData = JSON.parse(fs.readFileSync(process.env.APP_PATH));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.all('/', (req, res) => {
console.log(applicationData, `${req.method} /`);
return res.json(applicationData);
});
app.post('/callStatus', (req, res) => {
console.log({payload: req.body}, 'POST /callStatus');
return res.sendStatus(200);
});
app.post('/actionHook', (req, res) => {
console.log({payload: req.body}, 'POST /actionHook');
lastAction = req.body;
return res.sendStatus(200);
});
app.get('/actionHook', (req, res) => {
console.log({payload: lastAction}, 'GET /actionHook');
return res.json(lastAction);
});
app.post('/eventHook', (req, res) => {
console.log({payload: req.body}, 'POST /eventHook');
lastEvent = req.body;
return res.sendStatus(200);
});
app.get('/eventHook', (req, res) => {
console.log({payload: lastEvent}, 'GET /eventHook');
return res.json(lastEvent);
});

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cd /opt/app/
npm start

374
test/webhook/package-lock.json generated Normal file
View File

@@ -0,0 +1,374 @@
{
"name": "webhook",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"requires": {
"mime-db": "1.45.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

14
test/webhook/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "webhook",
"version": "1.0.0",
"description": "simple webhook app for test purposes",
"main": "app.js",
"scripts": {
"start": "node app"
},
"author": "Dave Horton",
"license": "MIT",
"dependencies": {
"express": "^4.17.1"
}
}

32
test/webhooks-tests.js Normal file
View File

@@ -0,0 +1,32 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('basic webhook tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
await sippUac('uac-expect-603.xml', '172.38.0.10');
t.pass('webhook successfully declines call');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});