Compare commits

..
15 Commits
Author SHA1 Message Date
Dave Horton e3963bf057 bugfix: re-invite handling from UAC side now handled properly 2021-04-27 08:16:36 -04:00
Dave Horton 238f3fe05a add test case for cidr matching of carrier inbound calls 2021-03-10 10:45:56 -05:00
Dave Horton 51e926629c update to db-helpers that supports sip gateways identified by cidr range 2021-03-09 19:00:35 -05:00
Dave Horton 3df94ced57 Merge pull request #8 from radicaldrew/master
Updated Dockerfile
2020-12-30 08:57:55 -05:00
Andrew 62063a7250 Updated Dockerfile
created multistage build and tested in compose
2020-12-30 15:56:58 +02:00
Dave Horton 3d58fdd242 dep change to @jambonz/http-authenticator 2020-12-15 14:26:09 -05:00
Dave Horton 12ba6b3968 Merge pull request #7 from jambonz/gh-actions
migrate ci to github actions
2020-12-14 14:50:04 -05:00
Dave Horton ae8469cfee jslint 2020-12-14 14:47:05 -05:00
Dave Horton bf7c09fc83 migrate ci to github actions 2020-12-14 14:44:41 -05:00
Dave Horton 4476c233ff update deps 2020-12-13 14:23:17 -05:00
Dave Horton 45883238e1 update to stats-collector that reconnects when socket dropped 2020-12-11 14:44:57 -05:00
Dave Horton 471f3dc0bb deps 2020-10-26 12:03:56 -04:00
Dave Horton 5ba9058216 update realtimedb-helpers 2020-10-26 10:04:30 -04:00
Dave Horton 6f82454be2 bump deps 2020-10-26 09:29:03 -04:00
Dave Horton 5f60580040 update stats 2020-10-26 09:26:43 -04:00
18 changed files with 5586 additions and 1254 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"jsx": false,
"modules": false
},
"ecmaVersion": 2017
"ecmaVersion": 2018
},
"plugins": ["promise"],
"rules": {
+19
View File
@@ -0,0 +1,19 @@
name: CI
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm install
- run: npm run jslint
- run: npm test
-9
View File
@@ -1,9 +0,0 @@
sudo: required
language: node_js
node_js:
- "lts/*"
services:
- docker
- mysql
script:
- npm test
+10 -7
View File
@@ -1,13 +1,16 @@
FROM node:lts-alpine
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
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/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
COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app
CMD [ "npm", "start" ]
+1 -1
View File
@@ -1,4 +1,4 @@
# sbc-inbound [![Build Status](https://secure.travis-ci.org/jambonz/sbc-inbound.png)](http://travis-ci.org/jambonz/sbc-inbound)
# sbc-inbound ![Build Status](https://github.com/jambonz/sbc-inbound/workflows/CI/badge.svg)
This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
+37 -22
View File
@@ -79,7 +79,13 @@ class CallSession extends Emitter {
this.logger.info(`uri will be: ${uri}, proxy ${proxy}`);
try {
const opts = Object.assign(this.rtpEngineOpts.offer, {sdp: this.req.body});
const opts = {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['public', 'private'],
sdp: this.req.body
};
const response = await this.offer(opts);
this.logger.debug({opts, response}, 'response from rtpengine to offer');
if ('ok' !== response.result) {
@@ -129,8 +135,14 @@ class CallSession extends Emitter {
proxyResponseHeaders: ['all'],
localSdpB: response.sdp,
localSdpA: async(sdp, res) => {
this.toTag = res.getParsedHeader('To').params.tag;
const opts = Object.assign(this.rtpEngineOpts.answer, {sdp, 'to-tag': this.toTag});
this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
const opts = {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uas.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
sdp
};
const response = await this.answer(opts);
if ('ok' !== response.result) {
this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`);
@@ -190,8 +202,8 @@ class CallSession extends Emitter {
});
});
uas.on('modify', this._onReinvite.bind(this, uas));
uac.on('modify', this._onReinvite.bind(this, uac));
uac.on('modify', this._onFeatureServerReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uas.on('refer', this._onRefer.bind(this, uas));
@@ -253,41 +265,44 @@ class CallSession extends Emitter {
async _onReinvite(dlg, req, res) {
try {
let opts = Object.assign(this.rtpEngineOpts.offer, {sdp: req.body, 'to-tag': this.toTag});
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
const direction = dlg.type === 'uas' ? ['private', 'public'] : ['public', 'private'];
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
'from-tag': fromTag,
'to-tag': toTag,
direction,
sdp: req.body,
};
let response = await this.offer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`);
}
this.logger.info({opts, response}, 'sent offer for reinvite to rtpengine');
const sdp = await dlg.other.modify(response.sdp);
opts = Object.assign(this.rtpEngineOpts.answer, {sdp});
opts = {
...this.rtpEngineOpts.common,
...answerMedia,
'from-tag': fromTag,
'to-tag': toTag,
sdp
};
response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
this.logger.info({opts, response}, 'sent answer for reinvite to rtpengine');
res.send(200, {body: response.sdp});
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
}
}
async _onFeatureServerReinvite(dlg, req, res) {
try {
const opts = Object.assign(this.rtpEngineOpts.answer, {sdp: req.body, 'to-tag': this.toTag});
const response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onFeatureServerReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
res.send(200, {body: dlg.local.sdp});
} catch (err) {
this.logger.error(err, 'Error handling reinvite from feature server');
}
}
async _onFeatureServerTransfer(dlg, req, res) {
try {
const referTo = req.getParsedHeader('Refer-To');
+4 -2
View File
@@ -1,7 +1,9 @@
const debug = require('debug')('jambonz:sbc-inbound');
const Emitter = require('events');
const parseUri = require('drachtio-srf').parseUri;
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ? process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) : [];
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ?
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
[];
class AuthOutcomeReporter extends Emitter {
constructor(stats) {
@@ -15,7 +17,7 @@ class AuthOutcomeReporter extends Emitter {
module.exports = function(srf, logger) {
const {lookupSipGatewayBySignalingAddress, lookupAuthHook} = srf.locals.dbHelpers;
const {stats} = srf.locals;
const authenticator = require('jambonz-http-authenticator')(lookupAuthHook, logger, {
const authenticator = require('@jambonz/http-authenticator')(lookupAuthHook, logger, {
blacklistUnknownRealms: true,
emitter: new AuthOutcomeReporter(stats)
});
+15 -11
View File
@@ -12,20 +12,24 @@ function getAppserver(srf) {
}
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
const from = req.getParsedHeader('from');
const common = {'call-id': req.get('Call-ID'), 'from-tag': from.params.tag};
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const srctOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
const common = {
'call-id': req.get('Call-ID'),
'replace': ['origin', 'session-connection']
};
return {
common,
offer: Object.assign(
{'sdp': req.body, 'replace': ['origin', 'session-connection']},
{'direction': [ 'public', 'private']},
common,
dstIsUsingSrtp ? srtpOpts : rtpCharacteristics),
answer: Object.assign(
{'replace': ['origin', 'session-connection']},
common,
srcIsUsingSrtp ? srtpOpts : rtpCharacteristics)
uas: {
tag: from.params.tag,
mediaOpts: srctOpts
},
uac: {
tag: null,
mediaOpts: dstOpts
}
};
}
+5451 -1163
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -20,26 +20,26 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test JAMBONES_MYSQL_HOST=localhost 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=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 JAMBONES_FEATURE_SERVERS=172.38.0.11 node test/ | ./node_modules/.bin/tap-spec",
"test": "NODE_ENV=test JAMBONES_MYSQL_HOST=127.0.0.1 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=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 JAMBONES_FEATURE_SERVERS=172.38.0.11 node test/ | ./node_modules/.bin/tap-spec",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.4.2",
"@jambonz/realtimedb-helpers": "0.2.16",
"@jambonz/db-helpers": "^0.5.12",
"@jambonz/http-authenticator": "^0.1.6",
"@jambonz/realtimedb-helpers": "^0.2.20",
"@jambonz/rtpengine-utils": "^0.1.7",
"@jambonz/stats-collector": "0.0.4",
"debug": "^4.1.1",
"@jambonz/stats-collector": "^0.1.4",
"debug": "^4.3.1",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.4.37",
"jambonz-http-authenticator": "0.1.5",
"pino": "^6.5.1",
"drachtio-srf": "^4.4.44",
"pino": "^6.8.0",
"rtpengine-client": "^0.1.1"
},
"devDependencies": {
"blue-tape": "^1.0.0",
"clear-module": "^4.1.1",
"eslint": "^7.7.0",
"eslint": "^7.15.0",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^15.1.0",
"tap-spec": "^5.0.0"
+3 -4
View File
@@ -1,9 +1,8 @@
const test = require('tape').test ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
test('creating jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp < ${__dirname}/db/create_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('database successfully created');
t.end();
@@ -11,7 +10,7 @@ test('creating jambones_test database', (t) => {
});
test('creating schema', (t) => {
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/db/jambones-sql.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/jambones-sql.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('schema successfully created');
t.end();
@@ -19,7 +18,7 @@ test('creating schema', (t) => {
});
test('populating test case data', (t) => {
exec(`mysql -h localhost -u root ${pwd} -D jambones_test < ${__dirname}/db/populate-test-data.sql`, (err, stdout, stderr) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/populate-test-data.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('test data set created');
t.end();
+2 -2
View File
@@ -1,3 +1,3 @@
create database jambones_test;
create user jambones_test@localhost IDENTIFIED WITH mysql_native_password by 'jambones_test';
grant all on jambones_test.* to jambones_test@localhost;
create user jambones_test@'%' IDENTIFIED WITH mysql_native_password by 'jambones_test';
grant all on jambones_test.* to jambones_test@'%';
+10 -3
View File
@@ -55,8 +55,8 @@ api_key_sid CHAR(36) NOT NULL UNIQUE ,
token CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36),
service_provider_sid CHAR(36),
expires_at TIMESTAMP NULL,
last_used TIMESTAMP NULL,
expires_at TIMESTAMP NULL DEFAULT NULL,
last_used TIMESTAMP NULL DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (api_key_sid)
) ENGINE=InnoDB COMMENT='An authorization token that is used to access the REST api';
@@ -98,6 +98,10 @@ description VARCHAR(255),
account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account',
application_sid CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application',
e164_leading_plus BOOLEAN NOT NULL DEFAULT false,
requires_register BOOLEAN NOT NULL DEFAULT false,
register_username VARCHAR(64),
register_sip_realm VARCHAR(64),
register_password VARCHAR(64),
PRIMARY KEY (voip_carrier_sid)
) ENGINE=InnoDB COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -148,8 +152,9 @@ CREATE TABLE applications
application_sid CHAR(36) NOT NULL UNIQUE ,
name VARCHAR(64) NOT NULL,
account_sid CHAR(36) NOT NULL COMMENT 'account that this application belongs to',
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls to phone numbers owned by this account',
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
@@ -242,6 +247,8 @@ ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REF
ALTER TABLE applications ADD FOREIGN KEY call_status_hook_sid_idxfk (call_status_hook_sid) REFERENCES webhooks (webhook_sid);
ALTER TABLE applications ADD FOREIGN KEY messaging_hook_sid_idxfk (messaging_hook_sid) REFERENCES webhooks (webhook_sid);
CREATE INDEX service_provider_sid_idx ON service_providers (service_provider_sid);
CREATE INDEX name_idx ON service_providers (name);
CREATE INDEX root_domain_idx ON service_providers (root_domain);
+1 -1
View File
@@ -7,7 +7,7 @@ values ('ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3
insert into voip_carriers (voip_carrier_sid, name) values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.20', true, true);
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.20/32', true, true);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound)
values ('efbc4830-57cd-4c78-a56f-d64fdf210fe8', '287c1452-620d-4195-9f19-c9814ef90d78', '3.3.3.3', 5062, false, true);
+17 -1
View File
@@ -7,7 +7,20 @@ networks:
config:
- subnet: 172.38.0.0/16
services:
services:
mysql:
image: mysql:5.7
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "--protocol", "tcp"]
timeout: 5s
retries: 10
networks:
sbc-inbound:
ipv4_address: 172.38.0.2
sbc:
image: drachtio/drachtio-server:latest
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
@@ -16,6 +29,9 @@ services:
networks:
sbc-inbound:
ipv4_address: 172.38.0.10
depends_on:
mysql:
condition: service_healthy
appserver:
image: drachtio/sipp:latest
-1
View File
@@ -1,5 +1,4 @@
require('./docker_start');
require('./create-test-db');
require('./sip-tests');
require('./remove-test-db');
require('./docker_stop');
-11
View File
@@ -1,11 +0,0 @@
const test = require('tape').test ;
const exec = require('child_process').exec ;
const pwd = process.env.TRAVIS ? '' : '-p$MYSQL_ROOT_PASSWORD';
test('dropping jambones_test database', (t) => {
exec(`mysql -h localhost -u root ${pwd} < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err);
t.pass('database successfully dropped');
t.end();
});
});
+6 -6
View File
@@ -1,5 +1,5 @@
const { spawn } = require('child_process');
const debug = require('debug')('test:sipp');
const debug = require('debug')('jambonz:ci');
let network;
const obj = {};
let output = '';
@@ -27,7 +27,7 @@ obj.output = () => {
obj.sippUac = (file, bindAddress) => {
const cmd = 'docker';
const args = [
'run', '-ti', '--rm', '--net', `${network}`,
'run', '--rm', '--net', `${network}`,
'-v', `${__dirname}/scenarios:/tmp/scenarios`,
'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
'-m', '1',
@@ -37,7 +37,7 @@ obj.sippUac = (file, bindAddress) => {
'sbc'
];
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
if (bindAddress) args.splice(4, 0, '--ip', bindAddress);
//console.log(args.join(' '));
clearOutput();
@@ -57,11 +57,11 @@ obj.sippUac = (file, bindAddress) => {
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
debug(`stderr: ${data}`);
addOutput(data.toString());
});
child_process.stdout.on('data', (data) => {
//debug(`stdout: ${data}`);
child_process.stderr.on('data', (data) => {
debug(`stderr: ${data}`);
addOutput(data.toString());
});
});