mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
major revamp of http client functionalit
This commit is contained in:
10
lib/utils/basic-auth.js
Normal file
10
lib/utils/basic-auth.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
||||
|
||||
module.exports = (auth) => {
|
||||
if (!auth || !auth.username ||
|
||||
typeof auth.username !== 'string' ||
|
||||
(auth.password && typeof auth.password !== 'string')) return {};
|
||||
const creds = `${auth.username}:${auth.password || ''}`;
|
||||
const header = `Basic ${toBase64(creds)}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
42
lib/utils/dtmf-collector.js
Normal file
42
lib/utils/dtmf-collector.js
Normal file
@@ -0,0 +1,42 @@
|
||||
class DtmfEntry {
|
||||
constructor(key, time) {
|
||||
this.key = key;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @classdesc Represents an object that collects dtmf key entries and
|
||||
* reports when a match is detected
|
||||
*/
|
||||
class DtmfCollector {
|
||||
constructor({logger, patterns, interDigitTimeout}) {
|
||||
this.logger = logger;
|
||||
this.patterns = patterns;
|
||||
this.idt = interDigitTimeout || 3000;
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
keyPress(key) {
|
||||
const now = Date.now();
|
||||
|
||||
// age out previous entries if interdigit timer has elapsed
|
||||
const lastDtmf = this.buffer.pop();
|
||||
if (lastDtmf) {
|
||||
if (now - lastDtmf.time < this.idt) this.buffer.push(lastDtmf);
|
||||
else {
|
||||
this.buffer = [];
|
||||
}
|
||||
}
|
||||
// add new entry
|
||||
this.buffer.push(new DtmfEntry(key, now));
|
||||
|
||||
// check for a match
|
||||
const collectedDigits = this.buffer
|
||||
.map((entry) => entry.key)
|
||||
.join('');
|
||||
return this.patterns.find((pattern) => collectedDigits.endsWith(pattern));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DtmfCollector;
|
||||
@@ -1,52 +0,0 @@
|
||||
const request = require('request');
|
||||
//require('request-debug')(request);
|
||||
const retrieveApp = require('./retrieve-app');
|
||||
|
||||
function hooks(logger, callInfo) {
|
||||
function actionHook(hook, obj = {}, expectResponse = true) {
|
||||
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.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) opts.auth = auth;
|
||||
if ('POST' === method) opts.body = data;
|
||||
else opts.qs = data;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) {
|
||||
logger.info(`actionHook error ${method} ${hook.url}: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
if (body && expectResponse) {
|
||||
logger.debug(body, `actionHook response ${method} ${hook.url}`);
|
||||
return resolve(retrieveApp(logger, body));
|
||||
}
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function notifyHook(hook, opts = {}) {
|
||||
return actionHook(hook, opts, false);
|
||||
}
|
||||
|
||||
return {
|
||||
actionHook,
|
||||
notifyHook
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = hooks;
|
||||
@@ -5,9 +5,7 @@ const {TaskPreconditions, CallDirection} = require('../utils/constants');
|
||||
const CallInfo = require('../session/call-info');
|
||||
const assert = require('assert');
|
||||
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 {
|
||||
@@ -20,8 +18,7 @@ class SingleDialer extends Emitter {
|
||||
this.sbcAddress = sbcAddress;
|
||||
this.opts = opts;
|
||||
this.application = application;
|
||||
this.url = target.url;
|
||||
this.method = target.method;
|
||||
this.confirmHook = target.confirmHook;
|
||||
|
||||
this.bindings = logger.bindings();
|
||||
|
||||
@@ -37,6 +34,22 @@ class SingleDialer extends Emitter {
|
||||
return this.callInfo.callStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http requests within this session
|
||||
*/
|
||||
get requestor() {
|
||||
assert(this.application.requestor);
|
||||
return this.application.requestor;
|
||||
}
|
||||
|
||||
/**
|
||||
* can be used for all http call status notifications within this session
|
||||
*/
|
||||
get notifier() {
|
||||
assert(this.application.notifier);
|
||||
return this.application.notifier;
|
||||
}
|
||||
|
||||
async exec(srf, ms, opts) {
|
||||
let uri, to;
|
||||
switch (this.target.type) {
|
||||
@@ -106,9 +119,6 @@ class SingleDialer extends Emitter {
|
||||
callId: this.callInfo.callId
|
||||
});
|
||||
this.inviteInProgress = req;
|
||||
const {actionHook, notifyHook} = hooks(this.logger, this.callInfo);
|
||||
this.actionHook = actionHook;
|
||||
this.notifyHook = notifyHook;
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Trying, sipStatus: 100});
|
||||
},
|
||||
cbProvisional: (prov) => {
|
||||
@@ -135,7 +145,7 @@ class SingleDialer extends Emitter {
|
||||
this.ep.destroy();
|
||||
});
|
||||
|
||||
if (this.url) this._executeApp(this.url);
|
||||
if (this.confirmHook) this._executeApp(this.confirmHook);
|
||||
else this.emit('accept');
|
||||
} catch (err) {
|
||||
const status = {callStatus: CallStatus.Failed};
|
||||
@@ -178,29 +188,12 @@ class SingleDialer extends Emitter {
|
||||
* Note: the application to run may not include a dial or sip:decline verb
|
||||
* @param {*} url - url for application
|
||||
*/
|
||||
async _executeApp(url) {
|
||||
this.logger.debug(`SingleDialer:_executeApp: executing ${url} after connect`);
|
||||
async _executeApp(confirmHook) {
|
||||
try {
|
||||
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 {
|
||||
const u = parseUrl(url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
app.originalRequest = {
|
||||
baseUrl: `${u.protocol}://${u.resource}${myPort}`
|
||||
};
|
||||
method = this.method || 'POST';
|
||||
}
|
||||
// retrieve set of tasks
|
||||
const tasks = await this.requestor.request(confirmHook, this.callInfo);
|
||||
|
||||
const tasks = await this.actionHook({url, method, auth});
|
||||
// verify it contains only allowed verbs
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
return [
|
||||
TaskPreconditions.StableCall,
|
||||
@@ -211,16 +204,19 @@ class SingleDialer extends Emitter {
|
||||
throw new Error('unsupported verb in dial url');
|
||||
}
|
||||
|
||||
// now execute it in a new ConfirmCallSession
|
||||
this.logger.debug(`SingleDialer:_executeApp: executing ${tasks.length} tasks`);
|
||||
const cs = new ConfirmCallSession({
|
||||
logger: this.logger,
|
||||
application: app,
|
||||
application: this.application,
|
||||
dlg: this.dlg,
|
||||
ep: this.ep,
|
||||
callInfo: this.callInfo,
|
||||
tasks
|
||||
});
|
||||
await cs.exec();
|
||||
|
||||
// still connected after app is completed? Signal parent call we are good
|
||||
this.emit(this.dlg.connected ? 'accept' : 'decline');
|
||||
} catch (err) {
|
||||
this.logger.debug(err, 'SingleDialer:_executeApp: error');
|
||||
@@ -233,6 +229,7 @@ class SingleDialer extends Emitter {
|
||||
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 {
|
||||
|
||||
64
lib/utils/requestor.js
Normal file
64
lib/utils/requestor.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const bent = require('bent');
|
||||
const parseUrl = require('parse-url');
|
||||
const basicAuth = require('./basic-auth');
|
||||
const assert = require('assert');
|
||||
|
||||
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, hook) {
|
||||
this.logger = logger;
|
||||
this.url = hook.url;
|
||||
this.method = hook.method || 'POST';
|
||||
this.authHeader = basicAuth(hook.auth);
|
||||
|
||||
const u = parseUrl(this.url);
|
||||
const myPort = u.port ? `:${u.port}` : '';
|
||||
const baseUrl = `${u.protocol}://${u.resource}${myPort}`;
|
||||
|
||||
this.get = bent(baseUrl, 'GET', 'json', 200);
|
||||
this.post = bent(baseUrl, 'POST', 'json', 200);
|
||||
|
||||
assert(isAbsoluteUrl(this.url));
|
||||
assert(['GET', 'POST'].includes(this.method));
|
||||
assert(!this.auth || typeof auth == 'object');
|
||||
}
|
||||
|
||||
get hasAuth() {
|
||||
return 'Authorization' in this.authHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {object} [params] - request parameters
|
||||
*/
|
||||
async request(hook, params) {
|
||||
params = params || null;
|
||||
if (isRelativeUrl(hook)) {
|
||||
this.logger.debug({params}, `Requestor:request relative url ${hook}`);
|
||||
return await this.post(hook, params, this.authHeader);
|
||||
}
|
||||
const url = hook.url;
|
||||
const method = hook.method || 'POST';
|
||||
const authHeader = isRelativeUrl(url) ? this.authHeader : basicAuth(hook.auth);
|
||||
|
||||
assert(url);
|
||||
assert(['GET', 'POST'].includes(method));
|
||||
return await this[method.toLowerCase()](url, params, authHeader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Requestor;
|
||||
@@ -1,31 +0,0 @@
|
||||
const request = require('request');
|
||||
//require('request-debug')(request);
|
||||
const makeTask = require('../tasks/make_task');
|
||||
const normalizeJamones = require('./normalize-jamones');
|
||||
|
||||
|
||||
function retrieveUrl(logger, url, method, auth, obj) {
|
||||
const opts = {url, method, auth, json: true};
|
||||
if (method === 'GET') Object.assign(opts, {qs: obj});
|
||||
else Object.assign(opts, {body: obj});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) throw err;
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function retrieveApp(logger, url, method, auth, obj) {
|
||||
let json;
|
||||
|
||||
if (typeof url === 'object') json = url;
|
||||
else json = await retrieveUrl(logger, url, method, auth, obj);
|
||||
return normalizeJamones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||
}
|
||||
|
||||
module.exports = retrieveApp;
|
||||
3
lib/utils/summarize-tasks.js
Normal file
3
lib/utils/summarize-tasks.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = function(tasks) {
|
||||
return `[${tasks.map((t) => t.name).join(',')}]`;
|
||||
};
|
||||
Reference in New Issue
Block a user