write service_provider_sid with alerts

This commit is contained in:
Dave Horton
2022-09-07 23:51:40 +02:00
parent e90ef6bc70
commit 889257d7db
14 changed files with 85 additions and 175 deletions

View File

@@ -127,10 +127,22 @@ router.post('/', async(req, res) => {
} }
else { else {
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook'); 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) { 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'); logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
} }
else if (!app.notifier) { else if (!app.notifier) {

View File

@@ -26,7 +26,13 @@ router.post('/:partner', async(req, res) => {
app.notifier = app.requestor; app.notifier = app.requestor;
} }
else { 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: () => {}}; app.notifier = {request: () => {}};
} }

View File

@@ -17,7 +17,8 @@ module.exports = function(srf, logger) {
lookupAppByRegex, lookupAppByRegex,
lookupAppBySid, lookupAppBySid,
lookupAppByRealm, lookupAppByRealm,
lookupAppByTeamsTenant lookupAppByTeamsTenant,
lookupAccountBySid
} = srf.locals.dbHelpers; } = srf.locals.dbHelpers;
const { const {
writeAlerts, writeAlerts,
@@ -25,7 +26,7 @@ module.exports = function(srf, logger) {
} = srf.locals; } = srf.locals;
const {lookupAccountDetails} = dbUtils(logger, srf); const {lookupAccountDetails} = dbUtils(logger, srf);
function initLocals(req, res, next) { const initLocals = async(req, res, next) => {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
logger.info({callId}, 'new incoming call'); logger.info({callId}, 'new incoming call');
if (!req.has('X-Account-Sid')) { 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 callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
const account_sid = req.get('X-Account-Sid'); 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')) { if (req.has('X-Application-Sid')) {
const application_sid = req.get('X-Application-Sid'); const application_sid = req.get('X-Application-Sid');
logger.debug(`got application from X-Application-Sid header: ${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'); if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
next(); next();
} };
function createRootSpan(req, res, next) { function createRootSpan(req, res, next) {
const {callId, callSid, account_sid} = req.locals; 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 * Given the dialed DID/phone number, retrieve the application to invoke
*/ */
async function retrieveApplication(req, res, next) { 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'); const {span} = rootSpan.startChildSpan('lookupApplication');
try { try {
let app; let app;
@@ -231,9 +235,22 @@ module.exports = function(srf, logger) {
app.call_hook.method = 'WS'; app.call_hook.method = 'WS';
} }
else { else {
app.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret); app.requestor = new HttpRequestor(
if (app.call_status_hook) app.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook, logger,
accountInfo.account.webhook_secret); 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: () => {}}; else app.notifier = {request: () => {}};
} }
@@ -316,6 +333,7 @@ module.exports = function(srf, logger) {
span?.setAttributes({webhookStatus: err.statusCode}); span?.setAttributes({webhookStatus: err.statusCode});
span?.end(); span?.end();
writeAlerts({ writeAlerts({
service_provider_sid: req.locals.service_provider_sid,
account_sid: req.locals.account_sid, account_sid: req.locals.account_sid,
target_sid: req.locals.callSid, target_sid: req.locals.callSid,
alert_type: AlertType.INVALID_APP_PAYLOAD, alert_type: AlertType.INVALID_APP_PAYLOAD,

View File

@@ -21,8 +21,9 @@ class CallInfo {
// inbound call // inbound call
const {app, req} = opts; const {app, req} = opts;
srf = req.srf; srf = req.srf;
this.callSid = req.locals.callSid, this.callSid = req.locals.callSid;
this.accountSid = app.account_sid, this.serviceProviderSid = req.locals.service_provider_sid;
this.accountSid = app.account_sid;
this.applicationSid = app.application_sid; this.applicationSid = app.application_sid;
this.from = from || req.callingNumber; this.from = from || req.callingNumber;
this.to = req.calledNumber; this.to = req.calledNumber;
@@ -39,6 +40,7 @@ class CallInfo {
srf = req.srf; srf = req.srf;
this.callSid = callSid || uuidv4(); this.callSid = callSid || uuidv4();
this.parentCallSid = parentCallInfo.callSid; this.parentCallSid = parentCallInfo.callSid;
this.serviceProviderSid = parentCallInfo.serviceProviderSid;
this.accountSid = parentCallInfo.accountSid; this.accountSid = parentCallInfo.accountSid;
this.applicationSid = parentCallInfo.applicationSid; this.applicationSid = parentCallInfo.applicationSid;
this.from = from || req.callingNumber; this.from = from || req.callingNumber;
@@ -51,18 +53,20 @@ class CallInfo {
} }
else if (this.direction === CallDirection.None) { else if (this.direction === CallDirection.None) {
// outbound SMS // outbound SMS
const {messageSid, accountSid, applicationSid, res} = opts; const {messageSid, serviceProviderSid, accountSid, applicationSid, res} = opts;
srf = res.srf; srf = res.srf;
this.messageSid = messageSid; this.messageSid = messageSid;
this.serviceProviderSid = serviceProviderSid;
this.accountSid = accountSid; this.accountSid = accountSid;
this.applicationSid = applicationSid; this.applicationSid = applicationSid;
this.res = res; this.res = res;
} }
else { else {
// outbound call triggered by REST // 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; srf = req.srf;
this.callSid = callSid; this.callSid = callSid;
this.serviceProviderSid = serviceProviderSid;
this.accountSid = accountSid; this.accountSid = accountSid;
this.applicationSid = applicationSid; this.applicationSid = applicationSid;
this.callStatus = CallStatus.Trying, this.callStatus = CallStatus.Trying,

View File

@@ -201,6 +201,12 @@ class CallSession extends Emitter {
return this.direction === CallDirection.Inbound && this.res.finalResponseSent; return this.direction === CallDirection.Inbound && this.res.finalResponseSent;
} }
/**
* returns the account sid
*/
get serviceProviderSid() {
return this.callInfo.serviceProviderSid;
}
/** /**
* returns the account sid * returns the account sid
*/ */
@@ -520,6 +526,7 @@ class CallSession extends Emitter {
this.logger.info({err}, `malformed google service_key provisioned for account ${sid}`); this.logger.info({err}, `malformed google service_key provisioned for account ${sid}`);
writeAlerts({ writeAlerts({
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_FAILURE,
service_provider_sid: this.serviceProviderSid,
account_sid: this.accountSid, account_sid: this.accountSid,
vendor vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert')); }).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
@@ -550,6 +557,7 @@ class CallSession extends Emitter {
else { else {
writeAlerts({ writeAlerts({
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
service_provider_sid: this.serviceProviderSid,
account_sid: this.accountSid, account_sid: this.accountSid,
vendor vendor
}).catch((err) => this.logger.error({err}, 'Error writing tts alert')); }).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
@@ -1382,8 +1390,13 @@ class CallSession extends Emitter {
} }
else { else {
this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found'); this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid, this.queueEventHookRequestor = new HttpRequestor(
r[0], this.webhook_secret); this.logger,
this.serviceProviderSid,
this.accountSid,
r[0],
this.webhook_secret
);
this.queueEventHook = r[0]; this.queueEventHook = r[0];
} }
} catch (err) { } catch (err) {

View File

@@ -163,6 +163,7 @@ class TaskGather extends Task {
const {writeAlerts, AlertType} = cs.srf.locals; const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`); this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({ writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor vendor: this.vendor
@@ -399,6 +400,7 @@ class TaskGather extends Task {
const {writeAlerts, AlertType} = this.cs.srf.locals; const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error'); this.logger.error(err, 'TaskGather:_startTranscribing error');
writeAlerts({ writeAlerts({
service_provider_sid: this.cs.serviceProviderSid,
account_sid: this.cs.accountSid, account_sid: this.cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
vendor: this.vendor, vendor: this.vendor,

View File

@@ -47,6 +47,7 @@ class TaskSay extends Task {
try { try {
if (!credentials) { if (!credentials) {
writeAlerts({ writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor vendor
@@ -92,6 +93,7 @@ class TaskSay extends Task {
this.logger.info({err}, 'Error synthesizing tts'); this.logger.info({err}, 'Error synthesizing tts');
span.end(); span.end();
writeAlerts({ writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor, vendor,

View File

@@ -86,6 +86,7 @@ class TaskTranscribe extends Task {
const {writeAlerts, AlertType} = cs.srf.locals; const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`); this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({ writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor vendor: this.vendor

View File

@@ -193,6 +193,7 @@ module.exports = (logger) => {
task.emit(AmdEvents.Error, err); task.emit(AmdEvents.Error, err);
logger.error(err, 'amd:_startTranscribing error'); logger.error(err, 'amd:_startTranscribing error');
writeAlerts({ writeAlerts({
service_provider_sid: cs.serviceProviderSid,
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
vendor: vendor, vendor: vendor,

View File

@@ -5,7 +5,7 @@ const timeSeries = require('@jambonz/time-series');
let alerter ; let alerter ;
class BaseRequestor extends Emitter { class BaseRequestor extends Emitter {
constructor(logger, account_sid, hook, secret) { constructor(logger, service_provider_sid, account_sid, hook, secret) {
super(); super();
assert(typeof hook === 'object'); assert(typeof hook === 'object');
@@ -15,6 +15,7 @@ class BaseRequestor extends Emitter {
this.username = hook.username; this.username = hook.username;
this.password = hook.password; this.password = hook.password;
this.secret = secret; this.secret = secret;
this.service_provider_sid = service_provider_sid;
this.account_sid = account_sid; this.account_sid = account_sid;
const {stats} = require('../../').srf.locals; const {stats} = require('../../').srf.locals;

View File

@@ -18,8 +18,8 @@ function basicAuth(username, password) {
class HttpRequestor extends BaseRequestor { class HttpRequestor extends BaseRequestor {
constructor(logger, account_sid, hook, secret) { constructor(logger, service_provider_sid, account_sid, hook, secret) {
super(logger, account_sid, hook, secret); super(logger, service_provider_sid, account_sid, hook, secret);
this.method = hook.method || 'POST'; this.method = hook.method || 'POST';
this.authHeader = basicAuth(hook.username, hook.password); this.authHeader = basicAuth(hook.username, hook.password);
@@ -142,7 +142,7 @@ class HttpRequestor extends BaseRequestor {
this.logger.error({err, baseUrl: this.baseUrl, url}, this.logger.error({err, baseUrl: this.baseUrl, url},
'web callback returned unexpected error'); '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') { if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url}; opts = {...opts, alert_type: this.Alerter.AlertType.WEBHOOK_CONNECTION_FAILURE, url};
} }

View File

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

View File

@@ -9,8 +9,8 @@ const MAX_RECONNECTS = 5;
const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000; const RESPONSE_TIMEOUT_MS = process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT || 5000;
class WsRequestor extends BaseRequestor { class WsRequestor extends BaseRequestor {
constructor(logger, account_sid, hook, secret) { constructor(logger, service_provider_sid, account_sid, hook, secret) {
super(logger, account_sid, hook, secret); super(logger, service_provider_sid, account_sid, hook, secret);
this.connections = 0; this.connections = 0;
this.messagesInFlight = new Map(); this.messagesInFlight = new Map();
this.maliciousClient = false; 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 we have an absolute url, and it is http then do a standard webhook */
if (this._isAbsoluteUrl(url) && url.startsWith('http')) { if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
this.logger.debug({hook}, 'WsRequestor: sending a webhook (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); return requestor.request(type, hook, params, httpHeaders);
} }

View File

@@ -30,7 +30,7 @@
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.4.29", "@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/stats-collector": "^0.1.6", "@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.12", "@jambonz/time-series": "^0.2.0",
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
"@opentelemetry/exporter-jaeger": "^1.3.1", "@opentelemetry/exporter-jaeger": "^1.3.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0", "@opentelemetry/exporter-trace-otlp-http": "^0.27.0",