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 {
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) {

View File

@@ -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: () => {}};
}

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
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);
}

View File

@@ -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",