mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Compare commits
20 Commits
v0.6.0-rc1
...
v0.6.6-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5350f7bea0 | ||
|
|
446cc57e09 | ||
|
|
9525cf5a36 | ||
|
|
43393a2e4a | ||
|
|
a06bba60e6 | ||
|
|
318a8f0822 | ||
|
|
ecdf9898f8 | ||
|
|
e0bacb55e7 | ||
|
|
0eb365ea58 | ||
|
|
f7fcbd4c7c | ||
|
|
bc3b5bb1dc | ||
|
|
a5a759940b | ||
|
|
6c01d28288 | ||
|
|
a3b9727d64 | ||
|
|
ac4ea4b265 | ||
|
|
ec6d2d310a | ||
|
|
7b9390be50 | ||
|
|
0589328f24 | ||
|
|
f66814fff2 | ||
|
|
a79f77934e |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -43,4 +43,7 @@ create_db.sql
|
||||
.vscode
|
||||
|
||||
.env.*
|
||||
.env
|
||||
.env
|
||||
|
||||
test/postgres-data
|
||||
db/remove-account.sh
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Drachtio Communications Services, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
40
db/generate-beta-invite-codes.js
Normal file
40
db/generate-beta-invite-codes.js
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
const crypto = require('crypto');
|
||||
const {promisePool} = require('../lib/db');
|
||||
const sql = 'INSERT INTO beta_invite_codes (invite_code) VALUES (?);';
|
||||
|
||||
const rand_string = (n) => {
|
||||
if (n <= 0) {
|
||||
return '';
|
||||
}
|
||||
var rs = '';
|
||||
try {
|
||||
rs = crypto.randomBytes(Math.ceil(n/2)).toString('hex').slice(0,n);
|
||||
/* note: could do this non-blocking, but still might fail */
|
||||
}
|
||||
catch (ex) {
|
||||
/* known exception cause: depletion of entropy info for randomBytes */
|
||||
console.error('Exception generating random string: ' + ex);
|
||||
/* weaker random fallback */
|
||||
rs = '';
|
||||
var r = n % 8, q = (n - r) / 8, i;
|
||||
for (i = 0; i < q; i++) {
|
||||
rs += Math.random().toString(16).slice(2);
|
||||
}
|
||||
if (r > 0) {
|
||||
rs += Math.random().toString(16).slice(2, i);
|
||||
}
|
||||
}
|
||||
return rs;
|
||||
};
|
||||
|
||||
const doIt = async(len) => {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const val = rand_string(len).toUpperCase();
|
||||
await promisePool.execute(sql, [val]);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
doIt(6);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('reset_admin_password');
|
||||
const {promisePool} = require('../lib/db');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const {generateHashedPassword} = require('../lib/utils/password-utils');
|
||||
@@ -7,18 +6,22 @@ const sqlInsert = `INSERT into users
|
||||
(user_sid, name, email, hashed_password, force_change, provider, email_validated)
|
||||
values (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const sqlChangeAdminToken = `UPDATE api_keys set token = ?
|
||||
WHERE account_sid IS NULL
|
||||
AND service_provider_sid IS NULL`;
|
||||
const sqlQueryAccount = 'SELECT * from accounts LIMIT 1';
|
||||
const sqlAddAccountAdminToken = `INSERT into api_keys (api_key_sid, token, account_sid)
|
||||
const sqlInsertAdminToken = `INSERT into api_keys
|
||||
(api_key_sid, token)
|
||||
values (?, ?)`;
|
||||
const sqlQueryAccount = 'SELECT * from accounts LEFT JOIN api_keys ON api_keys.account_sid = accounts.account_sid';
|
||||
const sqlAddAccountToken = `INSERT into api_keys (api_key_sid, token, account_sid)
|
||||
VALUES (?, ?, ?)`;
|
||||
|
||||
const password = process.env.JAMBONES_ADMIN_INITIAL_PASSWORD || 'admin';
|
||||
console.log(`reset_admin_password, initial admin password is ${password}`);
|
||||
|
||||
const doIt = async() => {
|
||||
const passwordHash = await generateHashedPassword('admin');
|
||||
const passwordHash = await generateHashedPassword(password);
|
||||
const sid = uuidv4();
|
||||
await promisePool.execute('DELETE from users where name = "admin"');
|
||||
await promisePool.execute(sqlInsert,
|
||||
await promisePool.execute('DELETE from api_keys where account_sid is null and service_provider_sid is null');
|
||||
await promisePool.execute(sqlInsert,
|
||||
[
|
||||
sid,
|
||||
'admin',
|
||||
@@ -29,16 +32,16 @@ const doIt = async() => {
|
||||
1
|
||||
]
|
||||
);
|
||||
|
||||
/* reset admin token */
|
||||
const uuid = uuidv4();
|
||||
await promisePool.query(sqlChangeAdminToken, [uuid]);
|
||||
await promisePool.execute(sqlInsertAdminToken, [uuidv4(), uuidv4()]);
|
||||
|
||||
/* create admin token for single account */
|
||||
const api_key_sid = uuidv4();
|
||||
const token = uuidv4();
|
||||
const [r] = await promisePool.query(sqlQueryAccount);
|
||||
await promisePool.execute(sqlAddAccountAdminToken, [api_key_sid, token, r[0].account_sid]);
|
||||
const [r] = await promisePool.query({sql: sqlQueryAccount, nestTables: true});
|
||||
if (1 === r.length && r[0].api_keys.api_key_sid === null) {
|
||||
const api_key_sid = uuidv4();
|
||||
const token = uuidv4();
|
||||
const {account_sid} = r[0].accounts;
|
||||
await promisePool.execute(sqlAddAccountToken, [api_key_sid, token, account_sid]);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
@@ -73,19 +73,6 @@ VALUES
|
||||
('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- Peerless gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0),
|
||||
('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1),
|
||||
('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1);
|
||||
|
||||
-- 382com gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0),
|
||||
('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
|
||||
@@ -51,19 +51,6 @@ VALUES
|
||||
('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0),
|
||||
('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1);
|
||||
|
||||
-- Peerless gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0),
|
||||
('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1),
|
||||
('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1);
|
||||
|
||||
-- 382com gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0),
|
||||
('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1);
|
||||
|
||||
-- simwood gateways
|
||||
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
|
||||
VALUES
|
||||
|
||||
@@ -7,7 +7,9 @@ const {encrypt} = require('../utils/encrypt-decrypt');
|
||||
|
||||
const retrieveSql = `SELECT * from accounts acc
|
||||
LEFT JOIN webhooks AS rh
|
||||
ON acc.registration_hook_sid = rh.webhook_sid`;
|
||||
ON acc.registration_hook_sid = rh.webhook_sid
|
||||
LEFT JOIN webhooks AS qh
|
||||
ON acc.queue_event_hook_sid = qh.webhook_sid`;
|
||||
|
||||
const insertPendingAccountSubscriptionSql = `INSERT account_subscriptions
|
||||
(account_subscription_sid, account_sid, pending, stripe_subscription_id,
|
||||
@@ -55,12 +57,23 @@ AND pending = 0`;
|
||||
function transmogrifyResults(results) {
|
||||
return results.map((row) => {
|
||||
const obj = row.acc;
|
||||
|
||||
/* registration hook */
|
||||
if (row.rh && Object.keys(row.rh).length && row.rh.url !== null) {
|
||||
Object.assign(obj, {registration_hook: row.rh});
|
||||
delete obj.registration_hook.webhook_sid;
|
||||
}
|
||||
else obj.registration_hook = null;
|
||||
delete obj.registration_hook_sid;
|
||||
|
||||
/* queue event hook */
|
||||
if (row.qh && Object.keys(row.qh).length && row.qh.url !== null) {
|
||||
Object.assign(obj, {queue_event_hook: row.qh});
|
||||
delete obj.queue_event_hook.webhook_sid;
|
||||
}
|
||||
else obj.queue_event_hook = null;
|
||||
delete obj.queue_event_hook_sid;
|
||||
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
@@ -248,6 +261,10 @@ Account.fields = [
|
||||
name: 'sip_realm',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'queue_event_hook_sid',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'registration_hook_sid',
|
||||
type: 'string',
|
||||
|
||||
@@ -212,7 +212,7 @@ async function validateCreateCall(logger, sid, req) {
|
||||
|
||||
async function validateCreateMessage(logger, sid, req) {
|
||||
const obj = req.body;
|
||||
//const {lookupAccountByPhoneNumber} = req.app.locals;
|
||||
logger.debug({payload: req.body}, 'validateCreateMessage');
|
||||
|
||||
if (req.user.account_sid !== sid) {
|
||||
throw new DbErrorBadRequest(`unauthorized createMessage request for account ${sid}`);
|
||||
@@ -253,6 +253,9 @@ async function validateAdd(req) {
|
||||
if (req.body.registration_hook && typeof req.body.registration_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'registration_hook\' must be an object when adding an account');
|
||||
}
|
||||
if (req.body.queue_event_hook && typeof req.body.queue_event_hook !== 'object') {
|
||||
throw new DbErrorBadRequest('\'queue_event_hook\' must be an object when adding an account');
|
||||
}
|
||||
}
|
||||
async function validateUpdate(req, sid) {
|
||||
if (req.user.hasAccountAuth && req.user.account_sid !== sid) {
|
||||
@@ -291,12 +294,12 @@ router.post('/', async(req, res) => {
|
||||
await validateAdd(req);
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({webhook_secret: secret}, req.body);
|
||||
for (const prop of ['registration_hook']) {
|
||||
if (obj[prop]) {
|
||||
const obj = {...req.body, webhook_secret: secret};
|
||||
for (const prop of ['registration_hook', 'queue_event_hook']) {
|
||||
if (obj[prop] && obj[prop].url && obj[prop].url.length > 0) {
|
||||
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
|
||||
delete obj[prop];
|
||||
}
|
||||
delete obj[prop];
|
||||
}
|
||||
|
||||
logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
|
||||
@@ -361,12 +364,14 @@ router.put('/:sid', async(req, res) => {
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({}, req.body);
|
||||
if (null !== obj.registration_hook) {
|
||||
for (const prop of ['registration_hook']) {
|
||||
if (prop in obj && Object.keys(obj[prop]).length) {
|
||||
for (const prop of ['registration_hook', 'queue_event_hook']) {
|
||||
if (prop in obj) {
|
||||
if (null === obj[prop] || !obj[prop].url || 0 === obj[prop].url.length) {
|
||||
obj[`${prop}_sid`] = null;
|
||||
}
|
||||
else if (typeof obj[prop] === 'object') {
|
||||
if ('webhook_sid' in obj[prop]) {
|
||||
const sid = obj[prop]['webhook_sid'];
|
||||
delete obj[prop]['webhook_sid'];
|
||||
await Webhook.update(sid, obj[prop]);
|
||||
}
|
||||
else {
|
||||
@@ -374,30 +379,35 @@ router.put('/:sid', async(req, res) => {
|
||||
obj[`${prop}_sid`] = sid;
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj[`${prop}_sid`] = null;
|
||||
}
|
||||
delete obj[prop];
|
||||
}
|
||||
}
|
||||
|
||||
await validateUpdate(req, sid);
|
||||
|
||||
if (Object.keys(obj).length) {
|
||||
let orphanedHook;
|
||||
let orphanedRegHook, orphanedQueueHook;
|
||||
if (null === obj.registration_hook) {
|
||||
const results = await Account.retrieve(sid);
|
||||
if (results.length && results[0].registration_hook_sid) orphanedHook = results[0].registration_hook_sid;
|
||||
if (results.length && results[0].registration_hook_sid) orphanedRegHook = results[0].registration_hook_sid;
|
||||
obj.registration_hook_sid = null;
|
||||
delete obj.registration_hook;
|
||||
}
|
||||
logger.info({obj}, `about to update Account ${sid}`);
|
||||
if (null === obj.queue_event_hook) {
|
||||
const results = await Account.retrieve(sid);
|
||||
if (results.length && results[0].queue_event_hook_sid) orphanedQueueHook = results[0].queue_event_hook_sid;
|
||||
obj.queue_event_hook_sid = null;
|
||||
}
|
||||
delete obj.registration_hook;
|
||||
delete obj.queue_event_hook;
|
||||
|
||||
const rowsAffected = await Account.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
}
|
||||
if (orphanedHook) {
|
||||
await Webhook.remove(orphanedHook);
|
||||
if (orphanedRegHook) {
|
||||
await Webhook.remove(orphanedRegHook);
|
||||
}
|
||||
if (orphanedQueueHook) {
|
||||
await Webhook.remove(orphanedQueueHook);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,7 +652,9 @@ router.post('/:sid/Messages', async(req, res) => {
|
||||
logger.info('No available feature servers to handle createMessage API request');
|
||||
return res.json({msg: 'no available feature servers at this time'}).status(500);
|
||||
}
|
||||
const ip = fs[idx++ % fs.length];
|
||||
let ip = fs[idx++ % fs.length];
|
||||
const arr = /^(.*):\d+$/.exec(ip);
|
||||
if (arr) ip = arr[1];
|
||||
logger.info({fs}, `feature servers available for createMessage API request, selecting ${ip}`);
|
||||
const serviceUrl = `http://${ip}:3000/v1/createMessage/${account_sid}`;
|
||||
await validateCreateMessage(logger, account_sid, req);
|
||||
|
||||
@@ -4,7 +4,6 @@ const ApiKey = require('../../models/api-key');
|
||||
const Account = require('../../models/account');
|
||||
const decorate = require('./decorate');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const assert = require('assert');
|
||||
const sysError = require('../error');
|
||||
const preconditions = {
|
||||
'add': validateAddToken,
|
||||
@@ -71,10 +70,7 @@ async function validateDeleteToken(req, sid) {
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
try {
|
||||
if ('add' in preconditions) {
|
||||
assert(typeof preconditions.add === 'function');
|
||||
await preconditions.add(req);
|
||||
}
|
||||
await validateAddToken(req);
|
||||
const uuid = await ApiKey.make(req.body);
|
||||
res.status(201).json({sid: uuid, token: req.body.token});
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const {DbErrorBadRequest} = require('../../utils/errors');
|
||||
|
||||
const {getHomerApiKey, getHomerSipTrace, getHomerPcap} = require('../../utils/homer-utils');
|
||||
const parseAccountSid = (url) => {
|
||||
const arr = /Accounts\/([^\/]*)/.exec(url);
|
||||
if (arr) return arr[1];
|
||||
@@ -34,4 +34,42 @@ router.get('/', async(req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:call_id', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
try {
|
||||
const token = await getHomerApiKey(logger);
|
||||
if (!token) return res.sendStatus(400, {msg: 'Failed to get Homer API token; check server config'});
|
||||
const obj = await getHomerSipTrace(logger, token, req.params.call_id);
|
||||
if (!obj) {
|
||||
logger.info(`/RecentCalls: unable to get sip traces from Homer for ${req.params.call_id}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.status(200).json(obj);
|
||||
} catch (err) {
|
||||
logger.error({err}, '/RecentCalls error retrieving sip traces from homer');
|
||||
res.sendStatus(err.statusCode || 500);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:call_id/pcap', async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
try {
|
||||
const token = await getHomerApiKey(logger);
|
||||
if (!token) return res.sendStatus(400, {msg: 'getHomerApiKey: Failed to get Homer API token; check server config'});
|
||||
const stream = await getHomerPcap(logger, token, [req.params.call_id]);
|
||||
if (!stream) {
|
||||
logger.info(`getHomerApiKey: unable to get sip traces from Homer for ${req.params.call_id}`);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.set({
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': `attachment; filename=callid-${req.params.call_id}.pcap`
|
||||
});
|
||||
stream.pipe(res);
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getHomerApiKey error retrieving sip traces from homer');
|
||||
res.sendStatus(err.statusCode || 500);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -20,6 +20,13 @@ values (?, ?, ?, ?, ?, 0, 'local', ?)`;
|
||||
const insertAccountSql = `INSERT into accounts
|
||||
(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date)
|
||||
values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`;
|
||||
const insertWebookSql = `INSERT INTO webhooks (webhook_sid, url, method)
|
||||
VALUES (?, ?, ?)`;
|
||||
const insertApplicationSql = `INSERT INTO applications
|
||||
(application_sid, account_sid, name, call_hook_sid, call_status_hook_sid,
|
||||
speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice,
|
||||
speech_recognizer_vendor, speech_recognizer_language)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)`;
|
||||
const queryRootDomainSql = `SELECT root_domain
|
||||
FROM service_providers
|
||||
WHERE service_providers.service_provider_sid = ?`;
|
||||
@@ -149,7 +156,7 @@ router.post('/', async(req, res) => {
|
||||
const user = await doGoogleAuth(logger, req.body);
|
||||
logger.info({user}, 'retrieved user details from google');
|
||||
Object.assign(userProfile, {
|
||||
name: user.name,
|
||||
name: user.name || user.email,
|
||||
email: user.email,
|
||||
email_validated: user.verified_email,
|
||||
picture: user.picture,
|
||||
@@ -281,6 +288,22 @@ router.post('/', async(req, res) => {
|
||||
userProfile.provider_userid);
|
||||
}
|
||||
|
||||
/* add hello-world and dial-time as starter applications */
|
||||
const callStatusSid = uuid();
|
||||
const helloWordSid = uuid();
|
||||
const dialTimeSid = uuid();
|
||||
|
||||
/* 3 webhooks */
|
||||
await promisePool.execute(insertWebookSql, [callStatusSid, 'https://public-apps.jambonz.us/call-status', 'POST']);
|
||||
await promisePool.execute(insertWebookSql, [helloWordSid, 'https://public-apps.jambonz.us/hello-world', 'POST']);
|
||||
await promisePool.execute(insertWebookSql, [dialTimeSid, 'https://public-apps.jambonz.us/dial-time', 'POST']);
|
||||
|
||||
/* 2 applications */
|
||||
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'hello world',
|
||||
helloWordSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
|
||||
await promisePool.execute(insertApplicationSql, [uuid(), userProfile.account_sid, 'dial time clock',
|
||||
dialTimeSid, callStatusSid, 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US']);
|
||||
|
||||
Object.assign(userProfile, {
|
||||
pristine: true,
|
||||
is_active: req.body.provider !== 'local',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const router = require('express').Router();
|
||||
const {promisePool} = require('../../db');
|
||||
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
|
||||
const Webhook = require('../../models/webhook');
|
||||
const ServiceProvider = require('../../models/service-provider');
|
||||
@@ -12,11 +13,29 @@ const decorate = require('./decorate');
|
||||
const preconditions = {
|
||||
'delete': noActiveAccounts
|
||||
};
|
||||
const sqlDeleteSipGateways = `DELETE from sip_gateways
|
||||
WHERE voip_carrier_sid IN (
|
||||
SELECT voip_carrier_sid
|
||||
FROM voip_carriers
|
||||
WHERE service_provider_sid = ?
|
||||
)`;
|
||||
const sqlDeleteSmppGateways = `DELETE from smpp_gateways
|
||||
WHERE voip_carrier_sid IN (
|
||||
SELECT voip_carrier_sid
|
||||
FROM voip_carriers
|
||||
WHERE service_provider_sid = ?
|
||||
)`;
|
||||
|
||||
/* can not delete a service provider if it has any active accounts */
|
||||
async function noActiveAccounts(req, sid) {
|
||||
const activeAccounts = await ServiceProvider.getForeignKeyReferences('accounts.service_provider_sid', sid);
|
||||
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete service provider with active accounts');
|
||||
|
||||
/* ok we can delete -- no active accounts. remove carriers and speech credentials */
|
||||
await promisePool.execute('DELETE from speech_credentials WHERE service_provider_sid = ?', [sid]);
|
||||
await promisePool.query(sqlDeleteSipGateways, [sid]);
|
||||
await promisePool.query(sqlDeleteSmppGateways, [sid]);
|
||||
await promisePool.query('DELETE from voip_carriers WHERE service_provider_sid = ?', [sid]);
|
||||
}
|
||||
|
||||
decorate(router, ServiceProvider, ['delete'], preconditions);
|
||||
|
||||
@@ -14,7 +14,7 @@ const {
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {use_for_stt, use_for_tts, vendor, service_key, access_key_id, secret_access_key, aws_region} = req.body;
|
||||
const {account_sid} = req.user;
|
||||
const account_sid = req.user.account_sid || req.body.account_sid;
|
||||
let service_provider_sid;
|
||||
if (!account_sid) {
|
||||
if (!req.user.hasServiceProviderAuth) {
|
||||
|
||||
@@ -74,12 +74,13 @@ const createTestCdrs = async(writeCdrs, account_sid) => {
|
||||
for (let i = 0 ; i < points; i++) {
|
||||
const attempted_at = new Date(start.getTime() + (i * increment));
|
||||
const failed = 0 === i % 5;
|
||||
const sip_callid = `685cd008-0a66-4974-b37a-bdd6d9a3c4a-${i % 2}`;
|
||||
data.push({
|
||||
call_sid: 'b6f48929-8e86-4d62-ae3b-64fb574d91f6',
|
||||
from: '15083084809',
|
||||
to: '18882349999',
|
||||
answered: !failed,
|
||||
sip_callid: '685cd008-0a66-4974-b37a-bdd6d9a3c4aa@192.168.1.100',
|
||||
sip_callid,
|
||||
sip_status: 200,
|
||||
duration: failed ? 0 : 45,
|
||||
attempted_at: attempted_at.getTime(),
|
||||
@@ -167,6 +168,62 @@ const hasServiceProviderPermissions = (req, res, next) => {
|
||||
});
|
||||
};
|
||||
|
||||
const checkLimits = async(req, res, next) => {
|
||||
const logger = req.app.locals.logger;
|
||||
if (process.env.APPLY_JAMBONZ_DB_LIMITS && req.user.hasScope('account')) {
|
||||
const account_sid = req.user.account_sid;
|
||||
const url = req.originalUrl;
|
||||
let sql;
|
||||
let limit;
|
||||
|
||||
if (/Applications/.test(url)) {
|
||||
limit = 50;
|
||||
sql = 'SELECT count(*) as count from applications where account_sid = ?';
|
||||
}
|
||||
else if (/VoipCarriers/.test(url)) {
|
||||
limit = 10;
|
||||
sql = 'SELECT count(*) as count from voip_carriers where account_sid = ?';
|
||||
}
|
||||
else if (/SipGateways/.test(url)) {
|
||||
limit = 150;
|
||||
sql = `SELECT count(*) as count
|
||||
from sip_gateways
|
||||
where voip_carrier_sid IN (
|
||||
SELECT voip_carrier_sid from voip_carriers
|
||||
where account_sid = ?
|
||||
)`;
|
||||
}
|
||||
else if (/PhoneNumbers/.test(url)) {
|
||||
limit = 200;
|
||||
sql = 'SELECT count(*) as count from phone_numbers where account_sid = ?';
|
||||
}
|
||||
else if (/SpeechCredentials/.test(url)) {
|
||||
limit = 10;
|
||||
sql = 'SELECT count(*) as count from speech_credentials where account_sid = ?';
|
||||
}
|
||||
else if (/ApiKeys/.test(url)) {
|
||||
limit = 10;
|
||||
sql = 'SELECT count(*) as count from api_keys where account_sid = ?';
|
||||
}
|
||||
|
||||
if (sql) {
|
||||
try {
|
||||
const [r] = await promisePool.execute(sql, [account_sid]);
|
||||
if (r[0].count >= limit) {
|
||||
res.status(422).json({
|
||||
status: 'fail',
|
||||
message: `exceeded limits - you have created ${r.count} instances of this resource`
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error checking limits');
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupFreeTrial,
|
||||
createTestCdrs,
|
||||
@@ -174,5 +231,6 @@ module.exports = {
|
||||
parseAccountSid,
|
||||
parseServiceProviderSid,
|
||||
hasAccountPermissions,
|
||||
hasServiceProviderPermissions
|
||||
hasServiceProviderPermissions,
|
||||
checkLimits
|
||||
};
|
||||
|
||||
@@ -5,9 +5,17 @@ const path = require('path');
|
||||
const swaggerDocument = YAML.load(path.resolve(__dirname, '../swagger/swagger.yaml'));
|
||||
const api = require('./api');
|
||||
const stripe = require('./stripe');
|
||||
const {checkLimits} = require('./api/utils');
|
||||
|
||||
const routes = express.Router();
|
||||
|
||||
routes.post([
|
||||
'/v1/Applications',
|
||||
'/v1/VoipCarriers',
|
||||
'/v1/SipGateways',
|
||||
'/v1/PhoneNumbers',
|
||||
'/v1/Accounts'
|
||||
], checkLimits);
|
||||
routes.use('/v1', api);
|
||||
routes.use('/stripe', stripe);
|
||||
routes.use('/swagger', swaggerUi.serve);
|
||||
|
||||
@@ -1958,7 +1958,7 @@ paths:
|
||||
|
||||
put:
|
||||
summary: update tenant
|
||||
operationId: updateAccount
|
||||
operationId: putTenant
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -2004,6 +2004,9 @@ paths:
|
||||
registration_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: authentication webhook for registration
|
||||
queue_event_hook:
|
||||
$ref: '#/components/schemas/Webhook'
|
||||
description: webhook called when members join or leave a queue
|
||||
service_provider_sid:
|
||||
type: string
|
||||
format: uuid
|
||||
@@ -2635,6 +2638,56 @@ paths:
|
||||
- duration
|
||||
404:
|
||||
description: account not found
|
||||
/Accounts/{AccountSid}/RecentCalls/{CallId}:
|
||||
parameters:
|
||||
- name: AccountSid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: CallId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
summary: retrieve sip trace detail for a call
|
||||
operationId: getRecentCallTrace
|
||||
responses:
|
||||
200:
|
||||
description: retrieve sip trace data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
404:
|
||||
description: account or call not found
|
||||
/Accounts/{AccountSid}/RecentCalls/{CallId}/pcap:
|
||||
parameters:
|
||||
- name: AccountSid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: CallId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
summary: retrieve pcap for a call
|
||||
operationId: getRecentCallTrace
|
||||
responses:
|
||||
200:
|
||||
description: retrieve sip trace data
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: object
|
||||
404:
|
||||
description: account or call not found
|
||||
/Accounts/{AccountSid}/Alerts:
|
||||
parameters:
|
||||
- name: AccountSid
|
||||
@@ -3632,6 +3685,8 @@ components:
|
||||
type: string
|
||||
registration_hook_sid:
|
||||
type: string
|
||||
queue_event_hook_sid:
|
||||
type: string
|
||||
device_calling_application_sid:
|
||||
type: string
|
||||
is_active:
|
||||
|
||||
93
lib/utils/homer-utils.js
Normal file
93
lib/utils/homer-utils.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
const bent = require('bent');
|
||||
const basicAuth = (apiKey) => {
|
||||
const header = `Bearer ${apiKey}`;
|
||||
return {Authorization: header};
|
||||
};
|
||||
const postJSON = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 'json', 200, 201);
|
||||
const postPcap = bent(process.env.HOMER_BASE_URL || 'http://127.0.0.1', 'POST', 200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
});
|
||||
const SEVEN_DAYS_IN_MS = (1000 * 3600 * 24 * 7);
|
||||
|
||||
const getHomerApiKey = async(logger) => {
|
||||
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
|
||||
logger.debug('getHomerApiKey: Homer integration not installed');
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = await postJSON('/api/v3/auth', {
|
||||
username: process.env.HOMER_USERNAME,
|
||||
password: process.env.HOMER_PASSWORD
|
||||
});
|
||||
debug(obj);
|
||||
logger.debug({obj}, `getHomerApiKey for user ${process.env.HOMER_USERNAME}`);
|
||||
return obj.token;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
logger.info({err}, `getHomerApiKey: Error retrieving apikey for user ${process.env.HOMER_USERNAME}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getHomerSipTrace = async(logger, apiKey, callId) => {
|
||||
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
|
||||
logger.debug('getHomerSipTrace: Homer integration not installed');
|
||||
}
|
||||
try {
|
||||
const now = Date.now();
|
||||
const obj = await postJSON('/api/v3/call/transaction', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true
|
||||
},
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: [callId]
|
||||
}
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
}, basicAuth(apiKey));
|
||||
return obj;
|
||||
} catch (err) {
|
||||
logger.info({err}, `getHomerSipTrace: Error retrieving messages for callid ${callId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getHomerPcap = async(logger, apiKey, callIds) => {
|
||||
if (!process.env.HOMER_BASE_URL || !process.env.HOMER_USERNAME || !process.env.HOMER_PASSWORD) {
|
||||
logger.debug('getHomerPcap: Homer integration not installed');
|
||||
}
|
||||
try {
|
||||
const now = Date.now();
|
||||
const stream = await postPcap('/api/v3/export/call/messages/pcap', {
|
||||
param: {
|
||||
transaction: {
|
||||
call: true
|
||||
},
|
||||
search: {
|
||||
'1_call': {
|
||||
callid: callIds
|
||||
}
|
||||
},
|
||||
},
|
||||
timestamp: {
|
||||
from: now - SEVEN_DAYS_IN_MS,
|
||||
to: now
|
||||
}
|
||||
}, basicAuth(apiKey));
|
||||
return stream;
|
||||
} catch (err) {
|
||||
logger.info({err}, `getHomerPcap: Error retrieving messages for callid ${callIds}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getHomerApiKey,
|
||||
getHomerSipTrace,
|
||||
getHomerPcap
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"test": "NODE_ENV=test JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ",
|
||||
"test": "NODE_ENV=test APPLY_JAMBONZ_DB_LIMITS=1 JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ",
|
||||
"integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
@@ -28,17 +28,13 @@
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.17.1",
|
||||
"form-data": "^2.3.3",
|
||||
"form-urlencoded": "^4.2.1",
|
||||
"google-libphonenumber": "^3.2.15",
|
||||
"form-urlencoded": "^6.0.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mailgun.js": "^3.3.0",
|
||||
"mysql2": "^2.2.5",
|
||||
"passport": "^0.4.1",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"pino": "^5.17.0",
|
||||
"qs": "^6.7.0",
|
||||
"request": "^2.88.2",
|
||||
"request-debug": "^0.2.0",
|
||||
"short-uuid": "^4.1.0",
|
||||
"stripe": "^8.138.0",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
@@ -49,6 +45,7 @@
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.9",
|
||||
"tape": "^5.2.2"
|
||||
}
|
||||
|
||||
@@ -154,6 +154,10 @@ test('account tests', async(t) => {
|
||||
registration_hook: {
|
||||
url: 'http://example.com/reg2',
|
||||
method: 'get'
|
||||
},
|
||||
queue_event_hook: {
|
||||
url: 'http://example.com/q',
|
||||
method: 'post'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
version: '3'
|
||||
|
||||
networks:
|
||||
jambonz-api:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.58.0.0/16
|
||||
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
@@ -10,7 +18,11 @@ services:
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
retries: 10
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.2
|
||||
|
||||
redis:
|
||||
image: redis:5-alpine
|
||||
ports:
|
||||
@@ -18,8 +30,101 @@ services:
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.3
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.8-alpine
|
||||
ports:
|
||||
- "8086:8086"
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.4
|
||||
|
||||
db:
|
||||
image: postgres:11-alpine
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: homerSeven
|
||||
POSTGRES_USER: root
|
||||
expose:
|
||||
- 5432
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./postgresql/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh
|
||||
- ./postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "psql -h 'localhost' -U 'root' -c '\\l'"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 60
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.5
|
||||
|
||||
heplify-server:
|
||||
image: sipcapture/heplify-server
|
||||
container_name: heplify-server
|
||||
ports:
|
||||
- "9069:9060"
|
||||
- "9060:9060/udp"
|
||||
- "9061:9061/tcp"
|
||||
command:
|
||||
- './heplify-server'
|
||||
environment:
|
||||
- "HEPLIFYSERVER_HEPADDR=0.0.0.0:9060"
|
||||
- "HEPLIFYSERVER_HEPTCPADDR=0.0.0.0:9061"
|
||||
- "HEPLIFYSERVER_DBDRIVER=postgres"
|
||||
- "HEPLIFYSERVER_DBSHEMA=homer7"
|
||||
- "HEPLIFYSERVER_DBADDR=db:5432"
|
||||
- "HEPLIFYSERVER_DBUSER=root"
|
||||
- "HEPLIFYSERVER_DBPASS=homerSeven"
|
||||
- "HEPLIFYSERVER_DBDATATABLE=homer_data"
|
||||
- "HEPLIFYSERVER_DBROTATE=true"
|
||||
- "HEPLIFYSERVER_LOGLVL=debug"
|
||||
- "HEPLIFYSERVER_LOGSTD=true"
|
||||
- "HEPLIFYSERVER_DBDROPDAYS=7"
|
||||
- "HEPLIFYSERVER_ALEGIDS=X-CID"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.6
|
||||
|
||||
homer-webapp:
|
||||
container_name: homer-webapp
|
||||
image: sipcapture/webapp
|
||||
environment:
|
||||
- "DB_HOST=db"
|
||||
- "DB_USER=root"
|
||||
- "DB_PASS=homerSeven"
|
||||
ports:
|
||||
- "9090:80"
|
||||
expose:
|
||||
- 80
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./bootstrap:/app/bootstrap
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.7
|
||||
|
||||
drachtio:
|
||||
container_name: drachtio
|
||||
image: drachtio/drachtio-server:latest
|
||||
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9 --homer 172.58.0.6:9060 --homer-id 10
|
||||
networks:
|
||||
jambonz-api:
|
||||
ipv4_address: 172.58.0.8
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
|
||||
45
test/homer.js
Normal file
45
test/homer.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const test = require('tape') ;
|
||||
const noopLogger = {debug: () => {}, info: () => {}, error: () => {}};
|
||||
const fs = require('fs');
|
||||
|
||||
test('homer tests', async(t, done) => {
|
||||
//const {getHomerApiKey, getHomerSipTrace, getHomerPcap} = require('../lib/utils/homer-utils');
|
||||
if (process.env.HOMER_BASE_URL && process.env.HOMER_USERNAME && process.env.HOMER_PASSWORD) {
|
||||
try {
|
||||
/* get a token */
|
||||
/*
|
||||
let token = await getHomerApiKey(noopLogger);
|
||||
console.log(token);
|
||||
t.ok(token, 'successfully created an api key for homer');
|
||||
const result = await getHomerSipTrace(noopLogger, token, '224f0f24-69aa-123a-eaa6-0ea24be4d211');
|
||||
console.log(`got trace: ${JSON.stringify(result)}`);
|
||||
|
||||
var writeStream = fs.createWriteStream('./call.pcap');
|
||||
const stream = await getHomerPcap(noopLogger, token, ['224f0f24-69aa-123a-eaa6-0ea24be4d211']);
|
||||
stream.pipe(writeStream);
|
||||
stream.on('end', () => {
|
||||
console.log('finished writing');
|
||||
done();
|
||||
});
|
||||
*/
|
||||
|
||||
let result = await request.get('/RecentCalls/224f0f24-69aa-123a-eaa6-0ea24be4d211', {
|
||||
resolveWithFullResponse: true,
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
tenant_fqdn: 'foo.bar.baz'
|
||||
}
|
||||
});
|
||||
t.ok(result.statusCode === 201, 'successfully added ms teams tenant');
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
t.end(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,4 +13,5 @@ require('./ms-teams');
|
||||
require('./speech-credentials');
|
||||
require('./recent-calls');
|
||||
require('./webapp_tests');
|
||||
//require('./homer');
|
||||
require('./docker_stop');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('GET', 200);
|
||||
const request = require('request');
|
||||
require('request-debug')(request);
|
||||
|
||||
const test = async() => {
|
||||
request.get('https://api.github.com/user', {
|
||||
|
||||
6
test/postgresql/init-user-db.sh
Executable file
6
test/postgresql/init-user-db.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
CREATE DATABASE homer_config;
|
||||
EOSQL
|
||||
@@ -66,6 +66,22 @@ test('recent calls tests', async(t) => {
|
||||
json: true,
|
||||
});
|
||||
|
||||
/* pull sip traces and pcap from homer */
|
||||
/*
|
||||
result = await request.get(`/Accounts/${account_sid}/RecentCalls/224f0f24-69aa-123a-eaa6-0ea24be4d211`, {
|
||||
auth: authUser,
|
||||
json: true
|
||||
});
|
||||
console.log(result);
|
||||
|
||||
const writeStream = fs.createWriteStream('./call.pcap');
|
||||
const ret = await request.get(`/Accounts/${account_sid}/RecentCalls/224f0f24-69aa-123a-eaa6-0ea24be4d211/pcap`, {
|
||||
auth: authUser,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
writeStream.write(ret.body);
|
||||
*/
|
||||
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
|
||||
|
||||
|
||||
68
test/scenarios/uac.xml
Normal file
68
test/scenarios/uac.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<scenario name="UAC with media">
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
INVITE sip:+15083871234@echo.sip.jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:15083871234@echo.sip.jambonz.org>
|
||||
Call-ID: 685cd008-0a66-4974-b37a-bdd6d9a3c4a-0
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
v=0
|
||||
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
||||
s=-
|
||||
c=IN IP[local_ip_type] [local_ip]
|
||||
t=0 0
|
||||
m=audio [auto_media_port] RTP/AVP 8 101
|
||||
a=rtpmap:8 PCMA/8000
|
||||
a=rtpmap:101 telephone-event/8000
|
||||
a=fmtp:101 0-11,16
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="100" optional="true">
|
||||
</recv>
|
||||
|
||||
|
||||
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
|
||||
<!-- are saved and used for following messages sent. Useful to test -->
|
||||
<!-- against stateful SIP proxies/B2BUAs. -->
|
||||
<recv response="503" rtd="true" crlf="true">
|
||||
</recv>
|
||||
|
||||
<!-- Packet lost can be simulated in any send/recv message by -->
|
||||
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
|
||||
<send>
|
||||
<![CDATA[
|
||||
|
||||
ACK sip:15083871234@echo.sip.jambonz.org SIP/2.0
|
||||
[last_Via]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:15083871234@echo.sip.jambonz.org>[peer_tag_param]
|
||||
Call-ID: 685cd008-0a66-4974-b37a-bdd6d9a3c4a-0
|
||||
CSeq: 1 ACK
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-max-call-limit
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
|
||||
<!-- definition of the response time repartition table (unit is ms) -->
|
||||
<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
|
||||
|
||||
<!-- definition of the call length repartition table (unit is ms) -->
|
||||
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
|
||||
|
||||
</scenario>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const exec = require('child_process').exec ;
|
||||
const { sippUac } = require('./sipp')('test_jambonz-api');
|
||||
let stopping = false;
|
||||
|
||||
process.on('SIGINT', async() => {
|
||||
@@ -66,6 +67,14 @@ const resetAdminPassword = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const generateSipTrace = async() => {
|
||||
try {
|
||||
await sippUac('uac.xml', '172.58.0.30');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
const stopDocker = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('stopping docker network..')
|
||||
@@ -81,6 +90,7 @@ startDocker()
|
||||
.then(createSchema)
|
||||
.then(seedDb)
|
||||
.then(resetAdminPassword)
|
||||
.then(generateSipTrace)
|
||||
.then(() => {
|
||||
console.log('ready for testing!');
|
||||
require('..');
|
||||
|
||||
68
test/sipp.js
Normal file
68
test/sipp.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { spawn } = require('child_process');
|
||||
const debug = require('debug')('jambonz:ci');
|
||||
let network;
|
||||
const obj = {};
|
||||
let output = '';
|
||||
let idx = 1;
|
||||
|
||||
function clearOutput() {
|
||||
output = '';
|
||||
}
|
||||
|
||||
function addOutput(str) {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str.charCodeAt(i) < 128) output += str.charAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (networkName) => {
|
||||
network = networkName ;
|
||||
return obj;
|
||||
};
|
||||
|
||||
obj.output = () => {
|
||||
return output;
|
||||
};
|
||||
|
||||
obj.sippUac = (file, bindAddress) => {
|
||||
const cmd = 'docker';
|
||||
const args = [
|
||||
'run', '--rm', '--net', `${network}`,
|
||||
'-v', `${__dirname}/scenarios:/tmp/scenarios`,
|
||||
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
|
||||
'-m', '1',
|
||||
'-sleep', '250ms',
|
||||
'-nostdin',
|
||||
'-cid_str', `%u-%p@%s-${idx++}`,
|
||||
'drachtio'
|
||||
];
|
||||
|
||||
if (bindAddress) args.splice(4, 0, '--ip', bindAddress);
|
||||
|
||||
//console.log(args.join(' '));
|
||||
clearOutput();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child_process = spawn(cmd, args, {stdio: ['inherit', 'pipe', 'pipe']});
|
||||
|
||||
child_process.on('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
return resolve();
|
||||
}
|
||||
console.log(`sipp exited with non-zero code ${code} signal ${signal}`);
|
||||
reject(code);
|
||||
});
|
||||
child_process.on('error', (error) => {
|
||||
console.log(`error spawing child process for docker: ${args}`);
|
||||
});
|
||||
|
||||
child_process.stdout.on('data', (data) => {
|
||||
debug(`stderr: ${data}`);
|
||||
addOutput(data.toString());
|
||||
});
|
||||
child_process.stderr.on('data', (data) => {
|
||||
debug(`stderr: ${data}`);
|
||||
addOutput(data.toString());
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user