Compare commits

..

1 Commits

Author SHA1 Message Date
Dave Horton
f66aabba53 handle PUT of VoipCarrier that is associated to a service provider 2021-06-18 12:13:38 -04:00
68 changed files with 402 additions and 15649 deletions

View File

@@ -1 +0,0 @@
node_modules

View File

@@ -8,7 +8,7 @@
"jsx": false,
"modules": false
},
"ecmaVersion": 2020
"ecmaVersion": 2018
},
"plugins": ["promise"],
"rules": {

View File

@@ -1,51 +0,0 @@
name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: db-create
jobs:
push:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build . --file Dockerfile.db-create --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

View File

@@ -1,51 +0,0 @@
name: Docker
on:
push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: api-server
jobs:
push:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION

6
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Logs
logs
*.log
package-lock.json
# Runtime data
pids
@@ -42,7 +43,4 @@ create_db.sql
.vscode
.env.*
.env
test/postgres-data
db/remove-account.sh
.env

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run jslint

View File

@@ -1,23 +1,16 @@
FROM --platform=linux/amd64 node:18.8.0-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
FROM node:alpine as builder
RUN apk update && apk add --no-cache python make g++
WORKDIR /opt/app/
COPY package.json ./
RUN npm install
RUN npm prune
FROM base as build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
FROM base
COPY --from=build /opt/app /opt/app/
FROM node:alpine as app
WORKDIR /opt/app
COPY . /opt/app
COPY --from=builder /opt/app/node_modules ./node_modules
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]
CMD [ "npm", "start" ]

View File

@@ -1,23 +0,0 @@
FROM --platform=linux/amd64 node:18.8.0-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base as build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "npm", "run", "upgrade-db" ]

21
LICENSE
View File

@@ -1,21 +0,0 @@
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.

36
app.js
View File

@@ -9,8 +9,6 @@ const opts = Object.assign({
const logger = require('pino')(opts);
const express = require('express');
const app = express();
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const passport = require('passport');
const routes = require('./lib/routes');
@@ -21,15 +19,7 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
const {
queryCdrs,
queryCdrsSP,
queryAlerts,
queryAlertsSP,
writeCdrs,
writeAlerts,
AlertType
} = require('@jambonz/time-series')(
const {queryCdrs, queryAlerts, writeCdrs, writeAlerts, AlertType} = require('@jambonz/time-series')(
logger, process.env.JAMBONES_TIME_SERIES_HOST
);
const {
@@ -86,9 +76,7 @@ app.locals = {
lookupSipGatewayBySid,
lookupSmppGatewayBySid,
queryCdrs,
queryCdrsSP,
queryAlerts,
queryAlertsSP,
writeCdrs,
writeAlerts,
AlertType
@@ -101,28 +89,6 @@ const unless = (paths, middleware) => {
};
};
const limiter = rateLimit({
windowMs: (process.env.RATE_LIMIT_WINDOWS_MINS || 5) * 60 * 1000, // 5 minutes
max: process.env.RATE_LIMIT_MAX_PER_WINDOW || 600, // Limit each IP to 600 requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
if (process.env.JAMBONES_TRUST_PROXY) {
const proxyCount = parseInt(process.env.JAMBONES_TRUST_PROXY);
if (!isNaN(proxyCount) && proxyCount > 0) {
logger.info(`setting trust proxy to ${proxyCount} and mounting endpoint /ip`);
app.set('trust proxy', proxyCount);
app.get('/ip', (req, res) => {
logger.info({headers: req.headers}, 'received GET /ip');
res.send(req.ip);
});
}
}
app.use(limiter);
app.use(helmet());
app.use(helmet.hidePoweredBy());
app.use(passport.initialize());
app.use(cors());
app.use(express.urlencoded({extended: true}));
app.use(unless(['/stripe'], express.json()));

View File

@@ -1,40 +0,0 @@
#!/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);

View File

@@ -8,8 +8,6 @@ DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS account_subscriptions;
DROP TABLE IF EXISTS account_limits;
DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes;
@@ -22,16 +20,12 @@ DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways;
DROP TABLE IF EXISTS predefined_carriers;
DROP TABLE IF EXISTS account_offers;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS schema_version;
DROP TABLE IF EXISTS api_keys;
DROP TABLE IF EXISTS sbc_addresses;
@@ -40,8 +34,6 @@ DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS service_provider_limits;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS speech_credentials;
@@ -92,15 +84,6 @@ pending_reason VARBINARY(52),
PRIMARY KEY (account_subscription_sid)
);
CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
CREATE TABLE beta_invite_codes
(
invite_code CHAR(6) NOT NULL UNIQUE ,
@@ -165,20 +148,6 @@ predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_sip_gateway_sid)
);
CREATE TABLE predefined_smpp_gateways
(
predefined_smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. ',
port INTEGER NOT NULL DEFAULT 2775 COMMENT 'smpp signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound SMS from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'i',
netmask INTEGER NOT NULL DEFAULT 32,
is_primary BOOLEAN NOT NULL DEFAULT 1,
use_tls BOOLEAN DEFAULT 0,
predefined_carrier_sid CHAR(36) NOT NULL,
PRIMARY KEY (predefined_smpp_gateway_sid)
);
CREATE TABLE products
(
product_sid CHAR(36) NOT NULL UNIQUE ,
@@ -205,11 +174,6 @@ stripe_product_id VARCHAR(56) NOT NULL,
PRIMARY KEY (account_offer_sid)
);
CREATE TABLE schema_version
(
version VARCHAR(16)
);
CREATE TABLE api_keys
(
api_key_sid CHAR(36) NOT NULL UNIQUE ,
@@ -249,15 +213,6 @@ signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (email)
);
CREATE TABLE service_provider_limits
(
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
CREATE TABLE smpp_addresses
(
smpp_address_sid CHAR(36) NOT NULL UNIQUE ,
@@ -440,11 +395,6 @@ disable_cdrs BOOLEAN NOT NULL DEFAULT 0,
trial_end_date DATETIME,
deactivated_reason VARCHAR(255),
device_to_call_ratio INTEGER NOT NULL DEFAULT 5,
subspace_client_id VARCHAR(255),
subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36),
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
@@ -456,27 +406,20 @@ CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subs
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid);
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX account_sid_idx ON account_limits (account_sid);
ALTER TABLE account_limits ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX predefined_smpp_gateway_sid_idx ON predefined_smpp_gateways (predefined_smpp_gateway_sid);
CREATE INDEX predefined_carrier_sid_idx ON predefined_smpp_gateways (predefined_carrier_sid);
ALTER TABLE predefined_smpp_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk_1 (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
CREATE INDEX product_sid_idx ON products (product_sid);
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
@@ -486,14 +429,14 @@ ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REF
CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid);
CREATE INDEX account_sid_idx ON account_offers (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX product_sid_idx ON account_offers (product_sid);
ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid);
CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid);
CREATE INDEX account_sid_idx ON api_keys (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid);
ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -507,44 +450,41 @@ ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_
CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
CREATE INDEX email_idx ON signup_history (email);
CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid);
ALTER TABLE service_provider_limits ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid) ON DELETE CASCADE;
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
CREATE INDEX account_sid_idx ON users (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON users (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX email_activation_code_idx ON users (email_activation_code);
CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid);
CREATE INDEX account_sid_idx ON voip_carriers (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
@@ -557,12 +497,12 @@ CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid);
CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
@@ -578,10 +518,10 @@ CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name);
CREATE INDEX application_sid_idx ON applications (application_sid);
CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE INDEX account_sid_idx ON applications (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_12 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid);
ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -597,7 +537,7 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis
CREATE INDEX account_sid_idx ON accounts (account_sid);
CREATE INDEX sip_realm_idx ON accounts (sip_realm);
CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_10 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid);
@@ -605,6 +545,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;

File diff suppressed because one or more lines are too long

View File

@@ -1,27 +1,24 @@
#!/usr/bin/env node
console.log('reset_admin_password');
const {promisePool} = require('../lib/db');
const { v4: uuidv4 } = require('uuid');
const uuidv4 = require('uuid/v4');
const {generateHashedPassword} = require('../lib/utils/password-utils');
const sqlInsert = `INSERT into users
(user_sid, name, email, hashed_password, force_change, provider, email_validated)
values (?, ?, ?, ?, ?, ?, ?)
`;
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)
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)
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(password);
const passwordHash = await generateHashedPassword('admin');
const sid = uuidv4();
await promisePool.execute('DELETE from users where name = "admin"');
await promisePool.execute('DELETE from api_keys where account_sid is null and service_provider_sid is null');
await promisePool.execute(sqlInsert,
await promisePool.execute(sqlInsert,
[
sid,
'admin',
@@ -32,16 +29,16 @@ const doIt = async() => {
1
]
);
await promisePool.execute(sqlInsertAdminToken, [uuidv4(), uuidv4()]);
/* reset admin token */
const uuid = uuidv4();
await promisePool.query(sqlChangeAdminToken, [uuid]);
/* create admin token for single account */
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]);
}
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]);
process.exit(0);
};

View File

