From 889257d7dbfa0feb2e6dabc9e94a93ab4416c9c3 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Wed, 7 Sep 2022 23:51:40 +0200 Subject: [PATCH] write service_provider_sid with alerts --- lib/http-routes/api/create-call.js | 16 ++- lib/http-routes/api/messaging.js | 8 +- lib/middleware.js | 34 +++++-- lib/session/call-info.js | 12 ++- lib/session/call-session.js | 17 +++- lib/tasks/gather.js | 2 + lib/tasks/say.js | 2 + lib/tasks/transcribe.js | 1 + lib/utils/amd-utils.js | 1 + lib/utils/base-requestor.js | 3 +- lib/utils/http-requestor.js | 6 +- lib/utils/requestor.js | 150 ----------------------------- lib/utils/ws-requestor.js | 6 +- package.json | 2 +- 14 files changed, 85 insertions(+), 175 deletions(-) delete mode 100644 lib/utils/requestor.js diff --git a/lib/http-routes/api/create-call.js b/lib/http-routes/api/create-call.js index 5be4a9c8..2f260637 100644 --- a/lib/http-routes/api/create-call.js +++ b/lib/http-routes/api/create-call.js @@ -127,10 +127,22 @@ router.post('/', async(req, res) => { } else { logger.debug({call_hook: app.call_hook}, 'creating http client for call hook'); - app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret); + app.requestor = new HttpRequestor( + logger, + account.service_provider_sid, + account.account_sid, + app.call_hook, + account.webhook_secret + ); } if (!app.notifier && app.call_status_hook) { - app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret); + app.notifier = new HttpRequestor( + logger, + account.service_provider_sid, + account.account_sid, + app.call_status_hook, + account.webhook_secret + ); logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook'); } else if (!app.notifier) { diff --git a/lib/http-routes/api/messaging.js b/lib/http-routes/api/messaging.js index c372c0c0..261107f7 100644 --- a/lib/http-routes/api/messaging.js +++ b/lib/http-routes/api/messaging.js @@ -26,7 +26,13 @@ router.post('/:partner', async(req, res) => { app.notifier = app.requestor; } else { - app.requestor = new HttpRequestor(logger, account.account_sid, hook, account.webhook_secret); + app.requestor = new HttpRequestor( + logger, + account.service_provider_sid, + account.account_sid, + hook, + account.webhook_secret + ); app.notifier = {request: () => {}}; } diff --git a/lib/middleware.js b/lib/middleware.js index 21df271c..b4189964 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -17,7 +17,8 @@ module.exports = function(srf, logger) { lookupAppByRegex, lookupAppBySid, lookupAppByRealm, - lookupAppByTeamsTenant + lookupAppByTeamsTenant, + lookupAccountBySid } = srf.locals.dbHelpers; const { writeAlerts, @@ -25,7 +26,7 @@ module.exports = function(srf, logger) { } = srf.locals; const {lookupAccountDetails} = dbUtils(logger, srf); - function initLocals(req, res, next) { + const initLocals = async(req, res, next) => { const callId = req.get('Call-ID'); logger.info({callId}, 'new incoming call'); if (!req.has('X-Account-Sid')) { @@ -34,7 +35,10 @@ module.exports = function(srf, logger) { } const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4(); const account_sid = req.get('X-Account-Sid'); - req.locals = {callSid, account_sid, callId}; + console.log(`account_sid: ${account_sid}`); + const account = await lookupAccountBySid(account_sid); + console.log(`account: ${JSON.stringify(account)}`); + req.locals = {callSid, account_sid, service_provider_sid: account?.service_provider_sid, callId}; if (req.has('X-Application-Sid')) { const application_sid = req.get('X-Application-Sid'); logger.debug(`got application from X-Application-Sid header: ${application_sid}`); @@ -44,7 +48,7 @@ module.exports = function(srf, logger) { if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN'); next(); - } + }; function createRootSpan(req, res, next) { const {callId, callSid, account_sid} = req.locals; @@ -161,7 +165,7 @@ module.exports = function(srf, logger) { * Given the dialed DID/phone number, retrieve the application to invoke */ async function retrieveApplication(req, res, next) { - const {logger, accountInfo, account_sid, rootSpan} = req.locals; + const {logger, accountInfo, service_provider_sid, account_sid, rootSpan} = req.locals; const {span} = rootSpan.startChildSpan('lookupApplication'); try { let app; @@ -231,9 +235,22 @@ module.exports = function(srf, logger) { app.call_hook.method = 'WS'; } else { - app.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret); - if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook, - accountInfo.account.webhook_secret); + app.requestor = new HttpRequestor( + logger, + service_provider_sid, + account_sid, + app.call_hook, + accountInfo.account.webhook_secret + ); + if (app.call_status_hook) { + app.notifier = new HttpRequestor( + logger, + service_provider_sid, + account_sid, + app.call_status_hook, + accountInfo.account.webhook_secret + ); + } else app.notifier = {request: () => {}}; } @@ -316,6 +333,7 @@ module.exports = function(srf, logger) { span?.setAttributes({webhookStatus: err.statusCode}); span?.end(); writeAlerts({ + service_provider_sid: req.locals.service_provider_sid, account_sid: req.locals.account_sid, target_sid: req.locals.callSid, alert_type: AlertType.INVALID_APP_PAYLOAD, diff --git a/lib/session/call-info.js b/lib/session/call-info.js index dcf31ce9..9cde7393 100644 --- a/lib/session/call-info.js +++ b/lib/session/call-info.js @@ -21,8 +21,9 @@ class CallInfo { // inbound call const {app, req} = opts; srf = req.srf; - this.callSid = req.locals.callSid, - this.accountSid = app.account_sid, + this.callSid = req.locals.callSid; + this.serviceProviderSid = req.locals.service_provider_sid; + this.accountSid = app.account_sid; this.applicationSid = app.application_sid; this.from = from || req.callingNumber; this.to = req.calledNumber; @@ -39,6 +40,7 @@ class CallInfo { srf = req.srf; this.callSid = callSid || uuidv4(); this.parentCallSid = parentCallInfo.callSid; + this.serviceProviderSid = parentCallInfo.serviceProviderSid; this.accountSid = parentCallInfo.accountSid; this.applicationSid = parentCallInfo.applicationSid; this.from = from || req.callingNumber; @@ -51,18 +53,20 @@ class CallInfo { } else if (this.direction === CallDirection.None) { // outbound SMS - const {messageSid, accountSid, applicationSid, res} = opts; + const {messageSid, serviceProviderSid, accountSid, applicationSid, res} = opts; srf = res.srf; this.messageSid = messageSid; + this.serviceProviderSid = serviceProviderSid; this.accountSid = accountSid; this.applicationSid = applicationSid; this.res = res; } else { // outbound call triggered by REST - const {req, callSid, accountSid, applicationSid, to, tag} = opts; + const {req, callSid, accountSid, serviceProviderSid, applicationSid, to, tag} = opts; srf = req.srf; this.callSid = callSid; + this.serviceProviderSid = serviceProviderSid; this.accountSid = accountSid; this.applicationSid = applicationSid; this.callStatus = CallStatus.Trying, diff --git a/lib/session/call-session.js b/lib/session/call-session.js index 4d3bdbff..fc922b28 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -201,6 +201,12 @@ class CallSession extends Emitter { return this.direction === CallDirection.Inbound && this.res.finalResponseSent; } + /** + * returns the account sid + */ + get serviceProviderSid() { + return this.callInfo.serviceProviderSid; + } /** * returns the account sid */ @@ -520,6 +526,7 @@ class CallSession extends Emitter { this.logger.info({err}, `malformed google service_key provisioned for account ${sid}`); writeAlerts({ alert_type: AlertType.TTS_FAILURE, + service_provider_sid: this.serviceProviderSid, account_sid: this.accountSid, vendor }).catch((err) => this.logger.error({err}, 'Error writing tts alert')); @@ -550,6 +557,7 @@ class CallSession extends Emitter { else { writeAlerts({ alert_type: AlertType.STT_NOT_PROVISIONED, + service_provider_sid: this.serviceProviderSid, account_sid: this.accountSid, vendor }).catch((err) => this.logger.error({err}, 'Error writing tts alert')); @@ -1382,8 +1390,13 @@ class CallSession extends Emitter { } else { this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found'); - this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid, - r[0], this.webhook_secret); + this.queueEventHookRequestor = new HttpRequestor( + this.logger, + this.serviceProviderSid, + this.accountSid, + r[0], + this.webhook_secret + ); this.queueEventHook = r[0]; } } catch (err) { diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index d901518e..df421893 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -163,6 +163,7 @@ class TaskGather extends Task { const {writeAlerts, AlertType} = cs.srf.locals; this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`); writeAlerts({ + service_provider_sid: cs.serviceProviderSid, account_sid: cs.accountSid, alert_type: AlertType.STT_NOT_PROVISIONED, vendor: this.vendor @@ -399,6 +400,7 @@ class TaskGather extends Task { const {writeAlerts, AlertType} = this.cs.srf.locals; this.logger.error(err, 'TaskGather:_startTranscribing error'); writeAlerts({ + service_provider_sid: this.cs.serviceProviderSid, account_sid: this.cs.accountSid, alert_type: AlertType.STT_FAILURE, vendor: this.vendor, diff --git a/lib/tasks/say.js b/lib/tasks/say.js index fd43ae5e..aa35e1a1 100644 --- a/lib/tasks/say.js +++ b/lib/tasks/say.js @@ -47,6 +47,7 @@ class TaskSay extends Task { try { if (!credentials) { writeAlerts({ + service_provider_sid: cs.serviceProviderSid, account_sid: cs.accountSid, alert_type: AlertType.TTS_NOT_PROVISIONED, vendor @@ -92,6 +93,7 @@ class TaskSay extends Task { this.logger.info({err}, 'Error synthesizing tts'); span.end(); writeAlerts({ + service_provider_sid: cs.serviceProviderSid, account_sid: cs.accountSid, alert_type: AlertType.TTS_NOT_PROVISIONED, vendor, diff --git a/lib/tasks/transcribe.js b/lib/tasks/transcribe.js index f35158a5..f9c8d399 100644 --- a/lib/tasks/transcribe.js +++ b/lib/tasks/transcribe.js @@ -86,6 +86,7 @@ class TaskTranscribe extends Task { const {writeAlerts, AlertType} = cs.srf.locals; this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`); writeAlerts({ + service_provider_sid: cs.serviceProviderSid, account_sid: cs.accountSid, alert_type: AlertType.STT_NOT_PROVISIONED, vendor: this.vendor diff --git a/lib/utils/amd-utils.js b/lib/utils/amd-utils.js index 59514893..544db16e 100644 --- a/lib/utils/amd-utils.js +++ b/lib/utils/amd-utils.js @@ -193,6 +193,7 @@ module.exports = (logger) => { task.emit(AmdEvents.Error, err); logger.error(err, 'amd:_startTranscribing error'); writeAlerts({ + service_provider_sid: cs.serviceProviderSid, account_sid: cs.accountSid, alert_type: AlertType.STT_FAILURE, vendor: vendor, diff --git a/lib/utils/base-requestor.js b/lib/utils/base-requestor.js index c2f1be95..00e042c5 100644 --- a/lib/utils/base-requestor.js +++ b/lib/utils/base-requestor.js @@ -5,7 +5,7 @@ const timeSeries = require('@jambonz/time-series'); let alerter ; class BaseRequestor extends Emitter { - constructor(logger, account_sid, hook, secret) { + constructor(logger, service_provider_sid, account_sid, hook, secret) { super(); assert(typeof hook === 'object'); @@ -15,6 +15,7 @@ class BaseRequestor extends Emitter { this.username = hook.username; this.password = hook.password; this.secret = secret; + this.service_provider_sid = service_provider_sid; this.account_sid = account_sid; const {stats} = require('../../').srf.locals; diff --git a/lib/utils/http-requestor.js b/lib/utils/http-requestor.js index a3c4aa61..5cd9150e 100644 --- a/lib/utils/http-requestor.js +++ b/lib/utils/http-requestor.js @@ -18,8 +18,8 @@ function basicAuth(username, password) { class HttpRequestor extends BaseRequestor { - constructor(logger, account_sid, hook, secret) { - super(logger, account_sid, hook, secret); + constructor(logger, service_provider_sid, account_sid, hook, secret) { + super(logger, service_provider_sid, account_sid, hook, secret); this.method = hook.method || 'POST'; this.authHeader = basicAuth(hook.username, hook.password); @@ -142,7 +142,7 @@ class HttpRequestor extends BaseRequestor { this.logger.error({err, baseUrl: this.baseUrl, url}, 'web callback returned unexpected error'); } - let opts = {account_sid: this.account_sid}; + let opts = {account_sid: this.account_sid, service_provider_sid: this.service_provider_sid}; if (err.code === 'ECONNREFUSED') { opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url}; } diff --git a/lib/utils/requestor.js b/lib/utils/requestor.js deleted file mode 100644 index 72765412..00000000 --- a/lib/utils/requestor.js +++ /dev/null @@ -1,150 +0,0 @@ -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 || ''}`; - const header = `Basic ${toBase64(creds)}`; - return {Authorization: header}; -} - -function isRelativeUrl(u) { - return typeof u === 'string' && u.startsWith('/'); -} - -function isAbsoluteUrl(u) { - return typeof u === 'string' && - u.startsWith('https://') || u.startsWith('http://'); -} - -class Requestor { - constructor(logger, account_sid, hook, secret) { - assert(typeof hook === 'object'); - - this.logger = logger; - this.url = hook.url; - this.method = hook.method || 'POST'; - this.authHeader = basicAuth(hook.username, hook.password); - - const u = parseUrl(this.url); - const myPort = u.port ? `:${u.port}` : ''; - const baseUrl = this._baseUrl = `${u.protocol}://${u.resource}${myPort}`; - - this.get = bent(baseUrl, 'GET', 'buffer', 200, 201); - this.post = bent(baseUrl, 'POST', 'buffer', 200, 201); - - 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() { - return this._baseUrl; - } - - /** - * Make an HTTP request. - * All requests use json bodies. - * All requests expect a 200 statusCode on success - * @param {object|string} hook - may be a absolute or relative url, or an object - * @param {string} [hook.url] - an absolute or relative url - * @param {string} [hook.method] - 'GET' or 'POST' - * @param {string} [hook.username] - if basic auth is protecting the endpoint - * @param {string} [hook.password] - if basic auth is protecting the endpoint - * @param {object} [params] - request parameters - */ - async request(hook, params) { - const payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null; - const url = hook.url || hook; - const method = hook.method || 'POST'; - - 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}`); - const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass - this.logger.debug({url: urlInfo, method: methodInfo, payload}, `Requestor:request ${method} ${url}`); - const startAt = process.hrtime(); - - 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, headers) : - await bent(method, 'buffer', 200, 201, 202)(url, payload, headers); - } catch (err) { - 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); - const time = diff[0] * 1e3 + diff[1] * 1e-6; - const rtt = time.toFixed(0); - if (buf) this.stats.histogram('app.hook.response_time', rtt, ['hook_type:app']); - - if (buf && buf.toString().length > 0) { - try { - const json = JSON.parse(buf.toString()); - this.logger.info({response: json}, `Requestor:request ${method} ${url} succeeded in ${rtt}ms`); - return json; - } - catch (err) { - //this.logger.debug({err, url, method}, `Requestor:request returned non-JSON content: '${buf.toString()}'`); - } - } - } -} - -module.exports = Requestor; diff --git a/lib/utils/ws-requestor.js b/lib/utils/ws-requestor.js index d440a082..d9c851d8 100644 --- a/lib/utils/ws-requestor.js +++ b/lib/utils/ws-requestor.js @@ -9,8 +9,8 @@ const MAX_RECONNECTS = 5; const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000; class WsRequestor extends BaseRequestor { - constructor(logger, account_sid, hook, secret) { - super(logger, account_sid, hook, secret); + constructor(logger, service_provider_sid, account_sid, hook, secret) { + super(logger, service_provider_sid, account_sid, hook, secret); this.connections = 0; this.messagesInFlight = new Map(); this.maliciousClient = false; @@ -54,7 +54,7 @@ class WsRequestor extends BaseRequestor { /* if we have an absolute url, and it is http then do a standard webhook */ if (this._isAbsoluteUrl(url) && url.startsWith('http')) { this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)'); - const requestor = new HttpRequestor(this.logger, this.account_sid, hook, this.secret); + const requestor = new HttpRequestor(this.logger, this.service_provider_sid, this.account_sid, hook, this.secret); return requestor.request(type, hook, params, httpHeaders); } diff --git a/package.json b/package.json index e2721bb4..ae28a651 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@jambonz/http-health-check": "^0.0.1", "@jambonz/realtimedb-helpers": "^0.4.29", "@jambonz/stats-collector": "^0.1.6", - "@jambonz/time-series": "^0.1.12", + "@jambonz/time-series": "^0.2.0", "@opentelemetry/api": "^1.1.0", "@opentelemetry/exporter-jaeger": "^1.3.1", "@opentelemetry/exporter-trace-otlp-http": "^0.27.0",