Compare commits

...
Author SHA1 Message Date
Hoan Luu Huu bb857a72ff let realtimedb-utils build configuration from env vars (#122) 2023-11-14 09:13:08 -05:00
Hoan Luu Huu aea3033ae0 support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation (#124)
* support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation

* support multi PRIVATE_VOIP_NETWORK_CIDR
2023-11-13 15:24:41 -05:00
Dave Horton 9b9eaa37fb 0.8.5 2023-11-09 12:39:24 -05:00
Dave Horton 56e0e3c0f2 update drachtio-srf 2023-11-09 10:02:46 -05:00
Hoan Luu Huu 6a6b4dd26f upgrade drachtio srf (#121)
* upgrade drachtio srf

* fix review comments
2023-10-24 08:42:53 -04:00
Hoan Luu Huu a3799576a2 add sentinelPassword option (#119) 2023-10-04 19:41:21 -04:00
Antony Jukesandajukes 63e72fbfce feat/MS Teams IP check improvement (#117)
* added ip in cidr utilities

* middleware add ms teams cidr check

* use cidr-matcher lib

* removed redundant require

* moved cidr-matcher require to top of file

* moved express require to top of file

---------

Co-authored-by: ajukes <ajukes@callable.io>
2023-09-18 10:35:04 -04:00
Dave Horton e5dce35bab more efficient checking of device calls (#118)
* more efficient checking of device calls

* when searching for matching gateways choose largest enclosing netmask
2023-09-15 10:53:22 -04:00
two56andMatt Preskett b6675de2bd Fix: Feature server REFER (#114)
* Update _onFeatureServerTransfer to fix REFER case

* Add INFO listener

---------

Co-authored-by: Matt Preskett <matt.preskett@netcall.com>
2023-08-20 08:47:01 -04:00
Dave Horton 66e73d8353 logging 2023-08-14 14:36:07 -04:00
Hoan Luu Huu a25aa08e17 update stats colector version (#111) 2023-08-07 21:22:47 -04:00
Hoan Luu Huu b1296c8e89 correct pare callRecording headers from sip info (#110) 2023-07-20 09:02:04 -04:00
Hoan Luu Huu 17df3a4ca8 Feat/siprec custom headers (#109)
* siprec custom headers

* siprec custom headers

* update siprec client util
2023-07-20 08:21:09 -04:00
Hoan Luu Huu f2e820915d Multi srs (#106)
* multi srs

* multi srs

* multi srs

* fix review comment
2023-07-04 16:41:46 +01:00
Dave Horton a1ec0fd5da 0.8.4 2023-06-28 09:33:01 +01:00
Hoan Luu Huu d7654de526 fix: update aws sdk v3 (#108)
* fix: update aws sdk v3

* fix: update aws sdk v3

* fix jslint issue

* fix jslint issue

* fix parse aws response

* fix parse aws response
2023-06-28 09:20:37 +01:00
Hoan Luu Huu 6e47e37cc2 fix client encrypted password (#105)
* fix client encrypted password

* fix client encrypted password

* fix failing testcase
2023-06-15 20:47:31 -04:00
Hoan Luu Huu c37d417a69 Feat: jambonz clients (#104)
* authenticate user

* authenticate user

* update db helper

* update db helper

* fix jslint

* use digest-utils
2023-06-15 07:36:33 -04:00
Hoan Luu Huu f7b3815ee3 feat: record all calls (#98)
* feat: record all calls

* wip: add cdr

* wip: add cdr

* fix

* fix jslint

* fix: record format ext
2023-06-09 14:57:45 -04:00
Hoan Luu Huu f385a33297 redis sentinel configuration (#103)
* redis sentinel configuration

* redis sentinel configuration

* update redis version

* update redis version
2023-06-07 10:00:44 -04:00
Hoan Luu Huu a01febdc22 update dbhelper and redis (#101) 2023-06-01 08:32:32 -04:00
Dave Horton e641c590b2 Fix/cidr error handling (#102)
* fix docker build

* catch error from CIDR which can happen with invalid sip gateway data
2023-05-31 09:11:53 -04:00
Dave Horton 8448e003f6 update siprec-client-utils 2023-05-29 09:23:37 -04:00
Dave Horton f6e071f31e standardize on passing .query args as array (#99) 2023-05-22 09:56:48 -04:00
Dave Horton 54d9044937 0.8.3 2023-05-11 09:25:24 -04:00
Dave Horton bcc089058f update deps 2023-05-08 13:12:50 -04:00
Snyk bot 1a09a13cfa fix: package.json & package-lock.json to reduce vulnerabilities (#97)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-XML2JS-5414874
2023-04-25 07:47:16 -04:00
Hoan Luu Huu b311d4af48 fix: ice issue (#95)
* fix: ice issue

* fix: review comment
2023-04-13 08:43:56 -04:00
Anton Voylenko 0fe8843729 README and env variables validation (#91)
* validate env at startup

* README updated with environment variables
2023-04-10 15:36:36 -04:00
Dave Horton 0b0e37020c push to docker 2023-04-10 09:43:00 -04:00
Antony Jukes 01cde07117 if else made authorization check unreachable (#90) 2023-04-06 09:34:37 -04:00
Hoan Luu Huu 31b4534878 fix: update stat collector version (#89) 2023-04-05 12:03:36 -04:00
Hoan Luu Huu 0c6365a8df fix: update stat collector version (#89) 2023-04-05 12:03:17 -04:00
Dave Hortonandsnyk-bot def2cb2be2 fix: Dockerfile to reduce vulnerabilities (#88)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3368756
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-5291792

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-03-30 13:31:30 -04:00
Dave Horton baaee6f063 bump version 2023-03-28 14:16:07 -04:00
Hoan Luu HuuandQuan HL d2f32229a2 feat: add instance_id to gause calls metric (#87)
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-21 07:54:52 -04:00
Dave Horton 38f2d27246 refactor of speech-utils (#85) 2023-03-14 10:01:19 -04:00
Dave Horton d94977169e pass on displayName of From header if we get it (#82)
* pass on displayName of From header if we get it

* bump version
2023-03-03 18:55:56 -05:00
Dave Horton 0c9df878b8 update dockerfile 2023-02-23 08:23:17 -05:00
Dave Horton 93930fff18 fix for #79 (#80) 2023-02-22 08:51:35 -05:00
Dave Horton 2ce24659ca initial attempt (#77)
* initial attempt

* fix: special handling on uas dialog
2023-02-21 14:00:36 -05:00
EgleH 7c10e98a66 Upgrade node to node:18.14.0-alpine3.16 (#78) 2023-02-21 07:54:55 -05:00
Dave Horton f3fe46319d update siprec client with fix for stop recording 2023-02-17 12:10:14 -05:00
Dave Horton 43bfeb439a update siprec-client with fix for smart tap xml 2023-02-14 14:15:11 -05:00
Dave Horton 31f34fbffa update to latest siprec client 2023-02-13 16:50:07 -05:00
Dave Horton 52d0670b88 bump version 2023-02-13 09:15:05 -05:00
Snyk bot f42128765d fix: Dockerfile to reduce vulnerabilities (#75)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314623
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314624
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314641
- https://snyk.io/vuln/SNYK-ALPINE316-OPENSSL-3314643
2023-02-12 12:45:19 -05:00
Dave Horton eac1d7a9e9 K8s scale in (#73)
* changes for proper scale-in for K8S

* fix check for call count
2023-02-07 19:58:01 -05:00
Dave Horton fae1732ddf update to latest db-helpers 2023-02-07 13:31:48 -05:00
Dave Horton c858624815 Feature/tcp to fs (#72)
* add env var K8S_FEATURE_SERVER_TRANSPORT

* add Contact with private address when using tcp to FS on K8S

* further refinement
2023-01-26 13:33:24 -05:00
Dave Horton 2f137f2bfc bugfix: handle calls with no user in To header 2023-01-22 18:03:12 -05:00
Dave Horton ef0dcfc021 update to rtpengine-utils with fix for not discarding dtmf as dups 2023-01-12 13:09:15 -05:00
Dave Horton fb8baa34af always enable dtmf from rtpengine, since dtmfCapture on FS now uses this rather than freeswitch dtmf events (#70) 2023-01-12 08:45:39 -05:00
Dave Horton 7efce18a03 gh: run tests on PR 2023-01-09 10:13:25 -05:00
Dave Horton 84e5202607 bugfix: in EC2 deployment restrict udp localport for rtpengine 2022-12-30 08:54:33 -05:00
Dave Horton 8f856a30a6 update to drachtio-srf@4.5.21 with some perf fixes 2022-12-29 09:51:14 -05:00
Dave Horton d9881632c2 faster uuid 2022-12-28 10:06:31 -06:00
Dave Horton 6d2b1a94e9 bugfix: proper detection of reinvite from FS to release media 2022-12-27 13:40:56 -06:00
Dave Horton ded83ac39a bump version 2022-12-24 12:06:57 -06:00
Dave Horton 91a29d2b2c bump siprec-utils 2022-12-21 11:44:37 -05:00
Dave Horton 27686a80e8 update to latest rtpengine-utils and drachtio-server 2022-12-21 11:20:47 -05:00
Dave Horton a9e79b23bd bugfix: account returned when carrier default routing is used was wrong 2022-12-19 21:09:14 -05:00
Dave Horton faafaa8549 add env JAMBONES_RECORD_ALL_CALLS to enable global call recording 2022-12-02 13:53:15 -05:00
Dave Horton b24ecf84d3 minor logging 2022-12-01 12:39:51 -05:00
Dave Horton 3360ddb3e6 bugfix: proper removal of X-Refer-To-Leave-Untouched on outgoing REFER 2022-12-01 12:36:49 -05:00
Dave Horton b2d48c8954 Sdes testing (#67)
* sdes testing

* log rtpengine answer

* test with sdes off (again)
2022-12-01 11:41:30 -05:00
Dave Horton 1f1a4d2330 allow SDES in sip/tls scenarios 2022-12-01 10:13:52 -05:00
Dave Horton c34da3cfa8 support for gateways sending TLS with SRTP but not dtls (#66) 2022-12-01 09:49:25 -05:00
Dave Horton 5b88065925 bugfix: allow users to fully specify a Refer-To uri without us overiding it 2022-11-30 12:55:15 -05:00
Dave Horton 438924ca36 Bugfix/customer assigned name in uri (#65)
* dont force ice on srtp calls if not offered

* support customers creating their own dns name for our SBC IPs and putting that in the request-uri of INVITEs
2022-11-29 10:58:26 -05:00
Dave Horton c4d4d7bc0a if REFER from FS has a hostname in the Refer-To preserve it on the outbound REFER 2022-11-28 12:29:58 -05:00
Dave HortonandGuilherme Rauen ebcd0ce4a2 proper selection of account-level carrier with default routing (#63) (#64)
* update node image to the latest and most secure (#61)

Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>

* #62: dont modify refer-to host unless we have a dns name as outbound gateway

* added some tests and cleanup

* update drachtio-srf to latest

* gh action use node lts version

* revert and use node 18.x
2022-11-27 16:40:04 -05:00
Guilherme RauenandGuilherme Rauen cf239ca7fa update node image to the latest and most secure (#61)
Co-authored-by: Guilherme Rauen <g.rauen@cognigy.com>
2022-11-11 11:26:10 -05:00
Dave Horton b072524585 update deps, only subscribe for DTMF when client is possibly webrtc 2022-11-06 10:12:18 -05:00
Dave Horton 7fb9966d20 update to latest db-helpers 2022-11-05 10:42:14 -04:00
Dave Horton 42b3317206 update to db-helpers with caching fix 2022-11-01 20:40:53 -04:00
Dave Horton db66d98d0a write stat for db lookup 2022-11-01 18:57:31 -04:00
Dave Horton 26169517dc fix for dtmf handler when running multiple instances on same EC2 2022-11-01 15:40:37 -04:00
Dave Horton c78ec892b9 bugfix: running multiple instances under EC2 2022-10-31 14:45:20 -04:00
Dave Horton 6d237d43b3 update to db-helpers@0.7.0 with caching option 2022-10-31 11:44:53 -04:00
Dave Horton 44d298a4f3 Issue/58 (#59)
* feature: provide a range of health check ports, HTTP_PORT to HTTP_PORT_MAX, bind to first available

* feature: provide a range of aws sns ports, AWS_SNS_PORT to AWS_SNS_PORT_MAX, bind to first available

* AWS SNS port range 3010-3019
2022-10-30 13:08:07 -04:00
Dave Horton 030059a596 bugfix: transferring conference legs to different FS 2022-10-27 10:27:09 -04:00
Dave Horton 78b60525e2 Feature/support carrier domain in invite (#57)
* add support for incoming calls from carriers we register with, who then put their domain in the host part of incoming INVITE

* fix query to lookup registration carriers
2022-10-25 13:44:43 -04:00
Dave Horton dc9103cfa1 update deps 2022-10-23 15:27:18 -04:00
Dave Horton 4dd4247dc2 update package-lock.json 2022-10-23 12:20:28 -04:00
Dave Horton 055350903a update rtpengine-utils again 2022-10-23 11:25:06 -04:00
Dave Horton 7e83791ef0 update to latest rtpengine-utils 2022-10-22 22:17:02 -04:00
Dave Horton dbedd7419e block media going back to Five9 voicestream 2022-10-20 23:05:34 -04:00
Markus FrindtandMarkus Frindt c4d07b517e [snyk] fix vulnerability (#56)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2022-10-20 21:35:55 -04:00
Dave Horton 2693077871 include X-Account-Sid header when moving call between FS 2022-10-15 10:58:27 -04:00
Dave Horton ad0912d302 bugfix for release media (#55) 2022-10-14 12:48:22 -04:00
Dave Horton e715433534 bump version 2022-10-13 16:00:41 -04:00
Dave Horton f315b1b41f update time-series 2022-10-10 09:18:35 +01:00
Dave Horton bd09703732 bugfix: inject DMTF flag was inserted over and over 2022-10-08 18:36:40 +01:00
JoanandJoan Salvatella 363eb676a3 added quotes around in the sql query (#54)
Co-authored-by: Joan Salvatella <joan@bookline.io>
2022-10-08 11:33:27 +01:00
20 changed files with 5147 additions and 3274 deletions
+31 -28
View File
@@ -2,16 +2,10 @@ name: Docker
on: on:
push: push:
# Publish `master` as Docker `latest` image.
branches: branches:
- main - main
# Publish `v1.2.3` tags as releases.
tags: tags:
- v* - '*'
env:
IMAGE_NAME: sbc-inbound
jobs: jobs:
push: push:
@@ -20,32 +14,41 @@ jobs:
if: github.event_name == 'push' if: github.event_name == 'push'
steps: steps:
- uses: actions/checkout@v2 - name: Checkout code
uses: actions/checkout@v3
- name: Build image - name: prepare tag
run: docker build . --file Dockerfile --tag $IMAGE_NAME id: prepare_tag
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: | run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME IMAGE_ID=jambonz/sbc-inbound
# Change all uppercase to lowercase # Strip git ref prefix from version
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version # Strip "v" prefix from tag name
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name # Use Docker `latest` tag convention
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') [ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention echo IMAGE_ID=$IMAGE_ID
[ "$VERSION" == "main" ] && VERSION=latest echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo VERSION=$VERSION echo "version=$VERSION" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - name: Login to Docker Hub
docker push $IMAGE_ID:$VERSION uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF
+4 -6
View File
@@ -1,17 +1,15 @@
name: CI name: CI
on: on: [push, pull_request]
push:
workflow_dispatch:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with: with:
node-version: 14.x node-version: 18.x
- run: npm ci - run: npm ci
- run: npm run jslint - run: npm run jslint
- run: npm test - run: npm test
+1 -1
View File
@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.8.0-alpine as base FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3 RUN apk --update --no-cache add --virtual .builds-deps build-base python3
+33 -2
View File
@@ -1,10 +1,41 @@
# sbc-inbound ![Build Status](https://github.com/jambonz/sbc-inbound/workflows/CI/badge.svg) # 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. This application provides a part of the SBC (Session Border Controller) functionality of jambonz platform. 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.
## Configuration ## Configuration
Configuration is provided via the [npmjs config](https://www.npmjs.com/package/config) package. The following elements make up the configuration for the application: Configuration is provided via environment variables:
| variable | meaning | required?|
|----------|----------|---------|
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|DRACHTIO_SECRET| shared secret|yes|
|HTTP_PORT| http listen port |no|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|JAMBONES_MYSQL_HOST| mysql host|yes|
|JAMBONES_MYSQL_PORT| mysql port |no|
|JAMBONES_MYSQL_USER| mysql username|yes|
|JAMBONES_MYSQL_PASSWORD| mysql password|yes|
|JAMBONES_MYSQL_DATABASE| mysql data|yes|
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|DTMF_LISTEN_PORT| DTMF listening port |no|
|JAMBONES_NG_PROTOCOL| rtpengine NG protocol |no|
|RTPENGINE_PORT| rtpengine port |no|
|JAMBONES_CLUSTER_ID| cluster id |no|
|JAMBONES_NETWORK_CIDR| CIDR of private network that feature server is running in (e.g. '172.31.0.0/16')|yes|
|JAMBONES_REDIS_HOST| redis host|yes|
|JAMBONES_REDIS_PORT|redis port|no|
|JAMBONES_RTPENGINES| commas-separated list of ip:ng-port for rtpengines (e.g. '172.31.32.10:22222')|no|
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|JAMBONES_TIME_SERIES_PORT| influxdb port |no|
|JAMBONES_RECORD_ALL_CALLS| enable auto record calls, 'yes' or 'no' |no|
|K8S| service running as kubernetes service |no|
|K8S_RTPENGINE_SERVICE_NAME| rtpengine service name(required for K8S) |no|
|K8S_FEATURE_SERVER_SERVICE_NAME| feature server service name(required for K8S) |no|
|JWT_SECRET| secret for signing JWT token |yes|
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
##### drachtio server location ##### drachtio server location
``` ```
{ {
+51 -15
View File
@@ -3,10 +3,17 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER && process.env.JAMBONES_MYSQL_USER &&
process.env.JAMBONES_MYSQL_PASSWORD && process.env.JAMBONES_MYSQL_PASSWORD &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars'); process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var'); if (process.env.JAMBONES_REDIS_SENTINELS) {
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
} else {
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
}
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env vars');
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var'); assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var'); assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var'); assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
const Srf = require('drachtio-srf'); const Srf = require('drachtio-srf');
const srf = new Srf('sbc-inbound'); const srf = new Srf('sbc-inbound');
const opts = Object.assign({ const opts = Object.assign({
@@ -28,6 +35,7 @@ const {
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20 commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
}); });
const StatsCollector = require('@jambonz/stats-collector'); const StatsCollector = require('@jambonz/stats-collector');
const CIDRMatcher = require('cidr-matcher');
const stats = new StatsCollector(logger); const stats = new StatsCollector(logger);
const {equalsIgnoreOrder, createHealthCheckApp, systemHealth} = require('./lib/utils'); const {equalsIgnoreOrder, createHealthCheckApp, systemHealth} = require('./lib/utils');
const {LifeCycleEvents} = require('./lib/constants'); const {LifeCycleEvents} = require('./lib/constants');
@@ -46,9 +54,11 @@ const {
lookupAccountBySipRealm, lookupAccountBySipRealm,
lookupAccountBySid, lookupAccountBySid,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
queryCallLimits queryCallLimits,
lookupClientByAccountAndUsername
} = require('@jambonz/db-helpers')({ } = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST, host: process.env.JAMBONES_MYSQL_HOST,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
user: process.env.JAMBONES_MYSQL_USER, user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD, password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE, database: process.env.JAMBONES_MYSQL_DATABASE,
@@ -61,15 +71,12 @@ const {
addToSet, addToSet,
removeFromSet, removeFromSet,
incrKey, incrKey,
decrKey} = require('@jambonz/realtimedb-helpers')({ decrKey} = require('@jambonz/realtimedb-helpers')({}, logger);
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp'; const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080); const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, { const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {
emitter: stats, //emitter: stats,
dtmfListenPort: process.env.DTMF_LISTEN_PORT || 22224, dtmfListenPort: process.env.DTMF_LISTEN_PORT || 22224,
protocol: ngProtocol protocol: ngProtocol
}); });
@@ -94,7 +101,8 @@ srf.locals = {...srf.locals,
lookupAccountBySid, lookupAccountBySid,
lookupAccountBySipRealm, lookupAccountBySipRealm,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
queryCallLimits queryCallLimits,
lookupClientByAccountAndUsername
}, },
realtimeDbHelpers: { realtimeDbHelpers: {
createSet, createSet,
@@ -107,7 +115,8 @@ const {
getSPForAccount, getSPForAccount,
wasOriginatedFromCarrier, wasOriginatedFromCarrier,
getApplicationForDidAndCarrier, getApplicationForDidAndCarrier,
getOutboundGatewayForRefer getOutboundGatewayForRefer,
getApplicationBySid
} = require('./lib/db-utils')(srf, logger); } = require('./lib/db-utils')(srf, logger);
srf.locals = { srf.locals = {
...srf.locals, ...srf.locals,
@@ -115,7 +124,8 @@ srf.locals = {
wasOriginatedFromCarrier, wasOriginatedFromCarrier,
getApplicationForDidAndCarrier, getApplicationForDidAndCarrier,
getOutboundGatewayForRefer, getOutboundGatewayForRefer,
getFeatureServer: require('./lib/fs-tracking')(srf, logger) getFeatureServer: require('./lib/fs-tracking')(srf, logger),
getApplicationBySid
}; };
const activeCallIds = srf.locals.activeCallIds; const activeCallIds = srf.locals.activeCallIds;
@@ -129,7 +139,6 @@ const {
const CallSession = require('./lib/call-session'); const CallSession = require('./lib/call-session');
if (process.env.DRACHTIO_HOST && !process.env.K8S) { if (process.env.DRACHTIO_HOST && !process.env.K8S) {
const CIDRMatcher = require('cidr-matcher');
const cidrs = process.env.JAMBONES_NETWORK_CIDR const cidrs = process.env.JAMBONES_NETWORK_CIDR
.split(',') .split(',')
.map((s) => s.trim()); .map((s) => s.trim());
@@ -164,6 +173,23 @@ else {
logger.info(`listening in outbound mode on port ${process.env.DRACHTIO_PORT}`); logger.info(`listening in outbound mode on port ${process.env.DRACHTIO_PORT}`);
}); });
srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET}); srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET});
srf.on('connect', (err, hp) => {
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
logger.info(`connected to drachtio listening on ${hp}`);
if (process.env.K8S_FEATURE_SERVER_TRANSPORT === 'tcp') {
const matcher = new CIDRMatcher(['192.168.0.0/24', '172.16.0.0/16', '10.0.0.0/8']);
const hostports = hp.split(',');
for (const hp of hostports) {
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
if (arr && matcher.contains(arr[2])) {
const hostport = `${arr[2]}:${arr[3]}`;
logger.info(`using sbc private address when sending to feature-server: ${hostport}`);
srf.locals.privateSipAddress = hostport;
}
}
}
});
} }
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
srf.on('error', (err) => { srf.on('error', (err) => {
@@ -230,12 +256,12 @@ if (process.env.K8S || process.env.HTTP_PORT) {
if ('test' !== process.env.NODE_ENV) { if ('test' !== process.env.NODE_ENV) {
/* update call stats periodically */ /* update call stats periodically */
setInterval(() => { setInterval(() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:inbound']); stats.gauge('sbc.sip.calls.count', activeCallIds.size,
['direction:inbound', `instance_id:${process.env.INSTANCE_ID || 0}`]);
}, 20000); }, 20000);
} }
const lookupRtpServiceEndpoints = (lookup, serviceName) => { const lookupRtpServiceEndpoints = (lookup, serviceName) => {
logger.debug(`dns lookup for ${serviceName}..`);
lookup(serviceName, {family: 4, all: true}, (err, addresses) => { lookup(serviceName, {family: 4, all: true}, (err, addresses) => {
if (err) { if (err) {
logger.error({err}, `Error looking up ${serviceName}`); logger.error({err}, `Error looking up ${serviceName}`);
@@ -305,8 +331,18 @@ process.on('SIGUSR2', handle.bind(null, removeFromSet, setName));
process.on('SIGTERM', handle.bind(null, removeFromSet, setName)); process.on('SIGTERM', handle.bind(null, removeFromSet, setName));
function handle(removeFromSet, setName, signal) { function handle(removeFromSet, setName, signal) {
logger.info(`got signal ${signal}, removing ${srf.locals.privateSipAddress} from set ${setName}`); logger.info(`got signal ${signal}`);
removeFromSet(setName, srf.locals.privateSipAddress); if (srf.locals.privateSipAddress && setName) {
logger.info(`removing ${srf.locals.privateSipAddress} from set ${setName}`);
removeFromSet(setName, srf.locals.privateSipAddress);
}
if (process.env.K8S) {
lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
if (0 === activeCallIds.size) {
logger.info('exiting immediately since we have no calls in progress');
process.exit(0);
}
}
} }
module.exports = {srf, logger}; module.exports = {srf, logger};
+2 -2
View File
@@ -1,14 +1,14 @@
{ {
"default": { "default": {
"transport-protocol": "UDP/TLS/RTP/SAVPF", "transport-protocol": "UDP/TLS/RTP/SAVPF",
"ICE": "force", "ICE": "default",
"SDES": "off", "SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"], "flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"rtcp-mux": ["require"] "rtcp-mux": ["require"]
}, },
"teams": { "teams": {
"transport-protocol": "RTP/SAVP", "transport-protocol": "RTP/SAVP",
"ICE": "force", "ICE": "default",
"SDES": "off", "SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"], "flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"rtcp-mux": ["accept"] "rtcp-mux": ["accept"]
+3
View File
@@ -57,6 +57,9 @@ module.exports = (logger) => {
} }
})(); })();
} }
else if (process.env.K8S) {
lifecycleEmitter.scaleIn = () => process.exit(0);
}
return {lifecycleEmitter}; return {lifecycleEmitter};
}; };
+61 -29
View File
@@ -1,26 +1,51 @@
const Emitter = require('events'); const Emitter = require('events');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
const PORT = process.env.AWS_SNS_PORT || 3001; const PORT = process.env.AWS_SNS_PORT || 3010;
const {LifeCycleEvents} = require('./constants'); const {LifeCycleEvents} = require('./constants');
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const getString = bent('string'); const getString = bent('string');
const AWS = require('aws-sdk'); const {
const sns = new AWS.SNS({apiVersion: '2010-03-31'}); SNSClient,
const autoscaling = new AWS.AutoScaling({apiVersion: '2011-01-01'}); SubscribeCommand,
UnsubscribeCommand } = require('@aws-sdk/client-sns');
const snsClient = new SNSClient({ region: process.env.AWS_REGION, apiVersion: '2010-03-31' });
const {
AutoScalingClient,
DescribeAutoScalingGroupsCommand,
CompleteLifecycleActionCommand } = require('@aws-sdk/client-auto-scaling');
const autoScalingClient = new AutoScalingClient({ region: process.env.AWS_REGION, apiVersion: '2011-01-01' });
const {Parser} = require('xml2js'); const {Parser} = require('xml2js');
const parser = new Parser(); const parser = new Parser();
const {validatePayload} = require('verify-aws-sns-signature'); const {validatePayload} = require('verify-aws-sns-signature');
AWS.config.update({region: process.env.AWS_REGION});
class SnsNotifier extends Emitter { class SnsNotifier extends Emitter {
constructor(logger) { constructor(logger) {
super(); super();
this.logger = logger; this.logger = logger;
} }
_doListen(logger, app, port, resolve) {
return app.listen(port, () => {
this.snsEndpoint = `http://${this.publicIp}:${port}`;
logger.info(`SNS lifecycle server listening on http://localhost:${port}`);
resolve(app);
});
}
_handleErrors(logger, app, resolve, reject, e) {
if (e.code === 'EADDRINUSE' &&
process.env.AWS_SNS_PORT_MAX &&
e.port < process.env.AWS_SNS_PORT_MAX) {
logger.info(`SNS lifecycle server failed to bind port on ${e.port}, will try next port`);
const server = this._doListen(logger, app, ++e.port, resolve);
server.on('error', this._handleErrors.bind(this, logger, app, resolve, reject));
return;
}
reject(e);
}
async _handlePost(req, res) { async _handlePost(req, res) {
try { try {
@@ -44,7 +69,8 @@ class SnsNotifier extends Emitter {
subscriptionRequestId: this.subscriptionRequestId subscriptionRequestId: this.subscriptionRequestId
}, 'response from SNS SubscribeURL'); }, 'response from SNS SubscribeURL');
const data = await this.describeInstance(); const data = await this.describeInstance();
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState; this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
break; break;
case 'Notification': case 'Notification':
@@ -80,14 +106,12 @@ class SnsNotifier extends Emitter {
async init() { async init() {
try { try {
this.logger.info('SnsNotifier: retrieving instance data'); this.logger.debug('SnsNotifier: retrieving instance data');
this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id'); this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id');
this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4'); this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4');
this.snsEndpoint = `http://${this.publicIp}:${PORT}`;
this.logger.info({ this.logger.info({
instanceId: this.instanceId, instanceId: this.instanceId,
publicIp: this.publicIp, publicIp: this.publicIp
snsEndpoint: this.snsEndpoint
}, 'retrieved AWS instance data'); }, 'retrieved AWS instance data');
// start listening // start listening
@@ -99,7 +123,10 @@ class SnsNotifier extends Emitter {
this.logger.error(err, 'burped error'); this.logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message}); res.status(err.status || 500).json({msg: err.message});
}); });
app.listen(PORT); return new Promise((resolve, reject) => {
const server = this._doListen(this.logger, app, PORT, resolve);
server.on('error', this._handleErrors.bind(this, this.logger, app, resolve, reject));
});
} catch (err) { } catch (err) {
this.logger.error({err}, 'Error retrieving AWS instance metadata'); this.logger.error({err}, 'Error retrieving AWS instance metadata');
@@ -108,11 +135,12 @@ class SnsNotifier extends Emitter {
async subscribe() { async subscribe() {
try { try {
const response = await sns.subscribe({ const params = {
Protocol: 'http', Protocol: 'http',
TopicArn: process.env.AWS_SNS_TOPIC_ARM, TopicArn: process.env.AWS_SNS_TOPIC_ARM,
Endpoint: this.snsEndpoint Endpoint: this.snsEndpoint
}).promise(); };
const response = await snsClient.send(new SubscribeCommand(params));
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`); this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) { } catch (err) {
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`); this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
@@ -122,9 +150,10 @@ class SnsNotifier extends Emitter {
async unsubscribe() { async unsubscribe() {
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription'); if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
try { try {
const response = await sns.unsubscribe({ const params = {
SubscriptionArn: this.subscriptionArn SubscriptionArn: this.subscriptionArn
}).promise(); };
const response = await snsClient.send(new UnsubscribeCommand(params));
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`); this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) { } catch (err) {
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`); this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
@@ -133,26 +162,29 @@ class SnsNotifier extends Emitter {
completeScaleIn() { completeScaleIn() {
assert(this.scaleInParams); assert(this.scaleInParams);
autoscaling.completeLifecycleAction(this.scaleInParams, (err, response) => { autoScalingClient.send(new CompleteLifecycleActionCommand(this.scaleInParams))
if (err) return this.logger.error({err}, 'Error completing scale-in'); .then((data) => {
this.logger.info({response}, 'Successfully completed scale-in action'); return this.logger.info({data}, 'Successfully completed scale-in action');
}); })
.catch((err) => {
this.logger.error({err}, 'Error completing scale-in');
});
} }
describeInstance() { describeInstance() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.instanceId) return reject('instance-id unknown'); if (!this.instanceId) return reject('instance-id unknown');
autoscaling.describeAutoScalingInstances({ autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
InstanceIds: [this.instanceId] InstanceIds: [this.instanceId]
}, (err, data) => { }))
if (err) { .then((data) => {
this.logger.info({data}, 'SnsNotifier: describeInstance');
return resolve(data);
})
.catch((err) => {
this.logger.error({err}, 'Error describing instances'); this.logger.error({err}, 'Error describing instances');
reject(err); reject(err);
} else { });
this.logger.info({data}, 'SnsNotifier: describeInstance');
resolve(data);
}
});
}); });
} }
@@ -166,7 +198,7 @@ module.exports = async function(logger) {
process.on('SIGHUP', async() => { process.on('SIGHUP', async() => {
try { try {
const data = await notifier.describeInstance(); const data = await notifier.describeInstance();
const state = data.AutoScalingInstances[0].LifecycleState; const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
if (state !== notifier.lifecycleState) { if (state !== notifier.lifecycleState) {
notifier.lifecycleState = state; notifier.lifecycleState = state;
switch (state) { switch (state) {
+210 -84
View File
@@ -3,9 +3,11 @@ const SrsClient = require('@jambonz/siprec-client-utils');
const { const {
makeRtpEngineOpts, makeRtpEngineOpts,
SdpWantsSrtp, SdpWantsSrtp,
SdpWantsSDES,
nudgeCallCounts, nudgeCallCounts,
roundTripTime, roundTripTime,
parseConnectionIp parseConnectionIp,
isPrivateVoipNetwork
} = require('./utils'); } = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
@@ -21,8 +23,10 @@ const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
const createBLegFromHeader = (req) => { const createBLegFromHeader = (req) => {
const from = req.getParsedHeader('From'); const from = req.getParsedHeader('From');
const uri = parseUri(from.uri); const uri = parseUri(from.uri);
if (uri && uri.user) return `<sip:${uri.user}@localhost>`; const name = from.name;
return '<sip:anonymous@localhost>'; const displayName = name ? `${name} ` : '';
if (uri && uri.user) return `${displayName}<sip:${uri.user}@localhost>`;
else return `${displayName}<sip:anonymous@localhost>`;
}; };
const createSiprecBody = (headers, sdp, type, content) => { const createSiprecBody = (headers, sdp, type, content) => {
@@ -61,6 +65,7 @@ class CallSession extends Emitter {
this.application_sid = req.locals.application_sid; this.application_sid = req.locals.application_sid;
this.account_sid = req.locals.account_sid; this.account_sid = req.locals.account_sid;
this.service_provider_sid = req.locals.service_provider_sid; this.service_provider_sid = req.locals.service_provider_sid;
this.srsClients = [];
} }
get isFromMSTeams() { get isFromMSTeams() {
@@ -80,6 +85,24 @@ class CallSession extends Emitter {
return tp && -1 !== tp.indexOf('SAVP'); return tp && -1 !== tp.indexOf('SAVP');
} }
get isFive9VoiceStream() {
return this.req.has('X-Five9-StreamingPairId');
}
subscribeForDTMF(dlg) {
if (!this._subscribedForDTMF) {
this._subscribedForDTMF = true;
this.subscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag,
this._onDTMF.bind(this, dlg));
}
}
unsubscribeForDTMF() {
if (this._subscribedForDTMF) {
this._subscribedForDTMF = false;
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag);
}
}
async connect() { async connect() {
const {sdp} = this.req.locals; const {sdp} = this.req.locals;
this.logger.info('inbound call accepted for routing'); this.logger.info('inbound call accepted for routing');
@@ -129,7 +152,9 @@ class CallSession extends Emitter {
} }
this.logger.debug(`using feature server ${featureServer}`); this.logger.debug(`using feature server ${featureServer}`);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, SdpWantsSrtp(sdp), false, this.isFromMSTeams); const wantsSrtp = this.req.locals.possibleWebRtcClient = SdpWantsSrtp(sdp);
const wantsSDES = SdpWantsSDES(sdp);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, wantsSrtp, false, this.isFromMSTeams || wantsSDES);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)}; this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
const obj = parseUri(this.req.uri); const obj = parseUri(this.req.uri);
let proxy, host, uri; let proxy, host, uri;
@@ -153,7 +178,7 @@ class CallSession extends Emitter {
...this.rtpEngineOpts.common, ...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts, ...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag, 'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['public', 'private'], direction: [isPrivateVoipNetwork(this.req.source_address) ? 'private' : 'public', 'private'],
sdp sdp
}; };
const startAt = process.hrtime(); const startAt = process.hrtime();
@@ -200,7 +225,7 @@ class CallSession extends Emitter {
if (this.req.locals.application_sid) { if (this.req.locals.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid}); Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid});
} }
else if (this.req.authorization) { if (this.req.authorization) {
if (this.req.authorization.grant && this.req.authorization.grant.application_sid) { if (this.req.authorization.grant && this.req.authorization.grant.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid}); Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid});
} }
@@ -224,7 +249,8 @@ class CallSession extends Emitter {
'-Max-Forwards', '-Max-Forwards',
'-Record-Route', '-Record-Route',
'-Session-Expires', '-Session-Expires',
'-X-Subspace-Forwarded-For' '-X-Application-Sid',
'-X-Authenticated-User'
], ],
proxyResponseHeaders: ['all', '-X-Trace-ID'], proxyResponseHeaders: ['all', '-X-Trace-ID'],
localSdpB: spdOfferB, localSdpB: spdOfferB,
@@ -239,6 +265,7 @@ class CallSession extends Emitter {
}; };
const startAt = process.hrtime(); const startAt = process.hrtime();
const response = await this.answer(opts); const response = await this.answer(opts);
this.logger.debug({response, opts}, 'response from rtpengine to answer');
const rtt = roundTripTime(startAt); const rtt = roundTripTime(startAt);
this.stats.histogram('app.rtpengine.response_time', rtt, [ this.stats.histogram('app.rtpengine.response_time', rtt, [
'direction:inbound', 'command:answer', `rtpengine:${this.rtpengineIp}`]); 'direction:inbound', 'command:answer', `rtpengine:${this.rtpengineIp}`]);
@@ -246,6 +273,17 @@ class CallSession extends Emitter {
this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`); this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer'); throw new Error('rtpengine failed: answer');
} }
/* special case: Five9 Voicestream calls do not advertise a:sendonly, though they should */
if (this.isFive9VoiceStream) {
const opts = {
...this.rtpEngineOpts.common,
'from-tag':this.rtpEngineOpts.uac.tag
};
this.logger.info('Voicestream call from Five9, blocking audio in the reverse direction');
const response = await Promise.all([this.blockMedia(opts), this.blockDTMF(opts)]);
this.logger.debug({response}, 'response to blockMedia/blockDTMF');
}
return response.sdp; return response.sdp;
} }
}); });
@@ -258,7 +296,6 @@ class CallSession extends Emitter {
} catch (err) { } catch (err) {
this.rtpEngineResource.destroy().catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure')); this.rtpEngineResource.destroy().catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure'));
this.activeCallIds.delete(this.req.get('Call-ID')); this.activeCallIds.delete(this.req.get('Call-ID'));
this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size);
if (err instanceof SipError) { if (err instanceof SipError) {
const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`]; const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`];
this.stats.increment('sbc.terminations', tags); this.stats.increment('sbc.terminations', tags);
@@ -275,8 +312,7 @@ class CallSession extends Emitter {
_setDlgHandlers(dlg) { _setDlgHandlers(dlg) {
const {callId} = dlg.sip; const {callId} = dlg.sip;
this.activeCallIds.set(callId, this); this.activeCallIds.set(callId, this);
this.subscribeDTMF(this.logger, callId, this.rtpEngineOpts.uas.tag, this.subscribeForDTMF(this.uac);
this._onDTMF.bind(this));
dlg.on('destroy', () => { dlg.on('destroy', () => {
debug('call ended with normal termination'); debug('call ended with normal termination');
this.logger.info('call ended with normal termination'); this.logger.info('call ended with normal termination');
@@ -284,10 +320,7 @@ class CallSession extends Emitter {
this.activeCallIds.delete(callId); this.activeCallIds.delete(callId);
if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {}); if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {});
if (this.srsClient) { this._stopRecording();
this.srsClient.stop();
this.srsClient = null;
}
this.srf.endSession(this.req); this.srf.endSession(this.req);
}); });
@@ -296,14 +329,21 @@ class CallSession extends Emitter {
dlg.on('modify', this._onReinvite.bind(this, dlg)); dlg.on('modify', this._onReinvite.bind(this, dlg));
} }
_stopRecording() {
if (this.srsClients.length) {
this.srsClients.forEach((c) => c.stop());
this.srsClients = [];
}
}
_setHandlers({uas, uac}) { _setHandlers({uas, uac}) {
this.emit('connected'); this.emit('connected');
const callStart = Date.now(); const callStart = Date.now();
const call_sid = uac.res?.get('X-Call-Sid');
const application_sid = this.application_sid || uac.res?.get('X-Application-Sid');
const tags = ['accepted:yes', 'sipStatus:200', `originator:${this.req.locals.originator}`]; const tags = ['accepted:yes', 'sipStatus:200', `originator:${this.req.locals.originator}`];
this.stats.increment('sbc.terminations', tags); this.stats.increment('sbc.terminations', tags);
this.activeCallIds.set(this.req.get('Call-ID'), this); this.activeCallIds.set(this.req.get('Call-ID'), this);
const call_sid = uac.res?.get('X-Call-Sid');
const application_sid = this.application_sid || uac.res?.get('X-Application-Sid');
if (this.req.locals.cdr) { if (this.req.locals.cdr) {
this.req.locals.cdr = { this.req.locals.cdr = {
...this.req.locals.cdr, ...this.req.locals.cdr,
@@ -324,7 +364,8 @@ class CallSession extends Emitter {
try { try {
await other.destroy(); await other.destroy();
} catch (err) {} } catch (err) {}
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag); this.unsubscribeForDTMF();
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS || const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
process.env.JAMBONES_TRACK_SP_CALLS || process.env.JAMBONES_TRACK_SP_CALLS ||
@@ -346,12 +387,19 @@ class CallSession extends Emitter {
const trunk = ['trunk', 'teams'].includes(this.req.locals.originator) ? const trunk = ['trunk', 'teams'].includes(this.req.locals.originator) ?
this.req.locals.carrier : this.req.locals.carrier :
this.req.locals.originator; this.req.locals.originator;
const application = await this.srf.locals.getApplicationBySid(application_sid);
const isRecording = this.req.locals.account.record_all_calls || (application && application.record_all_calls);
const day = new Date();
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${this.req.locals.account.record_format}`;
const cdr = {...this.req.locals.cdr, const cdr = {...this.req.locals.cdr,
terminated_at: now, terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup', termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200, sip_status: 200,
duration: Math.floor((now - callStart) / 1000), duration: Math.floor((now - callStart) / 1000),
trunk trunk,
...(isRecording && {recording_url})
}; };
this.logger.info({cdr}, 'going to write a cdr now..'); this.logger.info({cdr}, 'going to write a cdr now..');
this.writeCdrs({...this.req.locals.cdr, this.writeCdrs({...this.req.locals.cdr,
@@ -359,7 +407,8 @@ class CallSession extends Emitter {
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup', termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200, sip_status: 200,
duration: Math.floor((now - callStart) / 1000), duration: Math.floor((now - callStart) / 1000),
trunk trunk,
...(isRecording && {recording_url})
}) })
.then(() => this.logger.debug('successfully wrote cdr')) .then(() => this.logger.debug('successfully wrote cdr'))
.catch((err) => this.logger.error({err}, 'Error writing cdr for completed call')); .catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
@@ -370,18 +419,14 @@ class CallSession extends Emitter {
dlg.other = null; dlg.other = null;
other.other = null; other.other = null;
if (this.srsClient) { this._stopRecording();
this.srsClient.stop();
this.srsClient = null;
}
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`); this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
this.srf.endSession(this.req); this.srf.endSession(this.req);
}); });
}); });
this.subscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag, this.subscribeForDTMF(uac);
this._onDTMF.bind(this, uac));
uas.on('modify', this._onReinvite.bind(this, uas)); uas.on('modify', this._onReinvite.bind(this, uas));
uac.on('modify', this._onReinvite.bind(this, uac)); uac.on('modify', this._onReinvite.bind(this, uac));
@@ -394,6 +439,12 @@ class CallSession extends Emitter {
// default forwarding of other request types // default forwarding of other request types
forwardInDialogRequests(uas, ['notify', 'options', 'message']); forwardInDialogRequests(uas, ['notify', 'options', 'message']);
// we need special handling for invite with null sdp followed by 3pcc re-invite
if (uas.local.sdp.includes('a=recvonly') || uas.local.sdp.includes('a=inactive')) {
this.logger.info('incoming call is recvonly or inactive, waiting for re-invite');
this._recvonly = true;
}
} }
async _onDTMF(dlg, payload) { async _onDTMF(dlg, payload) {
@@ -475,7 +526,7 @@ Duration=${payload.duration} `
}); });
} }
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag); this.unsubscribeForDTMF();
const uas = await this.srf.createUAS(req, res, { const uas = await this.srf.createUAS(req, res, {
localSdp: response.sdp, localSdp: response.sdp,
@@ -496,23 +547,74 @@ Duration=${payload.duration} `
} }
async _onReinvite(dlg, req, res) { async _onReinvite(dlg, req, res) {
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 reason = req.get('X-Reason');
const isReleasingMedia = reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason);
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' ? ['public', 'private'] : ['private', 'public'];
try { try {
/* check for re-invite with no SDP -- seen that from BT when they provide UUI info */ /* check for re-invite with no SDP -- seen that from BT when they provide UUI info */
if (!req.body) { if (!req.body) {
this.logger.info('got a reINVITE with no SDP; just respond with our current offer'); if (dlg.type === 'uas' && this._recvonly) {
res.send(200, {body: dlg.local.sdp}); /* seen this from Broadworks - initial INVITE has no SDP, then reINVITE with SDP */
this._recvonly = false; //one-time only
const myMungedSdp = dlg.local.sdp.replace('a=recvonly', 'a=sendrecv').replace('a=inactive', 'a=sendrecv');
this.logger.info({myMungedSdp}, '_onReinvite (3gpp): got a reINVITE with no SDP while in recvonly mode');
res.send(200,
{
body: myMungedSdp
},
(err, req) => {},
async(ack) => {
const remoteOffer = ack.body;
this.logger.info({remoteOffer}, '_onReinvite (3gpp): got ACK for reINVITE with SDP');
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
'from-tag': fromTag,
'to-tag': toTag,
direction,
sdp: remoteOffer,
};
let response = await this.offer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite (3gpp): rtpengine failed: offer: ${JSON.stringify(response)}`);
}
this.logger.info({response}, '_onReinvite (3gpp): response from rtpengine for offer');
const fsSdp = await dlg.other.modify(response.sdp);
opts = {
...this.rtpEngineOpts.common,
...answerMedia,
'from-tag': fromTag,
'to-tag': toTag,
sdp: fsSdp
};
response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite(3gpp): rtpengine failed answer: ${JSON.stringify(response)}`);
}
}
);
}
else {
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
res.send(200, {body: dlg.local.sdp});
}
return; return;
} }
const offeredSdp = Array.isArray(req.payload) && req.payload.length > 1 ? const offeredSdp = Array.isArray(req.payload) && req.payload.length > 1 ?
req.payload.find((p) => p.type === 'application/sdp').content : req.payload.find((p) => p.type === 'application/sdp').content :
req.body; req.body;
const reason = req.get('X-Reason'); if (isReleasingMedia) {
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag; if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag; offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
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' ? ['public', 'private'] : ['private', 'public'];
let opts = { let opts = {
...this.rtpEngineOpts.common, ...this.rtpEngineOpts.common,
...offerMedia, ...offerMedia,
@@ -521,7 +623,8 @@ Duration=${payload.duration} `
direction, direction,
sdp: offeredSdp, sdp: offeredSdp,
}; };
if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset'); // Dont reset ICE - causes audiocodes webrtrc to fail with "missing ice-ufrag and ice-pwd in re-invite"
// if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
let response = await this.offer(opts); let response = await this.offer(opts);
if ('ok' !== response.result) { if ('ok' !== response.result) {
@@ -531,11 +634,11 @@ Duration=${payload.duration} `
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */ /* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
let sdp; let sdp;
if (reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason) && if (isReleasingMedia && !this.callerIsUsingSrtp) {
!this.callerIsUsingSrtp) {
this.logger.info({response}, `got a reinvite from FS to ${reason}`); this.logger.info({response}, `got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp; sdp = dlg.other.remote.sdp;
answerMedia.flags = ['asymmetric', 'port latching']; if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
this._mediaReleased = 'release-media' === reason; this._mediaReleased = 'release-media' === reason;
} }
else { else {
@@ -590,6 +693,7 @@ Duration=${payload.duration} `
const to = this.req.getParsedHeader('To'); const to = this.req.getParsedHeader('To');
const aorFrom = from.uri; const aorFrom = from.uri;
const aorTo = to.uri; const aorTo = to.uri;
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
this.logger.info({to, from}, 'startCallRecording request for a call'); this.logger.info({to, from}, 'startCallRecording request for a call');
const srsUrl = req.get('X-Srs-Url'); const srsUrl = req.get('X-Srs-Url');
@@ -597,7 +701,7 @@ Duration=${payload.duration} `
const callSid = req.get('X-Call-Sid'); const callSid = req.get('X-Call-Sid');
const accountSid = req.get('X-Account-Sid'); const accountSid = req.get('X-Account-Sid');
const applicationSid = req.get('X-Application-Sid'); const applicationSid = req.get('X-Application-Sid');
if (this.srsClient) { if (this.srsClients.length) {
res.send(400); res.send(400);
this.logger.info('discarding duplicate startCallRecording request for a call'); this.logger.info('discarding duplicate startCallRecording request for a call');
return; return;
@@ -607,13 +711,14 @@ Duration=${payload.duration} `
res.send(400); res.send(400);
return; return;
} }
this.srsClient = new SrsClient(this.logger, { const arr = srsUrl.split(',');
this.srsClients = arr.map((url) => new SrsClient(this.logger, {
srf: dlg.srf, srf: dlg.srf,
direction: 'inbound', direction: 'inbound',
originalInvite: this.req, originalInvite: this.req,
callingNumber: this.req.callingNumber, callingNumber: this.req.callingNumber,
calledNumber: this.req.calledNumber, calledNumber: this.req.calledNumber,
srsUrl, srsUrl: url,
srsRecordingId, srsRecordingId,
callSid, callSid,
accountSid, accountSid,
@@ -628,42 +733,51 @@ Duration=${payload.duration} `
del: this.del, del: this.del,
blockMedia: this.blockMedia, blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia, unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe unsubscribe: this.unsubscribe,
}); headers
}));
try { try {
succeeded = await this.srsClient.start(); succeeded = (await Promise.all(
this.srsClients.map((c) => c.start())
)).every((r) => r);
} catch (err) { } catch (err) {
this.logger.error({err}, 'Error starting SipRec call recording'); this.logger.error({err}, 'Error starting SipRec call recording');
} }
} }
else if (reason === 'stopCallRecording') { else if (reason === 'stopCallRecording') {
if (!this.srsClient) { if (!this.srsClients.length) {
res.send(400); res.send(400);
this.logger.info('discarding stopCallRecording request because we are not recording'); this.logger.info('discarding stopCallRecording request because we are not recording');
return; return;
} }
try { try {
succeeded = await this.srsClient.stop(); succeeded = (await Promise.all(
this.srsClients.map((c) => c.stop())
)).every((r) => r);
} catch (err) { } catch (err) {
this.logger.error({err}, 'Error stopping SipRec call recording'); this.logger.error({err}, 'Error stopping SipRec call recording');
} }
this.srsClient = null; this.srsClients = [];
} }
else if (reason === 'pauseCallRecording') { else if (reason === 'pauseCallRecording') {
if (!this.srsClient || this.srsClient.paused) { if (!this.srsClients.length || this.srsClients.every((c) => c.paused)) {
this.logger.info('discarding invalid pauseCallRecording request'); this.logger.info('discarding invalid pauseCallRecording request');
res.send(400); res.send(400);
return; return;
} }
succeeded = await this.srsClient.pause(); succeeded = (await Promise.all(
this.srsClients.map((c) => c.pause())
)).every((r) => r);
} }
else if (reason === 'resumeCallRecording') { else if (reason === 'resumeCallRecording') {
if (!this.srsClient || !this.srsClient.paused) { if (!this.srsClients.length || !this.srsClients.every((c) => c.paused)) {
res.send(400); res.send(400);
this.logger.info('discarding invalid resumeCallRecording request'); this.logger.info('discarding invalid resumeCallRecording request');
return; return;
} }
succeeded = await this.srsClient.resume(); succeeded = (await Promise.all(
this.srsClients.map((c) => c.resume())
)).every((r) => r);
} }
res.send(succeeded ? 200 : 503); res.send(succeeded ? 200 : 503);
} }
@@ -716,8 +830,8 @@ Duration=${payload.duration} `
res.send(response.status, {headers: responseHeaders, body: response.body}); res.send(response.status, {headers: responseHeaders, body: response.body});
} }
} catch (err) { } catch (err) {
if (this.srsClient) { if (this.srsClients.length) {
this.srsClient = null; this.srsClients = [];
} }
res.send(500); res.send(500);
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`); this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
@@ -736,30 +850,38 @@ Duration=${payload.duration} `
const referredBy = req.getParsedHeader('Referred-By'); const referredBy = req.getParsedHeader('Referred-By');
if (!referredBy) return res.send(400); if (!referredBy) return res.send(400);
const u = parseUri(referredBy.uri); const u = parseUri(referredBy.uri);
const leaveReferToAlone = req.has('X-Refer-To-Leave-Untouched');
let selectedGateway = false; if (leaveReferToAlone) {
let e164 = false; this.logger.debug({referTo}, 'passing Refer-To header through untouched');
if (gateway) { }
/* host of Refer-to to an outbound gateway */ else {
const gw = await this.srf.locals.getOutboundGatewayForRefer(gateway.voip_carrier_sid); const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
if (gw) { let selectedGateway = false;
selectedGateway = true; let e164 = false;
e164 = gw.e164_leading_plus; if (gateway && isDotDecimal) {
uri.host = gw.ipv4; /* host of Refer-to to an outbound gateway */
uri.port = gw.port; const gw = await this.srf.locals.getOutboundGatewayForRefer(gateway.voip_carrier_sid);
if (gw) {
selectedGateway = true;
e164 = gw.e164_leading_plus;
uri.host = gw.ipv4;
uri.port = gw.port;
}
}
if (!selectedGateway && isDotDecimal) {
uri.host = this.req.source_address;
uri.port = this.req.source_port;
}
if (e164 && !uri.user.startsWith('+')) {
uri.user = `+${uri.user}`;
} }
}
if (!selectedGateway) {
uri.host = this.req.source_address;
uri.port = this.req.source_port;
}
if (e164 && !uri.user.startsWith('+')) {
uri.user = `+${uri.user}`;
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const {via, from, to, 'call-id':callid, cseq, 'max-forwards':maxforwards, const {via, from, to, 'call-id':callid, cseq, 'max-forwards':maxforwards,
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
'content-length':contentlength, 'refer-to':_referto, 'referred-by':_referredby, 'content-length':contentlength, 'refer-to':_referto, 'referred-by':_referredby,
// eslint-disable-next-line no-unused-vars
'X-Refer-To-Leave-Untouched': _leave,
...customHeaders ...customHeaders
} = req.headers; } = req.headers;
@@ -776,15 +898,16 @@ Duration=${payload.duration} `
res.send(202); res.send(202);
// invite to new fs // invite to new fs
const headers = {}; const headers = {
if (req.has('X-Retain-Call-Sid')) { ...(req.has('X-Retain-Call-Sid') && {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')}),
Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')}); ...(req.has('X-Account-Sid') && {'X-Account-Sid': req.get('X-Account-Sid')})
} };
const uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers}); const uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uac = uac; this.uac = uac;
uac.other = this.uas; uac.other = this.uas;
this.uas.other = uac; this.uas.other = uac;
uac.on('modify', this._onFeatureServerReinvite.bind(this, uac)); uac.on('info', this._onInfo.bind(this, uac));
uac.on('modify', this._onReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac)); uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uac.on('destroy', () => { uac.on('destroy', () => {
this.logger.info('call ended with normal termination'); this.logger.info('call ended with normal termination');
@@ -793,19 +916,22 @@ Duration=${payload.duration} `
uac.other.destroy(); uac.other.destroy();
this.srf.endSession(this.req); this.srf.endSession(this.req);
}); });
// now we can destroy the old dialog
dlg.destroy().catch(() => {});
// modify rtpengine to stream to new feature server const opts = {
const opts = Object.assign({sdp: uac.remote.sdp, 'to-tag': res.getParsedHeader('To').params.tag}, ...this.rtpEngineOpts.common,
this.rtpEngineOpts.answer); 'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
sdp: uac.remote.sdp,
flags: ['port latching']
};
const response = await this.answer(opts); const response = await this.answer(opts);
if ('ok' !== response.result) { if ('ok' !== response.result) {
res.send(488); throw new Error(`_onFeatureServerTransfer: rtpengine answer failed: ${JSON.stringify(response)}`);
throw new Error(`_onFeatureServerReinvite: rtpengine failed: ${JSON.stringify(response)}`);
} }
dlg.destroy().catch(() => {});
this.logger.info('successfully moved call to new feature server'); this.logger.info('successfully moved call to new feature server');
} catch (err) { } catch (err) {
res.send(488);
this.logger.error(err, 'Error handling refer from feature server'); this.logger.error(err, 'Error handling refer from feature server');
} }
} }
@@ -863,7 +989,7 @@ Duration=${payload.duration} `
// successfully connected // successfully connected
this.logger.info('successfully connected new call leg for REFER'); this.logger.info('successfully connected new call leg for REFER');
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag); this.unsubscribeForDTMF();
this.referInvite = null; this.referInvite = null;
sendNotify(this.uas, '200 OK'); sendNotify(this.uas, '200 OK');
this.uas.destroy(); this.uas.destroy();
+210 -68
View File
@@ -11,6 +11,8 @@ vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
FROM sip_gateways sg, voip_carriers vc, accounts acc FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ? WHERE acc.sip_realm = ?
AND vc.account_sid = acc.account_sid AND vc.account_sid = acc.account_sid
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`; AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllCarriersForSPByRealm = const sqlSelectAllCarriersForSPByRealm =
@@ -20,15 +22,18 @@ FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ? WHERE acc.sip_realm = ?
AND vc.service_provider_sid = acc.service_provider_sid AND vc.service_provider_sid = acc.service_provider_sid
AND vc.account_sid IS NULL AND vc.account_sid IS NULL
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`; AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllGatewaysForSP = const sqlSelectAllGatewaysForSP =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid, `SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
FROM sip_gateways sg, voip_carriers vc FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.service_provider_sid IS NOT NULL AND vc.service_provider_sid IS NOT NULL
AND vc.is_active = 1`; AND vc.is_active = 1
AND sg.inbound = 1`;
const sqlCarriersForAccountBySid = const sqlCarriersForAccountBySid =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid, `SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
@@ -36,10 +41,13 @@ vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
FROM sip_gateways sg, voip_carriers vc, accounts acc FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.account_sid = ? WHERE acc.account_sid = ?
AND vc.account_sid = acc.account_sid AND vc.account_sid = acc.account_sid
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`; AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ?'; const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ? AND is_active = 1';
const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?'; const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?';
const sqlApplicationBySid = 'SELECT * from applications WHERE application_sid = ?';
const sqlQueryApplicationByDid = ` const sqlQueryApplicationByDid = `
SELECT * FROM phone_numbers SELECT * FROM phone_numbers
@@ -57,11 +65,25 @@ WHERE sg.voip_carrier_sid = ?
AND sg.voip_carrier_sid = vc.voip_carrier_sid AND sg.voip_carrier_sid = vc.voip_carrier_sid
AND outbound = 1`; AND outbound = 1`;
const gatewayMatchesSourceAddress = (source_address, gw) => { const sqlSelectCarrierRequiringRegistration = `
SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.requires_register = 1
AND vc.is_active = 1
AND vc.register_sip_realm = ?
AND vc.register_username = ?`;
const gatewayMatchesSourceAddress = (logger, source_address, gw) => {
if (32 === gw.netmask && gw.ipv4 === source_address) return true; if (32 === gw.netmask && gw.ipv4 === source_address) return true;
if (gw.netmask < 32) { if (gw.netmask < 32) {
const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]); try {
return matcher.contains(source_address); const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]);
return matcher.contains(source_address);
} catch (err) {
logger.info({err, gw}, 'gatewayMatchesSourceAddress: Error parsing netmask');
}
} }
return false; return false;
}; };
@@ -70,6 +92,12 @@ module.exports = (srf, logger) => {
const {pool} = srf.locals.dbHelpers; const {pool} = srf.locals.dbHelpers;
const pp = pool.promise(); const pp = pool.promise();
const getApplicationBySid = async(application_sid) => {
const [r] = await pp.query(sqlApplicationBySid, [application_sid]);
if (0 === r.length) return null;
return r[0];
};
const getSPForAccount = async(account_sid) => { const getSPForAccount = async(account_sid) => {
const [r] = await pp.query(sqlSelectSPForAccount, [account_sid]); const [r] = await pp.query(sqlSelectSPForAccount, [account_sid]);
if (0 === r.length) return null; if (0 === r.length) return null;
@@ -83,14 +111,14 @@ module.exports = (srf, logger) => {
/* if multiple, prefer a DNS name */ /* if multiple, prefer a DNS name */
const hasDns = r.find((row) => row.ipv4.match(/^[A-Za-z]/)); const hasDns = r.find((row) => row.ipv4.match(/^[A-Za-z]/));
return hasDns || r[0]; return hasDns /* || r[0] */;
} catch (err) { } catch (err) {
logger.error({err}, 'getOutboundGatewayForRefer'); logger.error({err}, 'getOutboundGatewayForRefer');
} }
}; };
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => { const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
const did = normalizeDID(req.calledNumber); const did = normalizeDID(req.calledNumber) || 'anonymous';
try { try {
/* straight DID match */ /* straight DID match */
@@ -115,45 +143,167 @@ module.exports = (srf, logger) => {
const uri = parseUri(req.uri); const uri = parseUri(req.uri);
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host); const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
if (isDotDecimal) { if (!isDotDecimal) {
if (process.env.JAMBONES_HOSTING) { /**
if (!process.env.SBC_ACCOUNT_SID) return failure; * The host part of the SIP URI is not a dot-decimal IP address,
* so this can be one of two things:
* (1) a sip realm value associate with an account, or
* (2) a carrier name for a carrier that we send outbound registrations to
*
* Let's look for case #1 first...
*/
/* look for carrier only within that account */ /* does anyone own this sip realm? */
const [r] = await pp.query(sqlCarriersForAccountBySid, const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
[process.env.SBC_ACCOUNT_SID, req.source_address, req.source_port]); if (a.length) {
if (0 === r.length) return failure; assert(a.length === 1);
const service_provider_sid = await getSPForAccount(process.env.SBC_ACCOUNT_SID);
return { /* yes they do */
fromCarrier: true, logger.debug(`sip realm is associated with account_sid: ${a[0].account_sid}`);
gateway: r[0],
account_sid: process.env.SBC_ACCOUNT_SID, /**
service_provider_sid * We have one of two cases:
}; * (1a). The user configured his or her carrier to send to their sip realm, or
* (1b). The user is making a call from a sip device.
*/
/* get all the carriers and gateways for the account owning this sip realm */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]);
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
const gw = gwAcc
.concat(gwSP)
.sort((a, b) => b.netmask - a.netmask);
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
if (selected) {
return {
fromCarrier: true,
gateway: selected,
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: selected.application_sid,
account: a[0]
};
}
return failure;
} }
else {
/* we may have a carrier at the service provider level */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
const matches = gw.filter(gatewayMatchesSourceAddress.bind(null, req.source_address));
if (matches.length) {
/* we have one or more carriers that match. Now we need to find one with a provisioned phone number */
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const did = normalizeDID(req.calledNumber);
const sql = `SELECT * FROM phone_numbers WHERE number = ${did} AND voip_carrier_sid IN (${vc_sids})`;
logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
const [r] = await pp.query(sql);
if (0 === r.length) { /* no match, so let's look for case #2 */
/* came from a carrier, but number is not provisioned.. try {
check if we only have a single account, otherwise we have no logger.info({
way of knowing which account this is for host: uri.host,
user: uri.user
}, 'sip realm is not associated with an account, checking carriers');
const [gw] = await pp.query(sqlSelectCarrierRequiringRegistration, [uri.host, uri.user]);
const matches = gw
.sort((a, b) => b.netmask - a.netmask)
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
if (1 === matches.length) {
// bingo
//TODO: this assumes the carrier is associate to an account, not an SP
//if the carrier is associated with an SP (which would mean we
//must see a dialed number in the To header, not the register username),
//then we need to look up the account based on the dialed number in the To header
const [a] = await pp.query(sqlAccountBySid, [[matches[0].account_sid]]);
if (0 === a.length) return failure;
logger.debug({matches}, `found registration carrier using ${uri.host} and ${uri.user}`);
return {
fromCarrier: true,
gateway: matches[0],
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: matches[0].application_sid,
account: a[0]
};
}
else if (matches.length > 1) {
logger.warn({matches, source_address: req.source_address}, 'multiple gateways match source address');
return {
fromCarrier: true,
error: 'Multiple gateways match registration carrier source address'
};
}
} catch (err) {
logger.info({err, host: uri.host, user: uri.user}, 'Error looking up carrier by host and user');
}
/* no match, so fall through */
}
if (isDotDecimal && process.env.JAMBONES_HOSTING) {
if (!process.env.SBC_ACCOUNT_SID) return failure;
/* look for carrier only within that account */
const [r] = await pp.query(sqlCarriersForAccountBySid,
[process.env.SBC_ACCOUNT_SID, req.source_address, req.source_port]);
if (0 === r.length) return failure;
const service_provider_sid = await getSPForAccount(process.env.SBC_ACCOUNT_SID);
return {
fromCarrier: true,
gateway: r[0],
account_sid: process.env.SBC_ACCOUNT_SID,
service_provider_sid
};
}
else {
/* find all carrier entries that have an inbound gateway matching the source IP */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
let matches = gw
.sort((a, b) => b.netmask - a.netmask)
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address))
.map((gw) => {
return {
voip_carrier_sid: gw.voip_carrier_sid,
name: gw.name,
service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid,
application_sid: gw.application_sid
};
});
/* remove duplicates, winnow down to voip_carriers, not gateways */
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
if (matches.length) {
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const did = normalizeDID(req.calledNumber) || 'anonymous';
const sql = `SELECT * FROM phone_numbers WHERE number = '${did}' AND voip_carrier_sid IN (${vc_sids})`;
logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
const [r] = await pp.query(sql);
if (0 === r.length) {
/* came from a provisioned carrier, but the dialed number is not provisioned.
check if we have an account with default routing of that carrier to an application
*/
const accountLevelGateways = matches.filter((m) => m.account_sid && m.application_sid);
if (accountLevelGateways.length > 1) {
logger.info({accounts: accountLevelGateways.map((m) => m.account_sid)},
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
return {
fromCarrier: true,
error: 'Multiple accounts are attempting to default route this carrier'
};
}
else if (accountLevelGateways.length === 1) {
const [accounts] = await pp.query('SELECT * from accounts where account_sid = ?',
[accountLevelGateways[0].account_sid]);
return {
fromCarrier: true,
gateway: accountLevelGateways[0],
service_provider_sid: accountLevelGateways[0].service_provider_sid,
account_sid: accountLevelGateways[0].account_sid,
application_sid: accountLevelGateways[0].application_sid,
account: accounts[0]
};
}
else {
/* check if we only have a single account, otherwise we have no
- way of knowing which account this is for
*/ */
const [r] = await pp.query('SELECT count(*) as count from accounts where service_provider_sid = ?', const [r] = await pp.query('SELECT count(*) as count from accounts where service_provider_sid = ?',
matches[0].service_provider_sid); [matches[0].service_provider_sid]);
if (r[0].count === 0) return {fromCarrier: true}; if (r[0].count === 0 || r[0].count > 1) return {fromCarrier: true};
else { else {
const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?', const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?',
matches[0].service_provider_sid); [matches[0].service_provider_sid]);
return { return {
fromCarrier: true, fromCarrier: true,
gateway: matches[0], gateway: matches[0],
@@ -163,38 +313,29 @@ module.exports = (srf, logger) => {
}; };
} }
} }
const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid); }
const [accounts] = await pp.query(sqlAccountBySid, r[0].account_sid); else if (r.length > 1) {
assert(accounts.length); logger.info({r},
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
return { return {
fromCarrier: true, fromCarrier: true,
gateway, error: 'Multiple accounts are attempting to route the same phone number from the same carrier'
service_provider_sid: accounts[0].service_provider_sid,
account_sid: r[0].account_sid,
application_sid: r[0].application_sid,
account: accounts[0]
}; };
} }
return failure;
}
}
/* get all the carriers and gateways for the account owning this sip realm */ /* we have a route for this phone number and carrier combination */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, uri.host); const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid);
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host); const [accounts] = await pp.query(sqlAccountBySid, [r[0].account_sid]);
const gw = gwAcc.concat(gwSP); assert(accounts.length);
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, req.source_address)); return {
if (selected) { fromCarrier: true,
const [a] = await pp.query(sqlAccountByRealm, uri.host); gateway,
if (0 === a.length) return failure; service_provider_sid: accounts[0].service_provider_sid,
return { account_sid: r[0].account_sid,
fromCarrier: true, application_sid: r[0].application_sid,
gateway: selected, account: accounts[0]
service_provider_sid: a[0].service_provider_sid, };
account_sid: a[0].account_sid, }
application_sid: selected.application_sid,
account: a[0]
};
} }
return failure; return failure;
}; };
@@ -203,6 +344,7 @@ module.exports = (srf, logger) => {
wasOriginatedFromCarrier, wasOriginatedFromCarrier,
getApplicationForDidAndCarrier, getApplicationForDidAndCarrier,
getOutboundGatewayForRefer, getOutboundGatewayForRefer,
getSPForAccount getSPForAccount,
getApplicationBySid
}; };
}; };
+3 -1
View File
@@ -15,7 +15,9 @@ module.exports = (srf, logger) => {
return async() => { return async() => {
try { try {
if (process.env.K8S) { if (process.env.K8S) {
return process.env.K8S_FEATURE_SERVER_SERVICE_NAME; return process.env.K8S_FEATURE_SERVER_TRANSPORT ?
`${process.env.K8S_FEATURE_SERVER_SERVICE_NAME};transport=${process.env.K8S_FEATURE_SERVER_TRANSPORT}` :
process.env.K8S_FEATURE_SERVER_SERVICE_NAME;
} }
else { else {
const fs = await retrieveSet(setName); const fs = await retrieveSet(setName);
+28 -47
View File
@@ -1,8 +1,8 @@
const debug = require('debug')('jambonz:sbc-inbound'); const debug = require('debug')('jambonz:sbc-inbound');
const assert = require('assert'); const assert = require('assert');
const Emitter = require('events');
const parseUri = require('drachtio-srf').parseUri; const parseUri = require('drachtio-srf').parseUri;
const {nudgeCallCounts} = require('./utils'); const {nudgeCallCounts, roundTripTime, isMSTeamsCIDR} = require('./utils');
const digestChallenge = require('@jambonz/digest-utils');
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ? const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ?
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) : process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
[]; [];
@@ -22,42 +22,8 @@ const initCdr = (req) => {
}; };
module.exports = function(srf, logger) { module.exports = function(srf, logger) {
class AuthOutcomeReporter extends Emitter {
constructor(stats) {
super();
this
.on('regHookOutcome', ({rtt, status}) => {
stats.histogram('app.hook.response_time', rtt, ['hook_type:auth', `status:${status}`]);
})
.on('error', async(err, req) => {
const {account_sid, account} = req.locals;
const {writeAlerts, AlertType} = req.srf.locals;
if (account_sid) {
let opts = {account_sid, service_provider_sid: account.service_provider_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook};
}
else if (err.code === 'ENOTFOUND') {
opts = {...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: AlertType.WEBHOOK_STATUS_FAILURE, url: err.hook, status: err.statusCode};
}
if (opts.alert_type) {
try {
await writeAlerts(opts);
} catch (err) {
logger.error({err, opts}, 'Error writing alert');
}
}
}
});
}
}
const { const {
lookupAuthHook,
lookupAppByTeamsTenant, lookupAppByTeamsTenant,
lookupAccountBySipRealm, lookupAccountBySipRealm,
lookupAccountBySid, lookupAccountBySid,
@@ -65,10 +31,6 @@ module.exports = function(srf, logger) {
queryCallLimits queryCallLimits
} = srf.locals.dbHelpers; } = srf.locals.dbHelpers;
const {stats, writeCdrs} = srf.locals; const {stats, writeCdrs} = srf.locals;
const authenticator = require('@jambonz/http-authenticator')(lookupAuthHook, logger, {
blacklistUnknownRealms: true,
emitter: new AuthOutcomeReporter(stats)
});
const initLocals = (req, res, next) => { const initLocals = (req, res, next) => {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
@@ -135,15 +97,20 @@ module.exports = function(srf, logger) {
const identifyAccount = async(req, res, next) => { const identifyAccount = async(req, res, next) => {
try { try {
const {siprec, callId} = req.locals; const {siprec, callId} = req.locals;
const {getSPForAccount, wasOriginatedFromCarrier, getApplicationForDidAndCarrier} = req.srf.locals; const {getSPForAccount, wasOriginatedFromCarrier, getApplicationForDidAndCarrier, stats} = req.srf.locals;
const startAt = process.hrtime();
const { const {
fromCarrier, fromCarrier,
gateway, gateway,
account_sid, account_sid,
application_sid, application_sid,
service_provider_sid, service_provider_sid,
account account,
error
} = await wasOriginatedFromCarrier(req); } = await wasOriginatedFromCarrier(req);
const rtt = roundTripTime(startAt);
stats.histogram('app.mysql.response_time', rtt, [
'query:wasOriginatedFromCarrier', 'app:sbc-inbound']);
/** /**
* calls come from 3 sources: * calls come from 3 sources:
* (1) A carrier * (1) A carrier
@@ -151,11 +118,18 @@ module.exports = function(srf, logger) {
* (3) A SIP user * (3) A SIP user
*/ */
if (fromCarrier) { if (fromCarrier) {
if (error) {
return res.send(503, {
headers: {
'X-Reason': error
}
});
}
if (!gateway) { if (!gateway) {
logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned'); logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned');
return res.send(404, 'Number Not Provisioned'); return res.send(404, 'Number Not Provisioned');
} }
logger.debug({gateway}, 'identifyAccount: incoming call from gateway'); logger.info({gateway}, 'identifyAccount: incoming call from gateway');
let sid; let sid;
if (siprec) { if (siprec) {
@@ -181,8 +155,8 @@ module.exports = function(srf, logger) {
...req.locals ...req.locals
}; };
} }
else if (msProxyIps.includes(req.source_address)) { else if (msProxyIps.includes(req.source_address) || isMSTeamsCIDR(req.source_address)) {
logger.debug({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams'); logger.info({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
const uri = parseUri(req.uri); const uri = parseUri(req.uri);
const app = await lookupAppByTeamsTenant(uri.host); const app = await lookupAppByTeamsTenant(uri.host);
@@ -204,7 +178,7 @@ module.exports = function(srf, logger) {
else { else {
req.locals.originator = 'user'; req.locals.originator = 'user';
const uri = parseUri(req.uri); const uri = parseUri(req.uri);
logger.debug({source_address: req.source_address, realm: uri.host}, logger.info({source_address: req.source_address, realm: uri.host},
'identifyAccount: incoming user call'); 'identifyAccount: incoming user call');
const account = await lookupAccountBySipRealm(uri.host); const account = await lookupAccountBySipRealm(uri.host);
if (!account) { if (!account) {
@@ -228,6 +202,13 @@ module.exports = function(srf, logger) {
account, account,
application_sid: account.device_calling_application_sid, application_sid: account.device_calling_application_sid,
webhook_secret: account.webhook_secret, webhook_secret: account.webhook_secret,
realm: uri.host,
...(account.registration_hook && {
registration_hook_url: account.registration_hook.url,
registration_hook_method: account.registration_hook.method,
registration_hook_username: account.registration_hook.username,
registration_hook_password: account.registration_hook.password
}),
...req.locals ...req.locals
}; };
} }
@@ -370,7 +351,7 @@ module.exports = function(srf, logger) {
try { try {
/* TODO: check if this is a gateway that we have an ACL for */ /* TODO: check if this is a gateway that we have an ACL for */
if (req.locals.originator !== 'user') return next(); if (req.locals.originator !== 'user') return next();
return authenticator(req, res, next); return digestChallenge(req, res, next);
} catch (err) { } catch (err) {
stats.increment('sbc.terminations', ['sipStatus:500']); stats.increment('sbc.terminations', ['sipStatus:500']);
logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`); logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`);
+62 -11
View File
@@ -1,5 +1,8 @@
const CIDRMatcher = require('cidr-matcher');
const express = require('express');
const rtpCharacteristics = require('../data/rtp-transcoding'); const rtpCharacteristics = require('../data/rtp-transcoding');
const srtpCharacteristics = require('../data/srtp-transcoding'); const srtpCharacteristics = require('../data/srtp-transcoding');
let idx = 0; let idx = 0;
const isWSS = (req) => { const isWSS = (req) => {
@@ -12,10 +15,12 @@ const getAppserver = (srf) => {
}; };
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) { function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
const from = req.getParsedHeader('from'); const from = req.getParsedHeader('from');
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default']; const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics; const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics; const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */ /* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) { if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
@@ -24,7 +29,8 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
} }
const common = { const common = {
'call-id': req.get('Call-ID'), 'call-id': req.get('Call-ID'),
'replace': ['origin', 'session-connection'] 'replace': ['origin', 'session-connection'],
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no'
}; };
return { return {
common, common,
@@ -39,6 +45,9 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
}; };
} }
const SdpWantsSDES = (sdp) => {
return /m=audio.*\s+RTP\/SAVP/.test(sdp);
};
const SdpWantsSrtp = (sdp) => { const SdpWantsSrtp = (sdp) => {
return /m=audio.*SAVP/.test(sdp); return /m=audio.*SAVP/.test(sdp);
}; };
@@ -69,18 +78,35 @@ const systemHealth = async(redisClient, ping, getCount) => {
return getCount(); return getCount();
}; };
const doListen = (logger, app, port, resolve) => {
return app.listen(port, () => {
logger.info(`Health check server listening on http://localhost:${port}`);
resolve(app);
});
};
const handleErrors = (logger, app, resolve, reject, e) => {
if (e.code === 'EADDRINUSE' &&
process.env.HTTP_PORT_MAX &&
e.port < process.env.HTTP_PORT_MAX) {
logger.info(`Health check server failed to bind port on ${e.port}, will try next port`);
const server = doListen(logger, app, ++e.port, resolve);
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
return;
}
reject(e);
};
const createHealthCheckApp = (port, logger) => { const createHealthCheckApp = (port, logger) => {
const express = require('express');
const app = express(); const app = express();
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
return new Promise((resolve) => { return new Promise((resolve, reject) => {
app.listen(port, () => { const server = doListen(logger, app, port, resolve);
logger.info(`Health check server started at http://localhost:${port}`); server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
resolve(app);
});
}); });
}; };
@@ -154,10 +180,33 @@ const parseConnectionIp = (sdp) => {
return arr ? arr[1] : null; return arr ? arr[1] : null;
}; };
/**
* Checks if ip is one of MS Teams sip signalling ips
* https://learn.microsoft.com/en-us/azure/communication-services/concepts
* /telephony/direct-routing-infrastructure#sip-signaling-fqdns
* @param ip IP address, example 172.31.0.1
* */
const isMSTeamsCIDR = (ip) => {
const cidrs = [
'52.112.0.0/14',
'52.120.0.0/14'
];
const matcher = new CIDRMatcher(cidrs);
return matcher.contains(ip);
};
const isPrivateVoipNetwork = (ip) => {
if (process.env.PRIVATE_VOIP_NETWORK_CIDR) {
const matcher = new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(','));
return matcher.contains(ip);
}
return false;
};
module.exports = { module.exports = {
isWSS, isWSS,
SdpWantsSrtp, SdpWantsSrtp,
SdpWantsSDES,
getAppserver, getAppserver,
makeRtpEngineOpts, makeRtpEngineOpts,
makeAccountCallCountKey, makeAccountCallCountKey,
@@ -169,5 +218,7 @@ module.exports = {
createHealthCheckApp, createHealthCheckApp,
nudgeCallCounts, nudgeCallCounts,
roundTripTime, roundTripTime,
parseConnectionIp parseConnectionIp,
isMSTeamsCIDR,
isPrivateVoipNetwork
}; };
+4293 -2935
View File
File diff suppressed because it is too large Load Diff
+12 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "sbc-inbound", "name": "sbc-inbound",
"version": "v0.7.6", "version": "0.8.5",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 12.0.0" "node": ">= 12.0.0"
@@ -20,29 +20,28 @@
}, },
"scripts": { "scripts": {
"start": "node app", "start": "node app",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_NETWORK_CIDR='127.0.0.1/32' JAMBONES_HOSTING=1 SBC_ACCOUNT_SID=ed649e33-e771-403a-8c99-1780eabbc803 JAMBONES_TIME_SERIES_HOST=127.0.0.1 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/ ", "test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_NETWORK_CIDR='127.0.0.1/32' JAMBONES_HOSTING=1 JWT_SECRET=foobarbazzle SBC_ACCOUNT_SID=ed649e33-e771-403a-8c99-1780eabbc803 JAMBONES_TIME_SERIES_HOST=127.0.0.1 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/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib" "jslint": "eslint app.js lib"
}, },
"dependencies": { "dependencies": {
"@jambonz/db-helpers": "^0.6.19", "@jambonz/db-helpers": "^0.9.1",
"@jambonz/http-authenticator": "^0.2.2",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.4.29", "@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/rtpengine-utils": "^0.3.6", "@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/siprec-client-utils": "^0.1.4", "@jambonz/siprec-client-utils": "^0.2.6",
"@jambonz/stats-collector": "^0.1.6", "@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.4", "@jambonz/time-series": "^0.2.5",
"aws-sdk": "^2.1152.0", "@jambonz/digest-utils": "^0.0.3",
"@aws-sdk/client-sns": "^3.360.0",
"@aws-sdk/client-auto-scaling": "^3.360.0",
"bent": "^7.3.12", "bent": "^7.3.12",
"cidr-matcher": "^2.1.1", "cidr-matcher": "^2.1.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "0.0.12", "drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.5.1", "drachtio-srf": "^4.5.31",
"express": "^4.18.1", "express": "^4.18.1",
"pino": "^7.11.0", "pino": "^7.11.0",
"sdp-transform": "^2.14.1",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
+40 -27
View File
@@ -4,12 +4,12 @@ SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips; DROP TABLE IF EXISTS account_static_ips;
DROP TABLE IF EXISTS account_limits;
DROP TABLE IF EXISTS account_products; DROP TABLE IF EXISTS account_products;
DROP TABLE IF EXISTS account_subscriptions; DROP TABLE IF EXISTS account_subscriptions;
DROP TABLE IF EXISTS account_limits;
DROP TABLE IF EXISTS beta_invite_codes; DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes; DROP TABLE IF EXISTS call_routes;
@@ -20,6 +20,8 @@ DROP TABLE IF EXISTS lcr_carrier_set_entry;
DROP TABLE IF EXISTS lcr_routes; DROP TABLE IF EXISTS lcr_routes;
DROP TABLE IF EXISTS password_settings;
DROP TABLE IF EXISTS predefined_sip_gateways; DROP TABLE IF EXISTS predefined_sip_gateways;
DROP TABLE IF EXISTS predefined_smpp_gateways; DROP TABLE IF EXISTS predefined_smpp_gateways;
@@ -38,10 +40,10 @@ DROP TABLE IF EXISTS sbc_addresses;
DROP TABLE IF EXISTS ms_teams_tenants; DROP TABLE IF EXISTS ms_teams_tenants;
DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS service_provider_limits; DROP TABLE IF EXISTS service_provider_limits;
DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses; DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS speech_credentials; DROP TABLE IF EXISTS speech_credentials;
@@ -73,6 +75,15 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
PRIMARY KEY (account_static_ip_sid) PRIMARY KEY (account_static_ip_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','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (account_limits_sid)
);
CREATE TABLE account_subscriptions CREATE TABLE account_subscriptions
( (
account_subscription_sid CHAR(36) NOT NULL UNIQUE , account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
@@ -92,15 +103,6 @@ pending_reason VARBINARY(52),
PRIMARY KEY (account_subscription_sid) 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 CREATE TABLE beta_invite_codes
( (
invite_code CHAR(6) NOT NULL UNIQUE , invite_code CHAR(6) NOT NULL UNIQUE ,
@@ -136,6 +138,13 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f
PRIMARY KEY (lcr_route_sid) PRIMARY KEY (lcr_route_sid)
) COMMENT='Least cost routing table'; ) COMMENT='Least cost routing table';
CREATE TABLE password_settings
(
min_password_length INTEGER NOT NULL DEFAULT 8,
require_digit BOOLEAN NOT NULL DEFAULT false,
require_special_character BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE predefined_carriers CREATE TABLE predefined_carriers
( (
predefined_carrier_sid CHAR(36) NOT NULL UNIQUE , predefined_carrier_sid CHAR(36) NOT NULL UNIQUE ,
@@ -241,6 +250,15 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
PRIMARY KEY (ms_teams_tenant_sid) PRIMARY KEY (ms_teams_tenant_sid)
) COMMENT='A Microsoft Teams customer tenant'; ) COMMENT='A Microsoft Teams customer tenant';
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','voice_call_minutes','voice_call_session_license', 'voice_call_minutes_license') NOT NULL,
quantity INTEGER NOT NULL,
PRIMARY KEY (service_provider_limits_sid)
);
CREATE TABLE signup_history CREATE TABLE signup_history
( (
email VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL,
@@ -249,15 +267,6 @@ signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (email) 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 CREATE TABLE smpp_addresses
( (
smpp_address_sid CHAR(36) NOT NULL UNIQUE , smpp_address_sid CHAR(36) NOT NULL UNIQUE ,
@@ -305,6 +314,7 @@ email_activation_code VARCHAR(16),
email_validated BOOLEAN NOT NULL DEFAULT false, email_validated BOOLEAN NOT NULL DEFAULT false,
phone_validated BOOLEAN NOT NULL DEFAULT false, phone_validated BOOLEAN NOT NULL DEFAULT false,
email_content_opt_out BOOLEAN NOT NULL DEFAULT false, email_content_opt_out BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (user_sid) PRIMARY KEY (user_sid)
); );
@@ -332,6 +342,9 @@ smpp_password VARCHAR(64),
smpp_enquire_link_interval INTEGER DEFAULT 0, smpp_enquire_link_interval INTEGER DEFAULT 0,
smpp_inbound_system_id VARCHAR(255), smpp_inbound_system_id VARCHAR(255),
smpp_inbound_password VARCHAR(64), smpp_inbound_password VARCHAR(64),
register_from_user VARCHAR(128),
register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (voip_carrier_sid) PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls'; ) COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -452,12 +465,12 @@ CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_
CREATE INDEX account_sid_idx ON account_static_ips (account_sid); CREATE INDEX account_sid_idx ON account_static_ips (account_sid);
ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid); ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (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_1 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid); CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid);
CREATE INDEX account_sid_idx ON account_subscriptions (account_sid); 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); ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (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 invite_code_idx ON beta_invite_codes (invite_code);
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid); CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
@@ -512,10 +525,10 @@ ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) R
ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_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 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); 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; 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 email_idx ON signup_history (email);
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid); CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_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_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
+24 -2
View File
@@ -9,7 +9,8 @@ insert into webhooks(webhook_sid, url, username, password) values('90dda62e-0ea2
insert into service_providers (service_provider_sid, name, root_domain, registration_hook_sid) insert into service_providers (service_provider_sid, name, root_domain, registration_hook_sid)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'SP A', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c'); values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'SP A', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c');
insert into service_provider_limits (service_provider_limits_sid, service_provider_sid, category, quantity) VALUES ('a79d3ade-e0da-4461-80f3-7c73f01e18b4', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'voice_call_session', 1); insert into service_provider_limits (service_provider_limits_sid, service_provider_sid, category, quantity)
values ('a79d3ade-e0da-4461-80f3-7c73f01e18b4', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'voice_call_session', 1);
insert into accounts(account_sid, service_provider_sid, name, sip_realm, registration_hook_sid, webhook_secret) insert into accounts(account_sid, service_provider_sid, name, sip_realm, registration_hook_sid, webhook_secret)
values ('ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'test account', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c', 'foobar'); values ('ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'test account', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c', 'foobar');
@@ -66,4 +67,25 @@ insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_s
values ('d458bf7a-bcea-47b2-ac96-66dfc9c5c220', '150822233*', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803'); values ('d458bf7a-bcea-47b2-ac96-66dfc9c5c220', '150822233*', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid) insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid)
values ('f7ad205d-b92f-4363-8160-f8b5216b40d3', '15083871234', '287c1452-620d-4195-9f19-c9814ef90d78', 'd7cc37cb-d152-49ef-a51b-485f6e917089'); values ('f7ad205d-b92f-4363-8160-f8b5216b40d3', '15083871234', '287c1452-620d-4195-9f19-c9814ef90d78', 'd7cc37cb-d152-49ef-a51b-485f6e917089');
-- two accounts that both have the same carrier with default routing (ambiguity test)
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('239d7d49-b3e4-4fdb-9d66-661149f717e8', 'Account B1', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'echo2.sip.jambonz.org');
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('909d7d49-b3e4-4fdb-9d66-661149f717e8', 'Account B2', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'foxtrot.sip.jambonz.org');
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('8843e39f-4346-4218-8434-a53130e8be49', 'test', '239d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '4d7ce0aa-5ead-4e61-9a6b-3daa732218b1');
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('7743e39f-4346-4218-8434-a53130e8be49', 'test', '909d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '4d7ce0aa-5ead-4e61-9a6b-3daa732218b1');
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid, account_sid, application_sid)
values ('731abdc7-0220-4964-bc66-32b5c70cd9ab', 'twilio-1', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '239d7d49-b3e4-4fdb-9d66-661149f717e8', '8843e39f-4346-4218-8434-a53130e8be49');
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid, account_sid, application_sid)
values ('987abdc7-0220-4964-bc66-32b5c70cd9ab', 'twilio-2', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '909d7d49-b3e4-4fdb-9d66-661149f717e8', '7743e39f-4346-4218-8434-a53130e8be49');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('664a5339-c62c-4075-9e19-f4de70a96597', '731abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('554a5339-c62c-4075-9e19-f4de70a96597', '987abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false);
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC with media">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:+15083871234@172.38.0.10 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@172.38.0.10>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-carrier-fail-ambiguous
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@172.38.0.10 SIP/2.0
[last_Via]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:15083871234@172.38.0.10>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-pcap-carrier-fail-ambiguous
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>
+2
View File
@@ -36,6 +36,8 @@
Subject: uac-pcap-carrier-success Subject: uac-pcap-carrier-success
Content-Type: application/sdp Content-Type: application/sdp
Content-Length: [len] Content-Length: [len]
X-Authenticated-User: xhoaluu@jambonz.org
X-Application-Sid: APP_ID_1
v=0 v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+7 -3
View File
@@ -33,7 +33,6 @@ test('incoming call tests', async(t) => {
t.ok(obj.calls === 0, 'HTTP GET / works (current call count)') t.ok(obj.calls === 0, 'HTTP GET / works (current call count)')
obj = await getJSON('http://127.0.0.1:3050/system-health'); obj = await getJSON('http://127.0.0.1:3050/system-health');
t.ok(obj.calls === 0, 'HTTP GET /system-health works (health check)') t.ok(obj.calls === 0, 'HTTP GET /system-health works (health check)')
await sippUac('uac-pcap-carrier-success.xml', '172.38.0.20'); await sippUac('uac-pcap-carrier-success.xml', '172.38.0.20');
t.pass('incoming call from carrier completed successfully'); t.pass('incoming call from carrier completed successfully');
@@ -59,11 +58,16 @@ test('incoming call tests', async(t) => {
t.pass('handles in-dialog requests'); t.pass('handles in-dialog requests');
await sippUac('uac-pcap-carrier-max-call-limit.xml', '172.38.0.20'); await sippUac('uac-pcap-carrier-max-call-limit.xml', '172.38.0.20');
t.pass('rejects incoming call with 503 when max calls per account reached') t.pass('rejects incoming call with 503 when max calls per account reached');
/* switch off this env for remaining tests (JAMBONES_HOSTING is for Saas sts) */
delete process.env.JAMBONES_HOSTING;
await sippUac('uac-pcap-carrier-fail-ambiguous.xml', '172.38.0.40');
t.pass('rejects incoming call with 503 when multiple accounts have same carrier witrh default routing')
await waitFor(12); await waitFor(12);
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'}); const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
console.log(`cdrs: ${JSON.stringify(res)}`); //console.log(`cdrs: ${JSON.stringify(res)}`);
t.ok(7 === res.total, 'successfully wrote 7 cdrs for calls'); t.ok(7 === res.total, 'successfully wrote 7 cdrs for calls');
srf.disconnect(); srf.disconnect();