From 2525b8c70a7d2c0067c4a43270d3c813b4743498 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Sat, 1 Feb 2020 16:16:00 -0500 Subject: [PATCH] added initial support for REST-initiated outdials --- app.js | 20 ++ config/default.json.example | 12 +- lib/http-routes/api/create-call.js | 155 +++++++++++ lib/http-routes/api/error.js | 20 ++ lib/http-routes/api/index.js | 5 + lib/http-routes/index.js | 16 ++ lib/http-routes/utils/errors.js | 23 ++ lib/middleware.js | 11 +- lib/session/call-info.js | 31 ++- lib/session/call-session.js | 29 +- lib/session/confirm-call-session.js | 7 +- lib/session/inbound-call-session.js | 14 - lib/session/rest-call-session.js | 34 +++ lib/tasks/dial.js | 62 +++-- lib/tasks/listen.js | 37 ++- lib/tasks/make_task.js | 7 +- lib/tasks/redirect.js | 2 +- lib/tasks/rest_dial.js | 82 ++++++ lib/tasks/specs.json | 23 +- lib/tasks/tag.js | 2 +- lib/tasks/task.js | 28 +- lib/utils/constants.json | 1 + lib/utils/normalize-jamones.js | 8 +- lib/utils/notifiers.js | 23 +- lib/utils/place-outdial.js | 53 ++-- lib/utils/retrieve-app.js | 4 +- package-lock.json | 402 +++++++++++++++++++++++++++- package.json | 1 + 28 files changed, 985 insertions(+), 127 deletions(-) create mode 100644 lib/http-routes/api/create-call.js create mode 100644 lib/http-routes/api/error.js create mode 100644 lib/http-routes/api/index.js create mode 100644 lib/http-routes/index.js create mode 100644 lib/http-routes/utils/errors.js create mode 100644 lib/session/rest-call-session.js create mode 100644 lib/tasks/rest_dial.js diff --git a/app.js b/app.js index 3c8a3fcf..b6fc9999 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,13 @@ const { invokeWebCallback } = require('./lib/middleware')(srf, logger); +// HTTP +const PORT = process.env.HTTP_PORT || 3000; +const express = require('express'); +const app = express(); +app.locals.logger = logger; +const httpRoutes = require('./lib/http-routes'); + const InboundCallSession = require('./lib/session/inbound-call-session'); // disable logging in test mode @@ -46,4 +53,17 @@ srf.invite((req, res) => { session.exec(); }); + +// HTTP +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); +app.use('/', httpRoutes); +app.use((err, req, res, next) => { + logger.error(err, 'burped error'); + res.status(err.status || 500).json({msg: err.message}); +}); +app.listen(PORT); + +logger.info(`listening for HTTP requests on port ${PORT}`); + module.exports = {srf}; diff --git a/config/default.json.example b/config/default.json.example index f6a3f6d0..10b2d3e5 100644 --- a/config/default.json.example +++ b/config/default.json.example @@ -3,7 +3,7 @@ "port": 3010, "secret": "cymru" }, - "freeswitch: { + "freeswitch": { "address": "127.0.0.1", "port": 8021, "secret": "ClueCon" @@ -16,5 +16,15 @@ "user": "jambones", "password": "jambones", "database": "jambones" + }, + "outdials": { + "drachtio": [ + { + "host": "127.0.0.1", + "port": 9022, + "secret": "cymru" + } + ], + "sbc": ["127.0.0.1:5060"] } } \ No newline at end of file diff --git a/lib/http-routes/api/create-call.js b/lib/http-routes/api/create-call.js new file mode 100644 index 00000000..68454ece --- /dev/null +++ b/lib/http-routes/api/create-call.js @@ -0,0 +1,155 @@ +const config = require('config'); +const router = require('express').Router(); +const sysError = require('./error'); +const makeTask = require('../../tasks/make_task'); +const RestCallSession = require('../../session/rest-call-session'); +const CallInfo = require('../../session/call-info'); +const {CallDirection} = require('../../utils/constants'); +const parseUrl = require('parse-url'); +const SipError = require('drachtio-srf').SipError; +const Srf = require('drachtio-srf'); +const drachtio = config.get('outdials.drachtio'); +const sbcs = config.get('outdials.sbc'); +const Mrf = require('drachtio-fsmrf'); +let idxDrachtio = 0; +let idxSbc = 0; + +const srfs = drachtio.map((d) => { + const srf = new Srf(); + srf.connect(d); + srf + .on('connect', (err, hp) => { + console.log(err, `Connected to drachtio at ${hp}`); + srf.locals.mrf = new Mrf(srf); + }) + .on('error', (err) => console.log(err)); + return srf; +}); + +async function validate(logger, payload) { + const data = Object.assign({}, { + from: payload.from, + to: payload.to, + call_hook: payload.call_hook + }); + + const u = parseUrl(payload.call_hook.url); + const myPort = u.port ? `:${u.port}` : ''; + payload.originalRequest = { + baseUrl: `${u.protocol}://${u.resource}${myPort}`, + method: payload.call_hook.method + }; + if (payload.call_hook.username && payload.call_hook.password) { + payload.originalRequest.auth = { + username: payload.call_hook.username, + password: payload.call_hook.password + }; + } + + return makeTask(logger, {'rest:dial': data}); +} + +router.post('/', async(req, res) => { + const logger = req.app.locals.logger; + logger.debug({body: req.body}, 'got createCall request'); + try { + let uri; + const restDial = await validate(logger, req.body); + const sbcAddress = sbcs[idxSbc++ % sbcs.length]; + const srf = srfs[idxDrachtio++ % srfs.length]; + const target = restDial.to; + const opts = { + 'callingNumber': restDial.from + }; + + switch (target.type) { + case 'phone': + uri = `sip:${target.number}@${sbcAddress}`; + break; + case 'user': + uri = `sip:${target.name}`; + break; + case 'sip': + uri = target.sipUri; + break; + } + + /* create endpoint for outdial */ + const mrf = srf.locals.mrf; + const ms = await mrf.connect(config.get('freeswitch')); + logger.debug('createCall: successfully connected to media server'); + const ep = await ms.createEndpoint(); + logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`); + ms.destroy(); + + /* launch outdial */ + let sdp, sipLogger; + const connectStream = async(remoteSdp) => { + if (remoteSdp !== sdp) { + ep.modify(sdp = remoteSdp); + return true; + } + return false; + }; + Object.assign(opts, { + proxy: `sip:${sbcAddress}`, + localSdp: ep.local.sdp + }); + if (target.auth) opts.auth = this.target.auth; + const application = req.body; + + try { + const dlg = await srf.createUAC(uri, opts, { + cbRequest: (err, inviteReq) => { + if (err) { + this.logger.error(err, 'createCall Error creating call'); + res.status(500).send('Call Failure'); + ep.destroy(); + } + + /* call is in flight */ + const tasks = [restDial]; + const callInfo = new CallInfo({ + direction: CallDirection.Outbound, + req: inviteReq, + to: req.body.to, + tag: req.body.tag, + accountSid: req.body.account_sid, + applicationSid: req.body.application_sid + }); + const cs = new RestCallSession({logger, application, srf, req: inviteReq, ep, tasks, callInfo}); + cs.exec(req); + + res.status(201).json({sid: cs.callSid}); + + sipLogger = logger.child({ + callSid: cs.callSid, + callId: callInfo.callId + }); + sipLogger.info(`outbound REST call attempt to ${JSON.stringify(target)} has been sent`); + }, + cbProvisional: (prov) => { + if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body); + restDial.emit('callStatus', prov.status, !!prov.body); + } + }); + connectStream(dlg.remote.sdp); + restDial.emit('callStatus', 200); + restDial.emit('connect', dlg); + } + catch (err) { + if (err instanceof SipError) { + sipLogger.info(`REST outdial failed with ${err.status}`); + } + else { + sipLogger.error({err}, 'REST outdial failed'); + } + ep.destroy(); + } + } catch (err) { + sysError(logger, res, err); + } + +}); + +module.exports = router; diff --git a/lib/http-routes/api/error.js b/lib/http-routes/api/error.js new file mode 100644 index 00000000..52661e74 --- /dev/null +++ b/lib/http-routes/api/error.js @@ -0,0 +1,20 @@ +const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../utils/errors'); + +function sysError(logger, res, err) { + if (err instanceof DbErrorBadRequest) { + logger.info(err, 'invalid client request'); + return res.status(400).json({msg: err.message}); + } + if (err instanceof DbErrorUnprocessableRequest) { + logger.info(err, 'unprocessable request'); + return res.status(422).json({msg: err.message}); + } + if (err.code === 'ER_DUP_ENTRY') { + logger.info(err, 'duplicate entry on insert'); + return res.status(422).json({msg: err.message}); + } + logger.error(err, 'Database error'); + res.status(500).json({msg: err.message}); +} + +module.exports = sysError; diff --git a/lib/http-routes/api/index.js b/lib/http-routes/api/index.js new file mode 100644 index 00000000..cbc448b6 --- /dev/null +++ b/lib/http-routes/api/index.js @@ -0,0 +1,5 @@ +const api = require('express').Router(); + +api.use('/createCall', require('./create-call')); + +module.exports = api; diff --git a/lib/http-routes/index.js b/lib/http-routes/index.js new file mode 100644 index 00000000..846a4dbd --- /dev/null +++ b/lib/http-routes/index.js @@ -0,0 +1,16 @@ +const express = require('express'); +const api = require('./api'); +const routes = express.Router(); + +routes.use('/v1', api); + +// health checks +routes.get('/', (req, res) => { + res.sendStatus(200); +}); + +routes.get('/health', (req, res) => { + res.sendStatus(200); +}); + +module.exports = routes; diff --git a/lib/http-routes/utils/errors.js b/lib/http-routes/utils/errors.js new file mode 100644 index 00000000..49f35324 --- /dev/null +++ b/lib/http-routes/utils/errors.js @@ -0,0 +1,23 @@ +class DbError extends Error { + constructor(msg) { + super(msg); + } +} + +class DbErrorBadRequest extends DbError { + constructor(msg) { + super(msg); + } +} + +class DbErrorUnprocessableRequest extends DbError { + constructor(msg) { + super(msg); + } +} + +module.exports = { + DbError, + DbErrorBadRequest, + DbErrorUnprocessableRequest +}; diff --git a/lib/middleware.js b/lib/middleware.js index 22795eaa..647efb17 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -1,6 +1,6 @@ //const debug = require('debug')('jambonz:feature-server'); const uuidv4 = require('uuid/v4'); -const {CallStatus, CallDirection} = require('./utils/constants'); +const {CallDirection} = require('./utils/constants'); const CallInfo = require('./session/call-info'); const retrieveApp = require('./utils/retrieve-app'); const parseUrl = require('parse-url'); @@ -71,7 +71,7 @@ module.exports = function(srf, logger) { const logger = req.locals.logger; const app = req.locals.application; const call_hook = app.call_hook; - const method = (call_hook.method || 'POST').toUpperCase(); + const method = call_hook.method.toUpperCase(); let auth; if (call_hook.username && call_hook.password) { auth = {username: call_hook.username, password: call_hook.password}; @@ -81,7 +81,8 @@ module.exports = function(srf, logger) { const myPort = u.port ? `:${u.port}` : ''; app.originalRequest = { baseUrl: `${u.protocol}://${u.resource}${myPort}`, - auth + auth, + method }; logger.debug({url: call_hook.url, method}, 'invokeWebCallback'); const obj = Object.assign({}, req.locals.callInfo); @@ -91,8 +92,8 @@ module.exports = function(srf, logger) { app.tasks = await retrieveApp(logger, call_hook.url, method, auth, obj); next(); } catch (err) { - logger.error(err, 'Error retrieving or parsing application'); - res.send(500); + logger.info(`Error retrieving or parsing application: ${err.message}`); + res.send(480, {headers: {'X-Reason': err.message}}); } } diff --git a/lib/session/call-info.js b/lib/session/call-info.js index 5b1ac3d8..edccfbd0 100644 --- a/lib/session/call-info.js +++ b/lib/session/call-info.js @@ -5,6 +5,7 @@ class CallInfo { constructor(opts) { this.direction = opts.direction; if (this.direction === CallDirection.Inbound) { + // inbound call const {app, req} = opts; this.callSid = req.locals.callSid, this.accountSid = app.account_sid, @@ -19,19 +20,32 @@ class CallInfo { this.originatingSipTrunkName = req.get('X-Originating-Carrier'); } else if (opts.parentCallInfo) { - console.log(`is opts.parentCallInfo a CallInfo ${opts.parentCallInfo instanceof CallInfo}`); - const {req, parentCallInfo} = opts; - this.callSid = uuidv4(); + // outbound call that is a child of an existing call + const {req, parentCallInfo, to, callSid} = opts; + this.callSid = callSid || uuidv4(); this.parentCallSid = parentCallInfo.callSid; this.accountSid = parentCallInfo.accountSid; this.applicationSid = parentCallInfo.applicationSid; this.from = req.callingNumber; - this.to = req.calledNumber; + this.to = to || req.calledNumber; this.callerName = this.from.name || req.callingNumber; this.callId = req.get('Call-ID'); this.callStatus = CallStatus.Trying, this.sipStatus = 100; } + else { + // outbound call triggered by REST + const {req, accountSid, applicationSid, to, tag} = opts; + this.callSid = uuidv4(); + this.accountSid = accountSid; + this.applicationSid = applicationSid; + this.callStatus = CallStatus.Trying, + this.callId = req.get('Call-ID'); + this.sipStatus = 100; + this.from = req.callingNumber; + this.to = to; + if (tag) this._customerData = tag; + } } updateCallStatus(callStatus, sipStatus) { @@ -43,6 +57,10 @@ class CallInfo { this._customerData = obj; } + get customerData() { + return this._customerData; + } + toJSON() { const obj = { callSid: this.callSid, @@ -59,9 +77,10 @@ class CallInfo { ['parentCallSid', 'originatingSipIP', 'originatingSipTrunkName'].forEach((prop) => { if (this[prop]) obj[prop] = this[prop]; }); + if (typeof this.duration === 'number') obj.duration = this.duration; - if (this._customerData && Object.keys(this._customerData).length) { - obj.customerData = this._customerData; + if (this._customerData) { + Object.assign(obj, {customerData: this._customerData}); } return obj; } diff --git a/lib/session/call-session.js b/lib/session/call-session.js index b57cd5b8..f88fccc4 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -39,6 +39,24 @@ class CallSession extends Emitter { return this.callInfo.direction; } + get call_status_hook() { + return this.application.call_status_hook; + } + + get speechSynthesisVendor() { + return this.application.speech_synthesis_vendor; + } + get speechSynthesisVoice() { + return this.application.speech_synthesis_voice; + } + + get speechRecognizerVendor() { + return this.application.speech_recognizer_vendor; + } + get speechRecognizerLanguage() { + return this.application.speech_recognizer_language; + } + async exec() { this.logger.info(`CallSession:exec starting task list with ${this.tasks.length} tasks`); while (this.tasks.length && !this.callGone) { @@ -196,11 +214,16 @@ class CallSession extends Emitter { } return {ms: this.ms, ep: this.ep}; } - _notifyCallStatusChange({callStatus, sipStatus}) { - this.logger.debug(`CallSession:_notifyCallStatusChange: ${callStatus} ${sipStatus}`); + _notifyCallStatusChange({callStatus, sipStatus, duration}) { + assert((typeof duration === 'number' && callStatus === CallStatus.Completed) || + (!duration && callStatus !== CallStatus.Completed), + 'duration MUST be supplied when call completed AND ONLY when call completed'); + + const call_status_hook = this.call_status_hook; this.callInfo.updateCallStatus(callStatus, sipStatus); + if (typeof duration === 'number') this.callInfo.duration = duration; try { - this.notifyHook(this.application.call_status_hook); + this.notifyHook(call_status_hook); } catch (err) { this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`); } diff --git a/lib/session/confirm-call-session.js b/lib/session/confirm-call-session.js index 5c1d0c68..8ab8f871 100644 --- a/lib/session/confirm-call-session.js +++ b/lib/session/confirm-call-session.js @@ -1,18 +1,17 @@ const CallSession = require('./call-session'); -const {CallDirection} = require('../utils/constants'); class ConfirmCallSession extends CallSession { - constructor({logger, application, dlg, ep, tasks}) { + constructor({logger, application, dlg, ep, tasks, callInfo}) { super({ logger, application, srf: dlg.srf, callSid: dlg.callSid, - tasks + tasks, + callInfo }); this.dlg = dlg; this.ep = ep; - this.direction = CallDirection.Outbound; } /** diff --git a/lib/session/inbound-call-session.js b/lib/session/inbound-call-session.js index 3cf0a6de..3db153f8 100644 --- a/lib/session/inbound-call-session.js +++ b/lib/session/inbound-call-session.js @@ -21,20 +21,6 @@ class InboundCallSession extends CallSession { this._notifyCallStatusChange({callStatus: CallStatus.Trying, sipStatus: 100}); } - get speechSynthesisVendor() { - return this.application.speech_synthesis_vendor; - } - get speechSynthesisVoice() { - return this.application.speech_synthesis_voice; - } - - get speechRecognizerVendor() { - return this.application.speech_recognizer_vendor; - } - get speechRecognizerLanguage() { - return this.application.speech_recognizer_language; - } - _onTasksDone() { if (!this.res.finalResponseSent) { this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite'); diff --git a/lib/session/rest-call-session.js b/lib/session/rest-call-session.js new file mode 100644 index 00000000..d2fb8c31 --- /dev/null +++ b/lib/session/rest-call-session.js @@ -0,0 +1,34 @@ +const CallSession = require('./call-session'); +const {CallStatus} = require('../utils/constants'); +const moment = require('moment'); + +class RestCallSession extends CallSession { + constructor({logger, application, srf, req, ep, tasks, callInfo}) { + super({ + logger, + application, + srf, + callSid: callInfo.callSid, + tasks, + callInfo + }); + this.req = req; + this.ep = ep; + } + + setDialog(dlg) { + this.dlg = dlg; + dlg.on('destroy', this._callerHungup.bind(this)); + dlg.connectTime = moment(); + } + + _callerHungup() { + const duration = moment().diff(this.dlg.connectTime, 'seconds'); + this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); + this.logger.debug('InboundCallSession: caller hung up'); + this._callReleased(); + } + +} + +module.exports = RestCallSession; diff --git a/lib/tasks/dial.js b/lib/tasks/dial.js index 4815c339..7ae5fb7d 100644 --- a/lib/tasks/dial.js +++ b/lib/tasks/dial.js @@ -4,7 +4,6 @@ const {CallStatus, CallDirection, TaskName, TaskPreconditions, MAX_SIMRINGS} = r const assert = require('assert'); const placeCall = require('../utils/place-outdial'); const config = require('config'); -const moment = require('moment'); const debug = require('debug')('jambonz:feature-server'); function compareTasks(t1, t2) { @@ -68,6 +67,14 @@ class TaskDial extends Task { this.dials = new Map(); } + get dlg() { + if (this.sd) return this.sd.dlg; + } + + get ep() { + if (this.sd) return this.sd.ep; + } + get name() { return TaskName.Dial; } async exec(cs) { @@ -87,24 +94,25 @@ class TaskDial extends Task { async kill() { super.kill(); - if (this.dlg) { - const duration = moment().diff(this.dlg.connectTime, 'seconds'); - this.results.dialCallDuration = duration; - this.logger.debug(`Dial:kill call ended after ${duration} seconds`); - } - this._killOutdials(); + if (this.sd) { + this.sd.kill(); + this.sd = null; + } if (this.listenTask) await this.listenTask.kill(); if (this.transcribeTask) await this.transcribeTask.kill(); - if (this.dlg) { - assert(this.ep); - if (this.dlg.connected) this.dlg.destroy(); - debug(`Dial:kill deleting endpoint ${this.ep.uuid}`); - this.ep.destroy(); - } + if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration); this.notifyTaskDone(); } + _killOutdials() { + for (const [callSid, sd] of Array.from(this.dials)) { + this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`); + sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`)); + } + this.dials.clear(); + } + async _initializeInbound(cs) { const {ep} = await cs.connectInboundCallToIvr(this.earlyMedia); this.epOther = ep; @@ -153,6 +161,13 @@ class TaskDial extends Task { this.dials.set(sd.callSid, sd); sd + .on('callCreateFail', () => { + this.dials.delete(sd.callSid); + if (this.dials.size === 0 && !this.sd) { + this.logger.debug('Dial:_attemptCalls - all calls failed after call create err, ending task'); + this.kill(); + } + }) .on('callStatusChange', (obj) => { switch (obj.callStatus) { case CallStatus.Trying: @@ -170,7 +185,7 @@ class TaskDial extends Task { case CallStatus.Busy: case CallStatus.NoAnswer: this.dials.delete(sd.callSid); - if (this.dials.size === 0 && !this.dlg) { + if (this.dials.size === 0 && !this.sd) { this.logger.debug('Dial:_attemptCalls - all calls failed after call failure, ending task'); clearTimeout(timerRing); this.kill(); @@ -191,7 +206,7 @@ class TaskDial extends Task { .on('decline', () => { this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`); this.dials.delete(sd.callSid); - if (this.dials.size === 0 && !this.dlg) { + if (this.dials.size === 0 && !this.sd) { this.logger.debug('Dial:_attemptCalls - all calls failed after decline, ending task'); this.kill(); } @@ -228,17 +243,14 @@ class TaskDial extends Task { debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`); this.dials.delete(sd.callSid); - this.ep = sd.ep; - this.dlg = sd.dlg; - this.dlg.connectTime = moment(); + this.sd = sd; this.callSid = sd.callSid; if (this.earlyMedia) { debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected'); cs.propagateAnswer(); } - let timerMaxCallDuration; if (this.timeLimit) { - timerMaxCallDuration = setTimeout(() => { + this.timerMaxCallDuration = setTimeout(() => { this.logger.info(`Dial:_selectSingleDial tearing down call as it has reached ${this.timeLimit}s`); this.ep.unbridge(); this.kill(); @@ -246,7 +258,7 @@ class TaskDial extends Task { } this.dlg.on('destroy', () => { this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation'); - if (timerMaxCallDuration) clearTimeout(timerMaxCallDuration); + if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration); this.ep.unbridge(); this.kill(); }); @@ -260,14 +272,6 @@ class TaskDial extends Task { if (this.listenTask) this.listenTask.exec(cs, this.ep, this); } - _killOutdials() { - for (const [callSid, sd] of Array.from(this.dials)) { - this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`); - sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`)); - } - this.dials.clear(); - } - _bridgeEarlyMedia(sd) { if (this.epOther && !this.bridged) { this.epOther.api('uuid_break', this.epOther.uuid); diff --git a/lib/tasks/listen.js b/lib/tasks/listen.js index 00e6f74a..ac869a04 100644 --- a/lib/tasks/listen.js +++ b/lib/tasks/listen.js @@ -4,19 +4,21 @@ const makeTask = require('./make_task'); const moment = require('moment'); class TaskListen extends Task { - constructor(logger, opts) { + constructor(logger, opts, parentTask) { super(logger, opts); this.preconditions = TaskPreconditions.Endpoint; [ - 'action', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep', - 'sampleRate', 'timeout', 'transcribe' + 'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep', + 'sampleRate', 'timeout', 'transcribe', 'wsAuth' ].forEach((k) => this[k] = this.data[k]); this.mixType = this.mixType || 'mono'; this.sampleRate = this.sampleRate || 8000; - this.method = this.method || 'POST'; this.earlyMedia = this.data.earlyMedia === true; + this.hook = this.normalizeUrl(this.url, 'GET', this.wsAuth); + this.nested = typeof parentTask !== 'undefined'; + this.results = {}; if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this); @@ -35,9 +37,9 @@ class TaskListen extends Task { this.logger.debug('TaskListen:exec - starting nested transcribe task'); this.transcribeTask.exec(cs, ep, this); } - await this._startListening(ep); + await this._startListening(cs, ep); await this.awaitTaskDone(); - if (this.action) await this.performAction(this.method, null, this.results); + if (this.action) await this.performAction(this.method, this.auth, this.results, !this.nested); } catch (err) { this.logger.info(err, `TaskListen:exec - error ${this.url}`); } @@ -47,8 +49,10 @@ class TaskListen extends Task { async kill() { super.kill(); + this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`); this._clearTimer(); - if (this.ep.connected) { + if (this.ep && this.ep.connected) { + this.logger.debug('TaskListen:kill closing websocket'); await this.ep.forkAudioStop() .catch((err) => this.logger.info(err, 'TaskListen:kill')); } @@ -65,13 +69,26 @@ class TaskListen extends Task { .catch((err) => this.logger.info(err, 'TaskListen:_playBeep Error playing beep')); } - async _startListening(ep) { + async _startListening(cs, ep) { this._initListeners(ep); + const metadata = Object.assign( + {sampleRate: this.sampleRate, mixType: this.mixType}, + cs.callInfo.toJSON(), + this.metadata); + this.logger.debug({metadata, hook: this.hook}, 'TaskListen:_startListening'); + if (this.hook.username && this.hook.password) { + this.logger.debug({username: this.hook.username, password: this.hook.password}, + 'TaskListen:_startListening basic auth'); + await this.ep.set({ + 'MOD_AUDIO_BASIC_AUTH_USERNAME': this.hook.username, + 'MOD_AUDIO_BASIC_AUTH_PASSWORD': this.hook.password + }); + } await ep.forkAudioStart({ - wsUrl: this.url, + wsUrl: this.hook.url, mixType: this.mixType, sampling: this.sampleRate, - metadata: this.metadata + metadata }); this.recordStartTime = moment(); if (this.maxLength) { diff --git a/lib/tasks/make_task.js b/lib/tasks/make_task.js index 0c46400c..9440a729 100644 --- a/lib/tasks/make_task.js +++ b/lib/tasks/make_task.js @@ -1,6 +1,6 @@ const Task = require('./task'); const {TaskName} = require('../utils/constants'); -const errBadInstruction = new Error('invalid instruction payload'); +const errBadInstruction = new Error('malformed jambonz application payload'); function makeTask(logger, obj) { const keys = Object.keys(obj); @@ -42,13 +42,16 @@ function makeTask(logger, obj) { case TaskName.Redirect: const TaskRedirect = require('./redirect'); return new TaskRedirect(logger, data); + case TaskName.RestDial: + const TaskRestDial = require('./rest_dial'); + return new TaskRestDial(logger, data); case TaskName.Tag: const TaskTag = require('./tag'); return new TaskTag(logger, data); } // should never reach - throw new Error(`invalid task ${name} (please update specs.json and make_task.js)`); + throw new Error(`invalid jambonz verb '${name}'`); } module.exports = makeTask; diff --git a/lib/tasks/redirect.js b/lib/tasks/redirect.js index a258ac9e..fb377729 100644 --- a/lib/tasks/redirect.js +++ b/lib/tasks/redirect.js @@ -9,7 +9,7 @@ class TaskRedirect extends Task { super(logger, opts); this.action = this.data.action; - this.method = this.data.method || 'POST'; + this.method = (this.data.method || 'POST').toUpperCase(); this.auth = this.data.auth; } diff --git a/lib/tasks/rest_dial.js b/lib/tasks/rest_dial.js new file mode 100644 index 00000000..a78aaaa4 --- /dev/null +++ b/lib/tasks/rest_dial.js @@ -0,0 +1,82 @@ +const Task = require('./task'); +const {TaskName} = require('../utils/constants'); + +/** + * Manages an outdial made via REST API + */ +class TaskRestDial extends Task { + constructor(logger, opts) { + super(logger, opts); + + this.from = this.data.from; + this.to = this.data.to; + this.call_hook = this.data.call_hook; + this.timeout = this.data.timeout || 60; + + this.on('connect', this._onConnect.bind(this)); + this.on('callStatus', this._onCallStatus.bind(this)); + } + + get name() { return TaskName.RestDial; } + + /** + * INVITE has just been sent at this point + */ + async exec(cs, req) { + super.exec(cs); + this.req = req; + + this._setCallTimer(); + await this.awaitTaskDone(); + } + + kill() { + super.kill(); + this._clearCallTimer(); + if (this.req) { + this.req.cancel(); + this.req = null; + } + this.notifyTaskDone(); + } + + async _onConnect(dlg) { + this.req = null; + const cs = this.callSession; + cs.setDialog(dlg); + const obj = Object.assign({}, cs.callInfo); + + const tasks = await this.actionHook(this.call_hook, obj); + if (tasks && Array.isArray(tasks)) { + this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`); + cs.replaceApplication(tasks); + } + this.notifyTaskDone(); + } + + _onCallStatus(status) { + this.logger.debug(`CallStatus: ${status}`); + if (status >= 200) { + this.req = null; + this._clearCallTimer(); + if (status !== 200) this.notifyTaskDone(); + } + } + + _setCallTimer() { + this.timer = setTimeout(this._onCallTimeout.bind(this), this.timeout * 1000); + } + + _clearCallTimer() { + if (this.timer) clearTimeout(this.timer); + this.timer = null; + } + + _onCallTimeout() { + this.logger.debug('TaskRestDial: timeout expired without answer, killing task'); + this.timer = null; + this.kill(); + } +} + +module.exports = TaskRestDial; diff --git a/lib/tasks/specs.json b/lib/tasks/specs.json index 8c65e2ee..f56d2cae 100644 --- a/lib/tasks/specs.json +++ b/lib/tasks/specs.json @@ -83,9 +83,14 @@ "listen": { "properties": { "action": "string", + "auth": "#auth", "finishOnKey": "string", "maxLength": "number", "metadata": "object", + "method": { + "type": "string", + "enum": ["GET", "POST"] + }, "mixType": { "type": "string", "enum": ["mono", "stereo", "mixed"] @@ -96,6 +101,7 @@ "timeout": "number", "transcribe": "#transcribe", "url": "string", + "wsAuth": "#auth", "earlyMedia": "boolean" }, "required": [ @@ -115,6 +121,19 @@ "action" ] }, + "rest:dial": { + "properties": { + "call_hook": "object", + "from": "string", + "to": "#target", + "timeout": "number" + }, + "required": [ + "call_hook", + "from", + "to" + ] + }, "tag": { "properties": { "data": "object" @@ -155,11 +174,11 @@ }, "auth": { "properties": { - "user": "string", + "username": "string", "password": "string" }, "required": [ - "user", + "username", "password" ] }, diff --git a/lib/tasks/tag.js b/lib/tasks/tag.js index 46b6ae56..9cc3b855 100644 --- a/lib/tasks/tag.js +++ b/lib/tasks/tag.js @@ -12,7 +12,7 @@ class TaskTag extends Task { async exec(cs) { super.exec(cs); cs.callInfo.customerData = this.data; - this.logger.debug({customerData: cs.callInfo.customerData}, 'TaskTag:exec set customer data'); + this.logger.debug({customerData: this.data}, 'TaskTag:exec set customer data'); } } diff --git a/lib/tasks/task.js b/lib/tasks/task.js index 9ce5a4ab..c30c0cd3 100644 --- a/lib/tasks/task.js +++ b/lib/tasks/task.js @@ -56,16 +56,26 @@ class Task extends Emitter { return this._completionPromise; } - async performAction(method, auth, results) { - if (this.action) { - let action = this.action; - if (action.startsWith('/')) { - const or = this.callSession.originalRequest; - action = `${or.baseUrl}${this.action}`; - this.logger.debug({originalUrl: this.action, normalizedUrl: action}, 'Task:performAction normalized url'); - if (!auth && or.auth) auth = or.auth; + normalizeUrl(url, method, auth) { + const hook = {url, method}; + if (auth && auth.username && auth.password) Object.assign(hook, auth); + + if (url.startsWith('/')) { + const or = this.callSession.originalRequest; + if (or) { + hook.url = `${or.baseUrl}${url}`; + hook.method = hook.method || or.method || 'POST'; + if (!hook.auth && or.auth) Object.assign(hook, or.auth); } - const tasks = await this.actionHook(action, method, auth, results); + } + this.logger.debug({hook}, 'Task:normalizeUrl'); + return hook; + } + + async performAction(method, auth, results, expectResponse = true) { + if (this.action) { + const hook = this.normalizeUrl(this.action, method, auth); + const tasks = await this.actionHook(hook, results, expectResponse); if (tasks && Array.isArray(tasks)) { this.logger.debug({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`); this.callSession.replaceApplication(tasks); diff --git a/lib/utils/constants.json b/lib/utils/constants.json index a70b3f2b..70b65cc9 100644 --- a/lib/utils/constants.json +++ b/lib/utils/constants.json @@ -6,6 +6,7 @@ "Listen": "listen", "Play": "play", "Redirect": "redirect", + "RestDial": "rest:dial", "SipDecline": "sip:decline", "SipNotify": "sip:notify", "SipRedirect": "sip:redirect", diff --git a/lib/utils/normalize-jamones.js b/lib/utils/normalize-jamones.js index 10b87502..ae1e33de 100644 --- a/lib/utils/normalize-jamones.js +++ b/lib/utils/normalize-jamones.js @@ -1,9 +1,9 @@ function normalizeJambones(logger, obj) { logger.debug(`normalizeJambones: ${JSON.stringify(obj)}`); - if (!Array.isArray(obj)) throw new Error('invalid JSON: jambones docs must be array'); + if (!Array.isArray(obj)) throw new Error('malformed jambonz payload: must be array'); const document = []; for (const tdata of obj) { - if (typeof tdata !== 'object') throw new Error('invalid JSON: jambones docs must be array of objects'); + if (typeof tdata !== 'object') throw new Error('malformed jambonz payload: must be array of objects'); if ('verb' in tdata) { // {verb: 'say', text: 'foo..bar'..} const name = tdata.verb; @@ -21,8 +21,8 @@ function normalizeJambones(logger, obj) { document.push(tdata); } else { - logger.info(tdata, `invalid JSON: invalid verb form, numkeys ${Object.keys(tdata).length}`); - throw new Error('invalid JSON: invalid verb form'); + logger.info(tdata, 'malformed jambonz payload: missing verb property'); + throw new Error('malformed jambonz payload: missing verb property'); } } logger.debug(`returning document with ${document.length} tasks`); diff --git a/lib/utils/notifiers.js b/lib/utils/notifiers.js index cf3b2c11..ee06280f 100644 --- a/lib/utils/notifiers.js +++ b/lib/utils/notifiers.js @@ -1,27 +1,26 @@ const request = require('request'); -require('request-debug')(request); +//require('request-debug')(request); const retrieveApp = require('./retrieve-app'); function hooks(logger, callInfo) { - logger.debug({callInfo}, 'creating action hook'); function actionHook(hook, obj = {}, expectResponse = true) { - const method = hook.method.toUpperCase(); + const method = (hook.method || 'POST').toUpperCase(); const auth = (hook.username && hook.password) ? {username: hook.username, password: hook.password} : null; - const data = Object.assign({}, obj, callInfo); - logger.debug({data}, `actionhook sending to ${hook.url}`); - if ('GET' === method) { - // remove customer data - only for POSTs since it might be quite complex - delete data.customerData; - } + const data = Object.assign({}, obj, callInfo.toJSON()); + logger.debug({hook, data, auth}, 'actionhook'); + + /* customer data only on POSTs */ + if ('GET' === method) delete data.customerData; + const opts = { url: hook.url, method, json: 'POST' === method || expectResponse }; - if (auth) obj.auth = auth; + if (auth) opts.auth = auth; if ('POST' === method) opts.body = data; else opts.qs = data; @@ -40,8 +39,8 @@ function hooks(logger, callInfo) { }); } - function notifyHook(url, method, auth, opts = {}) { - return actionHook(url, method, auth, opts, false); + function notifyHook(hook, opts = {}) { + return actionHook(hook, opts, false); } return { diff --git a/lib/utils/place-outdial.js b/lib/utils/place-outdial.js index ec3e9af5..75a651dc 100644 --- a/lib/utils/place-outdial.js +++ b/lib/utils/place-outdial.js @@ -8,6 +8,7 @@ const ConfirmCallSession = require('../session/confirm-call-session'); const hooks = require('./notifiers'); const moment = require('moment'); const parseUrl = require('parse-url'); +const uuidv4 = require('uuid/v4'); class SingleDialer extends Emitter { constructor({logger, sbcAddress, target, opts, application, callInfo}) { @@ -25,23 +26,13 @@ class SingleDialer extends Emitter { this.bindings = logger.bindings(); this.parentCallInfo = callInfo; -/* - this.callInfo = Object.assign({}, callInfo, { - callSid: this._callSid, - parentCallSid: callInfo.callSid, - direction: CallDirection.Outbound, - callStatus: CallStatus.Trying, - sipStatus: 100 - }); -*/ this.callGone = false; + this.callSid = uuidv4(); + this.on('callStatusChange', this._notifyCallStatusChange.bind(this)); } - get callSid() { - return this._callSid; - } get callStatus() { return this.callInfo.callStatus; } @@ -88,21 +79,26 @@ class SingleDialer extends Emitter { if (this.target.auth) opts.auth = this.target.auth; this.dlg = await srf.createUAC(uri, opts, { cbRequest: (err, req) => { - if (err) return this.logger.error(err, 'SingleDialer:exec Error creating call'); + if (err) { + this.logger.error(err, 'SingleDialer:exec Error creating call'); + this.emit('callCreateFail', err); + return; + } /** * INVITE has been sent out + * (a) create a CallInfo for this call * (a) create a logger for this call - * (b) augment this.callInfo with additional call info */ - this.logger.debug(`call sent, creating CallInfo parentCallInfo is CallInfo? ${this.parentCallInfo instanceof CallInfo}`); this.callInfo = new CallInfo({ direction: CallDirection.Outbound, parentCallInfo: this.parentCallInfo, - req + req, + to, + callSid: this.callSid }); this.logger = srf.locals.parentLogger.child({ - callSid: this.callInfo.callSid, + callSid: this.callSid, parentCallSid: this.parentCallInfo.callSid, callId: this.callInfo.callId }); @@ -164,6 +160,7 @@ class SingleDialer extends Emitter { const duration = moment().diff(this.dlg.connectTime, 'seconds'); this.logger.debug('SingleDialer:kill hanging up called party'); this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); + this.dlg.destroy(); } if (this.ep) { this.logger.debug(`SingleDialer:kill - deleting endpoint ${this.ep.uuid}`); @@ -181,13 +178,14 @@ class SingleDialer extends Emitter { async _executeApp(url) { this.logger.debug(`SingleDialer:_executeApp: executing ${url} after connect`); try { - let auth; + let auth, method; const app = Object.assign({}, this.application); if (url.startsWith('/')) { const savedUrl = url; const or = app.originalRequest; url = `${or.baseUrl}${url}`; auth = or.auth; + method = this.method || or.method || 'POST'; this.logger.debug({originalUrl: savedUrl, normalizedUrl: url}, 'SingleDialer:_executeApp normalized url'); } else { @@ -196,9 +194,10 @@ class SingleDialer extends Emitter { app.originalRequest = { baseUrl: `${u.protocol}://${u.resource}${myPort}` }; + method = this.method || 'POST'; } - const tasks = await this.actionHook(url, this.method, auth); + const tasks = await this.actionHook({url, method, auth}); const allowedTasks = tasks.filter((task) => { return [ TaskPreconditions.StableCall, @@ -210,7 +209,14 @@ class SingleDialer extends Emitter { } this.logger.debug(`SingleDialer:_executeApp: executing ${tasks.length} tasks`); - const cs = new ConfirmCallSession({logger: this.logger, application: app, dlg: this.dlg, ep: this.ep, tasks}); + const cs = new ConfirmCallSession({ + logger: this.logger, + application: app, + dlg: this.dlg, + ep: this.ep, + callInfo: this.callInfo, + tasks + }); await cs.exec(); this.emit(this.dlg.connected ? 'accept' : 'decline'); } catch (err) { @@ -220,9 +226,12 @@ class SingleDialer extends Emitter { } } - _notifyCallStatusChange({callStatus, sipStatus}) { - this.logger.debug(`SingleDialer:_notifyCallStatusChange: ${callStatus} ${sipStatus}`); + _notifyCallStatusChange({callStatus, sipStatus, duration}) { + assert((typeof duration === 'number' && callStatus === CallStatus.Completed) || + (!duration && callStatus !== CallStatus.Completed), + 'duration MUST be supplied when call completed AND ONLY when call completed'); this.callInfo.updateCallStatus(callStatus, sipStatus); + if (typeof duration === 'number') this.callInfo.duration = duration; try { this.notifyHook(this.application.call_status_hook); } catch (err) { diff --git a/lib/utils/retrieve-app.js b/lib/utils/retrieve-app.js index e387bc35..7a1e6297 100644 --- a/lib/utils/retrieve-app.js +++ b/lib/utils/retrieve-app.js @@ -12,7 +12,9 @@ function retrieveUrl(logger, url, method, auth, obj) { return new Promise((resolve, reject) => { request(opts, (err, response, body) => { if (err) throw err; - if (body) logger.debug({body}, 'retrieveUrl: customer returned an application'); + if (response.statusCode === 401) return reject(new Error('HTTP request failed: Unauthorized')); + else if (response.statusCode !== 200) return reject(new Error(`HTTP request failed: ${response.statusCode}`)); + if (body) logger.debug({body}, 'retrieveUrl: ${method} ${url} returned an application'); resolve(body); }); }); diff --git a/package-lock.json b/package-lock.json index 104b6da1..d8e1d16b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -127,6 +127,15 @@ "to-fast-properties": "^2.0.0" } }, + "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" + } + }, "acorn": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", @@ -219,6 +228,11 @@ "sprintf-js": "~1.0.2" } }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -302,6 +316,43 @@ "tape": ">=2.0.0 <5.0.0" } }, + "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" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -339,6 +390,11 @@ "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", "dev": true }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, "caching-transform": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", @@ -533,6 +589,26 @@ "json5": "^1.0.1" } }, + "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" + }, + "dependencies": { + "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==" + } + } + }, + "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==" + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -550,6 +626,16 @@ } } }, + "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=" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -675,11 +761,21 @@ "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, "deprecate": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deprecate/-/deprecate-1.1.1.tgz", "integrity": "sha512-ZGDXefq1xknT292LnorMY5s8UVU08/WKdzDZCUT6t9JzsiMSP4uzUhgpqugffNVcT5WC6wMBiSQ+LFjlv3v7iQ==" }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, "diff": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", @@ -811,12 +907,22 @@ "safer-buffer": "^2.1.0" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -869,6 +975,11 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -1003,6 +1114,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, "eventemitter2": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz", @@ -1014,6 +1130,73 @@ "integrity": "sha1-LUH1Y+H+QA7Uli/hpNXGp1Od9/Y=", "dev": true }, + "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" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "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==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1088,6 +1271,35 @@ "to-regex-range": "^5.0.1" } }, + "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" + }, + "dependencies": { + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -1199,6 +1411,16 @@ "mime-types": "^2.1.12" } }, + "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=" + }, "fs-exists-cached": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz", @@ -1383,6 +1605,25 @@ "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "dev": true }, + "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" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1397,7 +1638,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -1478,6 +1718,11 @@ "through": "^2.3.6" } }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -1993,11 +2238,21 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, "merge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, "merge-source-map": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", @@ -2015,6 +2270,16 @@ } } }, + "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.42.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", @@ -2151,6 +2416,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", @@ -2281,6 +2551,14 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "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" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2450,6 +2728,11 @@ "protocols": "^1.4.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -2474,6 +2757,11 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "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=" + }, "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -2595,6 +2883,15 @@ "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz", "integrity": "sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg==" }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -2620,6 +2917,22 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-3.0.3.tgz", "integrity": "sha512-dy1yjycmn9blucmJLXOfZDx1ikZJUi6E8bBZLnhPG5gBrVhHXx2xVyqqgKBubVNEXmx51dBACMHpoMQK/N/AXQ==" }, + "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" + } + }, "re-emitter": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.4.tgz", @@ -2865,17 +3178,75 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "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": { + "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" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "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==" + } + } + }, "seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" }, + "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" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -3051,6 +3422,11 @@ "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -4696,6 +5072,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -4771,6 +5152,15 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "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" + } + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -4839,6 +5229,11 @@ } } }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -4879,6 +5274,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index 881f161b..a5f237aa 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "drachtio-fn-b2b-sugar": "0.0.12", "drachtio-fsmrf": "^1.5.12", "drachtio-srf": "^4.4.27", + "express": "^4.17.1", "jambonz-db-helpers": "^0.2.0", "moment": "^2.24.0", "parse-url": "^5.0.1",