@@ -44,23 +44,12 @@ insert into predefined_carriers (predefined_carrier_sid, name, requires_static_i
requires_register, register_username, register_password,
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
VALUES
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL),
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'Peerless Network (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL),
('bdf70650-5328-47aa-b3d0-47cb219d9c6e', '382 Communications (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL),
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
-- TelecomXchange gateways
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0),
('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1);
insert into predefined_smpp_gateways (predefined_smpp_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
('9b72467a-cfe3-491f-80bf-652c38e666b9', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'smpp01.telecomsxchange.com', 32, 2776, 0, 1),
('d22883b9-f124-4a89-bab2-4487cf783f64', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.11', 32, 2775, 1, 0),
('fdcf7f1e-1f5f-487b-afb3-c0f75ed0aa3d', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 2775, 1, 0);
-- twilio gateways
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
@@ -85,6 +74,19 @@ 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
@@ -97,5 +99,4 @@ VALUES
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -49,12 +49,6 @@ VALUES
('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0),
('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1);
insert into predefined_smpp_gateways (predefined_smpp_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
('9b72467a-cfe3-491f-80bf-652c38e666b9', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'smpp01.telecomsxchange.com', 32, 2776, 0, 1),
('d22883b9-f124-4a89-bab2-4487cf783f64', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.11', 32, 2775, 1, 0),
('fdcf7f1e-1f5f-487b-afb3-c0f75ed0aa3d', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 2775, 1, 0);
-- twilio gateways
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
@@ -79,6 +73,19 @@ 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

View File

@@ -1,8 +1,6 @@
SET FOREIGN_KEY_CHECKS=0;
-- create one service provider
insert into service_providers (service_provider_sid, name, description, root_domain)
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'sip.jambonz.xyz', 'jambonz.xyz service provider', 'sip.jambonz.xyz');
values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'sip.jambonz.us', 'jambonz.us service provider', 'sip.jambonz.us');
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
@@ -18,9 +16,9 @@ insert into predefined_carriers (predefined_carrier_sid, name, requires_static_i
requires_register, register_username, register_password,
register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion)
VALUES
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL),
('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '<your-twilio-credential-username>', '<your-twilio-credential-password>', NULL, NULL, NULL, NULL, NULL),
('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '<your-voxbone-outbound-username>', '<your-voxbone-outbound-password>', NULL, NULL, NULL, NULL, '<your-voxbone-DID>'),
('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL),
('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '<your-simwood-auth-trunk-username>', '<your-simwood-auth-trunk-password>', NULL, NULL, NULL, NULL, NULL);
-- TelecomXchange gateways
@@ -29,12 +27,6 @@ VALUES
('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0),
('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1);
insert into predefined_smpp_gateways (predefined_smpp_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
('9b72467a-cfe3-491f-80bf-652c38e666b9', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'smpp01.telecomsxchange.com', 32, 2776, 0, 1),
('d22883b9-f124-4a89-bab2-4487cf783f64', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.11', 32, 2775, 1, 0),
('fdcf7f1e-1f5f-487b-afb3-c0f75ed0aa3d', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 2775, 1, 0);
-- twilio gateways
insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound)
VALUES
@@ -59,6 +51,19 @@ 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
@@ -71,4 +76,3 @@ VALUES
('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0),
('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1);
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env node
/* eslint-disable max-len */
const assert = require('assert');
const mysql = require('mysql2/promise');
const {readFile} = require('fs/promises');
const {execSync} = require('child_process');
const {version:desiredVersion} = require('../package.json');
const logger = require('pino')();
logger.info(`upgrade-jambonz-db: desired version ${desiredVersion}`);
assert.ok(process.env.JAMBONES_MYSQL_HOST, 'missing env JAMBONES_MYSQL_HOST');
assert.ok(process.env.JAMBONES_MYSQL_DATABASE, 'missing env JAMBONES_MYSQL_DATABASE');
assert.ok(process.env.JAMBONES_MYSQL_PASSWORD, 'missing env JAMBONES_MYSQL_PASSWORD');
assert.ok(process.env.JAMBONES_MYSQL_USER, 'missing env JAMBONES_MYSQL_USER');
const opts = {
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
multipleStatements: true
};
const sql = {
'7006': [
'ALTER TABLE `accounts` ADD COLUMN `siprec_hook_sid` CHAR(36)',
'ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid)'
],
'7007': [
`CREATE TABLE service_provider_limits
(service_provider_limits_sid CHAR(36) NOT NULL UNIQUE,
service_provider_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
)`,
`CREATE TABLE account_limits
(
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
)`,
'CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid)',
`ALTER TABLE service_provider_limits
ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid)
REFERENCES service_providers (service_provider_sid)
ON DELETE CASCADE`,
'CREATE INDEX account_sid_idx ON account_limits (account_sid)',
`ALTER TABLE account_limits
ADD FOREIGN KEY account_sid_idxfk_2 (account_sid)
REFERENCES accounts (account_sid)
ON DELETE CASCADE`
]
};
const doIt = async() => {
let connection;
try {
logger.info({opts}, 'connecting to mysql database..');
connection = await mysql.createConnection(opts);
} catch (err) {
logger.error({err}, 'Error connecting to database with provided env vars');
process.exit(1);
}
try {
/* does the schema exist at all ? */
const [r] = await connection.execute('SELECT version from schema_version');
let errors = 0;
if (r.length) {
const {version} = r[0];
const arr = /v?(\d+)\.(\d+)\.(\d+)/.exec(version);
if (arr) {
const upgrades = [];
logger.info(`performing schema migration: ${version} => ${desiredVersion}`);
const val = (1000 * arr[1]) + (100 * arr[2]) + arr[3];
logger.info(`current schema value: ${val}`);
if (val < 7006) upgrades.push(...sql['7006']);
if (val < 7007) upgrades.push(...sql['7007']);
// perform all upgrades
logger.info({upgrades}, 'applying schema upgrades..');
for (const upgrade of upgrades) {
try {
await connection.execute(upgrade);
} catch (err) {
errors++;
logger.info({statement:upgrade, err}, 'Error applying statement');
}
}
}
if (errors === 0) await connection.execute(`UPDATE schema_version SET version = '${desiredVersion}'`);
await connection.end();
logger.info(`schema migration to ${desiredVersion} completed with ${errors} errors`);
return;
}
} catch (err) {
}
try {
await createSchema(connection);
await seedDatabase(connection);
logger.info('reset admin password..');
execSync(`${__dirname}/../db/reset_admin_password.js`);
await connection.query(`INSERT into schema_version (version) values('${desiredVersion}')`);
logger.info('database install/upgrade complete.');
await connection.end();
} catch (err) {
logger.error({err}, 'Error seeding database');
process.exit(1);
}
};
const createSchema = async(connection) => {
logger.info('reading schema..');
const sql = await readFile(`${__dirname}/../db/jambones-sql.sql`, {encoding: 'utf8'});
logger.info('creating schema..');
await connection.query(sql);
};
const seedDatabase = async(connection) => {
const sql = await readFile(`${__dirname}/../db/seed-production-database-open-source.sql`, {encoding: 'utf8'});
logger.info('seeding data..');
await connection.query(sql);
};
doIt();

View File

@@ -1,41 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sql = 'SELECT * FROM account_limits WHERE account_sid = ?';
class AccountLimits extends Model {
constructor() {
super();
}
static async retrieve(account_sid) {
const [rows] = await promisePool.query(sql, [account_sid]);
return rows;
}
}
AccountLimits.table = 'account_limits';
AccountLimits.fields = [
{
name: 'account_limits_sid',
type: 'string',
primaryKey: true
},
{
name: 'account_sid',
type: 'string',
required: true
},
{
name: 'category',
type: 'string',
required: true
},
{
name: 'quantity',
type: 'number',
required: true
}
];
module.exports = AccountLimits;

View File

@@ -2,15 +2,12 @@ const debug = require('debug')('jambonz:api-server');
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const {promisePool} = require('../db');
const { v4: uuid } = require('uuid');
const uuid = require('uuid').v4;
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
LEFT JOIN webhooks AS qh
ON acc.queue_event_hook_sid = qh.webhook_sid`;
ON acc.registration_hook_sid = rh.webhook_sid`;
const insertPendingAccountSubscriptionSql = `INSERT account_subscriptions
(account_subscription_sid, account_sid, pending, stripe_subscription_id,
@@ -58,23 +55,12 @@ 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;
});
}
@@ -262,10 +248,6 @@ Account.fields = [
name: 'sip_realm',
type: 'string',
},
{
name: 'queue_event_hook_sid',
type: 'string',
},
{
name: 'registration_hook_sid',
type: 'string',
@@ -297,27 +279,7 @@ Account.fields = [
{
name: 'disable_cdrs',
type: 'number',
},
{
name: 'subspace_client_id',
type: 'string',
},
{
name: 'subspace_client_secret',
type: 'string',
},
{
name: 'subspace_sip_teleport_id',
type: 'string',
},
{
name: 'subspace_sip_teleport_destinations',
type: 'string',
},
{
name: 'siprec_hook_sid',
type: 'string',
},
}
];
module.exports = Account;

View File

@@ -27,25 +27,6 @@ class ApiKey extends Model {
});
}
/**
* list all api keys for a service provider
*/
static retrieveAllForSP(service_provider_sid) {
const sql = 'SELECT * from api_keys WHERE service_provider_sid = ?';
const args = [service_provider_sid];
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, args, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* update last_used api key for an account
*/

View File

@@ -1,5 +1,5 @@
const Emitter = require('events');
const { v4: uuidv4 } = require('uuid');
const uuidv4 = require('uuid/v4');
const assert = require('assert');
const {getMysqlConnection} = require('../db');
const {DbErrorBadRequest} = require('../utils/errors');

View File

@@ -1,38 +1,45 @@
const Model = require('./model');
const {promisePool} = require('../db');
const {getMysqlConnection} = require('../db');
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
const sqlSP = `SELECT *
FROM phone_numbers
WHERE account_sid IN
(
SELECT account_sid
FROM accounts
WHERE service_provider_sid = ?
)`;
class PhoneNumber extends Model {
constructor() {
super();
}
static async retrieveAll(account_sid) {
static retrieveAll(account_sid) {
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(sql, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
const [rows] = await promisePool.query(sqlSP, service_provider_sid);
return rows;
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, account_sid, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* retrieve a phone number
* retrieve an application
*/
static async retrieve(sid, account_sid) {
static retrieve(sid, account_sid) {
if (!account_sid) return super.retrieve(sid);
const [rows] = await promisePool.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid]);
return rows;
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid], (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
}
PhoneNumber.table = 'phone_numbers';

View File

@@ -1,39 +0,0 @@
const Model = require('./model');
const {promisePool} = require('../db');
const sql = 'SELECT * FROM service_provider_limits WHERE service_provider_sid = ?';
class ServiceProviderLimits extends Model {
constructor() {
super();
}
static async retrieve(service_provider_sid) {
const [rows] = await promisePool.query(sql, [service_provider_sid]);
return rows;
}
}
ServiceProviderLimits.table = 'service_provider_limits';
ServiceProviderLimits.fields = [
{
name: 'service_provider_limits_sid',
type: 'string',
primaryKey: true
},
{
name: 'service_provider_sid',
type: 'string',
required: true
},
{
name: 'category',
type: 'string',
required: true
},
{
name: 'quantity',
type: 'number',
required: true
}
];
module.exports = ServiceProviderLimits;

View File

@@ -8,50 +8,23 @@ const ApiKey = require('../../models/api-key');
const ServiceProvider = require('../../models/service-provider');
const {deleteDnsRecords} = require('../../utils/dns-utils');
const {deleteCustomer} = require('../../utils/stripe-utils');
const { v4: uuidv4 } = require('uuid');
const uuidv4 = require('uuid/v4');
const snakeCase = require('../../utils/snake-case');
const sysError = require('../error');
const {promisePool} = require('../../db');
const {hasAccountPermissions, parseAccountSid, enableSubspace, disableSubspace} = require('./utils');
const {hasAccountPermissions, parseAccountSid} = require('./utils');
const short = require('short-uuid');
const VoipCarrier = require('../../models/voip-carrier');
const translator = short();
let idx = 0;
const getFsUrl = async(logger, retrieveSet, setName) => {
if (process.env.K8S) {
const port = process.env.K8S_FEATURE_SERVER_SERVICE_PORT || 3000;
return `http://${process.env.K8S_FEATURE_SERVER_SERVICE_NAME}:${port}/v1/createCall`;
}
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const ip = stripPort(fs[idx++ % fs.length]);
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
return `http://${ip}:3000/v1/createCall`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
router.use('/:sid/SpeechCredentials', hasAccountPermissions, require('./speech-credentials'));
router.use('/:sid/RecentCalls', hasAccountPermissions, require('./recent-calls'));
router.use('/:sid/Alerts', hasAccountPermissions, require('./alerts'));
router.use('/:sid/Charges', hasAccountPermissions, require('./charges'));
router.use('/:sid/SipRealms', hasAccountPermissions, require('./sip-realm'));
router.use('/:sid/PredefinedCarriers', hasAccountPermissions, require('./add-from-predefined-carrier'));
router.use('/:sid/Limits', hasAccountPermissions, require('./limits'));
router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
@@ -118,12 +91,7 @@ function validateUpdateCall(opts) {
'child_call_hook',
'call_status',
'listen_status',
'conf_hold_status',
'conf_mute_status',
'mute_status',
'sip_request',
'record'
]
'mute_status']
.reduce((acc, prop) => (opts[prop] ? ++acc : acc), 0);
switch (count) {
@@ -136,7 +104,6 @@ function validateUpdateCall(opts) {
break;
case 2:
if (opts.call_hook && opts.child_call_hook) break;
else if (opts.conf_hold_status && opts.waitHook) break;
// eslint-disable-next-line no-fallthrough
default:
throw new DbErrorBadRequest('multiple options are not allowed in updateCall');
@@ -151,22 +118,6 @@ function validateUpdateCall(opts) {
if (opts.mute_status && !['mute', 'unmute'].includes(opts.mute_status)) {
throw new DbErrorBadRequest('invalid mute_status');
}
if (opts.conf_hold_status && !['hold', 'unhold'].includes(opts.conf_hold_status)) {
throw new DbErrorBadRequest('invalid conf_hold_status');
}
if (opts.conf_mute_status && !['mute', 'unmute'].includes(opts.conf_mute_status)) {
throw new DbErrorBadRequest('invalid conf_mute_status');
}
if (opts.sip_request &&
(!opts.sip_request.method && !opts.sip_request.content_type || !opts.sip_request.content_type)) {
throw new DbErrorBadRequest('sip_request requires content_type and content properties');
}
if (opts.record && !opts.record.action) {
throw new DbErrorBadRequest('record requires action property');
}
if ('startCallRecording' === opts.record?.action && !opts.record.siprecServerURL) {
throw new DbErrorBadRequest('record requires siprecServerURL property when starting recording');
}
}
function validateTo(to) {
@@ -217,14 +168,15 @@ async function validateCreateCall(logger, sid, req) {
}
else {
delete obj.application_sid;
if (!obj.speech_synthesis_vendor ||
!obj.speech_synthesis_language ||
!obj.speech_synthesis_voice ||
!obj.speech_recognizer_vendor ||
!obj.speech_recognizer_language)
throw new DbErrorBadRequest('either application_sid or set of' +
' speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice,' +
' speech_recognizer_vendor, speech_recognizer_language required');
// TODO: these should be retrieved from account, using account_sid if provided
Object.assign(obj, {
speech_synthesis_vendor: 'google',
speech_synthesis_voice: 'en-US-Wavenet-C',
speech_synthesis_language: 'en-US',
speech_recognizer_vendor: 'google',
speech_recognizer_language: 'en-US'
});
}
if (!obj.call_hook && !obj.application_sid) {
@@ -250,17 +202,17 @@ async function validateCreateCall(logger, sid, req) {
if (typeof obj.call_status_hook === 'object' && typeof obj.call_status_hook.url != 'string') {
throw new DbErrorBadRequest('call_status_hook must be string or an object containing a url property');
}
if (obj.call_hook && !/^https?:/.test(obj.call_hook.url) && !/^wss?:/.test(obj.call_hook.url)) {
if (obj.call_hook && !/^https?:/.test(obj.call_hook.url)) {
throw new DbErrorBadRequest('call_hook url be an absolute url');
}
if (obj.call_status_hook && !/^https?:/.test(obj.call_status_hook.url) && !/^wss?:/.test(obj.call_status_hook.url)) {
if (obj.call_status_hook && !/^https?:/.test(obj.call_status_hook.url)) {
throw new DbErrorBadRequest('call_status_hook url be an absolute url');
}
}
async function validateCreateMessage(logger, sid, req) {
const obj = req.body;
logger.debug({payload: req.body}, 'validateCreateMessage');
//const {lookupAccountByPhoneNumber} = req.app.locals;
if (req.user.account_sid !== sid) {
throw new DbErrorBadRequest(`unauthorized createMessage request for account ${sid}`);
@@ -301,9 +253,6 @@ 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) {
@@ -342,12 +291,12 @@ router.post('/', async(req, res) => {
await validateAdd(req);
// create webhooks if provided
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) {
const obj = Object.assign({webhook_secret: secret}, req.body);
for (const prop of ['registration_hook']) {
if (obj[prop]) {
obj[`${prop}_sid`] = await Webhook.make(obj[prop]);
delete obj[prop];
}
delete obj[prop];
}
logger.debug(`Attempting to add account ${JSON.stringify(obj)}`);
@@ -404,58 +353,6 @@ router.get('/:sid/WebhookSecret', async(req, res) => {
}
});
router.post('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret} = results[0];
const {destination} = req.body;
const arr = /^(.*):\d+$/.exec(destination);
const dest = arr ? `sip:${arr[1]}` : `sip:${destination}`;
const teleport = await enableSubspace({
subspace_client_id,
subspace_client_secret,
destination: dest
});
logger.info({destination, teleport}, 'SubspaceTeleport - create teleport');
await Account.update(req.params.sid, {
subspace_sip_teleport_id: teleport.id,
subspace_sip_teleport_destinations: JSON.stringify(teleport.teleport_entry_points)//hacky
});
return res.status(200).json({
subspace_sip_teleport_id: teleport.id,
subspace_sip_teleport_destinations: teleport.teleport_entry_points
});
}
catch (err) {
sysError(logger, res, err);
}
});
router.delete('/:sid/SubspaceTeleport', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
const results = await Account.retrieve(req.params.sid, service_provider_sid);
if (results.length === 0) return res.status(404).end();
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = results[0];
await disableSubspace({subspace_client_id, subspace_client_secret, subspace_sip_teleport_id});
await Account.update(req.params.sid, {
subspace_sip_teleport_id: null,
subspace_sip_teleport_destinations: null
});
return res.sendStatus(204);
}
catch (err) {
sysError(logger, res, err);
}
});
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
@@ -464,14 +361,12 @@ router.put('/:sid', async(req, res) => {
// create webhooks if provided
const obj = Object.assign({}, req.body);
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 (null !== obj.registration_hook) {
for (const prop of ['registration_hook']) {
if (prop in obj && Object.keys(obj[prop]).length) {
if ('webhook_sid' in obj[prop]) {
const sid = obj[prop]['webhook_sid'];
delete obj[prop]['webhook_sid'];
await Webhook.update(sid, obj[prop]);
}
else {
@@ -479,35 +374,30 @@ 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 orphanedRegHook, orphanedQueueHook;
let orphanedHook;
if (null === obj.registration_hook) {
const results = await Account.retrieve(sid);
if (results.length && results[0].registration_hook_sid) orphanedRegHook = results[0].registration_hook_sid;
if (results.length && results[0].registration_hook_sid) orphanedHook = results[0].registration_hook_sid;
obj.registration_hook_sid = null;
delete obj.registration_hook;
}
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;
logger.info({obj}, `about to update Account ${sid}`);
const rowsAffected = await Account.update(sid, obj);
if (rowsAffected === 0) {
return res.status(404).end();
}
if (orphanedRegHook) {
await Webhook.remove(orphanedRegHook);
}
if (orphanedQueueHook) {
await Webhook.remove(orphanedQueueHook);
if (orphanedHook) {
await Webhook.remove(orphanedHook);
}
}
@@ -529,7 +419,7 @@ router.delete('/:sid', async(req, res) => {
await validateDelete(req, sid);
const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid);
const {sip_realm, stripe_customer_id, registration_hook_sid} = account[0];
const {sip_realm, stripe_customer_id} = account[0];
/* remove dns records */
if (process.env.NODE_ENV !== 'test' || process.env.DME_API_KEY) {
@@ -570,15 +460,6 @@ account_subscriptions WHERE account_sid = ?)
await promisePool.execute('DELETE from applications where account_sid = ?', [sid]);
await promisePool.execute('DELETE from accounts where account_sid = ?', [sid]);
if (registration_hook_sid) {
/* remove registration hook if only used by this account */
const sql = 'SELECT COUNT(*) as count FROM accounts WHERE registration_hook_sid = ?';
const [r] = await promisePool.query(sql, registration_hook_sid);
if (r[0]?.count === 0) {
await promisePool.execute('DELETE from webhooks where webhook_sid = ?', [registration_hook_sid]);
}
}
if (stripe_customer_id) {
const response = await deleteCustomer(logger, stripe_customer_id);
logger.info({response}, `deleted stripe customer_id ${stripe_customer_id} for account_si ${sid}`);
@@ -609,32 +490,37 @@ router.post('/:sid/Calls', async(req, res) => {
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
const {retrieveSet, logger} = req.app.locals;
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) {
res.status(480).json({msg: 'no available feature servers at this time'});
} else {
try {
await validateCreateCall(logger, sid, req);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${serviceUrl}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${serviceUrl}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle createCall API request');
return res.json({msg: 'no available feature servers at this time'}).status(500);
}
const ip = fs[idx++ % fs.length];
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
const serviceUrl = `http://${ip}:3000/v1/createCall`;
await validateCreateCall(logger, sid, req);
logger.debug({payload: req.body}, `sending createCall API request to to ${ip}`);
updateLastUsed(logger, sid, req).catch((err) => {});
request({
url: serviceUrl,
method: 'POST',
json: true,
body: Object.assign(req.body, {account_sid: sid})
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createCall POST to ${ip}`);
return res.sendStatus(500);
}
if (response.statusCode !== 201) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createCall ${ip}`);
return res.sendStatus(500);
}
res.status(201).json(body);
});
} catch (err) {
sysError(logger, res, err);
}
});
@@ -751,8 +637,14 @@ router.post('/:sid/Messages', async(req, res) => {
const {retrieveSet, logger} = req.app.locals;
try {
const serviceUrl = await getFsUrl(logger, retrieveSet, setName);
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
const fs = await retrieveSet(setName);
if (0 === fs.length) {
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];
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);
const payload = {
@@ -760,7 +652,7 @@ router.post('/:sid/Messages', async(req, res) => {
account_sid,
...req.body
};
logger.debug({payload}, `sending createMessage API request to to ${serviceUrl}`);
logger.debug({payload}, `sending createMessage API request to to ${ip}`);
updateLastUsed(logger, account_sid, req).catch(() => {});
request({
url: serviceUrl,
@@ -769,12 +661,12 @@ router.post('/:sid/Messages', async(req, res) => {
body: payload
}, (err, response, body) => {
if (err) {
logger.error(err, `Error sending createMessage POST to ${serviceUrl}`);
logger.error(err, `Error sending createMessage POST to ${ip}`);
return res.sendStatus(500);
}
if (response.statusCode !== 200) {
logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`);
return body ? res.status(response.statusCode).json(body) : res.sendStatus(response.statusCode);
return res.sendStatus(response.statusCode);
}
res.status(201).json(body);
});

View File

@@ -1,10 +1,9 @@
const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const PredefinedCarrier = require('../../models/predefined-carrier');
const VoipCarrier = require('../../models/voip-carrier');
const SipGateway = require('../../models/sip-gateway');
const SmppGateway = require('../../models/smpp-gateway');
const {parseServiceProviderSid} = require('./utils');
const short = require('short-uuid');
const {promisePool} = require('../../db');
const sysError = require('../error');
@@ -14,9 +13,7 @@ AND name = ?`;
const sqlSelectCarrierByNameForSP = `SELECT * FROM voip_carriers
WHERE service_provider_sid = ?
AND name = ?`;
const sqlSelectTemplateSipGateways = `SELECT * FROM predefined_sip_gateways
WHERE predefined_carrier_sid = ?`;
const sqlSelectTemplateSmppGateways = `SELECT * FROM predefined_smpp_gateways
const sqlSelectTemplateGateways = `SELECT * FROM predefined_sip_gateways
WHERE predefined_carrier_sid = ?`;
@@ -43,38 +40,26 @@ router.post('/:sid', async(req, res) => {
await promisePool.query(sqlSelectCarrierByNameForSP, [service_provider_sid, template.name]);
if (r2.length > 0) {
template.name = `${template.name}-${short.generate()}`;
logger.info({account_sid}, `Failed to add carrier with name ${template.name}, carrier of that name exists`);
throw new DbErrorBadRequest(`A carrier with name ${template.name} already exists`);
}
/* retrieve all the sip gateways */
const [r3] = await promisePool.query(sqlSelectTemplateSipGateways, template.predefined_carrier_sid);
logger.debug({r3}, `retrieved template sip gateways for ${template.name}`);
/* retrieve all the smpp gateways */
const [r4] = await promisePool.query(sqlSelectTemplateSmppGateways, template.predefined_carrier_sid);
logger.debug({r4}, `retrieved template smpp gateways for ${template.name}`);
/* retrieve all the gateways */
const [r3] = await promisePool.query(sqlSelectTemplateGateways, template.predefined_carrier_sid);
logger.debug({r3}, `retrieved template gateways for ${template.name}`);
/* add a voip_carrier */
// eslint-disable-next-line no-unused-vars
const {requires_static_ip, predefined_carrier_sid, ...obj} = template;
const uuid = await VoipCarrier.make({...obj, account_sid, service_provider_sid});
/* add all the sipp gateways */
/* add all the gateways */
for (const gw of r3) {
// eslint-disable-next-line no-unused-vars
const {predefined_carrier_sid, predefined_sip_gateway_sid, ...obj} = gw;
logger.debug({obj}, 'adding sip gateway');
logger.debug({obj}, 'adding gateway');
await SipGateway.make({...obj, voip_carrier_sid: uuid});
}
/* add all the smpp gateways */
for (const gw of r4) {
// eslint-disable-next-line no-unused-vars
const {predefined_carrier_sid, predefined_smpp_gateway_sid, ...obj} = gw;
logger.debug({obj}, 'adding smpp gateway');
await SmppGateway.make({...obj, voip_carrier_sid: uuid});
}
logger.debug({sid: uuid}, 'Successfully added carrier from predefined list');
res.status(201).json({sid: uuid});
} catch (err) {

View File

@@ -7,47 +7,26 @@ const parseAccountSid = (url) => {
if (arr) return arr[1];
};
const parseServiceProviderSid = (url) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(url);
if (arr) return arr[1];
};
router.get('/', async(req, res) => {
const {logger, queryAlerts, queryAlertsSP} = req.app.locals;
const {logger, queryAlerts} = req.app.locals;
try {
logger.debug({opts: req.query}, 'GET /Alerts');
const account_sid = parseAccountSid(req.originalUrl);
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
const {page, count, alert_type, days, start, end} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (account_sid) {
const data = await queryAlerts({
account_sid,
page,
page_size: count,
alert_type,
days,
start: days ? undefined : start,
end: days ? undefined : end,
});
const data = await queryAlerts({
account_sid,
page,
page_size: count,
alert_type,
days,
start: days ? undefined : start,
end: days ? undefined : end,
});
res.status(200).json(data);
}
else {
const data = await queryAlertsSP({
service_provider_sid,
page,
page_size: count,
alert_type,
days,
start: days ? undefined : start,
end: days ? undefined : end,
});
res.status(200).json(data);
}
res.status(200).json(data);
} catch (err) {
sysError(logger, res, err);
}

View File

@@ -3,7 +3,8 @@ const {DbErrorBadRequest} = require('../../utils/errors');
const ApiKey = require('../../models/api-key');
const Account = require('../../models/account');
const decorate = require('./decorate');
const { v4: uuidv4 } = require('uuid');
const uuidv4 = require('uuid/v4');
const assert = require('assert');
const sysError = require('../error');
const preconditions = {
'add': validateAddToken,
@@ -70,7 +71,10 @@ async function validateDeleteToken(req, sid) {
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
await validateAddToken(req);
if ('add' in preconditions) {
assert(typeof preconditions.add === 'function');
await preconditions.add(req);
}
const uuid = await ApiKey.make(req.body);
res.status(201).json({sid: uuid, token: req.body.token});
} catch (err) {

View File

@@ -3,12 +3,12 @@ const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/er
const Application = require('../../models/application');
const Account = require('../../models/account');
const Webhook = require('../../models/webhook');
const {promisePool} = require('../../db');
const decorate = require('./decorate');
const sysError = require('../error');
const preconditions = {
'add': validateAdd,
'update': validateUpdate
'update': validateUpdate,
'delete': validateDelete
};
/* only user-level tokens can add applications */
@@ -59,7 +59,7 @@ async function validateDelete(req, sid) {
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
}
decorate(router, Application, [], preconditions);
decorate(router, Application, ['delete'], preconditions);
/* add */
router.post('/', async(req, res) => {
@@ -104,54 +104,13 @@ router.get('/:sid', async(req, res) => {
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
const results = await Application.retrieve(req.params.sid, service_provider_sid, account_sid);
if (results.length === 0) return res.status(404).end();
return res.status(200).json(results[0]);
return res.status(200).json(results);
}
catch (err) {
sysError(logger, res, err);
}
});
/* delete */
router.delete('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
await validateDelete(req, sid);
const [application] = await promisePool.query('SELECT * FROM applications WHERE application_sid = ?', sid);
const {call_hook_sid, call_status_hook_sid, messaging_hook_sid} = application[0];
logger.info({call_hook_sid, call_status_hook_sid, messaging_hook_sid, sid}, 'deleting application');
await promisePool.execute('DELETE from applications where application_sid = ?', [sid]);
if (call_hook_sid) {
/* remove call hook if only used by this app */
const sql = 'SELECT COUNT(*) as count FROM applications WHERE call_hook_sid = ?';
const [r] = await promisePool.query(sql, call_hook_sid);
if (r[0]?.count === 0) {
await promisePool.execute('DELETE from webhooks where webhook_sid = ?', [call_hook_sid]);
}
}
if (call_status_hook_sid) {
const sql = 'SELECT COUNT(*) as count FROM applications WHERE call_status_hook_sid = ?';
const [r] = await promisePool.query(sql, call_status_hook_sid);
if (r[0]?.count === 0) {
await promisePool.execute('DELETE from webhooks where webhook_sid = ?', [call_status_hook_sid]);
}
}
if (messaging_hook_sid) {
const sql = 'SELECT COUNT(*) as count FROM applications WHERE messaging_hook_sid = ?';
const [r] = await promisePool.query(sql, messaging_hook_sid);
if (r[0]?.count === 0) {
await promisePool.execute('DELETE from webhooks where webhook_sid = ?', [messaging_hook_sid]);
}
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
/* update */
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;

View File

@@ -7,16 +7,9 @@ const isAdminScope = (req, res, next) => {
message: 'insufficient privileges'
});
};
const isAdminOrSPScope = (req, res, next) => {
if (req.user.hasScope('admin') || req.user.hasScope('service_provider')) return next();
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
});
};
api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes'));
api.use('/ServiceProviders', isAdminOrSPScope, require('./service-providers'));
api.use('/ServiceProviders', isAdminScope, require('./service-providers'));
api.use('/VoipCarriers', require('./voip-carriers'));
api.use('/Webhooks', require('./webhooks'));
api.use('/SipGateways', require('./sip-gateways'));

View File

@@ -1,128 +0,0 @@
const router = require('express').Router();
const sysError = require('../error');
const AccountLimits = require('../../models/account-limits');
const ServiceProviderLimits = require('../../models/service-provider-limits');
const {parseAccountSid, parseServiceProviderSid} = require('./utils');
const {promisePool} = require('../../db');
const sqlDeleteSPLimits = `
DELETE FROM service_provider_limits
WHERE service_provider_sid = ?
`;
const sqlDeleteSPLimitsByCategory = `
DELETE FROM service_provider_limits
WHERE service_provider_sid = ?
AND category = ?
`;
const sqlDeleteAccountLimits = `
DELETE FROM account_limits
WHERE account_sid = ?
`;
const sqlDeleteAccountLimitsByCategory = `
DELETE FROM account_limits
WHERE account_sid = ?
AND category = ?
`;
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {
category,
quantity
} = req.body;
const account_sid = parseAccountSid(req);
let service_provider_sid;
if (!account_sid) {
if (!req.user.hasServiceProviderAuth) {
logger.error('POST /SpeechCredentials invalid credentials');
return res.send(403);
}
service_provider_sid = parseServiceProviderSid(req);
}
try {
let uuid;
if (account_sid) {
const existing = (await AccountLimits.retrieve(account_sid) || [])
.find((el) => el.category === category);
if (existing) {
uuid = existing.account_limits_sid;
await AccountLimits.update(uuid, {category, quantity});
}
else {
uuid = await AccountLimits.make({
account_sid,
category,
quantity
});
}
}
else {
const existing = (await ServiceProviderLimits.retrieve(service_provider_sid) || [])
.find((el) => el.category === category);
if (existing) {
uuid = existing.service_provider_limits_sid;
await ServiceProviderLimits.update(uuid, {category, quantity});
}
else {
uuid = await ServiceProviderLimits.make({
service_provider_sid,
category,
quantity
});
}
}
res.status(201).json({sid: uuid});
} catch (err) {
sysError(logger, res, err);
}
});
/**
* retrieve all limits for an account or service provider
*/
router.get('/', async(req, res) => {
let service_provider_sid;
const account_sid = parseAccountSid(req);
if (!account_sid) service_provider_sid = parseServiceProviderSid(req);
const logger = req.app.locals.logger;
try {
const limits = account_sid ?
await AccountLimits.retrieve(account_sid) :
await ServiceProviderLimits.retrieve(service_provider_sid);
if (req.query?.category) {
return res.status(200).json(limits.filter((el) => el.category === req.query.category));
}
res.status(200).json(limits);
} catch (err) {
sysError(logger, res, err);
}
});
router.delete('/', async(req, res) => {
const logger = req.app.locals.logger;
const account_sid = parseAccountSid(req);
const {category} = req.query;
const service_provider_sid = parseServiceProviderSid(req);
try {
if (account_sid) {
if (category) {
await promisePool.execute(sqlDeleteAccountLimitsByCategory, [account_sid, category]);
}
else {
await promisePool.execute(sqlDeleteAccountLimits, [account_sid]);
}
}
else {
if (category) {
await promisePool.execute(sqlDeleteSPLimitsByCategory, [service_provider_sid, category]);
}
else {
await promisePool.execute(sqlDeleteSPLimits, [service_provider_sid]);
}
}
res.status(204).end();
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -1,96 +1,37 @@
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];
};
const parseServiceProviderSid = (url) => {
const arr = /ServiceProviders\/([^\/]*)/.exec(url);
if (arr) return arr[1];
};
router.get('/', async(req, res) => {
const {logger, queryCdrs, queryCdrsSP} = req.app.locals;
const {logger, queryCdrs} = req.app.locals;
try {
logger.debug({opts: req.query}, 'GET /RecentCalls');
const account_sid = parseAccountSid(req.originalUrl);
const service_provider_sid = account_sid ? null : parseServiceProviderSid(req.originalUrl);
const {page, count, trunk, direction, days, answered, start, end} = req.query || {};
if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg');
if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg');
if (account_sid) {
const data = await queryCdrs({
account_sid,
page,
page_size: count,
trunk,
direction,
days,
answered,
start: days ? undefined : start,
end: days ? undefined : end,
});
res.status(200).json(data);
}
else {
const data = await queryCdrsSP({
service_provider_sid,
page,
page_size: count,
trunk,
direction,
days,
answered,
start: days ? undefined : start,
end: days ? undefined : end,
});
res.status(200).json(data);
}
const data = await queryCdrs({
account_sid,
page,
page_size: count,
trunk,
direction,
days,
answered,
start: days ? undefined : start,
end: days ? undefined : end,
});
res.status(200).json(data);
} catch (err) {
sysError(logger, res, err);
}
});
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;

View File

@@ -4,7 +4,7 @@ const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/er
const {promisePool} = require('../../db');
const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils');
const {validateEmail} = require('../../utils/email-utils');
const { v4: uuid } = require('uuid');
const uuid = require('uuid').v4;
const short = require('short-uuid');
const translator = short();
const jwt = require('jsonwebtoken');
@@ -20,13 +20,6 @@ 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 = ?`;
@@ -156,7 +149,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 || user.email,
name: user.name,
email: user.email,
email_validated: user.verified_email,
picture: user.picture,
@@ -288,22 +281,6 @@ 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',

View File

@@ -1,50 +1,25 @@
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');
const Account = require('../../models/account');
const VoipCarrier = require('../../models/voip-carrier');
const Application = require('../../models/application');
const PhoneNumber = require('../../models/phone-number');
const ApiKey = require('../../models/api-key');
const {hasServiceProviderPermissions, parseServiceProviderSid} = require('./utils');
const sysError = require('../error');
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);
router.use('/:sid/RecentCalls', hasServiceProviderPermissions, require('./recent-calls'));
router.use('/:sid/Alerts', hasServiceProviderPermissions, require('./alerts'));
router.use('/:sid/SpeechCredentials', hasServiceProviderPermissions, require('./speech-credentials'));
router.use('/:sid/Limits', hasServiceProviderPermissions, require('./limits'));
router.use('/:sid/PredefinedCarriers', hasServiceProviderPermissions, require('./add-from-predefined-carrier'));
router.get('/:sid/Accounts', async(req, res) => {
const logger = req.app.locals.logger;
@@ -56,26 +31,6 @@ router.get('/:sid/Accounts', async(req, res) => {
sysError(logger, res, err);
}
});
router.get('/:sid/Applications', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = parseServiceProviderSid(req);
const results = await Application.retrieveAll(service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/PhoneNumbers', async(req, res) => {
const logger = req.app.locals.logger;
try {
const service_provider_sid = parseServiceProviderSid(req);
const results = await PhoneNumber.retrieveAllForSP(service_provider_sid);
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
router.get('/:sid/VoipCarriers', async(req, res) => {
const logger = req.app.locals.logger;
try {
@@ -118,17 +73,6 @@ router.get(':sid/Acccounts', async(req, res) => {
sysError(logger, res, err);
}
});
router.get('/:sid/ApiKeys', async(req, res) => {
const logger = req.app.locals.logger;
const {sid} = req.params;
try {
const results = await ApiKey.retrieveAllForSP(sid);
res.status(200).json(results);
await ApiKey.updateLastUsed(sid);
} catch (err) {
sysError(logger, res, err);
}
});
/* add */
router.post('/', async(req, res) => {

View File

@@ -2,7 +2,7 @@ const router = require('express').Router();
const {promisePool} = require('../../db');
const {DbErrorBadRequest} = require('../../utils/errors');
const {createDnsRecords, deleteDnsRecords} = require('../../utils/dns-utils');
const { v4: uuid } = require('uuid');
const uuid = require('uuid').v4;
const sysError = require('../error');
const insertDnsRecords = `INSERT INTO dns_records
(dns_record_sid, account_sid, record_type, record_id)

View File

@@ -1,5 +1,5 @@
const router = require('express').Router();
const SmppGateway = require('../../models/smpp-gateway');
const SmppGateway = require('../../models/smpp_gateway');
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const decorate = require('./decorate');
const sysError = require('../error');

View File

@@ -1,41 +1,19 @@
const router = require('express').Router();
const request = require('request');
const getProvider = require('../../utils/sms-provider');
const { v4: uuidv4 } = require('uuid');
const uuidv4 = require('uuid/v4');
const sysError = require('../error');
let idx = 0;
const getFsUrl = async(logger, retrieveSet, setName, provider) => {
if (process.env.K8S) return `http://${process.env.K8S_FEATURE_SERVER_SERVICE_NAME}:3000/v1/messaging/${provider}`;
try {
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle createCall API request');
return ;
}
const ip = stripPort(fs[idx++ % fs.length]);
logger.info({fs}, `feature servers available for createCall API request, selecting ${ip}`);
return `http://${ip}:3000/v1/messaging/${provider}`;
} catch (err) {
logger.error({err}, 'getFsUrl: error retreving feature servers from redis');
}
};
const stripPort = (hostport) => {
const arr = /^(.*):(.*)$/.exec(hostport);
if (arr) return arr[1];
return hostport;
};
const doSendResponse = async(res, respondFn, body) => {
async function doSendResponse(res, respondFn, body) {
if (typeof respondFn === 'number') res.sendStatus(respondFn);
else if (typeof respondFn !== 'function') res.sendStatus(200);
else {
const payload = await respondFn(body);
res.status(200).json(payload);
}
};
}
router.post('/:provider', async(req, res) => {
const provider = req.params.provider;
@@ -89,8 +67,17 @@ router.post('/:provider', async(req, res) => {
}
try {
const serviceUrl = await getFsUrl(logger, retrieveSet, setName, provider);
if (!serviceUrl) res.json({msg: 'no available feature servers at this time'}).status(480);
const fs = await retrieveSet(setName);
if (0 === fs.length) {
logger.info('No available feature servers to handle createCall API request');
return res
.json({
msg: 'no available feature servers at this time'
})
.status(480);
}
const ip = fs[idx++ % fs.length];
const serviceUrl = `http://${ip}:3000/v1/messaging/${provider}`;
const messageSid = uuidv4();
const payload = await Promise.resolve(filterFn({messageSid}, req.body));
@@ -126,7 +113,7 @@ router.post('/:provider', async(req, res) => {
logger.debug({body: req.body, payload}, 'filtered incoming SMS');
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${serviceUrl}`);
logger.info({payload, url: serviceUrl}, `sending incomingSms API request to FS at ${ip}`);
request({
url: serviceUrl,
@@ -136,7 +123,7 @@ router.post('/:provider', async(req, res) => {
},
async(err, response, body) => {
if (err) {
logger.error(err, `Error sending incomingSms POST to ${serviceUrl}`);
logger.error(err, `Error sending incomingSms POST to ${ip}`);
return res.sendStatus(500);
}
if (200 === response.statusCode) {
@@ -144,7 +131,7 @@ router.post('/:provider', async(req, res) => {
logger.info({body}, 'sending response to provider for incomingSMS');
return doSendResponse(res, respondFn, body);
}
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${serviceUrl}`);
logger.error({statusCode: response.statusCode}, `Non-success response returned by incomingSms ${ip}`);
return res.sendStatus(500);
});
} catch (err) {

View File

@@ -1,85 +1,20 @@
const router = require('express').Router();
const assert = require('assert');
const SpeechCredential = require('../../models/speech-credential');
const sysError = require('../error');
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
const {parseAccountSid, parseServiceProviderSid} = require('./utils');
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
const {
testGoogleTts,
testGoogleStt,
testAwsTts,
testAwsStt,
testMicrosoftStt,
testMicrosoftTts,
testWellSaidTts
testAwsStt
} = require('../../utils/speech-utils');
const obscureKey = (key) => {
const key_spoiler_length = 6;
const key_spoiler_char = 'X';
if (key.length <= key_spoiler_length) {
return key;
}
return `${key.slice(0, key_spoiler_length)}${key_spoiler_char.repeat(key.length - key_spoiler_length)}`;
};
const encryptCredential = (obj) => {
const {
vendor,
service_key,
access_key_id,
secret_access_key,
aws_region,
api_key,
region
} = obj;
switch (vendor) {
case 'google':
assert(service_key, 'invalid json key: service_key is required');
try {
const o = JSON.parse(service_key);
assert(o.client_email && o.private_key, 'invalid google service account key');
}
catch (err) {
assert(false, 'invalid google service account key - not JSON');
}
return encrypt(service_key);
case 'aws':
assert(access_key_id, 'invalid aws speech credential: access_key_id is required');
assert(secret_access_key, 'invalid aws speech credential: secret_access_key is required');
assert(aws_region, 'invalid aws speech credential: aws_region is required');
const awsData = JSON.stringify({aws_region, access_key_id, secret_access_key});
return encrypt(awsData);
case 'microsoft':
assert(region, 'invalid azure speech credential: region is required');
assert(api_key, 'invalid azure speech credential: api_key is required');
const azureData = JSON.stringify({region, api_key});
return encrypt(azureData);
case 'wellsaid':
assert(api_key, 'invalid wellsaid speech credential: api_key is required');
const wsData = JSON.stringify({api_key});
return encrypt(wsData);
default:
assert(false, `invalid or missing vendor: ${vendor}`);
}
};
router.post('/', async(req, res) => {
const logger = req.app.locals.logger;
const {
use_for_stt,
use_for_tts,
vendor,
} = req.body;
const account_sid = req.user.account_sid || req.body.account_sid;
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;
let service_provider_sid;
if (!account_sid) {
if (!req.user.hasServiceProviderAuth) {
@@ -89,7 +24,30 @@ router.post('/', async(req, res) => {
service_provider_sid = parseServiceProviderSid(req);
}
try {
const encrypted_credential = encryptCredential(req.body);
let encrypted_credential;
if (vendor === 'google') {
let obj;
if (!service_key) throw new DbErrorBadRequest('invalid json key: service_key is required');
try {
obj = JSON.parse(service_key);
if (!obj.client_email || !obj.private_key) {
throw new DbErrorBadRequest('invalid google service account key');
}
}
catch (err) {
throw new DbErrorBadRequest('invalid google service account key - not JSON');
}
encrypted_credential = encrypt(service_key);
}
else if (vendor === 'aws') {
const data = JSON.stringify({
aws_region: aws_region || 'us-east-1',
access_key_id,
secret_access_key
});
encrypted_credential = encrypt(data);
}
else throw new DbErrorBadRequest(`invalid speech vendor ${vendor}`);
const uuid = await SpeechCredential.make({
account_sid,
service_provider_sid,
@@ -120,30 +78,12 @@ router.get('/', async(req, res) => {
res.status(200).json(creds.map((c) => {
const {credential, ...obj} = c;
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = obscured;
obj.service_key = decrypt(credential);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const o = decrypt(credential);
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
logger.info({obj, o}, 'retrieving aws speech credential');
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
logger.info({obj, o}, 'retrieving azure speech credential');
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.secret_access_key = o.secret_access_key;
}
return obj;
}));
@@ -163,28 +103,12 @@ router.get('/:sid', async(req, res) => {
if (0 === cred.length) return res.sendStatus(404);
const {credential, ...obj} = cred[0];
if ('google' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
const key_header = '-----BEGIN PRIVATE KEY-----\n';
const obscured = {
...o,
private_key: `${key_header}${obscureKey(o.private_key.slice(key_header.length, o.private_key.length))}`
};
obj.service_key = JSON.stringify(obscured);
obj.service_key = decrypt(credential);
}
else if ('aws' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id;
obj.secret_access_key = obscureKey(o.secret_access_key);
obj.aws_region = o.aws_region;
}
else if ('microsoft' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.region = o.region;
}
else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = obscureKey(o.api_key);
obj.secret_access_key = o.secret_access_key;
}
res.status(200).json(obj);
} catch (err) {
@@ -227,11 +151,6 @@ router.put('/:sid', async(req, res) => {
obj.use_for_stt = use_for_stt;
}
/* update the credential if provided */
try {
obj.credential = encryptCredential(req.body);
} catch (err) {}
const rowsAffected = await SpeechCredential.update(sid, obj);
if (rowsAffected === 0) {
return res.sendStatus(404);
@@ -320,42 +239,6 @@ router.get('/:sid/test', async(req, res) => {
}
}
}
else if (cred.vendor === 'microsoft') {
const {api_key, region} = credential;
if (cred.use_for_tts) {
try {
await testMicrosoftTts(logger, {api_key, region});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
results.tts = {status: 'fail', reason: err.message};
SpeechCredential.ttsTestResult(sid, false);
}
}
if (cred.use_for_stt) {
try {
await testMicrosoftStt(logger, {api_key, region});
results.stt.status = 'ok';
SpeechCredential.sttTestResult(sid, true);
} catch (err) {
results.stt = {status: 'fail', reason: err.message};
SpeechCredential.sttTestResult(sid, false);
}
}
}
else if (cred.vendor === 'wellsaid') {
const {api_key} = credential;
if (cred.use_for_tts) {
try {
await testWellSaidTts(logger, {api_key});
results.tts.status = 'ok';
SpeechCredential.ttsTestResult(sid, true);
} catch (err) {
results.tts = {status: 'fail', reason: err.message};
SpeechCredential.ttsTestResult(sid, false);
}
}
}
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);

View File

@@ -1,5 +1,4 @@
const { v4: uuid } = require('uuid');
const bent = require('bent');
const uuid = require('uuid').v4;
const Account = require('../../models/account');
const {promisePool} = require('../../db');
const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils');
@@ -10,8 +9,6 @@ values (?, ?)`;
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
WHERE account_subscription_sid = ?`;
//const request = require('request');
//require('request-debug')(request);
const setupFreeTrial = async(logger, account_sid, isReturningUser) => {
const sid = uuid();
@@ -77,13 +74,12 @@ 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,
sip_callid: '685cd008-0a66-4974-b37a-bdd6d9a3c4aa@192.168.1.100',
sip_status: 200,
duration: failed ? 0 : 45,
attempted_at: attempted_at.getTime(),
@@ -126,7 +122,7 @@ const createTestAlerts = async(writeAlerts, AlertType, account_sid) => {
data.push({timestamp, account_sid, alert_type: AlertType.CARRIER_NOT_PROVISIONED});
break;
case 4:
data.push({timestamp, account_sid, alert_type: AlertType.ACCOUNT_CALL_LIMIT, count: 50});
data.push({timestamp, account_sid, alert_type: AlertType.CALL_LIMIT, count: 50});
break;
default:
break;
@@ -171,108 +167,6 @@ 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();
};
const getSubspaceJWT = async(id, secret) => {
const postJwt = bent('https://id.subspace.com', 'POST', 'json', 200);
const jwt = await postJwt('/oauth/token',
{
client_id: id,
client_secret: secret,
audience: 'https://api.subspace.com/',
grant_type: 'client_credentials',
}
);
return jwt.access_token;
};
const enableSubspace = async(opts) => {
const {subspace_client_id, subspace_client_secret, destination} = opts;
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
const postTeleport = bent('https://api.subspace.com', 'POST', 'json', 200);
const teleport = await postTeleport('/v1/sipteleport',
{
name: 'Jambonz',
destination,
status: 'ENABLED'
},
{
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
}
);
return teleport;
};
const disableSubspace = async(opts) => {
const {subspace_client_id, subspace_client_secret, subspace_sip_teleport_id} = opts;
const accessToken = await getSubspaceJWT(subspace_client_id, subspace_client_secret);
const relativeUrl = `/v1/sipteleport/${subspace_sip_teleport_id}`;
const deleteTeleport = bent('https://api.subspace.com', 'DELETE', 'json', 200);
await deleteTeleport(relativeUrl, {},
{
Authorization: `Bearer ${accessToken}`
}
);
return;
};
module.exports = {
setupFreeTrial,
createTestCdrs,
@@ -280,8 +174,5 @@ module.exports = {
parseAccountSid,
parseServiceProviderSid,
hasAccountPermissions,
hasServiceProviderPermissions,
checkLimits,
enableSubspace,
disableSubspace
hasServiceProviderPermissions
};

View File

@@ -5,17 +5,9 @@ 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);

View File

@@ -1346,12 +1346,6 @@ paths:
schema:
type: object
properties:
account_sid:
type: string
format: uuid
application_sid:
type: string
format: uuid
number:
type: string
description: telephone number
@@ -1806,7 +1800,7 @@ paths:
summary: test a speech credential
operationId: testSpeechCredential
parameters:
- name: ServiceProviderSid
- name: AccountSid
in: path
required: true
schema:
@@ -1855,122 +1849,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/ServiceProviders/{ServiceProviderSid}/Limits:
post:
summary: create a limit for a service provider
operationId: addLimitForServiceProvider
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Limits'
responses:
201:
description: limit successfully created or updated
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
404:
description: service provider not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve call capacity and other limits from the service provider
operationId: getServiceProviderLimits
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
responses:
200:
description: service provider limits
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Limits'
404:
description: service provider not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/Accounts/{AccountSid}/Limits:
post:
summary: create a limit for an account
operationId: addLimitForAccount
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Limits'
responses:
201:
description: limit successfully created or updated
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessfulAdd'
404:
description: account not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
get:
summary: retrieve call capacity and other limits from the account
operationId: getAccountLimits
parameters:
- name: AccountSid
in: path
required: true
schema:
type: string
format: uuid
responses:
200:
description: account limits
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Limits'
404:
description: account not found
500:
description: system error
content:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
/MicrosoftTeamsTenants:
post:
summary: provision a customer tenant for MS Teams
@@ -2080,7 +1958,7 @@ paths:
put:
summary: update tenant
operationId: putTenant
operationId: updateAccount
requestBody:
content:
application/json:
@@ -2126,9 +2004,6 @@ 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
@@ -2760,362 +2635,6 @@ 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
/ServiceProviders/{ServiceProviderSid}/RecentCalls/{CallId}/pcap:
parameters:
- name: ServiceProviderSid
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
/ServiceProviders/{ServiceProviderSid}/RecentCalls:
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
- in: query
name: page
required: true
schema:
type: number
format: integer
description: page number of data to retrieve
- in: query
name: count
required: true
schema:
type: number
format: integer
description: number of rows to retrieve in each page set
- in: query
name: days
required: false
schema:
type: number
format: integer
description: number of days back to retrieve, must be ge 1 and le 30
- in: query
name: start
required: false
schema:
type: string
format: date-time
description: start date to retrieve
- in: query
name: end
required: false
schema:
type: string
format: date-time
description: end date to retrieve
- in: query
name: answered
required: false
schema:
type: string
enum:
- true
- false
description: retrieve only answered calls
- in: query
name: direction
required: false
schema:
type: string
enum:
- inbound
- outbound
get:
summary: retrieve recent calls for an account
operationId: listRecentCalls
responses:
200:
description: retrieve recent call records for a specified account
content:
application/json:
schema:
type: object
properties:
total:
type: number
format: integer
description: total number of records in that database that match the filter criteria
batch:
type: number
format: integer
description: total number of records returned in this page set
page:
type: number
format: integer
description: page number that was requested, and is being returned
data:
type: array
items:
type: object
properties:
service_provider_sid:
type: string
format: uuid
account_sid:
type: string
format: uuid
call_sid:
type: string
format: uuid
from:
type: string
to:
type: string
answered:
type: boolean
sip_call_id:
type: string
sip_status:
type: number
format: integer
duration:
type: number
format: integer
attempted_at:
type: number
format: integer
answered_at:
type: number
format: integer
terminated_at:
type: number
format: integer
termination_reason:
type: string
host:
type: string
remote_host:
type: string
direction:
type: string
enum:
- inbound
- outbound
trunk:
type: string
required:
- account_sid
- call_sid
- attempted_at
- terminated_at
- answered
- direction
- from
- to
- sip_status
- duration
404:
description: account not found
/ServiceProviders/{ServiceProviderSid}/RecentCalls/{CallId}:
parameters:
- name: ServiceProviderSid
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: service provider 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
/ServiceProviders/{ServiceProviderSid}/Alerts:
parameters:
- name: ServiceProviderSid
in: path
required: true
schema:
type: string
format: uuid
- in: query
name: page
required: true
schema:
type: number
format: integer
description: page number of data to retrieve
- in: query
name: count
required: true
schema:
type: number
format: integer
description: number of rows to retrieve in each page set
- in: query
name: days
required: false
schema:
type: number
format: integer
description: number of days back to retrieve, must be ge 1 and le 30
- in: query
name: start
required: false
schema:
type: string
format: date-time
description: start date to retrieve
- in: query
name: end
required: false
schema:
type: string
format: date-time
description: end date to retrieve
- in: query
name: alert_type
required: false
schema:
type: string
enum:
- webhook-failure
- webhook-connection-failure
- webhook-auth-failure
- no-tts
- no-stt
- tts-failure
- stt-failure
- no-carrier
- call-limit
- device-limit
- api-limit
get:
summary: retrieve alerts for a service provider
operationId: listAlerts
responses:
200:
description: retrieve alerts for a specified account
content:
application/json:
schema:
type: object
properties:
total:
type: number
format: integer
description: total number of records in that database that match the filter criteria
batch:
type: number
format: integer
description: total number of records returned in this page set
page:
type: number
format: integer
description: page number that was requested, and is being returned
data:
type: array
items:
type: object
properties:
time:
type: string
format: date-time
service_provider_sid:
type: string
format: uuid
account_sid:
type: string
format: uuid
alert_type:
type: string
message:
type: string
detail:
type: string
required:
- time
- account_sid
- alert_type
- message
404:
description: service provider not found
/Accounts/{AccountSid}/Alerts:
parameters:
- name: AccountSid
@@ -3530,16 +3049,6 @@ paths:
enum:
- completed
- no-answer
conf_mute_status:
type: string
enum:
- mute
- unmute
conf_hold_status:
type: string
enum:
- hold
- unhold
listen_status:
type: string
enum:
@@ -3551,35 +3060,8 @@ paths:
- mute
- unmute
whisper:
$ref: '#/components/schemas/Webhook'
sip_request:
type: object
properties:
method:
type: string
content_type:
type: string
content:
type: string
headers:
type: object
record:
type: object
properties:
action:
type: string
enum:
- startCallRecording
- stopCallRecording
- pauseCallRecording
- resumeCallRecording
recordingID:
type: string
siprecServerURL:
type: string
$ref: '#/components/schemas/Webhook'
responses:
200:
description: Accepted
202:
description: Accepted
400:
@@ -3628,22 +3110,6 @@ paths:
example: 2531329f-fb09-4ef7-887e-84e648214436
providerResponse:
type: string
480:
description: temporary failure
content:
application/json:
schema:
required:
- sid
properties:
sid:
type: string
format: uuid
example: 2531329f-fb09-4ef7-887e-84e648214436
message:
type: string
smpp_err_code:
type: string
400:
description: bad request
components:
@@ -4166,8 +3632,6 @@ components:
type: string
registration_hook_sid:
type: string
queue_event_hook_sid:
type: string
device_calling_application_sid:
type: string
is_active:
@@ -4525,15 +3989,6 @@ components:
- requires_static_ip
- e164_leading_plus
- requires_register
Limits:
type: object
properties:
category:
type: string
enum:
- voice_call_session
- api_limit
- devices
security:
- bearerAuth: []

View File

@@ -1,5 +1,5 @@
const crypto = require('crypto');
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
const algorithm = 'aes-256-ctr';
const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256')
.update(String(process.env.JWT_SECRET))

View File

@@ -1,93 +0,0 @@
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
};

View File

@@ -2,7 +2,6 @@ const ttsGoogle = require('@google-cloud/text-to-speech');
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
const Polly = require('aws-sdk/clients/polly');
const AWS = require('aws-sdk');
const bent = require('bent');
const fs = require('fs');
const testGoogleTts = async(logger, credentials) => {
@@ -53,59 +52,9 @@ const testAwsStt = (logger, credentials) => {
});
};
const testMicrosoftTts = async(logger, credentials) => {
const {api_key, region} = credentials;
if (!api_key) throw new Error('testMicrosoftTts: credentials are missing api_key');
if (!region) throw new Error('testMicrosoftTts: credentials are missing region');
try {
const getJSON = bent('json', {
'Ocp-Apim-Subscription-Key': api_key
});
const response = await getJSON(`https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`);
return response;
} catch (err) {
logger.info({err}, `testMicrosoftTts - failed to list voices for region ${region}`);
throw err;
}
};
const testMicrosoftStt = async(logger, credentials) => {
//TODO
return true;
};
const testWellSaidTts = async(logger, credentials) => {
const {api_key} = credentials;
try {
const post = bent('https://api.wellsaidlabs.com', 'POST', 'buffer', {
'X-Api-Key': api_key,
'Accept': 'audio/mpeg',
'Content-Type': 'application/json'
});
const mp3 = await post('/v1/tts/stream', {
text: 'Hello, world',
speaker_id: '3'
});
return mp3;
} catch (err) {
logger.info({err}, 'testWellSaidTts returned error');
throw err;
}
};
const testWellSaidStt = async(logger, credentials) => {
//TODO
return true;
};
module.exports = {
testGoogleTts,
testGoogleStt,
testAwsTts,
testWellSaidTts,
testAwsStt,
testMicrosoftTts,
testMicrosoftStt,
testWellSaidStt,
testAwsStt
};

View File

@@ -7,7 +7,7 @@ assert.ok(process.env.STRIPE_BASE_URL || process.env.NODE_ENV === 'test',
'missing env STRIPE_BASE_URL for billing operations');
const bent = require('bent');
const formurlencoded = require('form-urlencoded');
const formurlencoded = require('form-urlencoded').default;
const qs = require('qs');
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
const basicAuth = () => {

11436
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,14 @@
{
"name": "jambonz-api-server",
"version": "v0.7.6",
"version": "1.2.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"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 K8S=true K8S_FEATURE_SERVER_SERVICE_NAME=127.0.0.1 K8S_FEATURE_SERVER_SERVICE_PORT=3100 node test/ ",
"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/ ",
"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",
"upgrade-db": "node ./db/upgrade-jambonz-db.js",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib",
"prepare": "husky install"
"jslint": "eslint app.js lib"
},
"author": "Dave Horton",
"license": "MIT",
@@ -18,40 +16,40 @@
"url": "https://github.com/jambonz/jambonz-api-server.git"
},
"dependencies": {
"@google-cloud/speech": "^4.10.2",
"@google-cloud/text-to-speech": "^3.4.0",
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/time-series": "^0.2.1",
"@google-cloud/speech": "^4.2.0",
"@google-cloud/text-to-speech": "^3.1.3",
"@jambonz/db-helpers": "^0.6.12",
"@jambonz/realtimedb-helpers": "^0.4.3",
"@jambonz/time-series": "^0.1.5",
"argon2-ffi": "^2.0.0",
"aws-sdk": "^2.1152.0",
"aws-sdk": "^2.839.0",
"bent": "^7.3.12",
"cors": "^2.8.5",
"debug": "^4.3.4",
"express": "^4.18.1",
"express-rate-limit": "^6.4.0",
"form-data": "^2.5.1",
"form-urlencoded": "^6.1.0",
"helmet": "^5.1.0",
"debug": "^4.3.1",
"express": "^4.17.1",
"form-data": "^2.3.3",
"form-urlencoded": "^4.2.1",
"google-libphonenumber": "^3.2.15",
"jsonwebtoken": "^8.5.1",
"mailgun.js": "^3.7.3",
"mysql2": "^2.3.3",
"passport": "^0.6.0",
"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.222.0",
"swagger-ui-express": "^4.4.0",
"uuid": "^8.3.2",
"stripe": "^8.138.0",
"swagger-ui-express": "^4.1.6",
"uuid": "^3.4.0",
"yamljs": "^0.3.0"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint": "^7.17.0",
"eslint-plugin-promise": "^4.2.1",
"husky": "7.0.4",
"nyc": "^15.1.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.9",
"tape": "^5.5.3"
"tape": "^5.2.2"
}
}

View File

@@ -154,10 +154,6 @@ test('account tests', async(t) => {
registration_hook: {
url: 'http://example.com/reg2',
method: 'get'
},
queue_event_hook: {
url: 'http://example.com/q',
method: 'post'
}
}
});
@@ -188,60 +184,6 @@ test('account tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully assigned phone number to account');
/* query all limits for an account */
result = await request.get(`/Accounts/${sid}/Limits`, {
auth: authAdmin,
json: true,
});
t.ok(result.length === 0, 'successfully queried account limits when there is none configured');
/* add a new limit for a account */
result = await request.post(`/Accounts/${sid}/Limits`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
category: 'voice_call_session',
quantity: 200
}
});
t.ok(result.statusCode === 201, 'successfully added a call session limit to an account');
/* update an existing limit for a account */
result = await request.post(`/Accounts/${sid}/Limits`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
category: 'voice_call_session',
quantity: 205
}
});
t.ok(result.statusCode === 201, 'successfully updated a call session limit to an account');
/* query all limits for an account */
result = await request.get(`/Accounts/${sid}/Limits`, {
auth: authAdmin,
json: true,
});
//console.log(result);
t.ok(result.length === 1 && result[0].quantity === 205, 'successfully queried account limits');
/* query all limits for an account by category*/
result = await request.get(`/Accounts/${sid}/Limits?category=voice_call_session`, {
auth: authAdmin,
json: true,
});
//console.log(result);
t.ok(result.length === 1 && result[0].quantity === 205, 'successfully queried account limits by category');
/* delete call session limits for a service provider */
result = await request.delete(`/Accounts/${sid}/Limits?category=voice_call_session`, {
auth: authAdmin,
resolveWithFullResponse: true
});
t.ok(result.statusCode === 204, 'successfully deleted a call session limit for an account');
/* delete account */
result = await request.delete(`/Accounts/${sid}`, {
auth: authAdmin,

View File

@@ -60,8 +60,8 @@ test('application tests', async(t) => {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'daveh' , 'successfully retrieved application by sid');
t.ok(result.messaging_hook.url === 'http://example.com/sms' , 'successfully retrieved messaging_hook from application');
t.ok(result[0].name === 'daveh' , 'successfully retrieved application by sid');
t.ok(result[0].messaging_hook.url === 'http://example.com/sms' , 'successfully retrieved messaging_hook from application');
/* update applications */
result = await request.put(`/Applications/${sid}`, {
@@ -84,7 +84,7 @@ test('application tests', async(t) => {
auth: authAdmin,
json: true,
});
t.ok(result.messaging_hook.url === 'http://example2.com/mms' , 'successfully updated messaging_hook');
t.ok(result[0].messaging_hook.url === 'http://example2.com/mms' , 'successfully updated messaging_hook');
/* assign phone number to application */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {

View File

@@ -318,7 +318,7 @@ test('authentication tests', async(t) => {
json: true
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.name === "A1-app", 'using account token A1 we are able to retrieve application A1');
t.ok(result.length === 1, 'using account token A1 we are able to retrieve application A1');
/* cannot see app under another account using account token */
result = await request.get(`/Applications/${appA2}`, {

View File

@@ -1,82 +0,0 @@
const test = require('tape');
const jwt = require('jsonwebtoken');
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const consoleLogger = { debug: console.log, info: console.log, error: console.error }
const {
createServiceProvider,
createAccount,
createGoogleSpeechCredentials,
getLastRequestFromFeatureServer
} = require('./utils');
test('Create Call Success With Synthesizer in Payload', async (t) => {
// GIVEN
const app = require('../app');
let result;
const service_provider_sid = await createServiceProvider(request, 'account_has_synthesizer');
const account_sid = await createAccount(request, service_provider_sid, 'account_has_synthesizer');
const token = jwt.sign({
account_sid
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = { bearer: token };
const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true)
// WHEN
result = await request.post(`/Accounts/${account_sid}/Calls`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
call_hook: "https://public-apps.jambonz.us/hello-world",
call_status_hook: "https://public-apps.jambonz.us/call-status",
from: "15083778299",
to: {
type: "phone",
number: "15089084809"
},
speech_synthesis_vendor: "google",
speech_synthesis_language: "en-US",
speech_synthesis_voice: "en-US-Standard-C",
speech_recognizer_vendor: "google",
speech_recognizer_language: "en-US"
}
});
// THEN
t.ok(result.statusCode === 201, 'successfully created Call without Synthesizer && application_sid');
const fs_request = await getLastRequestFromFeatureServer('15083778299_createCall');
const obj = JSON.parse(fs_request);
t.ok(obj.body.speech_synthesis_vendor == 'google', 'speech synthesizer successfully added')
t.ok(obj.body.speech_recognizer_vendor == 'google', 'speech recognizer successfully added')
});
test('Create Call Success Without Synthesizer in Payload', async (t) => {
// GIVEN
const app = require('../app');
let result;
const service_provider_sid = await createServiceProvider(request, 'account2_has_synthesizer');
const account_sid = await createAccount(request, service_provider_sid, 'account2_has_synthesizer');
const token = jwt.sign({
account_sid
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = { bearer: token };
const speech_sid = await createGoogleSpeechCredentials(request, account_sid, null, authUser, true, true)
// WHEN
result = await request.post(`/Accounts/${account_sid}/Calls`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
call_hook: "https://public-apps.jambonz.us/hello-world",
call_status_hook: "https://public-apps.jambonz.us/call-status",
from: "15083778299",
to: {
type: "phone",
number: "15089084809"
}
}
}).then(data => { t.ok(false, 'Create Call should not be success') })
.catch(error => { t.ok(error.response.statusCode === 400, 'Call failed for no synthesizer') });
});

View File

@@ -1,16 +1,7 @@
version: '3'
networks:
jambonz-api:
driver: bridge
ipam:
config:
- subnet: 172.58.0.0/16
services:
mysql:
platform: linux/x86_64
image: mysql:5.7
ports:
- "3360:3306"
@@ -19,11 +10,7 @@ services:
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "127.0.0.1", "--protocol", "tcp"]
timeout: 5s
retries: 10
networks:
jambonz-api:
ipv4_address: 172.58.0.2
retries: 10
redis:
image: redis:5-alpine
ports:
@@ -31,106 +18,8 @@ services:
depends_on:
mysql:
condition: service_healthy
networks:
jambonz-api:
ipv4_address: 172.58.0.3
influxdb:
platform: linux/x86_64
image: influxdb:1.8
image: influxdb:1.8-alpine
ports:
- "8086:8086"
networks:
jambonz-api:
ipv4_address: 172.58.0.4
db:
image: postgres:11-alpine
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
feature-server-test-scaffold:
image: jambonz/feature-server-test-scaffold:latest
ports:
- "3100:3000/tcp"
networks:
jambonz-api:
ipv4_address: 172.58.0.9

View File

@@ -1,23 +0,0 @@
FROM --platform=linux/amd64 node:18.6.0-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base as build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "node", "app.js" ]

View File

@@ -1,72 +0,0 @@
const assert = require('assert');
const fs = require('fs');
const express = require('express');
const app = express();
const listenPort = process.env.HTTP_PORT || 3000;
const { v4: uuidv4 } = require('uuid');
let hook_mapping = new Map();
app.listen(listenPort, () => {
console.log(`sample jambones app server listening on ${listenPort}`);
});
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
/*
* CreateCall
*/
app.all('/v1/createCall', (req, res) => {
console.log(req.body, 'POST /v1/createCall');
const key = req.body.from + '_createCall'
addRequestToMap(key, req, hook_mapping);
res.status(201).json({
sid: uuidv4(),
callId: uuidv4()
});
});
// Fetch Requests
app.get('/requests/:key', (req, res) => {
let key = req.params.key;
if (hook_mapping.has(key)) {
return res.json(hook_mapping.get(key));
} else {
return res.sendStatus(404);
}
})
app.get('/lastRequest/:key', (req, res) => {
let key = req.params.key;
if (hook_mapping.has(key)) {
let requests = hook_mapping.get(key);
return res.json(requests[requests.length - 1]);
} else {
return res.sendStatus(404);
}
})
/*
* private function
*/
function addRequestToMap(key, req, map) {
let headers = new Map()
for(let i = 0; i < req.rawHeaders.length; i++) {
if (i % 2 === 0) {
headers.set(req.rawHeaders[i], req.rawHeaders[i + 1])
}
}
let request = {
'url': req.url,
'headers': Object.fromEntries(headers),
'body': req.body
}
if (map.has(key)) {
map.get(key).push(request);
} else {
map.set(key, [request]);
}
}

View File

@@ -1,873 +0,0 @@
{
"name": "webhook",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "webhook",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"uuid": "^8.3.2"
}
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"dependencies": {
"mime-db": "1.45.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"dependencies": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
}
},
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
},
"mime-types": {
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"requires": {
"mime-db": "1.45.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

View File

@@ -1,15 +0,0 @@
{
"name": "webhook",
"version": "1.0.0",
"description": "simple webhook app for test purposes",
"main": "app.js",
"scripts": {
"start": "node app"
},
"author": "Dave Horton",
"license": "MIT",
"dependencies": {
"express": "^4.17.1",
"uuid": "^8.3.2"
}
}

View File

@@ -1,45 +0,0 @@
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);
}
}
});

View File

@@ -13,6 +13,4 @@ require('./ms-teams');
require('./speech-credentials');
require('./recent-calls');
require('./webapp_tests');
// require('./homer');
require('./call-test');
require('./docker_stop');

View File

@@ -1,6 +1,7 @@
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', {

View File

@@ -1,6 +0,0 @@
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE DATABASE homer_config;
EOSQL

View File

@@ -28,18 +28,13 @@ test('recent calls tests', async(t) => {
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUser = {bearer: token};
const tokenSP = jwt.sign({
service_provider_sid
}, process.env.JWT_SECRET, { expiresIn: '1h' });
const authUserSP = {bearer: token};
/* write sample cdr data */
const points = 500;
const data = [];
const start = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000));
const now = new Date();
const increment = (now.getTime() - start.getTime()) / points;
for (let i =0 ; i < 5; i++) {
for (let i =0 ; i < 500; i++) {
const attempted_at = new Date(start.getTime() + (i * increment));
const failed = 0 === i % 5;
data.push({
@@ -56,8 +51,7 @@ test('recent calls tests', async(t) => {
termination_reason: 'caller hungup',
host: "192.168.1.100",
remote_host: '3.55.24.34',
account_sid,
service_provider_sid,
account_sid: account_sid,
direction: 0 === i % 2 ? 'inbound' : 'outbound',
trunk: 0 === i % 2 ? 'twilio' : 'user'
});
@@ -66,38 +60,12 @@ test('recent calls tests', async(t) => {
await writeCdrs(data);
t.pass('seeded cdr data');
/* query last 7 days by account */
/* query last 7 days */
result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25`, {
auth: authUser,
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by account');
//console.log({data: result.data}, 'Account recent calls');
/* query last 7 days by service provider */
result = await request.get(`/ServiceProviders/${service_provider_sid}/RecentCalls?page=1&count=25`, {
auth: authAdmin,
json: true,
});
t.ok(result.data.length === 5, 'retrieved 5 recent calls by service provider');
//console.log({data: result.data}, 'SP recent calls');
/* 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);

View File

@@ -1,68 +0,0 @@
<?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>

View File

@@ -1,5 +1,4 @@
const exec = require('child_process').exec ;
const { sippUac } = require('./sipp')('test_jambonz-api');
let stopping = false;
process.on('SIGINT', async() => {
@@ -67,14 +66,6 @@ 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..')
@@ -90,7 +81,6 @@ startDocker()
.then(createSchema)
.then(seedDb)
.then(resetAdminPassword)
.then(generateSipTrace)
.then(() => {
console.log('ready for testing!');
require('..');

View File

@@ -106,33 +106,6 @@ test('service provider tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully updated service provider');
/* add an api key for a service provider */
result = await request.post(`/ApiKeys`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
service_provider_sid: sid
}
});
t.ok(result.statusCode === 201, 'successfully added an api_key for a service provider');
const apiKeySid = result.body.sid;
/* query all api keys for a service provider */
result = await request.get(`/ServiceProviders/${sid}/ApiKeys`, {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all service provider keys');
/* delete an api key */
result = await request.delete(`/ApiKeys/${apiKeySid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted an api_key for a service provider');
/* add a predefined carrier for a service provider */
result = await request.post(`/ServiceProviders/${sid}/PredefinedCarriers/7d509a18-bbff-4c5d-b21e-b99bf8f8c49a`, {
auth: authAdmin,
@@ -140,35 +113,9 @@ test('service provider tests', async(t) => {
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 201, 'successfully added predefined carrier to service provider');
await deleteObjectBySid(request, '/VoipCarriers', result.body.sid);
/* add a limit for a service provider */
result = await request.post(`/ServiceProviders/${sid}/Limits`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
category: 'voice_call_session',
quantity: 1000
}
});
t.ok(result.statusCode === 201, 'successfully added a call session limit to service provider');
/* query all limits for a service provider */
result = await request.get(`/ServiceProviders/${sid}/Limits`, {
auth: authAdmin,
json: true,
});
//console.log(result);
t.ok(result.length === 1 , 'successfully queried all limits');
/* delete call session limits for a service provider */
result = await request.delete(`/ServiceProviders/${sid}/Limits?category=voice_call_session`, {
auth: authAdmin,
resolveWithFullResponse: true
});
t.ok(result.statusCode === 204, 'successfully deleted a call session limit for a service provider');
/* delete service providers */
result = await request.delete(`/ServiceProviders/${sid}`, {
auth: authAdmin,

View File

@@ -1,68 +0,0 @@
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());
});
});
};

View File

@@ -7,7 +7,6 @@ const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils');
const { noopLogger } = require('@jambonz/realtimedb-helpers/lib/utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@@ -110,64 +109,10 @@ test('speech credentials tests', async(t) => {
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
/* add a credential for microsoft */
if (process.env.MICROSOFT_API_KEY && process.env.MICROSOFT_REGION) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'microsoft',
use_for_tts: true,
api_key: process.env.MICROSOFT_API_KEY,
region: process.env.MICROSOFT_REGION
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
}
/* add a credential for wellsaid */
if (process.env.WELLSAID_API_KEY) {
result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
body: {
vendor: 'wellsaid',
use_for_tts: true,
api_key: process.env.WELLSAID_API_KEY
}
});
t.ok(result.statusCode === 201, 'successfully added speech credential');
const ms_sid = result.body.sid;
/* test the speech credential */
result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, {
resolveWithFullResponse: true,
auth: authUser,
json: true,
});
console.log(JSON.stringify(result));
/* delete the credential */
result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, {
auth: authUser,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted speech credential');
}
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
//t.end();
}
catch (err) {

View File

@@ -1,8 +1,5 @@
const { v4: uuid } = require('uuid');
const fs = require('fs');
const request_fs_mock = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3100'
});
const bytesToUuid = require("uuid/lib/bytesToUuid");
const uuid = require('uuid').v4;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
@@ -91,42 +88,6 @@ async function deleteObjectBySid(request, path, sid) {
return result;
}
async function createGoogleSpeechCredentials(request, account_sid, service_provider_sid,token, use_tts, use_stt) {
const jsonKey = fs.readFileSync(`${__dirname}/data/test.json`, {encoding: 'utf8'});
if(account_sid) {
const result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, {
auth: token ? token : authAdmin,
json: true,
body: {
vendor: 'google',
service_key: jsonKey,
use_for_tts: use_tts,
use_for_stt: use_stt
}
});
return result.sid;
} else if(service_provider_sid) {
const result = await request.post(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, {
auth: token ? token : authAdmin,
json: true,
body: {
vendor: 'google',
service_key: jsonKey,
use_for_tts: use_tts,
use_for_stt: use_stt
}
});
return result.sid;
}
}
async function getLastRequestFromFeatureServer(key) {
const result = await request_fs_mock.get(`/lastRequest/${key}`);
return result;
}
module.exports = {
createServiceProvider,
createVoipCarrier,
@@ -134,7 +95,5 @@ module.exports = {
createAccount,
createApplication,
createApiKey,
deleteObjectBySid,
createGoogleSpeechCredentials,
getLastRequestFromFeatureServer
deleteObjectBySid
};