mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2026-10-04 02:04:22 +00:00
Compare commits
190
Commits
v0.7.7-rc4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d8ba93b2 | ||
|
|
3f729e232e | ||
|
|
509fdf6c37 | ||
|
|
b7b707cc2e | ||
|
|
ccbebc015f | ||
|
|
320a30e328 | ||
|
|
cd2fde360d | ||
|
|
e353b751c0 | ||
|
|
53b46f7e51 | ||
|
|
6608286e8c | ||
|
|
6ddfbc9373 | ||
|
|
cd48675499 | ||
|
|
cecdbdccef | ||
|
|
6586919c86 | ||
|
|
c919438af5 | ||
|
|
ab39525467 | ||
|
|
61a66ab181 | ||
|
|
48dd7ebfcd | ||
|
|
678fe9d9a8 | ||
|
|
39bd65fb97 | ||
|
|
3f0fd794ce | ||
|
|
876cb393f9 | ||
|
|
fa2259d6e5 | ||
|
|
2405190dcc | ||
|
|
e9a5921b19 | ||
|
|
6efe714d49 | ||
|
|
4150454736 | ||
|
|
73eb8e8d17 | ||
|
|
9beba4330a | ||
|
|
b2868842ad | ||
|
|
4f1b4815c4 | ||
|
|
025853934e | ||
|
|
8ce531ec81 | ||
|
|
7bc2a1a6f4 | ||
|
|
83461f99e8 | ||
|
|
45684aa337 | ||
|
|
9d0c829ae8 | ||
|
|
7a6551c141 | ||
|
|
5f267906b1 | ||
|
|
680d32e9a2 | ||
|
|
71c437ee07 | ||
|
|
35c38685a9 | ||
|
|
8d15098b77 | ||
|
|
656ad3b6cb | ||
|
|
7eccb6816d | ||
|
|
f58cf8eecf | ||
|
|
638c003dfc | ||
|
|
23c748046d | ||
|
|
086adfbeef | ||
|
|
d9b375738b | ||
|
|
ec3af43a41 | ||
|
|
0ba5cb53bb | ||
|
|
25415c0be9 | ||
|
|
c0e42bcdaa | ||
|
|
8f3ea260ca | ||
|
|
c807fdb93d | ||
|
|
a7d6cf5a31 | ||
|
|
1cd526c684 | ||
|
|
50b249544e | ||
|
|
4add329102 | ||
|
|
06b3122571 | ||
|
|
bcfbe9a0fc | ||
|
|
bb9e5e98f5 | ||
|
|
a714190aa4 | ||
|
|
e6d41dec71 | ||
|
|
a6b1e58b87 | ||
|
|
524a556a7c | ||
|
|
b6d856c626 | ||
|
|
3454232314 | ||
|
|
a97cadc4b9 | ||
|
|
20755f456d | ||
|
|
fec4c42008 | ||
|
|
34cfe8ca9d | ||
|
|
55bcba05f6 | ||
|
|
c270c0df43 | ||
|
|
b848ab99e1 | ||
|
|
4153a141d7 | ||
|
|
e999b4e5e8 | ||
|
|
0e2285c4b8 | ||
|
|
f9ae466709 | ||
|
|
831005f7a1 | ||
|
|
a46ad934a8 | ||
|
|
d200799997 | ||
|
|
2fef169e92 | ||
|
|
5829ecdf4c | ||
|
|
03d3a9695d | ||
|
|
f5be95006c | ||
|
|
2747bc15d6 | ||
|
|
cdfbf384fe | ||
|
|
96a83619c7 | ||
|
|
b8a67019a1 | ||
|
|
0e1627ba82 | ||
|
|
bb857a72ff | ||
|
|
aea3033ae0 | ||
|
|
9b9eaa37fb | ||
|
|
56e0e3c0f2 | ||
|
|
6a6b4dd26f | ||
|
|
a3799576a2 | ||
|
|
63e72fbfce | ||
|
|
e5dce35bab | ||
|
|
b6675de2bd | ||
|
|
66e73d8353 | ||
|
|
a25aa08e17 | ||
|
|
b1296c8e89 | ||
|
|
17df3a4ca8 | ||
|
|
f2e820915d | ||
|
|
a1ec0fd5da | ||
|
|
d7654de526 | ||
|
|
6e47e37cc2 | ||
|
|
c37d417a69 | ||
|
|
f7b3815ee3 | ||
|
|
f385a33297 | ||
|
|
a01febdc22 | ||
|
|
e641c590b2 | ||
|
|
8448e003f6 | ||
|
|
f6e071f31e | ||
|
|
54d9044937 | ||
|
|
bcc089058f | ||
|
|
1a09a13cfa | ||
|
|
b311d4af48 | ||
|
|
0fe8843729 | ||
|
|
0b0e37020c | ||
|
|
01cde07117 | ||
|
|
31b4534878 | ||
|
|
0c6365a8df | ||
|
|
def2cb2be2 | ||
|
|
baaee6f063 | ||
|
|
d2f32229a2 | ||
|
|
38f2d27246 | ||
|
|
d94977169e | ||
|
|
0c9df878b8 | ||
|
|
93930fff18 | ||
|
|
2ce24659ca | ||
|
|
7c10e98a66 | ||
|
|
f3fe46319d | ||
|
|
43bfeb439a | ||
|
|
31f34fbffa | ||
|
|
52d0670b88 | ||
|
|
f42128765d | ||
|
|
eac1d7a9e9 | ||
|
|
fae1732ddf | ||
|
|
c858624815 | ||
|
|
2f137f2bfc | ||
|
|
ef0dcfc021 | ||
|
|
fb8baa34af | ||
|
|
7efce18a03 | ||
|
|
84e5202607 | ||
|
|
8f856a30a6 | ||
|
|
d9881632c2 | ||
|
|
6d2b1a94e9 | ||
|
|
ded83ac39a | ||
|
|
91a29d2b2c | ||
|
|
27686a80e8 | ||
|
|
a9e79b23bd | ||
|
|
faafaa8549 | ||
|
|
b24ecf84d3 | ||
|
|
3360ddb3e6 | ||
|
|
b2d48c8954 | ||
|
|
1f1a4d2330 | ||
|
|
c34da3cfa8 | ||
|
|
5b88065925 | ||
|
|
438924ca36 | ||
|
|
c4d4d7bc0a | ||
|
|
ebcd0ce4a2 | ||
|
|
cf239ca7fa | ||
|
|
b072524585 | ||
|
|
7fb9966d20 | ||
|
|
42b3317206 | ||
|
|
db66d98d0a | ||
|
|
26169517dc | ||
|
|
c78ec892b9 | ||
|
|
6d237d43b3 | ||
|
|
44d298a4f3 | ||
|
|
030059a596 | ||
|
|
78b60525e2 | ||
|
|
dc9103cfa1 | ||
|
|
4dd4247dc2 | ||
|
|
055350903a | ||
|
|
7e83791ef0 | ||
|
|
dbedd7419e | ||
|
|
c4d07b517e | ||
|
|
2693077871 | ||
|
|
ad0912d302 | ||
|
|
e715433534 | ||
|
|
f315b1b41f | ||
|
|
bd09703732 | ||
|
|
363eb676a3 | ||
|
|
7b085e5763 | ||
|
|
04554fd60d | ||
|
|
9f1109c738 |
@@ -1 +0,0 @@
|
||||
test/*
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": false,
|
||||
"modules": false
|
||||
},
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"plugins": ["promise"],
|
||||
"rules": {
|
||||
"promise/always-return": "error",
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "error",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn",
|
||||
"promise/no-return-in-finally": "warn",
|
||||
|
||||
// Possible Errors
|
||||
// http://eslint.org/docs/rules/#possible-errors
|
||||
"comma-dangle": [2, "only-multiline"],
|
||||
"no-control-regex": 2,
|
||||
"no-debugger": 2,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-empty-character-class": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extra-boolean-cast" : 2,
|
||||
"no-extra-parens": [2, "functions"],
|
||||
"no-extra-semi": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-negated-in-lhs": 2,
|
||||
"no-obj-calls": 2,
|
||||
"no-proto": 2,
|
||||
"no-unexpected-multiline": 2,
|
||||
"no-unreachable": 2,
|
||||
"use-isnan": 2,
|
||||
"valid-typeof": 2,
|
||||
|
||||
// Best Practices
|
||||
// http://eslint.org/docs/rules/#best-practices
|
||||
"no-fallthrough": 2,
|
||||
"no-octal": 2,
|
||||
"no-redeclare": 2,
|
||||
"no-self-assign": 2,
|
||||
"no-unused-labels": 2,
|
||||
|
||||
// Strict Mode
|
||||
// http://eslint.org/docs/rules/#strict-mode
|
||||
"strict": [2, "never"],
|
||||
|
||||
// Variables
|
||||
// http://eslint.org/docs/rules/#variables
|
||||
"no-delete-var": 2,
|
||||
"no-undef": 2,
|
||||
"no-unused-vars": [2, {"args": "none"}],
|
||||
|
||||
// Node.js and CommonJS
|
||||
// http://eslint.org/docs/rules/#nodejs-and-commonjs
|
||||
"no-mixed-requires": 2,
|
||||
"no-new-require": 2,
|
||||
"no-path-concat": 2,
|
||||
"no-restricted-modules": [2, "sys", "_linklist"],
|
||||
|
||||
// Stylistic Issues
|
||||
// http://eslint.org/docs/rules/#stylistic-issues
|
||||
"comma-spacing": 2,
|
||||
"eol-last": 2,
|
||||
"indent": [2, 2, {"SwitchCase": 1}],
|
||||
"keyword-spacing": 2,
|
||||
"max-len": [2, 120, 2],
|
||||
"new-parens": 2,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multiple-empty-lines": [2, {"max": 2}],
|
||||
"no-trailing-spaces": [2, {"skipBlankLines": false }],
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"semi": 2,
|
||||
"space-before-blocks": [2, "always"],
|
||||
"space-before-function-paren": [2, "never"],
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": 2,
|
||||
|
||||
// ECMAScript 6
|
||||
// http://eslint.org/docs/rules/#ecmascript-6
|
||||
"arrow-parens": [2, "always"],
|
||||
"arrow-spacing": [2, {"before": true, "after": true}],
|
||||
"constructor-super": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-confusing-arrow": 2,
|
||||
"no-const-assign": 2,
|
||||
"no-dupe-class-members": 2,
|
||||
"no-new-symbol": 2,
|
||||
"no-this-before-super": 2,
|
||||
"prefer-const": 2
|
||||
},
|
||||
"globals": {
|
||||
"DTRACE_HTTP_CLIENT_REQUEST" : false,
|
||||
"LTTNG_HTTP_CLIENT_REQUEST" : false,
|
||||
"COUNTER_HTTP_CLIENT_REQUEST" : false,
|
||||
"DTRACE_HTTP_CLIENT_RESPONSE" : false,
|
||||
"LTTNG_HTTP_CLIENT_RESPONSE" : false,
|
||||
"COUNTER_HTTP_CLIENT_RESPONSE" : false,
|
||||
"DTRACE_HTTP_SERVER_REQUEST" : false,
|
||||
"LTTNG_HTTP_SERVER_REQUEST" : false,
|
||||
"COUNTER_HTTP_SERVER_REQUEST" : false,
|
||||
"DTRACE_HTTP_SERVER_RESPONSE" : false,
|
||||
"LTTNG_HTTP_SERVER_RESPONSE" : false,
|
||||
"COUNTER_HTTP_SERVER_RESPONSE" : false,
|
||||
"DTRACE_NET_STREAM_END" : false,
|
||||
"LTTNG_NET_STREAM_END" : false,
|
||||
"COUNTER_NET_SERVER_CONNECTION_CLOSE" : false,
|
||||
"DTRACE_NET_SERVER_CONNECTION" : false,
|
||||
"LTTNG_NET_SERVER_CONNECTION" : false,
|
||||
"COUNTER_NET_SERVER_CONNECTION" : false
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,10 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `master` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: sbc-inbound
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -20,32 +14,41 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
IMAGE_ID=jambonz/sbc-inbound
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
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
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 14.x
|
||||
- run: npm ci
|
||||
node-version: 20.x
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- run: npm test
|
||||
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: npm test
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run jslint
|
||||
# npm audit fix || true
|
||||
# npm run jslint
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
FROM --platform=linux/amd64 node:18.8.0-alpine as base
|
||||
FROM --platform=linux/amd64 node:24-alpine AS base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
WORKDIR /opt/app/
|
||||
|
||||
FROM base as build
|
||||
FROM base AS build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
ENV NODE_ENV=$NODE_ENV
|
||||
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
# sbc-inbound 
|
||||
|
||||
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 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
|
||||
```
|
||||
{
|
||||
|
||||
@@ -3,10 +3,17 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
||||
process.env.JAMBONES_MYSQL_USER &&
|
||||
process.env.JAMBONES_MYSQL_PASSWORD &&
|
||||
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.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');
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
const srf = new Srf('sbc-inbound');
|
||||
const opts = Object.assign({
|
||||
@@ -28,8 +35,9 @@ const {
|
||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
||||
});
|
||||
const StatsCollector = require('@jambonz/stats-collector');
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const stats = new StatsCollector(logger);
|
||||
const {equalsIgnoreOrder, createHealthCheckApp, systemHealth} = require('./lib/utils');
|
||||
const {equalsIgnoreOrder, createHealthCheckApp, systemHealth, parseHostPorts} = require('./lib/utils');
|
||||
const {LifeCycleEvents} = require('./lib/constants');
|
||||
const setNameRtp = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-rtp`;
|
||||
const rtpServers = [];
|
||||
@@ -46,30 +54,47 @@ const {
|
||||
lookupAccountBySipRealm,
|
||||
lookupAccountBySid,
|
||||
lookupAccountCapacitiesBySid,
|
||||
queryCallLimits
|
||||
queryCallLimits,
|
||||
lookupClientByAccountAndUsername,
|
||||
lookupSystemInformation
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
port: process.env.JAMBONES_MYSQL_PORT || 3306,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
password: process.env.JAMBONES_MYSQL_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
}, logger);
|
||||
}, logger, process.env.JAMBONES_MYSQL_WRITE_HOST && process.env.JAMBONES_MYSQL_WRITE_USER &&
|
||||
process.env.JAMBONES_MYSQL_WRITE_PASSWORD && process.env.JAMBONES_MYSQL_WRITE_DATABASE ? {
|
||||
host: process.env.JAMBONES_MYSQL_WRITE_HOST,
|
||||
port: process.env.JAMBONES_MYSQL_WRITE_PORT || 3306,
|
||||
user: process.env.JAMBONES_MYSQL_WRITE_USER,
|
||||
password: process.env.JAMBONES_MYSQL_WRITE_PASSWORD,
|
||||
database: process.env.JAMBONES_MYSQL_WRITE_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
} : null);
|
||||
const {
|
||||
client: redisClient,
|
||||
addKey,
|
||||
deleteKey,
|
||||
retrieveKey,
|
||||
retrieveHash,
|
||||
createSet,
|
||||
retrieveSet,
|
||||
addToSet,
|
||||
removeFromSet,
|
||||
incrKey,
|
||||
decrKey} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
decrKey,
|
||||
createEphemeralGateway,
|
||||
queryEphemeralGateways
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
|
||||
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
|
||||
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {
|
||||
emitter: stats,
|
||||
//emitter: stats,
|
||||
dtmfListenPort: process.env.DTMF_LISTEN_PORT || 22224,
|
||||
protocol: 'udp'
|
||||
protocol: ngProtocol
|
||||
});
|
||||
srf.locals = {...srf.locals,
|
||||
stats,
|
||||
@@ -82,6 +107,7 @@ srf.locals = {...srf.locals,
|
||||
AlertType,
|
||||
activeCallIds: new Map(),
|
||||
getRtpEngine,
|
||||
privateNetworkCidr: process.env.PRIVATE_VOIP_NETWORK_CIDR || null,
|
||||
dbHelpers: {
|
||||
pool,
|
||||
ping,
|
||||
@@ -92,20 +118,30 @@ srf.locals = {...srf.locals,
|
||||
lookupAccountBySid,
|
||||
lookupAccountBySipRealm,
|
||||
lookupAccountCapacitiesBySid,
|
||||
queryCallLimits
|
||||
queryCallLimits,
|
||||
lookupClientByAccountAndUsername,
|
||||
lookupSystemInformation
|
||||
},
|
||||
realtimeDbHelpers: {
|
||||
addKey,
|
||||
deleteKey,
|
||||
retrieveKey,
|
||||
retrieveHash,
|
||||
createSet,
|
||||
incrKey,
|
||||
decrKey,
|
||||
retrieveSet
|
||||
retrieveSet,
|
||||
createEphemeralGateway,
|
||||
queryEphemeralGateways
|
||||
}
|
||||
};
|
||||
const {
|
||||
getSPForAccount,
|
||||
wasOriginatedFromCarrier,
|
||||
getApplicationForDidAndCarrier,
|
||||
getOutboundGatewayForRefer
|
||||
getOutboundGatewayForRefer,
|
||||
getApplicationBySid,
|
||||
lookupAuthCarriersForAccountAndSP
|
||||
} = require('./lib/db-utils')(srf, logger);
|
||||
srf.locals = {
|
||||
...srf.locals,
|
||||
@@ -113,32 +149,62 @@ srf.locals = {
|
||||
wasOriginatedFromCarrier,
|
||||
getApplicationForDidAndCarrier,
|
||||
getOutboundGatewayForRefer,
|
||||
getFeatureServer: require('./lib/fs-tracking')(srf, logger)
|
||||
getFeatureServer: require('./lib/fs-tracking')(srf, logger),
|
||||
getApplicationBySid,
|
||||
lookupAuthCarriersForAccountAndSP
|
||||
};
|
||||
const activeCallIds = srf.locals.activeCallIds;
|
||||
|
||||
/* report our call count to redis so a draining process can count calls across
|
||||
all sbc-inbound and sbc-outbound processes on this server */
|
||||
if (!process.env.K8S && 'test' !== process.env.NODE_ENV) {
|
||||
srf.locals.callCountReporter = require('./lib/call-count-reporter')({
|
||||
logger,
|
||||
addKey,
|
||||
addToSet,
|
||||
getCount: () => activeCallIds.size
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
initLocals,
|
||||
handleSipRec,
|
||||
identifyAccount,
|
||||
checkLimits,
|
||||
challengeDeviceCalls
|
||||
challengeDeviceCalls,
|
||||
identifyAuthTrunk
|
||||
} = require('./lib/middleware')(srf, logger);
|
||||
const CallSession = require('./lib/call-session');
|
||||
|
||||
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const cidrs = process.env.JAMBONES_NETWORK_CIDR
|
||||
.split(',')
|
||||
.map((s) => s.trim());
|
||||
const matcher = new CIDRMatcher(cidrs);
|
||||
|
||||
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
|
||||
srf.on('connect', (err, hp) => {
|
||||
srf.on('connect', (err, hp, version, localHostports) => {
|
||||
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
|
||||
logger.info(`connected to drachtio listening on ${hp}`);
|
||||
let addedPrivateIp = false;
|
||||
logger.info(`connected to drachtio ${version} listening on ${hp}, local hostports: ${localHostports}`);
|
||||
|
||||
const hostports = hp.split(',');
|
||||
|
||||
if (localHostports) {
|
||||
const locals = localHostports.split(',');
|
||||
for (const hp of locals) {
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
|
||||
if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
const hostport = `${arr[2]}:${arr[3]}`;
|
||||
logger.info(`adding sbc private address to redis: ${hostport}`);
|
||||
srf.locals.privateSipAddress = hostport;
|
||||
srf.locals.addToRedis = () => addToSet(setName, hostport);
|
||||
srf.locals.removeFromRedis = () => removeFromSet(setName, hostport);
|
||||
srf.locals.addToRedis();
|
||||
addedPrivateIp = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const hp of hostports) {
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
|
||||
if (arr && 'udp' === arr[1] && !matcher.contains(arr[2])) {
|
||||
@@ -146,7 +212,7 @@ if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
srf.locals.sipAddress = arr[2];
|
||||
if (!process.env.SBC_ACCOUNT_SID) addSbcAddress(arr[2]);
|
||||
}
|
||||
else if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
else if (!addedPrivateIp && arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
const hostport = `${arr[2]}:${arr[3]}`;
|
||||
logger.info(`adding sbc private address to redis: ${hostport}`);
|
||||
srf.locals.privateSipAddress = hostport;
|
||||
@@ -155,6 +221,7 @@ if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
srf.locals.addToRedis();
|
||||
}
|
||||
}
|
||||
srf.locals.sbcPublicIpAddress = parseHostPorts(logger, hostports, srf);
|
||||
});
|
||||
}
|
||||
else {
|
||||
@@ -162,6 +229,24 @@ else {
|
||||
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.on('connect', (err, hp, version, localHostports) => {
|
||||
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
|
||||
logger.info(`connected to drachtio ${version} listening on ${hp}, local hostports: ${localHostports}`);
|
||||
|
||||
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 = localHostports ? localHostports.split(',') : 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
srf.locals.sbcPublicIpAddress = parseHostPorts(logger, hp, srf);
|
||||
});
|
||||
}
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
srf.on('error', (err) => {
|
||||
@@ -175,7 +260,9 @@ srf.use('invite', [
|
||||
handleSipRec,
|
||||
identifyAccount,
|
||||
checkLimits,
|
||||
challengeDeviceCalls
|
||||
challengeDeviceCalls,
|
||||
// challengeDeviceCalls will detect auth_trunk or device calls, identifyAuthTrunk have to be after that
|
||||
identifyAuthTrunk
|
||||
]);
|
||||
|
||||
srf.invite((req, res) => {
|
||||
@@ -194,7 +281,7 @@ srf.invite((req, res) => {
|
||||
session.connect();
|
||||
});
|
||||
|
||||
srf.use((req, res, next, err) => {
|
||||
srf.use((err, req, res, next) => {
|
||||
logger.error(err, 'hit top-level error handler');
|
||||
res.send(500);
|
||||
});
|
||||
@@ -226,14 +313,25 @@ if (process.env.K8S || process.env.HTTP_PORT) {
|
||||
});
|
||||
}
|
||||
if ('test' !== process.env.NODE_ENV) {
|
||||
/* update call stats periodically */
|
||||
setInterval(() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:inbound']);
|
||||
/* update call stats periodically as well as definition of private network cidr */
|
||||
setInterval(async() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size,
|
||||
['direction:inbound', `instance_id:${process.env.INSTANCE_ID || 0}`]);
|
||||
|
||||
const r = await lookupSystemInformation();
|
||||
if (r) {
|
||||
if (r.private_network_cidr !== srf.locals.privateNetworkCidr) {
|
||||
logger.info(`updating private network cidr from ${srf.locals.privateNetworkCidr} to ${r.private_network_cidr}`);
|
||||
srf.locals.privateNetworkCidr = r.private_network_cidr;
|
||||
}
|
||||
if (r.log_level) {
|
||||
logger.level = r.log_level;
|
||||
}
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
const lookupRtpServiceEndpoints = (lookup, serviceName) => {
|
||||
logger.debug(`dns lookup for ${serviceName}..`);
|
||||
lookup(serviceName, {family: 4, all: true}, (err, addresses) => {
|
||||
if (err) {
|
||||
logger.error({err}, `Error looking up ${serviceName}`);
|
||||
@@ -245,7 +343,7 @@ const lookupRtpServiceEndpoints = (lookup, serviceName) => {
|
||||
rtpServers.length = 0;
|
||||
Array.prototype.push.apply(rtpServers, addrs);
|
||||
logger.info({rtpServers}, 'rtpserver endpoints have been updated');
|
||||
setRtpEngines(rtpServers.map((a) => `${a}:${process.env.RTPENGINE_PORT || 22222}`));
|
||||
setRtpEngines(rtpServers.map((a) => `${a}:${ngPort}`));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -272,7 +370,7 @@ else {
|
||||
logger.debug({newArray, rtpServers}, 'getActiveRtpServers');
|
||||
if (!equalsIgnoreOrder(newArray, rtpServers)) {
|
||||
logger.info({newArray}, 'resetting active rtpengines');
|
||||
setRtpEngines(newArray.map((a) => `${a}:${process.env.RTPENGINE_PORT || 22222}`));
|
||||
setRtpEngines(newArray.map((a) => `${a}:${ngPort}`));
|
||||
rtpServers.length = 0;
|
||||
Array.prototype.push.apply(rtpServers, newArray);
|
||||
}
|
||||
@@ -303,8 +401,18 @@ process.on('SIGUSR2', handle.bind(null, removeFromSet, setName));
|
||||
process.on('SIGTERM', handle.bind(null, removeFromSet, setName));
|
||||
|
||||
function handle(removeFromSet, setName, signal) {
|
||||
logger.info(`got signal ${signal}, removing ${srf.locals.privateSipAddress} from set ${setName}`);
|
||||
removeFromSet(setName, srf.locals.privateSipAddress);
|
||||
logger.info(`got signal ${signal}`);
|
||||
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};
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"DTLS": "off",
|
||||
"SDES": "off",
|
||||
"ICE": "remove",
|
||||
"flags": ["media handover", "port latching"],
|
||||
"flags": ["port latching"],
|
||||
"rtcp-mux": ["accept"]
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"default": {
|
||||
"transport-protocol": "UDP/TLS/RTP/SAVPF",
|
||||
"ICE": "force",
|
||||
"ICE": "default",
|
||||
"SDES": "off",
|
||||
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
|
||||
"rtcp-mux": ["require"]
|
||||
"flags": ["generate mid", "SDES-no", "port latching"],
|
||||
"rtcp-mux": ["offer"]
|
||||
},
|
||||
"teams": {
|
||||
"transport-protocol": "RTP/SAVP",
|
||||
"ICE": "force",
|
||||
"ICE": "default",
|
||||
"SDES": "off",
|
||||
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
|
||||
"flags": ["generate mid", "SDES-no", "port latching"],
|
||||
"rtcp-mux": ["accept"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
const promisePlugin = require('eslint-plugin-promise');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: ['test/*']
|
||||
},
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
// Node.js globals
|
||||
console: 'readonly',
|
||||
process: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
exports: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
setImmediate: 'readonly',
|
||||
clearImmediate: 'readonly',
|
||||
// DTrace/LTTNG globals
|
||||
DTRACE_HTTP_CLIENT_REQUEST: false,
|
||||
LTTNG_HTTP_CLIENT_REQUEST: false,
|
||||
COUNTER_HTTP_CLIENT_REQUEST: false,
|
||||
DTRACE_HTTP_CLIENT_RESPONSE: false,
|
||||
LTTNG_HTTP_CLIENT_RESPONSE: false,
|
||||
COUNTER_HTTP_CLIENT_RESPONSE: false,
|
||||
DTRACE_HTTP_SERVER_REQUEST: false,
|
||||
LTTNG_HTTP_SERVER_REQUEST: false,
|
||||
COUNTER_HTTP_SERVER_REQUEST: false,
|
||||
DTRACE_HTTP_SERVER_RESPONSE: false,
|
||||
LTTNG_HTTP_SERVER_RESPONSE: false,
|
||||
COUNTER_HTTP_SERVER_RESPONSE: false,
|
||||
DTRACE_NET_STREAM_END: false,
|
||||
LTTNG_NET_STREAM_END: false,
|
||||
COUNTER_NET_SERVER_CONNECTION_CLOSE: false,
|
||||
DTRACE_NET_SERVER_CONNECTION: false,
|
||||
LTTNG_NET_SERVER_CONNECTION: false,
|
||||
COUNTER_NET_SERVER_CONNECTION: false
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
promise: promisePlugin
|
||||
},
|
||||
rules: {
|
||||
// Promise plugin rules
|
||||
'promise/always-return': 'error',
|
||||
'promise/no-return-wrap': 'error',
|
||||
'promise/param-names': 'error',
|
||||
'promise/catch-or-return': 'error',
|
||||
'promise/no-native': 'off',
|
||||
'promise/no-nesting': 'warn',
|
||||
'promise/no-promise-in-callback': 'warn',
|
||||
'promise/no-callback-in-promise': 'warn',
|
||||
'promise/no-return-in-finally': 'warn',
|
||||
|
||||
// Possible Errors
|
||||
'comma-dangle': [2, 'only-multiline'],
|
||||
'no-control-regex': 2,
|
||||
'no-debugger': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-empty-character-class': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-parens': [2, 'functions'],
|
||||
'no-extra-semi': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-proto': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unreachable': 2,
|
||||
'use-isnan': 2,
|
||||
'valid-typeof': 2,
|
||||
|
||||
// Best Practices
|
||||
'no-fallthrough': 2,
|
||||
'no-octal': 2,
|
||||
'no-redeclare': 2,
|
||||
'no-self-assign': 2,
|
||||
'no-unused-labels': 2,
|
||||
|
||||
// Strict Mode
|
||||
'strict': [2, 'never'],
|
||||
|
||||
// Variables
|
||||
'no-delete-var': 2,
|
||||
'no-undef': 2,
|
||||
'no-unused-vars': [2, {args: 'none', ignoreRestSiblings: true}],
|
||||
|
||||
// Node.js and CommonJS
|
||||
'no-mixed-requires': 2,
|
||||
'no-new-require': 2,
|
||||
'no-path-concat': 2,
|
||||
|
||||
// Stylistic Issues
|
||||
'comma-spacing': 2,
|
||||
'eol-last': 2,
|
||||
'indent': [2, 2, {SwitchCase: 1}],
|
||||
'keyword-spacing': 2,
|
||||
'max-len': [2, 120, 2],
|
||||
'new-parens': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multiple-empty-lines': [2, {max: 2}],
|
||||
'no-trailing-spaces': [2, {skipBlankLines: false}],
|
||||
'quotes': [2, 'single', 'avoid-escape'],
|
||||
'semi': 2,
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-before-function-paren': [2, 'never'],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': 2,
|
||||
|
||||
// ECMAScript 6
|
||||
'arrow-parens': [2, 'always'],
|
||||
'arrow-spacing': [2, {before: true, after: true}],
|
||||
'constructor-super': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-confusing-arrow': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-dupe-class-members': 2,
|
||||
'no-new-symbol': 2,
|
||||
'no-this-before-super': 2,
|
||||
'prefer-const': 2
|
||||
}
|
||||
}
|
||||
];
|
||||
+46
-10
@@ -8,7 +8,7 @@ module.exports = (logger) => {
|
||||
// listen for SNS lifecycle changes
|
||||
let lifecycleEmitter = new Emitter();
|
||||
lifecycleEmitter.dryUpCalls = false;
|
||||
if (process.env.AWS_SNS_TOPIC_ARM) {
|
||||
if (process.env.AWS_SNS_TOPIC_ARN) {
|
||||
|
||||
(async function() {
|
||||
try {
|
||||
@@ -23,23 +23,55 @@ module.exports = (logger) => {
|
||||
const {srf} = require('..');
|
||||
const {activeCallIds, removeFromRedis} = srf.locals;
|
||||
|
||||
/* reject new INVITEs with 503 so senders fail over to another SBC */
|
||||
srf.locals.dryUpCalls = true;
|
||||
|
||||
/* remove our private IP from the set of active SBCs so rtp and fs know we are gone */
|
||||
removeFromRedis();
|
||||
|
||||
/* if we have zero calls, we can complete the scale-in right now */
|
||||
const calls = activeCallIds.size;
|
||||
if (0 === calls) {
|
||||
logger.info('scale-in can complete immediately as we have no calls in progress');
|
||||
lifecycleEmitter.completeScaleIn();
|
||||
}
|
||||
else {
|
||||
logger.info(`${calls} calls in progress; scale-in will complete when they are done`);
|
||||
}
|
||||
/* count calls in progress across all sbc-inbound and sbc-outbound
|
||||
processes on this server, if they are reporting; otherwise
|
||||
fall back to counting only our own */
|
||||
const countServerCalls = async() => {
|
||||
const reporter = srf.locals.callCountReporter;
|
||||
if (!reporter) return activeCallIds.size;
|
||||
const {retrieveSet, retrieveKey} = srf.locals.realtimeDbHelpers;
|
||||
const keys = await retrieveSet(reporter.setName);
|
||||
let count = 0;
|
||||
for (const key of keys) {
|
||||
count += parseInt(await retrieveKey(key), 10) || 0;
|
||||
}
|
||||
return Math.max(count, activeCallIds.size);
|
||||
};
|
||||
|
||||
/* poll until calls have dried up, then complete the scale-in;
|
||||
require two consecutive zero readings since reported counts
|
||||
may be up to 15s stale */
|
||||
let consecutiveZeroCounts = 0;
|
||||
const timer = setInterval(async() => {
|
||||
try {
|
||||
const calls = await countServerCalls();
|
||||
if (0 === calls) {
|
||||
if (++consecutiveZeroCounts >= 2) {
|
||||
clearInterval(timer);
|
||||
logger.info('scale-in complete now that calls have dried up');
|
||||
lifecycleEmitter.completeScaleIn();
|
||||
}
|
||||
}
|
||||
else {
|
||||
consecutiveZeroCounts = 0;
|
||||
logger.info(`${calls} calls in progress on this server; scale-in will complete when they are done`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error counting calls in progress during scale-in');
|
||||
}
|
||||
}, 20000);
|
||||
})
|
||||
.on(LifeCycleEvents.StandbyEnter, () => {
|
||||
lifecycleEmitter.dryUpCalls = true;
|
||||
const {srf} = require('..');
|
||||
const {removeFromRedis} = srf.locals;
|
||||
srf.locals.dryUpCalls = true;
|
||||
removeFromRedis();
|
||||
|
||||
logger.info('AWS enter pending state notification: begin drying up calls');
|
||||
@@ -48,6 +80,7 @@ module.exports = (logger) => {
|
||||
lifecycleEmitter.dryUpCalls = false;
|
||||
const {srf} = require('..');
|
||||
const {addToRedis} = srf.locals;
|
||||
srf.locals.dryUpCalls = false;
|
||||
addToRedis();
|
||||
|
||||
logger.info('AWS exit pending state notification: re-enable calls');
|
||||
@@ -57,6 +90,9 @@ module.exports = (logger) => {
|
||||
}
|
||||
})();
|
||||
}
|
||||
else if (process.env.K8S) {
|
||||
lifecycleEmitter.scaleIn = () => process.exit(0);
|
||||
}
|
||||
|
||||
return {lifecycleEmitter};
|
||||
};
|
||||
|
||||
+76
-34
@@ -1,26 +1,51 @@
|
||||
const Emitter = require('events');
|
||||
const bent = require('bent');
|
||||
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 express = require('express');
|
||||
const app = express();
|
||||
const getString = bent('string');
|
||||
const AWS = require('aws-sdk');
|
||||
const sns = new AWS.SNS({apiVersion: '2010-03-31'});
|
||||
const autoscaling = new AWS.AutoScaling({apiVersion: '2011-01-01'});
|
||||
const {
|
||||
SNSClient,
|
||||
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 = new Parser();
|
||||
const {validatePayload} = require('verify-aws-sns-signature');
|
||||
|
||||
AWS.config.update({region: process.env.AWS_REGION});
|
||||
|
||||
class SnsNotifier extends Emitter {
|
||||
constructor(logger) {
|
||||
super();
|
||||
|
||||
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) {
|
||||
try {
|
||||
@@ -44,7 +69,18 @@ class SnsNotifier extends Emitter {
|
||||
subscriptionRequestId: this.subscriptionRequestId
|
||||
}, 'response from SNS SubscribeURL');
|
||||
const data = await this.describeInstance();
|
||||
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
|
||||
|
||||
const group = data.AutoScalingGroups.find((group) =>
|
||||
group.Instances && group.Instances.some((instance) => instance.InstanceId === this.instanceId)
|
||||
);
|
||||
if (!group) {
|
||||
this.logger.error('Current instance not found in any Auto Scaling group', data);
|
||||
} else {
|
||||
const instance = group.Instances.find((instance) => instance.InstanceId === this.instanceId);
|
||||
this.lifecycleState = instance.LifecycleState;
|
||||
}
|
||||
|
||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||
break;
|
||||
|
||||
case 'Notification':
|
||||
@@ -80,14 +116,12 @@ class SnsNotifier extends Emitter {
|
||||
|
||||
async init() {
|
||||
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.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4');
|
||||
this.snsEndpoint = `http://${this.publicIp}:${PORT}`;
|
||||
this.logger.info({
|
||||
instanceId: this.instanceId,
|
||||
publicIp: this.publicIp,
|
||||
snsEndpoint: this.snsEndpoint
|
||||
publicIp: this.publicIp
|
||||
}, 'retrieved AWS instance data');
|
||||
|
||||
// start listening
|
||||
@@ -99,7 +133,10 @@ class SnsNotifier extends Emitter {
|
||||
this.logger.error(err, 'burped error');
|
||||
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) {
|
||||
this.logger.error({err}, 'Error retrieving AWS instance metadata');
|
||||
@@ -108,51 +145,56 @@ class SnsNotifier extends Emitter {
|
||||
|
||||
async subscribe() {
|
||||
try {
|
||||
const response = await sns.subscribe({
|
||||
const params = {
|
||||
Protocol: 'http',
|
||||
TopicArn: process.env.AWS_SNS_TOPIC_ARM,
|
||||
TopicArn: process.env.AWS_SNS_TOPIC_ARN,
|
||||
Endpoint: this.snsEndpoint
|
||||
}).promise();
|
||||
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
|
||||
};
|
||||
const response = await snsClient.send(new SubscribeCommand(params));
|
||||
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARN}`);
|
||||
} 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_ARN}`);
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribe() {
|
||||
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
|
||||
try {
|
||||
const response = await sns.unsubscribe({
|
||||
const params = {
|
||||
SubscriptionArn: this.subscriptionArn
|
||||
}).promise();
|
||||
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
|
||||
};
|
||||
const response = await snsClient.send(new UnsubscribeCommand(params));
|
||||
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARN}`);
|
||||
} 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_ARN}`);
|
||||
}
|
||||
}
|
||||
|
||||
completeScaleIn() {
|
||||
assert(this.scaleInParams);
|
||||
autoscaling.completeLifecycleAction(this.scaleInParams, (err, response) => {
|
||||
if (err) return this.logger.error({err}, 'Error completing scale-in');
|
||||
this.logger.info({response}, 'Successfully completed scale-in action');
|
||||
});
|
||||
autoScalingClient.send(new CompleteLifecycleActionCommand(this.scaleInParams))
|
||||
.then((data) => {
|
||||
return this.logger.info({data}, 'Successfully completed scale-in action');
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error({err}, 'Error completing scale-in');
|
||||
});
|
||||
}
|
||||
|
||||
describeInstance() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.instanceId) return reject('instance-id unknown');
|
||||
autoscaling.describeAutoScalingInstances({
|
||||
autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
|
||||
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');
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info({data}, 'SnsNotifier: describeInstance');
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,7 +208,7 @@ module.exports = async function(logger) {
|
||||
process.on('SIGHUP', async() => {
|
||||
try {
|
||||
const data = await notifier.describeInstance();
|
||||
const state = data.AutoScalingInstances[0].LifecycleState;
|
||||
const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
||||
if (state !== notifier.lifecycleState) {
|
||||
notifier.lifecycleState = state;
|
||||
switch (state) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Periodically report this process's count of calls in progress to redis.
|
||||
* A server may host several sbc-inbound and sbc-outbound processes; when one
|
||||
* of them handles an autoscale drain it needs to know when the entire server
|
||||
* has no calls in progress, not just its own process. Each process writes
|
||||
* its own count under a per-pid key (with a short expiry, so keys from dead
|
||||
* processes evaporate) and registers that key in a per-host set that the
|
||||
* draining process can enumerate.
|
||||
*/
|
||||
const REPORT_INTERVAL = 15000;
|
||||
const KEY_EXPIRY_SECS = 120;
|
||||
|
||||
module.exports = ({logger, addKey, addToSet, getCount}) => {
|
||||
const prefix = process.env.JAMBONES_CLUSTER_ID || 'default';
|
||||
const setName = `${prefix}:call-count-keys:${os.hostname()}`;
|
||||
const key = `${prefix}:call-count:${os.hostname()}:${process.pid}`;
|
||||
|
||||
const report = () => {
|
||||
addKey(key, `${getCount()}`, KEY_EXPIRY_SECS)
|
||||
.catch((err) => logger.error({err}, 'call-count-reporter: error writing call count'));
|
||||
};
|
||||
|
||||
addToSet(setName, key)
|
||||
.catch((err) => logger.error({err}, `call-count-reporter: error adding ${key} to ${setName}`));
|
||||
setInterval(report, REPORT_INTERVAL);
|
||||
report();
|
||||
|
||||
return {key, setName};
|
||||
};
|
||||
+449
-127
@@ -3,16 +3,22 @@ const SrsClient = require('@jambonz/siprec-client-utils');
|
||||
const {
|
||||
makeRtpEngineOpts,
|
||||
SdpWantsSrtp,
|
||||
SdpWantsSDES,
|
||||
nudgeCallCounts,
|
||||
roundTripTime,
|
||||
parseConnectionIp
|
||||
parseConnectionIp,
|
||||
isPrivateVoipNetwork,
|
||||
makeFullMediaReleaseKey,
|
||||
makePartnerFullMediaReleaseKey
|
||||
} = require('./utils');
|
||||
const { MediaPath } = require('./constants.json');
|
||||
|
||||
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
|
||||
const {parseUri, stringifyUri, SipError} = require('drachtio-srf');
|
||||
const debug = require('debug')('jambonz:sbc-inbound');
|
||||
const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy';
|
||||
const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
|
||||
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
|
||||
|
||||
/**
|
||||
* this is to make sure the outgoing From has the number in the incoming From
|
||||
@@ -21,8 +27,10 @@ const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
|
||||
const createBLegFromHeader = (req) => {
|
||||
const from = req.getParsedHeader('From');
|
||||
const uri = parseUri(from.uri);
|
||||
if (uri && uri.user) return `<sip:${uri.user}@localhost>`;
|
||||
return '<sip:anonymous@localhost>';
|
||||
const name = from.name;
|
||||
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) => {
|
||||
@@ -56,11 +64,17 @@ class CallSession extends Emitter {
|
||||
this.activeCallIds = this.srf.locals.activeCallIds;
|
||||
|
||||
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
|
||||
this._mediaReleased = false;
|
||||
this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
|
||||
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
|
||||
this.retrieveHash = req.srf.locals.realtimeDbHelpers.retrieveHash;
|
||||
|
||||
this._mediaPath = MediaPath.FullMedia;
|
||||
|
||||
this.application_sid = req.locals.application_sid;
|
||||
this.account_sid = req.locals.account_sid;
|
||||
this.service_provider_sid = req.locals.service_provider_sid;
|
||||
this.srsClients = [];
|
||||
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
|
||||
}
|
||||
|
||||
get isFromMSTeams() {
|
||||
@@ -72,17 +86,35 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
get isMediaReleased() {
|
||||
return this._mediaReleased;
|
||||
return this._mediaPath !== MediaPath.FullMedia;
|
||||
}
|
||||
|
||||
get callerIsUsingSrtp() {
|
||||
const tp = this.rtpEngineOpts?.uas?.mediaOpts['transport-protocol'];
|
||||
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() {
|
||||
const {sdp} = this.req.locals;
|
||||
this.logger.info('inbound call accepted for routing');
|
||||
// use the parsed SDP from middleware (req.locals.sdp), not req.body:
|
||||
// for multipart (SIPREC) or IWF-originated bodies req.body may be empty
|
||||
// or not represent the actual offer, which would wrongly send an
|
||||
// SDP-bearing call down the no-offer 3pcc path.
|
||||
const is3pcc = !sdp || sdp.length === 0;
|
||||
this.logger.info(`inbound ${is3pcc ? '3pcc ' : ''}call accepted for routing`);
|
||||
const engine = this.getRtpEngine();
|
||||
if (!engine) {
|
||||
this.logger.info('No available rtpengines, rejecting call!');
|
||||
@@ -95,6 +127,7 @@ class CallSession extends Emitter {
|
||||
offer,
|
||||
answer,
|
||||
del,
|
||||
query,
|
||||
blockMedia,
|
||||
unblockMedia,
|
||||
blockDTMF,
|
||||
@@ -109,6 +142,7 @@ class CallSession extends Emitter {
|
||||
this.offer = offer;
|
||||
this.answer = answer;
|
||||
this.del = del;
|
||||
this.query = query;
|
||||
this.blockMedia = blockMedia;
|
||||
this.unblockMedia = unblockMedia;
|
||||
this.blockDTMF = blockDTMF;
|
||||
@@ -129,11 +163,18 @@ class CallSession extends Emitter {
|
||||
}
|
||||
this.logger.debug(`using feature server ${featureServer}`);
|
||||
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, SdpWantsSrtp(sdp), false, this.isFromMSTeams);
|
||||
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
|
||||
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),
|
||||
query: this.query.bind(null, this.rtpEngineOpts.common),
|
||||
};
|
||||
const obj = parseUri(this.req.uri);
|
||||
let proxy, host, uri;
|
||||
|
||||
const scheme = obj.scheme;
|
||||
|
||||
// replace host part of uri if its an ipv4 address, leave it otherwise
|
||||
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(obj.host)) {
|
||||
debug(`replacing host: was ${obj.host} is ${featureServer}`);
|
||||
@@ -144,17 +185,17 @@ class CallSession extends Emitter {
|
||||
host = obj.host;
|
||||
proxy = `sip:${featureServer}`;
|
||||
}
|
||||
if (obj.user) uri = `${obj.scheme}:${obj.user}@${host}`;
|
||||
else uri = `${obj.scheme}:${host}`;
|
||||
if (obj.user) uri = `sip:${obj.user}@${host}`;
|
||||
else uri = `sip:${host}`;
|
||||
this.logger.info(`uri will be: ${uri}, proxy ${proxy}`);
|
||||
|
||||
try {
|
||||
const sendOfferToRtpEngine = async(remoteSdp) => {
|
||||
const opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...this.rtpEngineOpts.uac.mediaOpts,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
direction: ['public', 'private'],
|
||||
sdp
|
||||
direction: [isPrivateVoipNetwork(this.req.source_address) ? 'private' : 'public', 'private'],
|
||||
sdp: remoteSdp
|
||||
};
|
||||
const startAt = process.hrtime();
|
||||
const response = await this.offer(opts);
|
||||
@@ -167,7 +208,11 @@ class CallSession extends Emitter {
|
||||
this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(response)}`);
|
||||
throw new Error('rtpengine failed: answer');
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await sendOfferToRtpEngine(sdp);
|
||||
let headers = {
|
||||
'From': createBLegFromHeader(this.req),
|
||||
'To': this.req.get('To'),
|
||||
@@ -177,30 +222,52 @@ class CallSession extends Emitter {
|
||||
};
|
||||
if (this.privateSipAddress) headers = {...headers, Contact: `<sip:${this.privateSipAddress}>`};
|
||||
|
||||
const spdOfferB = this.siprec && this.xml ?
|
||||
createSiprecBody(headers, response.sdp, this.xml.type, this.xml.content) :
|
||||
response.sdp;
|
||||
let spdOfferB;
|
||||
if (this.siprec && this.xml) {
|
||||
spdOfferB = createSiprecBody(headers, response.sdp, this.xml.type, this.xml.content);
|
||||
}
|
||||
else if (!is3pcc) {
|
||||
spdOfferB = response.sdp;
|
||||
}
|
||||
|
||||
const responseHeaders = {};
|
||||
if (this.req.locals.carrier) {
|
||||
Object.assign(headers, {
|
||||
'X-Originating-Carrier': this.req.locals.carrier,
|
||||
'X-Voip-Carrier-Sid': this.req.locals.voip_carrier_sid
|
||||
});
|
||||
}
|
||||
if (this.req.locals.msTeamsTenantFqdn) {
|
||||
|
||||
// set Contact header based on scenario, and transport protocol
|
||||
let responseHeaders = {};
|
||||
if (isPrivateVoipNetwork(this.req.source_address)) {
|
||||
this.contactHeader = `<${scheme}:${this.privateSipAddress};transport=${this.req.protocol}>`;
|
||||
responseHeaders = {
|
||||
...responseHeaders,
|
||||
'Contact': this.contactHeader
|
||||
};
|
||||
} else if (this.req.locals.msTeamsTenantFqdn) {
|
||||
Object.assign(headers, {'X-MS-Teams-Tenant-FQDN': this.req.locals.msTeamsTenantFqdn});
|
||||
|
||||
// for Microsoft Teams the Contact header must include the tenant FQDN
|
||||
Object.assign(responseHeaders, {
|
||||
this.contactHeader = `sip:${this.req.locals.msTeamsTenantFqdn}`;
|
||||
responseHeaders = {
|
||||
...responseHeaders,
|
||||
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
|
||||
Contact: `sip:${this.req.locals.msTeamsTenantFqdn}`
|
||||
});
|
||||
Contact: this.contactHeader
|
||||
};
|
||||
}
|
||||
else {
|
||||
const hostport = this.srf.locals.sbcPublicIpAddress[this.req.protocol];
|
||||
this.contactHeader = `<${scheme}:${hostport};transport=${this.req.protocol}>`;
|
||||
responseHeaders = {
|
||||
...responseHeaders,
|
||||
'Contact': this.contactHeader
|
||||
};
|
||||
}
|
||||
if (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) {
|
||||
Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid});
|
||||
}
|
||||
@@ -224,10 +291,14 @@ class CallSession extends Emitter {
|
||||
'-Max-Forwards',
|
||||
'-Record-Route',
|
||||
'-Session-Expires',
|
||||
'-X-Subspace-Forwarded-For'
|
||||
'-X-Application-Sid',
|
||||
'-X-Authenticated-User'
|
||||
],
|
||||
proxyResponseHeaders: ['all', '-X-Trace-ID'],
|
||||
localSdpB: spdOfferB,
|
||||
localSdpB: spdOfferB ? spdOfferB : async(ackBody) => {
|
||||
const response = await sendOfferToRtpEngine(ackBody);
|
||||
return response.sdp;
|
||||
},
|
||||
localSdpA: async(sdp, res) => {
|
||||
this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
|
||||
const opts = {
|
||||
@@ -238,7 +309,12 @@ class CallSession extends Emitter {
|
||||
sdp
|
||||
};
|
||||
const startAt = process.hrtime();
|
||||
const response = await this.answer(opts);
|
||||
const aOpts = {
|
||||
...opts,
|
||||
...(is3pcc && {direction: ['private', 'public']})
|
||||
};
|
||||
const response = await this.answer(aOpts);
|
||||
this.logger.debug({response, opts: aOpts}, 'response from rtpengine to answer');
|
||||
const rtt = roundTripTime(startAt);
|
||||
this.stats.histogram('app.rtpengine.response_time', rtt, [
|
||||
'direction:inbound', 'command:answer', `rtpengine:${this.rtpengineIp}`]);
|
||||
@@ -246,10 +322,30 @@ class CallSession extends Emitter {
|
||||
this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`);
|
||||
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;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// passing gateway outbound auth that sbc-inbound can send RE-INVITE with authentication process on UAS side
|
||||
const {gateway} = this.req.locals;
|
||||
if (gateway && gateway.register_username && gateway.register_password) {
|
||||
this.logger.debug('passing outbound gateway auth to CallSession for reinvite processing');
|
||||
uas.auth = {
|
||||
username: gateway.register_username,
|
||||
password: gateway.register_password
|
||||
};
|
||||
}
|
||||
// successfully connected
|
||||
this.logger.info('call connected successfully to feature server');
|
||||
debug('call connected successfully to feature server');
|
||||
@@ -258,7 +354,6 @@ class CallSession extends Emitter {
|
||||
} catch (err) {
|
||||
this.rtpEngineResource.destroy().catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure'));
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size);
|
||||
if (err instanceof SipError) {
|
||||
const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`];
|
||||
this.stats.increment('sbc.terminations', tags);
|
||||
@@ -266,7 +361,16 @@ class CallSession extends Emitter {
|
||||
this.emit('failed');
|
||||
}
|
||||
else if (err.message !== 'call canceled') {
|
||||
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
|
||||
this.logger.error(err, 'unexpected error routing inbound call');
|
||||
nudgeCallCounts(this.req, 'failure', {
|
||||
service_provider_sid: this.service_provider_sid,
|
||||
account_sid: this.account_sid,
|
||||
application_sid: this.application_sid,
|
||||
callId: this.req.locals.callId
|
||||
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
|
||||
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
|
||||
|
||||
}
|
||||
this.srf.endSession(this.req);
|
||||
}
|
||||
@@ -275,8 +379,7 @@ class CallSession extends Emitter {
|
||||
_setDlgHandlers(dlg) {
|
||||
const {callId} = dlg.sip;
|
||||
this.activeCallIds.set(callId, this);
|
||||
this.subscribeDTMF(this.logger, callId, this.rtpEngineOpts.uas.tag,
|
||||
this._onDTMF.bind(this));
|
||||
this.subscribeForDTMF(this.uac);
|
||||
dlg.on('destroy', () => {
|
||||
debug('call ended with normal termination');
|
||||
this.logger.info('call ended with normal termination');
|
||||
@@ -284,10 +387,7 @@ class CallSession extends Emitter {
|
||||
this.activeCallIds.delete(callId);
|
||||
if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {});
|
||||
|
||||
if (this.srsClient) {
|
||||
this.srsClient.stop();
|
||||
this.srsClient = null;
|
||||
}
|
||||
this._stopRecording();
|
||||
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
@@ -296,14 +396,37 @@ class CallSession extends Emitter {
|
||||
dlg.on('modify', this._onReinvite.bind(this, dlg));
|
||||
}
|
||||
|
||||
_startRecordingNoAnswerTimer(res) {
|
||||
this._clearRecordingNoAnswerTimer();
|
||||
this.recordingNoAnswerTimer = setTimeout(() => {
|
||||
this.logger.info('No response from SipRec server, return error to feature server');
|
||||
this.isRecordingNoAnswerResponded = true;
|
||||
res.send(400);
|
||||
}, this.recordingNoAnswerTimeout);
|
||||
}
|
||||
|
||||
_clearRecordingNoAnswerTimer() {
|
||||
if (this.recordingNoAnswerTimer) {
|
||||
clearTimeout(this.recordingNoAnswerTimer);
|
||||
this.recordingNoAnswerTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_stopRecording() {
|
||||
if (this.srsClients.length) {
|
||||
this.srsClients.forEach((c) => c.stop());
|
||||
this.srsClients = [];
|
||||
}
|
||||
}
|
||||
|
||||
_setHandlers({uas, uac}) {
|
||||
this.emit('connected');
|
||||
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}`];
|
||||
this.stats.increment('sbc.terminations', tags);
|
||||
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) {
|
||||
this.req.locals.cdr = {
|
||||
...this.req.locals.cdr,
|
||||
@@ -314,17 +437,38 @@ class CallSession extends Emitter {
|
||||
trace_id: uac.res?.get('X-Trace-ID') || '00000000000000000000000000000000'
|
||||
};
|
||||
}
|
||||
|
||||
/* save far end SDP for later use if we do a full media release */
|
||||
if (process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE) {
|
||||
const key = makeFullMediaReleaseKey(this.req.get('Call-ID'));
|
||||
const sdp = this.req.body;
|
||||
this.logger.info({key, sdp}, 'saving far end sdp for full media release feature');
|
||||
this.addKey(key, sdp, 3600).catch((err) => this.logger.error(err, 'Error saving far end sdp'));
|
||||
}
|
||||
this.uas = uas;
|
||||
this.uac = uac;
|
||||
[uas, uac].forEach((dlg) => {
|
||||
dlg.on('destroy', async() => {
|
||||
dlg.on('destroy', async(bye) => {
|
||||
const other = dlg.other;
|
||||
this.rtpEngineResource.destroy().catch((err) => {});
|
||||
/* DH: need a better understanding of why query before delete is a good idea
|
||||
this.rtpEngineResource.query()
|
||||
.then((results) => {
|
||||
this.logger.info({results}, 'rtpengine query results');
|
||||
return this.rtpEngineResource.destroy();
|
||||
})
|
||||
.catch((err) => {});
|
||||
*/
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
try {
|
||||
await other.destroy();
|
||||
} catch (err) {}
|
||||
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag);
|
||||
const headers = {};
|
||||
Object.keys(bye.headers).forEach((h) => {
|
||||
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
|
||||
});
|
||||
await other.destroy({headers});
|
||||
} catch {}
|
||||
this.unsubscribeForDTMF();
|
||||
|
||||
|
||||
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
|
||||
process.env.JAMBONES_TRACK_SP_CALLS ||
|
||||
@@ -332,12 +476,17 @@ class CallSession extends Emitter {
|
||||
|
||||
if (process.env.JAMBONES_HOSTING || trackingOn) {
|
||||
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
|
||||
await nudgeCallCounts(this.logger, {
|
||||
service_provider_sid: this.service_provider_sid,
|
||||
account_sid: this.account_sid,
|
||||
application_sid: this.application_sid
|
||||
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
|
||||
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
|
||||
if (!this.req.locals.callCountNudged) {
|
||||
this.req.locals.callCountNudged = true;
|
||||
this.logger.debug('decrementing call count at end of call');
|
||||
await nudgeCallCounts(this.req, 'complete', {
|
||||
service_provider_sid: this.service_provider_sid,
|
||||
account_sid: this.account_sid,
|
||||
application_sid: this.application_sid,
|
||||
callId: this.req.locals.callId
|
||||
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
|
||||
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
|
||||
}
|
||||
}
|
||||
|
||||
/* write cdr for connected call */
|
||||
@@ -346,12 +495,21 @@ class CallSession extends Emitter {
|
||||
const trunk = ['trunk', 'teams'].includes(this.req.locals.originator) ?
|
||||
this.req.locals.carrier :
|
||||
this.req.locals.originator;
|
||||
const application = await this.srf.locals.getApplicationBySid(application_sid);
|
||||
const {hasRecording} = await this.retrieveHash(`call:${this.account_sid}:${call_sid}`) ?? {};
|
||||
const isRecording = this.req.locals.account.record_all_calls ||
|
||||
(application && application.record_all_calls) || hasRecording;
|
||||
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,
|
||||
terminated_at: now,
|
||||
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
|
||||
sip_status: 200,
|
||||
duration: Math.floor((now - callStart) / 1000),
|
||||
trunk
|
||||
trunk,
|
||||
...(isRecording && {recording_url})
|
||||
};
|
||||
this.logger.info({cdr}, 'going to write a cdr now..');
|
||||
this.writeCdrs({...this.req.locals.cdr,
|
||||
@@ -359,7 +517,8 @@ class CallSession extends Emitter {
|
||||
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
|
||||
sip_status: 200,
|
||||
duration: Math.floor((now - callStart) / 1000),
|
||||
trunk
|
||||
trunk,
|
||||
...(isRecording && {recording_url})
|
||||
})
|
||||
.then(() => this.logger.debug('successfully wrote cdr'))
|
||||
.catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
|
||||
@@ -370,18 +529,14 @@ class CallSession extends Emitter {
|
||||
dlg.other = null;
|
||||
other.other = null;
|
||||
|
||||
if (this.srsClient) {
|
||||
this.srsClient.stop();
|
||||
this.srsClient = null;
|
||||
}
|
||||
this._stopRecording();
|
||||
|
||||
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
});
|
||||
|
||||
this.subscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uas.tag,
|
||||
this._onDTMF.bind(this, uac));
|
||||
this.subscribeForDTMF(uac);
|
||||
|
||||
uas.on('modify', this._onReinvite.bind(this, uas));
|
||||
uac.on('modify', this._onReinvite.bind(this, uac));
|
||||
@@ -394,6 +549,12 @@ class CallSession extends Emitter {
|
||||
|
||||
// default forwarding of other request types
|
||||
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) {
|
||||
@@ -475,7 +636,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, {
|
||||
localSdp: response.sdp,
|
||||
@@ -496,23 +657,100 @@ Duration=${payload.duration} `
|
||||
}
|
||||
|
||||
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 isFullMediaRelease = reason === 'release-media-entirely' && process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE;
|
||||
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 {
|
||||
/* check for re-invite with no SDP -- seen that from BT when they provide UUI info */
|
||||
if (!req.body) {
|
||||
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
|
||||
res.send(200, {body: dlg.local.sdp});
|
||||
if (dlg.type === 'uas' && this._recvonly) {
|
||||
/* 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, {
|
||||
headers: {
|
||||
'Contact': this.contactHeader
|
||||
},
|
||||
body: dlg.local.sdp
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFullMediaRelease) {
|
||||
const b_sdp = await this.retrieveKey(makePartnerFullMediaReleaseKey(this.req.get('Call-ID')));
|
||||
this.logger.info({b_sdp}, 'reinvite ourselves out of the media path with this reinvite offer');
|
||||
const answerSdp = await dlg.other.modify(b_sdp);
|
||||
this.logger.info({answerSdp}, 'far end response to full media release');
|
||||
res.send(200, {
|
||||
body: dlg.local.sdp,
|
||||
headers: {
|
||||
'Contact': this.contactHeader
|
||||
}
|
||||
});
|
||||
/* no media going through us now we can destroy the rtpengine resource */
|
||||
this.rtpEngineResource.destroy().catch((err) => {
|
||||
this.logger.info({err}, 'Error destroying rtpengine resource after full media release');
|
||||
});
|
||||
this._mediaPath = MediaPath.NoMedia;
|
||||
return;
|
||||
}
|
||||
|
||||
const offeredSdp = Array.isArray(req.payload) && req.payload.length > 1 ?
|
||||
req.payload.find((p) => p.type === 'application/sdp').content :
|
||||
req.body;
|
||||
|
||||
const reason = req.get('X-Reason');
|
||||
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
|
||||
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
|
||||
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
|
||||
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
|
||||
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
|
||||
if (isReleasingMedia) {
|
||||
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
|
||||
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
|
||||
}
|
||||
let opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...offerMedia,
|
||||
@@ -521,7 +759,8 @@ Duration=${payload.duration} `
|
||||
direction,
|
||||
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);
|
||||
if ('ok' !== response.result) {
|
||||
@@ -531,12 +770,16 @@ Duration=${payload.duration} `
|
||||
|
||||
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
|
||||
let sdp;
|
||||
if (reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason) &&
|
||||
!this.callerIsUsingSrtp) {
|
||||
this.logger.info({response}, `got a reinvite from FS to ${reason}`);
|
||||
//HL 2024-11-13: previously forwarded re-invites to webrtc clients but further testing has shown to be unnecessary
|
||||
//if (isReleasingMedia && !this.callerIsUsingSrtp) {
|
||||
|
||||
//DH 2024-11- 18: if we are going from no-media to either partial or full media, we need reinvite the far end
|
||||
if (isReleasingMedia && this._mediaPath !== MediaPath.NoMedia) {
|
||||
sdp = dlg.other.remote.sdp;
|
||||
answerMedia.flags = ['asymmetric', 'port latching'];
|
||||
this._mediaReleased = 'release-media' === reason;
|
||||
if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
|
||||
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
|
||||
this._mediaPath = 'release-media' === reason ? MediaPath.PartialMedia : MediaPath.FullMedia;
|
||||
this.logger.debug(`media path is now ${this._mediaPath}`);
|
||||
}
|
||||
else {
|
||||
sdp = await dlg.other.modify(response.sdp);
|
||||
@@ -553,8 +796,14 @@ Duration=${payload.duration} `
|
||||
res.send(488);
|
||||
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
|
||||
}
|
||||
res.send(200, {body: response.sdp});
|
||||
res.send(200, {
|
||||
headers: {
|
||||
'Contact': this.contactHeader
|
||||
},
|
||||
body: response.sdp
|
||||
});
|
||||
} catch (err) {
|
||||
res.send(err.status || 500);
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
}
|
||||
@@ -585,6 +834,7 @@ Duration=${payload.duration} `
|
||||
}
|
||||
else if (reason.includes('CallRecording')) {
|
||||
let succeeded = false;
|
||||
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
|
||||
if (reason === 'startCallRecording') {
|
||||
const from = this.req.getParsedHeader('From');
|
||||
const to = this.req.getParsedHeader('To');
|
||||
@@ -597,7 +847,7 @@ Duration=${payload.duration} `
|
||||
const callSid = req.get('X-Call-Sid');
|
||||
const accountSid = req.get('X-Account-Sid');
|
||||
const applicationSid = req.get('X-Application-Sid');
|
||||
if (this.srsClient) {
|
||||
if (this.srsClients.length) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding duplicate startCallRecording request for a call');
|
||||
return;
|
||||
@@ -607,13 +857,14 @@ Duration=${payload.duration} `
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
this.srsClient = new SrsClient(this.logger, {
|
||||
const arr = srsUrl.split(',');
|
||||
this.srsClients = arr.map((url) => new SrsClient(this.logger, {
|
||||
srf: dlg.srf,
|
||||
direction: 'inbound',
|
||||
originalInvite: this.req,
|
||||
callingNumber: this.req.callingNumber,
|
||||
calledNumber: this.req.calledNumber,
|
||||
srsUrl,
|
||||
srsUrl: url,
|
||||
srsRecordingId,
|
||||
callSid,
|
||||
accountSid,
|
||||
@@ -628,44 +879,104 @@ Duration=${payload.duration} `
|
||||
del: this.del,
|
||||
blockMedia: this.blockMedia,
|
||||
unblockMedia: this.unblockMedia,
|
||||
unsubscribe: this.unsubscribe
|
||||
});
|
||||
unsubscribe: this.unsubscribe,
|
||||
headers,
|
||||
isSipRecCall: Array.isArray(this.req.payload) && this.req.payload.length > 1
|
||||
}));
|
||||
try {
|
||||
succeeded = await this.srsClient.start();
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => c.start()));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error starting SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
else if (reason === 'stopCallRecording') {
|
||||
if (!this.srsClient) {
|
||||
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated)) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding stopCallRecording request because we are not recording');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
succeeded = await this.srsClient.stop();
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => {
|
||||
if (c.activated) {
|
||||
c.stop();
|
||||
}
|
||||
}));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error stopping SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
this.srsClient = null;
|
||||
this.srsClients = [];
|
||||
}
|
||||
else if (reason === 'pauseCallRecording') {
|
||||
if (!this.srsClient || this.srsClient.paused) {
|
||||
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && !c.paused)) {
|
||||
this.logger.info('discarding invalid pauseCallRecording request');
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.pause();
|
||||
try {
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => {
|
||||
if (c.activated && !c.paused) {
|
||||
c.pause({headers});
|
||||
}
|
||||
}));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error pausing SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
else if (reason === 'resumeCallRecording') {
|
||||
if (!this.srsClient || !this.srsClient.paused) {
|
||||
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && c.paused)) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding invalid resumeCallRecording request');
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.resume();
|
||||
try {
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => {
|
||||
if (c.activated && c.paused) {
|
||||
c.resume({headers});
|
||||
}
|
||||
}));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error resuming SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
res.send(succeeded ? 200 : 503);
|
||||
if (!this.isRecordingNoAnswerResponded) {
|
||||
this._clearRecordingNoAnswerTimer();
|
||||
res.send(succeeded ? 200 : 503);
|
||||
}
|
||||
} else if (reason.includes('Dtmf')) {
|
||||
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
|
||||
if (!arr) {
|
||||
this.logger.info({body: req.body}, '_onInfo: invalid INFO Dtmf');
|
||||
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
|
||||
}
|
||||
const code = arr[1];
|
||||
const arr2 = /Duration=\s*(\d+)/.exec(req.body);
|
||||
const duration = arr2 ? arr2[1] : 250;
|
||||
const volume = 13;
|
||||
const dtmfOpts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
'from-tag': this.rtpEngineOpts.uac.tag,
|
||||
code,
|
||||
duration,
|
||||
volume
|
||||
};
|
||||
const response = await this.playDTMF(dtmfOpts);
|
||||
if ('ok' !== response.result) {
|
||||
this.logger.info({response}, `rtpengine play Dtmf failed with ${JSON.stringify(response)}`);
|
||||
throw new Error('rtpengine failed: answer');
|
||||
}
|
||||
res.send(200);
|
||||
}
|
||||
}
|
||||
else if (dlg.type === 'uas' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
|
||||
@@ -703,10 +1014,9 @@ Duration=${payload.duration} `
|
||||
}
|
||||
}
|
||||
else {
|
||||
const immutableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
|
||||
const headers = {};
|
||||
Object.keys(req.headers).forEach((h) => {
|
||||
if (!immutableHdrs.includes(h)) headers[h] = req.headers[h];
|
||||
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = req.headers[h];
|
||||
});
|
||||
const response = await dlg.other.request({method: 'INFO', headers, body: req.body});
|
||||
const responseHeaders = {};
|
||||
@@ -716,8 +1026,8 @@ Duration=${payload.duration} `
|
||||
res.send(response.status, {headers: responseHeaders, body: response.body});
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.srsClient) {
|
||||
this.srsClient = null;
|
||||
if (this.srsClients.length) {
|
||||
this.srsClients = [];
|
||||
}
|
||||
res.send(500);
|
||||
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
|
||||
@@ -733,41 +1043,48 @@ Duration=${payload.duration} `
|
||||
if (!arr) {
|
||||
/* call transfer requested */
|
||||
const {gateway} = this.req.locals;
|
||||
const referredBy = req.getParsedHeader('Referred-By');
|
||||
if (!referredBy) return res.send(400);
|
||||
const u = parseUri(referredBy.uri);
|
||||
|
||||
let selectedGateway = false;
|
||||
let e164 = false;
|
||||
if (gateway) {
|
||||
/* host of Refer-to to an outbound gateway */
|
||||
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 (!req.has('Referred-By')) return res.send(400);
|
||||
const leaveReferToAlone = req.has('X-Refer-To-Leave-Untouched');
|
||||
if (leaveReferToAlone) {
|
||||
this.logger.debug({referTo}, 'passing Refer-To header through untouched');
|
||||
}
|
||||
else {
|
||||
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
|
||||
let selectedGateway = false;
|
||||
let e164 = false;
|
||||
if (gateway && isDotDecimal) {
|
||||
/* host of Refer-to to an outbound gateway */
|
||||
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
|
||||
const {via, from, to, 'call-id':callid, cseq, 'max-forwards':maxforwards,
|
||||
// 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, contact: _contact,
|
||||
...customHeaders
|
||||
} = req.headers;
|
||||
|
||||
const response = await this.uas.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
'Refer-To': stringifyUri(uri),
|
||||
'Referred-By': stringifyUri(u),
|
||||
'Refer-To': `<${stringifyUri(uri)}>`,
|
||||
'Referred-By': referredby,
|
||||
'Contact': this.contactHeader,
|
||||
...customHeaders
|
||||
}
|
||||
});
|
||||
@@ -776,36 +1093,41 @@ Duration=${payload.duration} `
|
||||
res.send(202);
|
||||
|
||||
// invite to new fs
|
||||
const headers = {};
|
||||
if (req.has('X-Retain-Call-Sid')) {
|
||||
Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')});
|
||||
}
|
||||
const headers = {
|
||||
...(req.has('X-Retain-Call-Sid') && {'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});
|
||||
this.uac = uac;
|
||||
uac.other = this.uas;
|
||||
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('destroy', () => {
|
||||
this.logger.info('call ended with normal termination');
|
||||
this.rtpEngineResource.destroy();
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
uac.other.destroy();
|
||||
this._stopRecording();
|
||||
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 = Object.assign({sdp: uac.remote.sdp, 'to-tag': res.getParsedHeader('To').params.tag},
|
||||
this.rtpEngineOpts.answer);
|
||||
const opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
'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);
|
||||
if ('ok' !== response.result) {
|
||||
res.send(488);
|
||||
throw new Error(`_onFeatureServerReinvite: rtpengine failed: ${JSON.stringify(response)}`);
|
||||
throw new Error(`_onFeatureServerTransfer: rtpengine answer failed: ${JSON.stringify(response)}`);
|
||||
}
|
||||
dlg.destroy().catch(() => {});
|
||||
this.logger.info('successfully moved call to new feature server');
|
||||
} catch (err) {
|
||||
res.send(488);
|
||||
this.logger.error(err, 'Error handling refer from feature server');
|
||||
}
|
||||
}
|
||||
@@ -863,7 +1185,7 @@ Duration=${payload.duration} `
|
||||
|
||||
// successfully connected
|
||||
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;
|
||||
sendNotify(this.uas, '200 OK');
|
||||
this.uas.destroy();
|
||||
|
||||
@@ -3,5 +3,10 @@
|
||||
"ScaleIn": "scale-in",
|
||||
"StandbyEnter": "standby-enter",
|
||||
"StandbyExit": "standby-exit"
|
||||
},
|
||||
"MediaPath": {
|
||||
"NoMedia": "no-media",
|
||||
"PartialMedia": "partial-media",
|
||||
"FullMedia": "full-media"
|
||||
}
|
||||
}
|
||||
|
||||
+546
-87
@@ -7,39 +7,55 @@ const sqlSelectSPForAccount = 'SELECT service_provider_sid FROM accounts WHERE a
|
||||
|
||||
const sqlSelectAllCarriersForAccountByRealm =
|
||||
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
|
||||
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
|
||||
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
|
||||
vc.register_username, vc.register_password
|
||||
FROM sip_gateways sg, voip_carriers vc, accounts acc
|
||||
WHERE acc.sip_realm = ?
|
||||
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`;
|
||||
|
||||
const sqlSelectAllCarriersForSPByRealm =
|
||||
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
|
||||
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
|
||||
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
|
||||
vc.register_username, vc.register_password
|
||||
FROM sip_gateways sg, voip_carriers vc, accounts acc
|
||||
WHERE acc.sip_realm = ?
|
||||
AND vc.service_provider_sid = acc.service_provider_sid
|
||||
AND vc.account_sid IS NULL
|
||||
AND vc.is_active = 1
|
||||
AND sg.inbound = 1
|
||||
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
|
||||
|
||||
const sqlSelectAllGatewaysForSP =
|
||||
`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
|
||||
const sqlSelectExactGatewayForSP =
|
||||
`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, sg.pad_crypto,
|
||||
vc.register_username, vc.register_password
|
||||
FROM sip_gateways sg, voip_carriers vc
|
||||
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
|
||||
AND vc.service_provider_sid IS NOT NULL
|
||||
AND vc.is_active = 1`;
|
||||
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
|
||||
AND vc.service_provider_sid IS NOT NULL
|
||||
AND vc.is_active = 1
|
||||
AND sg.inbound = 1
|
||||
AND sg.netmask = 32
|
||||
AND sg.ipv4 = ?
|
||||
ORDER BY vc.account_sid IS NOT NULL DESC`;
|
||||
|
||||
const sqlCarriersForAccountBySid =
|
||||
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
|
||||
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
|
||||
FROM sip_gateways sg, voip_carriers vc, accounts acc
|
||||
WHERE acc.account_sid = ?
|
||||
AND vc.account_sid = acc.account_sid
|
||||
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
|
||||
const sqlSelectCIDRGatewaysForSP =
|
||||
`SELECT STRAIGHT_JOIN 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, sg.pad_crypto,
|
||||
vc.register_username, vc.register_password
|
||||
FROM sip_gateways sg, voip_carriers vc
|
||||
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
|
||||
AND vc.service_provider_sid IS NOT NULL
|
||||
AND vc.is_active = 1
|
||||
AND sg.inbound = 1
|
||||
AND sg.netmask < 32
|
||||
ORDER BY sg.netmask DESC`;
|
||||
|
||||
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 sqlApplicationBySid = 'SELECT * from applications WHERE application_sid = ?';
|
||||
|
||||
const sqlQueryApplicationByDid = `
|
||||
SELECT * FROM phone_numbers
|
||||
@@ -57,19 +73,60 @@ WHERE sg.voip_carrier_sid = ?
|
||||
AND sg.voip_carrier_sid = vc.voip_carrier_sid
|
||||
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, sg.pad_crypto,
|
||||
vc.register_username, vc.register_password
|
||||
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 sqlSelectAuthCarriersForAccountAndSP = `
|
||||
SELECT * FROM voip_carriers
|
||||
WHERE trunk_type = 'auth'
|
||||
AND is_active = 1
|
||||
AND (
|
||||
(account_sid = ?)
|
||||
OR
|
||||
(service_provider_sid = ? AND account_sid IS NULL)
|
||||
)`;
|
||||
|
||||
const sqlSelectGatewaysByVoipCarrierSids = `
|
||||
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, sg.pad_crypto,
|
||||
vc.register_username, vc.register_password
|
||||
FROM sip_gateways sg, voip_carriers vc
|
||||
WHERE sg.voip_carrier_sid IN (?)
|
||||
AND sg.voip_carrier_sid = vc.voip_carrier_sid
|
||||
AND vc.is_active = 1`;
|
||||
|
||||
const gatewayMatchesSourceAddress = (logger, source_address, gw) => {
|
||||
if (32 === gw.netmask && gw.ipv4 === source_address) return true;
|
||||
if (gw.netmask < 32) {
|
||||
const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]);
|
||||
return matcher.contains(source_address);
|
||||
try {
|
||||
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;
|
||||
};
|
||||
|
||||
module.exports = (srf, logger) => {
|
||||
const {pool} = srf.locals.dbHelpers;
|
||||
const {queryEphemeralGateways} = srf.locals.realtimeDbHelpers;
|
||||
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 [r] = await pp.query(sqlSelectSPForAccount, [account_sid]);
|
||||
if (0 === r.length) return null;
|
||||
@@ -83,14 +140,30 @@ module.exports = (srf, logger) => {
|
||||
|
||||
/* if multiple, prefer a DNS name */
|
||||
const hasDns = r.find((row) => row.ipv4.match(/^[A-Za-z]/));
|
||||
return hasDns || r[0];
|
||||
return hasDns /* || r[0] */;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getOutboundGatewayForRefer');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the application for a given DID (phone number) and carrier combination.
|
||||
* First attempts an exact DID match, then falls back to pattern/wildcard matching
|
||||
* if no exact match is found. Pattern matching is done in order of pattern length
|
||||
* (longest/most specific first) and handles both regex and wildcard (*) patterns.
|
||||
*
|
||||
* @param {Object} req - The request object containing the called number
|
||||
* @param {string} voip_carrier_sid - The SID of the VoIP carrier
|
||||
* @returns {Promise<(string|Object|null)>} Returns either:
|
||||
* - application_sid (string) if appObject is false
|
||||
* - full application object if appObject is true
|
||||
* - null if no matching application is found
|
||||
* @throws {Error} Database errors or other unexpected errors
|
||||
*/
|
||||
// Refactored to handle invalid regular expressions gracefully as part of
|
||||
// https://github.com/jambonz/sbc-inbound/issues/201
|
||||
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
|
||||
const did = normalizeDID(req.calledNumber);
|
||||
const did = normalizeDID(req.calledNumber) || 'anonymous';
|
||||
|
||||
try {
|
||||
/* straight DID match */
|
||||
@@ -99,110 +172,496 @@ module.exports = (srf, logger) => {
|
||||
|
||||
/* wildcard / regex match */
|
||||
const [r2] = await pp.query(sqlQueryAllDidsForCarrier, [voip_carrier_sid]);
|
||||
const match = r2
|
||||
const patterns = r2
|
||||
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
|
||||
.sort((a, b) => b.number.length - a.number.length) // prefer longest match
|
||||
.find((o) => did.match(new RegExp(o.number.endsWith('*') ? `${o.number.slice(0, -1)}\\d*` : o.number)));
|
||||
if (match) return match.application_sid;
|
||||
.sort((a, b) => b.number.length - a.number.length);
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
const regexPattern = pattern.number.endsWith('*') ?
|
||||
`${pattern.number.slice(0, -1)}\\d*` :
|
||||
pattern.number;
|
||||
if (did.match(new RegExp(regexPattern))) {
|
||||
logger.debug({voip_carrier_sid, 'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
|
||||
return pattern.application_sid;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({err, voip_carrier_sid, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
logger.debug({voip_carrier_sid, did}, 'No matching pattern found');
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getApplicationForDidAndCarrier');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a matching application across multiple carriers using pattern/wildcard matching.
|
||||
* This function is similar to getApplicationForDidAndCarrier but operates on multiple carriers
|
||||
* simultaneously for better performance. It searches through all patterns from the specified
|
||||
* carriers, ordered by pattern length (longest/most specific first).
|
||||
*
|
||||
* @param {Object} req - The request object containing the called number
|
||||
* @param {string} voip_carrier_sids - Comma-separated string of carrier SIDs, formatted for SQL IN clause
|
||||
* (e.g., "'carrier1','carrier2'")
|
||||
* @returns {Promise<Object|null>} Returns either:
|
||||
* - The matching phone_numbers record containing application_sid, voip_carrier_sid, and account_sid
|
||||
* - null if no matching pattern is found
|
||||
* @throws {Error} Database errors or other unexpected errors
|
||||
*/
|
||||
const getApplicationForDidAndCarriers = async(req, voip_carrier_sids) => {
|
||||
const did = normalizeDID(req.calledNumber) || 'anonymous';
|
||||
const sql = `SELECT * FROM phone_numbers WHERE voip_carrier_sid IN (${voip_carrier_sids})`;
|
||||
|
||||
try {
|
||||
/* wildcard / regex match */
|
||||
const [r2] = await pp.query(sql);
|
||||
const patterns = r2
|
||||
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
|
||||
.sort((a, b) => b.number.length - a.number.length);
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
const regexPattern = pattern.number.endsWith('*') ?
|
||||
`${pattern.number.slice(0, -1)}\\d*` :
|
||||
pattern.number;
|
||||
if (did.match(new RegExp(regexPattern))) {
|
||||
logger.debug({'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
|
||||
return pattern;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({err, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
logger.debug('No matching pattern found');
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getApplicationForDidAndCarriers');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Queries ephemeral gateways in Redis for the given source IP address.
|
||||
* Returns an array of active voip_carrier_sid values.
|
||||
*
|
||||
* @param {string} source_address - The source IP address to query
|
||||
* @returns {Promise<Array<string>>} Array of voip_carrier_sid values, or empty array on error
|
||||
*/
|
||||
const lookupEphemeralGatewayCarriers = async(source_address) => {
|
||||
try {
|
||||
logger.debug({source_address}, 'querying ephemeral gateways');
|
||||
const carriers = await queryEphemeralGateways(source_address);
|
||||
if (carriers.length > 0) {
|
||||
logger.info({source_address, count: carriers.length, carriers},
|
||||
'found ephemeral gateway carriers');
|
||||
}
|
||||
return carriers;
|
||||
} catch (err) {
|
||||
logger.error({err, source_address}, 'Error querying ephemeral gateways');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves full gateway details (with carrier info) for the given voip_carrier_sid array.
|
||||
* Returns gateway records (joined with carrier data) matching the structure of other gateway queries.
|
||||
*
|
||||
* @param {Array<string>} voipCarrierSids - Array of voip_carrier_sid values
|
||||
* @returns {Promise<Array<Object>>} Array of gateway objects (with carrier fields included)
|
||||
*/
|
||||
const lookupGatewaysByCarrierSids = async(voipCarrierSids) => {
|
||||
if (!voipCarrierSids || voipCarrierSids.length === 0) return [];
|
||||
|
||||
try {
|
||||
const [r] = await pp.query(sqlSelectGatewaysByVoipCarrierSids, [voipCarrierSids]);
|
||||
logger.debug({count: r.length, voipCarrierSids}, 'retrieved gateway details for ephemeral gateways');
|
||||
return r;
|
||||
} catch (err) {
|
||||
logger.error({err, voipCarrierSids}, 'Error retrieving gateway details by carrier IDs');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const wasOriginatedFromCarrier = async(req) => {
|
||||
const failure = {fromCarrier: false};
|
||||
const uri = parseUri(req.uri);
|
||||
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
|
||||
const did = normalizeDID(req.calledNumber) || 'anonymous';
|
||||
if (!isDotDecimal) {
|
||||
/**
|
||||
* 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...
|
||||
*/
|
||||
|
||||
if (isDotDecimal) {
|
||||
if (process.env.JAMBONES_HOSTING) {
|
||||
if (!process.env.SBC_ACCOUNT_SID) return failure;
|
||||
/* does anyone own this sip realm? */
|
||||
const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
|
||||
if (a.length) {
|
||||
assert(a.length === 1);
|
||||
|
||||
/* 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 {
|
||||
/* 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');
|
||||
/* yes they do */
|
||||
logger.debug(`sip realm is associated with account_sid: ${a[0].account_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] = await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
|
||||
const gw = gwAcc
|
||||
.concat(gwSP)
|
||||
.sort((a, b) => b.netmask - a.netmask);
|
||||
const gateways = gw.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
|
||||
let voip_carriers = gateways.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,
|
||||
register_username: gw.register_username,
|
||||
register_password: gw.register_password
|
||||
};
|
||||
});
|
||||
/* remove duplicates, winnow down to voip_carriers, not gateways */
|
||||
if (voip_carriers.length > 1) {
|
||||
voip_carriers = [...new Set(voip_carriers.map(JSON.stringify))].map(JSON.parse);
|
||||
}
|
||||
|
||||
/* if no static gateway matches, check ephemeral gateways in Redis */
|
||||
if (voip_carriers.length === 0 && gateways.length === 0) {
|
||||
const ephemeralCarrierSids = await lookupEphemeralGatewayCarriers(req.source_address);
|
||||
if (ephemeralCarrierSids.length > 0) {
|
||||
const ephemeralGateways = await lookupGatewaysByCarrierSids(ephemeralCarrierSids);
|
||||
gateways.push(...ephemeralGateways);
|
||||
voip_carriers = ephemeralGateways.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,
|
||||
register_username: gw.register_username,
|
||||
register_password: gw.register_password
|
||||
};
|
||||
});
|
||||
/* remove duplicates */
|
||||
if (voip_carriers.length > 1) {
|
||||
voip_carriers = [...new Set(voip_carriers.map(JSON.stringify))].map(JSON.parse);
|
||||
}
|
||||
logger.info({source_address: req.source_address, count: voip_carriers.length},
|
||||
'matched call to ephemeral gateway(s) from registration trunk');
|
||||
}
|
||||
}
|
||||
|
||||
if (voip_carriers.length) {
|
||||
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
|
||||
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const sql =
|
||||
`SELECT * FROM phone_numbers WHERE number = '${did}'
|
||||
AND voip_carrier_sid IN (${vc_sids})
|
||||
AND account_sid = '${a[0].account_sid}'`;
|
||||
//logger.debug({voip_carriers, sql, did}, 'looking up DID');
|
||||
|
||||
const [r] = await pp.query(sql);
|
||||
if (0 === r.length) {
|
||||
/* came from a carrier, but number is not provisioned..
|
||||
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 = ?',
|
||||
matches[0].service_provider_sid);
|
||||
if (r[0].count === 0) return {fromCarrier: true};
|
||||
else {
|
||||
const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?',
|
||||
matches[0].service_provider_sid);
|
||||
if (r.length === 0) {
|
||||
// if no matching phone number is found, find whether there are any applications having phone numbers
|
||||
// matching the regex pattern or wild card
|
||||
logger.debug({isDotDecimal},
|
||||
'Did not find a matching phone number, checking for applications with Regex or wildcard');
|
||||
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const application = await getApplicationForDidAndCarriers(req, vc_sids);
|
||||
if (application) {
|
||||
logger.debug({application}, 'sip_realm looking up DID: found application with Regex or wildcard ');
|
||||
const gateway = gateways.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
|
||||
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
|
||||
gateway.account_sid = gateway.account_sid ? gateway.account_sid : application.account_sid;
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: matches[0],
|
||||
service_provider_sid: accounts[0].service_provider_sid,
|
||||
account_sid: accounts[0].account_sid,
|
||||
account: accounts[0]
|
||||
gateway: gateway,
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
account_sid: gateway.account_sid,
|
||||
application_sid: application.application_sid,
|
||||
account: a[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid);
|
||||
const [accounts] = await pp.query(sqlAccountBySid, r[0].account_sid);
|
||||
assert(accounts.length);
|
||||
if (r.length > 1) {
|
||||
const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid}));
|
||||
logger.info({
|
||||
number: r[0].number,
|
||||
total: all.length,
|
||||
matches: all.slice(0, 5)
|
||||
},
|
||||
'multiple carriers with the same gateway have the same number provisioned for the same account'
|
||||
+ ' -- cannot determine which one to use');
|
||||
return {
|
||||
fromCarrier: true,
|
||||
error: 'Multiple carriers with the same gateway are attempting to route the same number for this account'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* We have one or no routes for this phone number, carrier and account combination
|
||||
* Either take the matching gateway, or if no route has matched, the first gateway of the ones available.
|
||||
*/
|
||||
const gateway = r[0] ? gateways.find((m) => m.voip_carrier_sid === r[0]?.voip_carrier_sid) : gateways[0];
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway,
|
||||
service_provider_sid: accounts[0].service_provider_sid,
|
||||
account_sid: r[0].account_sid,
|
||||
application_sid: r[0].application_sid,
|
||||
account: accounts[0]
|
||||
gateway: gateway,
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
account_sid: a[0].account_sid,
|
||||
application_sid: r[0]?.application_sid || gateway.application_sid,
|
||||
account: a[0]
|
||||
};
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
|
||||
/* no match, so let's look for case #2 */
|
||||
try {
|
||||
logger.info({
|
||||
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}`);
|
||||
const sql =
|
||||
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
|
||||
AND voip_carrier_sid = '${matches[0].voip_carrier_sid}'
|
||||
AND account_sid = '${matches[0].account_sid}'`;
|
||||
//logger.debug({matches: matches[0], sql, did}, 'looking up DID');
|
||||
|
||||
const [r] = await pp.query(sql);
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: matches[0],
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
account_sid: a[0].account_sid,
|
||||
application_sid: r[0]?.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 */
|
||||
}
|
||||
|
||||
/* 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);
|
||||
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, req.source_address));
|
||||
if (selected) {
|
||||
const [a] = await pp.query(sqlAccountByRealm, uri.host);
|
||||
if (0 === a.length) return failure;
|
||||
/* find all carrier entries that have an inbound gateway matching the source IP */
|
||||
/* Query both exact IP matches AND CIDR ranges in parallel to handle the case where
|
||||
multiple accounts have configured the same carrier with different netmasks.
|
||||
The phone number lookup will disambiguate which account owns the call. */
|
||||
const [[gwExact], [gwCidr]] = await Promise.all([
|
||||
pp.query(sqlSelectExactGatewayForSP, [req.source_address]),
|
||||
pp.query(sqlSelectCIDRGatewaysForSP)
|
||||
]);
|
||||
|
||||
/* Merge both result sets - exact matches first, then CIDR ranges (already sorted by netmask DESC) */
|
||||
const gw = [...gwExact, ...gwCidr];
|
||||
|
||||
//logger.debug({gw}, `checking gateways for source address ${req.source_address}`);
|
||||
let matches = gw
|
||||
.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,
|
||||
pad_crypto: gw.pad_crypto,
|
||||
register_username: gw.register_username,
|
||||
register_password: gw.register_password
|
||||
};
|
||||
});
|
||||
/* remove duplicates, winnow down to voip_carriers, not gateways */
|
||||
if (matches.length > 1) {
|
||||
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
|
||||
}
|
||||
|
||||
/* if no static gateway matches, check ephemeral gateways in Redis */
|
||||
if (matches.length === 0) {
|
||||
const ephemeralCarrierSids = await lookupEphemeralGatewayCarriers(req.source_address);
|
||||
if (ephemeralCarrierSids.length > 0) {
|
||||
const ephemeralGateways = await lookupGatewaysByCarrierSids(ephemeralCarrierSids);
|
||||
matches = ephemeralGateways.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,
|
||||
pad_crypto: gw.pad_crypto,
|
||||
register_username: gw.register_username,
|
||||
register_password: gw.register_password
|
||||
};
|
||||
});
|
||||
/* remove duplicates */
|
||||
if (matches.length > 1) {
|
||||
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
|
||||
}
|
||||
logger.info({source_address: req.source_address, count: matches.length},
|
||||
'matched call to ephemeral gateway(s) from registration trunk');
|
||||
}
|
||||
}
|
||||
|
||||
//logger.debug({matches}, `matches for source address ${req.source_address}`);
|
||||
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 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) {
|
||||
// if no matching phone number is found, find whether there are any applications having phone numbers
|
||||
// matching the regex pattern or wild card
|
||||
logger.debug({isDotDecimal},
|
||||
'Did not find a matching phone number, checking for applications with Regex or wildcard');
|
||||
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const application = await getApplicationForDidAndCarriers(req, vc_sids);
|
||||
if (application) {
|
||||
logger.debug({application}, 'looking up DID: found application with Regex or wildcard');
|
||||
const gateway = matches.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
|
||||
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
|
||||
const [a] = await pp.query(sqlAccountBySid, [application.account_sid]);
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: gateway,
|
||||
service_provider_sid: gateway.service_provider_sid,
|
||||
account_sid: application.account_sid,
|
||||
application_sid: application.application_sid,
|
||||
account: a[0],
|
||||
};
|
||||
}
|
||||
|
||||
/* 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 = ?',
|
||||
[matches[0].service_provider_sid]);
|
||||
if (r[0].count === 0 || r[0].count > 1) return {fromCarrier: true};
|
||||
else {
|
||||
const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?',
|
||||
[matches[0].service_provider_sid]);
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: matches[0],
|
||||
service_provider_sid: accounts[0].service_provider_sid,
|
||||
account_sid: accounts[0].account_sid,
|
||||
account: accounts[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (r.length > 1) {
|
||||
const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid}));
|
||||
logger.info({
|
||||
number: r[0].number,
|
||||
total: all.length,
|
||||
matches: all.slice(0, 5)
|
||||
},
|
||||
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
|
||||
return {
|
||||
fromCarrier: true,
|
||||
error: 'Multiple accounts are attempting to route the same phone number from the same carrier'
|
||||
};
|
||||
}
|
||||
|
||||
/* we have a route for this phone number and carrier combination */
|
||||
const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid);
|
||||
const [accounts] = await pp.query(sqlAccountBySid, [r[0].account_sid]);
|
||||
assert(accounts.length);
|
||||
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]
|
||||
gateway,
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves voip_carriers with trunk_type 'auth' that belong to either:
|
||||
* 1. The specified account (account_sid matches), OR
|
||||
* 2. The service provider but with null account_sid (shared across service provider)
|
||||
*
|
||||
* @param {string} account_sid - The SID of the account
|
||||
* @param {string} service_provider_sid - The SID of the service provider
|
||||
* @returns {Promise<Array>} Array of voip_carrier records matching the criteria
|
||||
* @throws {Error} Database errors or other unexpected errors
|
||||
*/
|
||||
const lookupAuthCarriersForAccountAndSP = async(account_sid, service_provider_sid) => {
|
||||
try {
|
||||
const [rows] = await pp.query(sqlSelectAuthCarriersForAccountAndSP, [account_sid, service_provider_sid]);
|
||||
return rows;
|
||||
} catch (err) {
|
||||
logger.error({err, account_sid, service_provider_sid}, 'lookupAuthCarriersForAccountAndSP');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
wasOriginatedFromCarrier,
|
||||
getApplicationForDidAndCarrier,
|
||||
getApplicationForDidAndCarriers,
|
||||
getOutboundGatewayForRefer,
|
||||
getSPForAccount
|
||||
getSPForAccount,
|
||||
getApplicationBySid,
|
||||
lookupAuthCarriersForAccountAndSP
|
||||
};
|
||||
};
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@ module.exports = (srf, logger) => {
|
||||
return async() => {
|
||||
try {
|
||||
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 {
|
||||
const fs = await retrieveSet(setName);
|
||||
|
||||
+121
-74
@@ -1,8 +1,8 @@
|
||||
const debug = require('debug')('jambonz:sbc-inbound');
|
||||
const assert = require('assert');
|
||||
const Emitter = require('events');
|
||||
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 ?
|
||||
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
|
||||
[];
|
||||
@@ -22,57 +22,29 @@ const initCdr = (req) => {
|
||||
};
|
||||
|
||||
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 {
|
||||
lookupAuthHook,
|
||||
lookupAppByTeamsTenant,
|
||||
lookupAccountBySipRealm,
|
||||
lookupAccountBySid,
|
||||
lookupAccountCapacitiesBySid,
|
||||
queryCallLimits
|
||||
queryCallLimits,
|
||||
} = srf.locals.dbHelpers;
|
||||
const {stats, writeCdrs} = srf.locals;
|
||||
const authenticator = require('@jambonz/http-authenticator')(lookupAuthHook, logger, {
|
||||
blacklistUnknownRealms: true,
|
||||
emitter: new AuthOutcomeReporter(stats)
|
||||
});
|
||||
const {stats, writeCdrs, lookupAuthCarriersForAccountAndSP, getApplicationForDidAndCarrier} = srf.locals;
|
||||
|
||||
const initLocals = (req, res, next) => {
|
||||
const callId = req.get('Call-ID');
|
||||
|
||||
/* if we are drying up calls prior to scale-in, reject new INVITEs so the
|
||||
sender fails over to another SBC; allow INVITE with Replaces through
|
||||
since it targets a call already in progress on this server */
|
||||
if (srf.locals.dryUpCalls && !req.has('Replaces')) {
|
||||
logger.info({callId}, 'rejecting INVITE with 503 as we are drying up calls before scale-in');
|
||||
return res.send(503);
|
||||
}
|
||||
|
||||
req.locals = req.locals || {callId};
|
||||
req.locals.nudge = 0;
|
||||
|
||||
/* check if forwarded by a proxy that applied an X-Forwarded-For Header */
|
||||
if (req.has('X-Forwarded-For') || req.has('X-Subspace-Forwarded-For')) {
|
||||
@@ -135,15 +107,20 @@ module.exports = function(srf, logger) {
|
||||
const identifyAccount = async(req, res, next) => {
|
||||
try {
|
||||
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 {
|
||||
fromCarrier,
|
||||
gateway,
|
||||
account_sid,
|
||||
application_sid,
|
||||
service_provider_sid,
|
||||
account
|
||||
account,
|
||||
error
|
||||
} = await wasOriginatedFromCarrier(req);
|
||||
const rtt = roundTripTime(startAt);
|
||||
stats.histogram('app.mysql.response_time', rtt, [
|
||||
'query:wasOriginatedFromCarrier', 'app:sbc-inbound']);
|
||||
/**
|
||||
* calls come from 3 sources:
|
||||
* (1) A carrier
|
||||
@@ -151,12 +128,24 @@ module.exports = function(srf, logger) {
|
||||
* (3) A SIP user
|
||||
*/
|
||||
if (fromCarrier) {
|
||||
if (error) {
|
||||
return res.send(503, {
|
||||
headers: {
|
||||
'X-Reason': error
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!gateway) {
|
||||
logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned');
|
||||
return res.send(404, 'Number Not Provisioned');
|
||||
}
|
||||
logger.debug({gateway}, 'identifyAccount: incoming call from gateway');
|
||||
|
||||
const {register_password, ...gatewayForLog} = gateway;
|
||||
logger.info({gateway: gatewayForLog}, 'identifyAccount: incoming call from gateway');
|
||||
const appSidHeader = req.get('x-application-sid');
|
||||
if (appSidHeader && appSidHeader == application_sid) {
|
||||
logger.info({callId}, 'Loop Detected, x-application-sid header on incoming call matches applicationSid');
|
||||
return res.send(482, 'Loop Detected on x-application-sid');
|
||||
}
|
||||
let sid;
|
||||
if (siprec) {
|
||||
if (!account.siprec_hook_sid) {
|
||||
@@ -181,8 +170,8 @@ module.exports = function(srf, logger) {
|
||||
...req.locals
|
||||
};
|
||||
}
|
||||
else if (msProxyIps.includes(req.source_address)) {
|
||||
logger.debug({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
|
||||
else if (msProxyIps.includes(req.source_address) || isMSTeamsCIDR(req.source_address)) {
|
||||
logger.info({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
|
||||
const uri = parseUri(req.uri);
|
||||
|
||||
const app = await lookupAppByTeamsTenant(uri.host);
|
||||
@@ -204,7 +193,7 @@ module.exports = function(srf, logger) {
|
||||
else {
|
||||
req.locals.originator = 'user';
|
||||
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');
|
||||
const account = await lookupAccountBySipRealm(uri.host);
|
||||
if (!account) {
|
||||
@@ -212,6 +201,10 @@ module.exports = function(srf, logger) {
|
||||
res.send(404);
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
const auth_trunks = await lookupAuthCarriersForAccountAndSP(
|
||||
account.account_sid,
|
||||
account.service_provider_sid
|
||||
);
|
||||
|
||||
/* if this is a dedicated SBC (static IP) only take calls for that account's sip realm */
|
||||
if (process.env.SBC_ACCOUNT_SID && account.account_sid !== process.env.SBC_ACCOUNT_SID) {
|
||||
@@ -228,6 +221,14 @@ module.exports = function(srf, logger) {
|
||||
account,
|
||||
application_sid: account.device_calling_application_sid,
|
||||
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
|
||||
}),
|
||||
...(auth_trunks?.length && {auth_trunks}),
|
||||
...req.locals
|
||||
};
|
||||
}
|
||||
@@ -254,6 +255,8 @@ module.exports = function(srf, logger) {
|
||||
callId: req.get('Call-ID'),
|
||||
service_provider_sid: req.locals.service_provider_sid,
|
||||
account_sid: req.locals.account_sid
|
||||
}, {
|
||||
...(req.locals.account.enable_debug_log && {level: 'debug'})
|
||||
});
|
||||
|
||||
next();
|
||||
@@ -276,13 +279,20 @@ module.exports = function(srf, logger) {
|
||||
assert(account_sid);
|
||||
assert(service_provider_sid);
|
||||
|
||||
/* if quick cancel then we are done */
|
||||
if (req.canceled) {
|
||||
logger.info('checkLimits: call was immediately canceled, no need to increment call count as we are done');
|
||||
return;
|
||||
}
|
||||
|
||||
/* decrement count if INVITE is later rejected */
|
||||
res.once('end', async({status}) => {
|
||||
if (status > 200) {
|
||||
nudgeCallCounts(logger, {
|
||||
nudgeCallCounts(req, 'failure', {
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
application_sid
|
||||
application_sid,
|
||||
callId: req.locals.callId
|
||||
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
|
||||
.catch((err) => logger.error(err, 'Error decrementing call counts'));
|
||||
}
|
||||
@@ -290,10 +300,11 @@ module.exports = function(srf, logger) {
|
||||
|
||||
try {
|
||||
/* increment the call count */
|
||||
const {callsSP, calls} = await nudgeCallCounts(logger, {
|
||||
const {callsSP, calls} = await nudgeCallCounts(req, 'init', {
|
||||
service_provider_sid,
|
||||
account_sid,
|
||||
application_sid
|
||||
application_sid,
|
||||
callId: req.locals.callId
|
||||
}, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp});
|
||||
|
||||
/* compare to account's limit, though avoid db hit when call count is low */
|
||||
@@ -303,24 +314,22 @@ module.exports = function(srf, logger) {
|
||||
logger.debug(`checkLimits: call count is now ${calls}, limit is ${minLimit}`);
|
||||
if (calls <= minLimit) return next();
|
||||
|
||||
if (process.env.JAMBONES_HOSTING) {
|
||||
const accountCapacities = await lookupAccountCapacitiesBySid(account_sid);
|
||||
const accountLimit = accountCapacities.find((c) => c.category == 'voice_call_session');
|
||||
if (accountLimit) {
|
||||
/* check account limit */
|
||||
const limit_sessions = accountLimit.quantity;
|
||||
if (calls > limit_sessions) {
|
||||
debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`);
|
||||
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
|
||||
writeAlerts({
|
||||
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
|
||||
service_provider_sid: account.service_provider_sid,
|
||||
account_sid,
|
||||
count: limit_sessions
|
||||
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
||||
res.send(503, 'Maximum Calls In Progress');
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
const accountCapacities = await lookupAccountCapacitiesBySid(account_sid);
|
||||
const accountLimit = accountCapacities.find((c) => c.category == 'voice_call_session');
|
||||
if (accountLimit) {
|
||||
/* check account limit */
|
||||
const limit_sessions = accountLimit.quantity;
|
||||
if (calls > limit_sessions) {
|
||||
debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`);
|
||||
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
|
||||
writeAlerts({
|
||||
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
|
||||
service_provider_sid: account.service_provider_sid,
|
||||
account_sid,
|
||||
count: limit_sessions
|
||||
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
||||
res.send(503, 'Maximum Calls In Progress');
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
}
|
||||
else if (trackingOn) {
|
||||
@@ -331,7 +340,7 @@ module.exports = function(srf, logger) {
|
||||
alert_type: AlertType.ACCOUNT_CALL_LIMIT,
|
||||
service_provider_sid: service_provider_sid,
|
||||
account_sid,
|
||||
count: calls
|
||||
count: account_limit
|
||||
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
||||
res.send(503, 'Max Account Calls In Progress', {
|
||||
headers: {
|
||||
@@ -341,12 +350,17 @@ module.exports = function(srf, logger) {
|
||||
});
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
if (!account_limit && !sp_limit && process.env.JAMBONES_HOSTING) {
|
||||
logger.info(`checkLimits: no active subscription found for account ${account_sid}, rejecting call`);
|
||||
res.send(503, 'No Active Subscription');
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
if (process.env.JAMBONES_TRACK_SP_CALLS && sp_limit > 0 && callsSP > sp_limit) {
|
||||
logger.info({callsSP, sp_limit}, 'checkLimits: service provider limits exceeded');
|
||||
writeAlerts({
|
||||
alert_type: AlertType.SP_CALL_LIMIT,
|
||||
service_provider_sid: service_provider_sid,
|
||||
count: callsSP
|
||||
count: sp_limit
|
||||
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
||||
res.send(503, 'Max Service Provider Calls In Progress', {
|
||||
headers: {
|
||||
@@ -366,11 +380,43 @@ module.exports = function(srf, logger) {
|
||||
}
|
||||
};
|
||||
|
||||
const identifyAuthTrunk = async(req, res, next) => {
|
||||
try {
|
||||
if (req.authorization) {
|
||||
const {grant} = req.authorization;
|
||||
if (grant && grant.status === 'ok' && grant.auth_trunk) {
|
||||
// we have successfully authenticated the call for an auth_trunk
|
||||
const application_sid = await getApplicationForDidAndCarrier(req, grant.auth_trunk.voip_carrier_sid);
|
||||
|
||||
req.locals = {
|
||||
...req.locals,
|
||||
originator: 'trunk',
|
||||
carrier: grant.auth_trunk.name,
|
||||
gateway: grant.auth_trunk,
|
||||
voip_carrier_sid: grant.auth_trunk.voip_carrier_sid,
|
||||
application_sid: application_sid || grant.auth_trunk.application_sid,
|
||||
};
|
||||
// as call from auth carrier, clean req.authorization that impact on legacy logic for authenticated user
|
||||
delete req.authorization;
|
||||
|
||||
logger.debug({callId: req.locals.callId, auth_trunk: grant.auth_trunk.name},
|
||||
'identifyAuthTrunk: call authenticated for auth trunk');
|
||||
}
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
stats.increment('sbc.terminations', ['sipStatus:500']);
|
||||
logger.error(err, `${req.get('Call-ID')} Error challenging auth trunk`);
|
||||
res.send(500);
|
||||
req.srf.endSession(req);
|
||||
}
|
||||
};
|
||||
|
||||
const challengeDeviceCalls = async(req, res, next) => {
|
||||
try {
|
||||
/* TODO: check if this is a gateway that we have an ACL for */
|
||||
if (req.locals.originator !== 'user') return next();
|
||||
return authenticator(req, res, next);
|
||||
return digestChallenge(req, res, next);
|
||||
} catch (err) {
|
||||
stats.increment('sbc.terminations', ['sipStatus:500']);
|
||||
logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`);
|
||||
@@ -384,6 +430,7 @@ module.exports = function(srf, logger) {
|
||||
handleSipRec,
|
||||
challengeDeviceCalls,
|
||||
identifyAccount,
|
||||
identifyAuthTrunk,
|
||||
checkLimits
|
||||
};
|
||||
};
|
||||
|
||||
+209
-17
@@ -1,5 +1,8 @@
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const express = require('express');
|
||||
const rtpCharacteristics = require('../data/rtp-transcoding');
|
||||
const srtpCharacteristics = require('../data/srtp-transcoding');
|
||||
|
||||
let idx = 0;
|
||||
|
||||
const isWSS = (req) => {
|
||||
@@ -8,23 +11,40 @@ const isWSS = (req) => {
|
||||
|
||||
const getAppserver = (srf) => {
|
||||
const len = srf.locals.featureServers.length;
|
||||
return srf.locals.featureServers[ idx++ % len];
|
||||
return srf.locals.featureServers[idx++ % len];
|
||||
};
|
||||
|
||||
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 srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
|
||||
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics;
|
||||
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
|
||||
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
|
||||
if ((req.locals.gateway?.pad_crypto || 0) > 0) {
|
||||
srtpOpts.flags.push('pad crypto');
|
||||
}
|
||||
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
|
||||
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
|
||||
|
||||
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
|
||||
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
|
||||
/* Allow Feature server to inject DTMF to both leg except call from Teams */
|
||||
if (!teams) {
|
||||
dstOpts.flags.push('inject DTMF');
|
||||
srcOpts.flags.push('inject DTMF');
|
||||
}
|
||||
|
||||
/** By default, and for backwards compatibility, use media handover
|
||||
* set env var to true to use strict source instead (needed for rtpbleed vulnerability)
|
||||
*/
|
||||
const enableStrictSource = !!process.env.RTPENGINE_ENABLE_STRICT_SOURCE;
|
||||
dstOpts.flags.push(enableStrictSource ? 'strict source' : 'media handover');
|
||||
srcOpts.flags.push(enableStrictSource ? 'strict source' : 'media handover');
|
||||
const acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ?
|
||||
process.env.JAMBONES_ACCEPT_AND_TRANSCODE :
|
||||
process.env.JAMBONES_ACCEPT_G729 ? 'g729' : '';
|
||||
const common = {
|
||||
'call-id': req.get('Call-ID'),
|
||||
'replace': ['origin', 'session-connection']
|
||||
'replace': ['origin', 'session-connection'],
|
||||
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no',
|
||||
...(acceptCodecs && { codec: { mask: acceptCodecs, transcode: 'pcmu,pcma' } })
|
||||
};
|
||||
return {
|
||||
common,
|
||||
@@ -39,6 +59,9 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
|
||||
};
|
||||
}
|
||||
|
||||
const SdpWantsSDES = (sdp) => {
|
||||
return /m=audio.*\s+RTP\/SAVP/.test(sdp);
|
||||
};
|
||||
const SdpWantsSrtp = (sdp) => {
|
||||
return /m=audio.*SAVP/.test(sdp);
|
||||
};
|
||||
@@ -64,32 +87,134 @@ const equalsIgnoreOrder = (a, b) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const systemHealth = async(redisClient, ping, getCount) => {
|
||||
const systemHealth = async(redisClient, ping, getCount) => {
|
||||
await Promise.all([redisClient.ping(), ping()]);
|
||||
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 express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
|
||||
return new Promise((resolve) => {
|
||||
app.listen(port, () => {
|
||||
logger.info(`Health check server started at http://localhost:${port}`);
|
||||
resolve(app);
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = doListen(logger, app, port, resolve);
|
||||
server.on('error', handleErrors.bind(null, logger, app, resolve, reject));
|
||||
});
|
||||
};
|
||||
|
||||
const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
|
||||
const {service_provider_sid, account_sid, application_sid} = sids;
|
||||
/**
|
||||
* nudgeCallCounts - increment or decrement call counts in redis
|
||||
*
|
||||
* current nudge value
|
||||
* -----------------------------------------
|
||||
* why | -1 | 0 | 1 |
|
||||
* -----------------------------------------
|
||||
* init | no-op | +1 | N/A |
|
||||
* failure | N/A | -1 | -1 |
|
||||
* complete| N/A | N/A | -1 |
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
const shouldNudge = (why, req) => {
|
||||
const {nudge, logger} = req.locals;
|
||||
let modifyCount = false;
|
||||
const originalNudge = nudge;
|
||||
|
||||
switch (why) {
|
||||
case 'init':
|
||||
if (nudge === 0) {
|
||||
// normal case: new call, increment call count
|
||||
req.locals.nudge = 1;
|
||||
modifyCount = true;
|
||||
}
|
||||
else if (nudge === -1) {
|
||||
// extremely quick cancel, don't increment call count
|
||||
req.locals.nudge = 0;
|
||||
}
|
||||
else {
|
||||
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
|
||||
}
|
||||
break;
|
||||
case 'failure':
|
||||
if (nudge === 1) {
|
||||
// normal case of call failed for any reason, decrement call count
|
||||
req.locals.nudge = 0;
|
||||
modifyCount = true;
|
||||
}
|
||||
else if (nudge === 0) {
|
||||
// very quick failure dont decrement call count
|
||||
req.locals.nudge = -1;
|
||||
}
|
||||
else {
|
||||
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
|
||||
}
|
||||
break;
|
||||
case 'complete':
|
||||
if (nudge === 1) {
|
||||
// normal case of call completed, decrement call count
|
||||
req.locals.nudge = 0;
|
||||
modifyCount = true;
|
||||
}
|
||||
else {
|
||||
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.info(`shouldNudge: unexpected why value ${why}`);
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info(`shouldNudge: '${why}': updating count: ${modifyCount}, nudge: ${originalNudge} -> ${req.locals.nudge}`);
|
||||
return modifyCount;
|
||||
};
|
||||
|
||||
const nudgeCallCounts = async(req, why, sids, nudgeOperator, writers) => {
|
||||
const {logger} = req.locals;
|
||||
const {service_provider_sid, account_sid, application_sid, callId} = sids;
|
||||
const {writeCallCount, writeCallCountSP, writeCallCountApp} = writers;
|
||||
const nudges = [];
|
||||
const writes = [];
|
||||
|
||||
if (!shouldNudge(why, req)) {
|
||||
return {callsSP: null, calls: null, callsApp: null};
|
||||
}
|
||||
|
||||
if (process.env.JAMBONES_DEBUG_CALL_COUNTS) {
|
||||
const {srf} = require('..');
|
||||
const {addKey, deleteKey} = srf.locals.realtimeDbHelpers;
|
||||
|
||||
if (why === 'init') {
|
||||
// save for 3 days
|
||||
await addKey(`debug:incalls:${account_sid}:${callId}`, new Date().toISOString(), 259200);
|
||||
}
|
||||
else {
|
||||
await deleteKey(`debug:incalls:${account_sid}:${callId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.JAMBONES_TRACK_SP_CALLS) {
|
||||
const key = makeSPCallCountKey(service_provider_sid);
|
||||
nudges.push(nudgeOperator(key));
|
||||
@@ -154,10 +279,71 @@ const parseConnectionIp = (sdp) => {
|
||||
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) => {
|
||||
const {srf, logger} = require('..');
|
||||
const {privateNetworkCidr} = srf.locals;
|
||||
if (privateNetworkCidr) {
|
||||
try {
|
||||
const matcher = new CIDRMatcher(privateNetworkCidr.split(','));
|
||||
return matcher.contains(ip);
|
||||
} catch (err) {
|
||||
logger.info({err, privateNetworkCidr},
|
||||
'Error checking private network CIDR, probably misconfigured must be a comma separated list of CIDRs');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param hostports can be a string or an array of hostports
|
||||
*/
|
||||
const parseHostPorts = (logger, hostports, srf) => {
|
||||
typeof hostports === 'string' && (hostports = hostports.split(','));
|
||||
const obj = {};
|
||||
for (const hp of hostports) {
|
||||
const [, protocol, ipv4, port] = hp.match(/^(.*)\/(.*):(\d+)$/);
|
||||
if (protocol && ipv4 && port) {
|
||||
obj[protocol] = `${ipv4}:${port}`;
|
||||
}
|
||||
}
|
||||
if (!obj.tls) {
|
||||
obj.tls = `${srf.locals.sipAddress}:5061`;
|
||||
}
|
||||
|
||||
if (!obj.tcp) {
|
||||
obj.tcp = `${srf.locals.sipAddress}:5060`;
|
||||
}
|
||||
|
||||
logger.info({ obj }, 'sip endpoints');
|
||||
return obj;
|
||||
};
|
||||
|
||||
const makeFullMediaReleaseKey = (callId) => {
|
||||
return `a_sdp:${callId}`;
|
||||
};
|
||||
const makePartnerFullMediaReleaseKey = (callId) => {
|
||||
return `b_sdp:${callId}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isWSS,
|
||||
SdpWantsSrtp,
|
||||
SdpWantsSDES,
|
||||
getAppserver,
|
||||
makeRtpEngineOpts,
|
||||
makeAccountCallCountKey,
|
||||
@@ -169,5 +355,11 @@ module.exports = {
|
||||
createHealthCheckApp,
|
||||
nudgeCallCounts,
|
||||
roundTripTime,
|
||||
parseConnectionIp
|
||||
parseConnectionIp,
|
||||
isMSTeamsCIDR,
|
||||
isPrivateVoipNetwork,
|
||||
parseHostPorts,
|
||||
makeFullMediaReleaseKey,
|
||||
makePartnerFullMediaReleaseKey
|
||||
};
|
||||
|
||||
|
||||
Generated
+3887
-6361
File diff suppressed because it is too large
Load Diff
+24
-23
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "v0.7.6",
|
||||
"version": "0.9.11",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
"node": ">= 20.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"sip",
|
||||
@@ -20,36 +20,37 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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=debug 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",
|
||||
"jslint": "eslint app.js lib"
|
||||
"jslint": "eslint app.js lib",
|
||||
"jslint:fix": "npm run jslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.6.19",
|
||||
"@jambonz/http-authenticator": "^0.2.2",
|
||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||
"@aws-sdk/client-sns": "^3.549.0",
|
||||
"@babel/helpers": "^7.26.10",
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/digest-utils": "^0.0.9",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.29",
|
||||
"@jambonz/rtpengine-utils": "^0.3.4",
|
||||
"@jambonz/siprec-client-utils": "^0.1.4",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.2.4",
|
||||
"aws-sdk": "^2.1152.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.21",
|
||||
"@jambonz/rtpengine-utils": "^0.4.4",
|
||||
"@jambonz/siprec-client-utils": "^0.2.10",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.10",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||
"drachtio-srf": "^4.5.1",
|
||||
"express": "^4.18.1",
|
||||
"pino": "^7.11.0",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"uuid": "^8.3.2",
|
||||
"debug": "^4.4.3",
|
||||
"drachtio-fn-b2b-sugar": "0.2.1",
|
||||
"drachtio-srf": "^5.0.27",
|
||||
"express": "^4.21.2",
|
||||
"pino": "^10.1.0",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"xml2js": "^0.4.23"
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^4.15.1"
|
||||
"tape": "^5.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
+180
-35
@@ -4,22 +4,32 @@ SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
DROP TABLE IF EXISTS account_static_ips;
|
||||
|
||||
DROP TABLE IF EXISTS account_limits;
|
||||
|
||||
DROP TABLE IF EXISTS account_products;
|
||||
|
||||
DROP TABLE IF EXISTS account_subscriptions;
|
||||
|
||||
DROP TABLE IF EXISTS account_limits;
|
||||
|
||||
DROP TABLE IF EXISTS beta_invite_codes;
|
||||
|
||||
DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS clients;
|
||||
|
||||
DROP TABLE IF EXISTS dns_records;
|
||||
|
||||
DROP TABLE IF EXISTS lcr;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||
|
||||
DROP TABLE IF EXISTS lcr_routes;
|
||||
|
||||
DROP TABLE IF EXISTS password_settings;
|
||||
|
||||
DROP TABLE IF EXISTS user_permissions;
|
||||
|
||||
DROP TABLE IF EXISTS permissions;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_sip_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_smpp_gateways;
|
||||
@@ -38,14 +48,18 @@ DROP TABLE IF EXISTS sbc_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS ms_teams_tenants;
|
||||
|
||||
DROP TABLE IF EXISTS signup_history;
|
||||
|
||||
DROP TABLE IF EXISTS service_provider_limits;
|
||||
|
||||
DROP TABLE IF EXISTS signup_history;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_addresses;
|
||||
|
||||
DROP TABLE IF EXISTS google_custom_voices;
|
||||
|
||||
DROP TABLE IF EXISTS speech_credentials;
|
||||
|
||||
DROP TABLE IF EXISTS system_information;
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
DROP TABLE IF EXISTS smpp_gateways;
|
||||
@@ -73,6 +87,15 @@ private_ipv4 VARBINARY(16) NOT NULL UNIQUE ,
|
||||
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
|
||||
(
|
||||
account_subscription_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -92,15 +115,6 @@ pending_reason VARBINARY(52),
|
||||
PRIMARY KEY (account_subscription_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE account_limits
|
||||
(
|
||||
account_limits_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
PRIMARY KEY (account_limits_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE beta_invite_codes
|
||||
(
|
||||
invite_code CHAR(6) NOT NULL UNIQUE ,
|
||||
@@ -118,6 +132,19 @@ application_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (call_route_sid)
|
||||
) COMMENT='a regex-based pattern match for call routing';
|
||||
|
||||
CREATE TABLE clients
|
||||
(
|
||||
client_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
username VARCHAR(64),
|
||||
password VARCHAR(1024),
|
||||
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
|
||||
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
|
||||
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (client_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE dns_records
|
||||
(
|
||||
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -130,11 +157,38 @@ PRIMARY KEY (dns_record_sid)
|
||||
CREATE TABLE lcr_routes
|
||||
(
|
||||
lcr_route_sid CHAR(36),
|
||||
lcr_sid CHAR(36) NOT NULL,
|
||||
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||
description VARCHAR(1024),
|
||||
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
|
||||
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||
PRIMARY KEY (lcr_route_sid)
|
||||
) COMMENT='Least cost routing table';
|
||||
) COMMENT='An ordered list of digit patterns in an LCR table. The pat';
|
||||
|
||||
CREATE TABLE lcr
|
||||
(
|
||||
lcr_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
|
||||
service_provider_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
PRIMARY KEY (lcr_sid)
|
||||
) COMMENT='An LCR (least cost routing) table that is used by a service ';
|
||||
|
||||
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 permissions
|
||||
(
|
||||
permission_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
name VARCHAR(32) NOT NULL UNIQUE ,
|
||||
description VARCHAR(255),
|
||||
PRIMARY KEY (permission_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE predefined_carriers
|
||||
(
|
||||
@@ -150,6 +204,7 @@ tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to thi
|
||||
inbound_auth_username VARCHAR(64),
|
||||
inbound_auth_password VARCHAR(64),
|
||||
diversion VARCHAR(32),
|
||||
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
|
||||
PRIMARY KEY (predefined_carrier_sid)
|
||||
);
|
||||
|
||||
@@ -227,7 +282,10 @@ CREATE TABLE sbc_addresses
|
||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(255) NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 5060,
|
||||
tls_port INTEGER,
|
||||
wss_port INTEGER,
|
||||
service_provider_sid CHAR(36),
|
||||
last_updated DATETIME,
|
||||
PRIMARY KEY (sbc_address_sid)
|
||||
);
|
||||
|
||||
@@ -241,6 +299,15 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE ,
|
||||
PRIMARY KEY (ms_teams_tenant_sid)
|
||||
) 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
|
||||
(
|
||||
email VARCHAR(255) NOT NULL,
|
||||
@@ -249,15 +316,6 @@ signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (email)
|
||||
);
|
||||
|
||||
CREATE TABLE service_provider_limits
|
||||
(
|
||||
service_provider_limits_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
service_provider_sid CHAR(36) NOT NULL,
|
||||
category ENUM('api_rate','voice_call_session', 'device') NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
PRIMARY KEY (service_provider_limits_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE smpp_addresses
|
||||
(
|
||||
smpp_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -283,9 +341,31 @@ last_tested DATETIME,
|
||||
tts_tested_ok BOOLEAN,
|
||||
stt_tested_ok BOOLEAN,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
label VARCHAR(64),
|
||||
PRIMARY KEY (speech_credential_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE google_custom_voices
|
||||
(
|
||||
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
speech_credential_sid CHAR(36) NOT NULL,
|
||||
model VARCHAR(512) NOT NULL,
|
||||
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
|
||||
name VARCHAR(64) NOT NULL,
|
||||
voice_cloning_key MEDIUMTEXT,
|
||||
use_voice_cloning_key BOOLEAN DEFAULT false,
|
||||
PRIMARY KEY (google_custom_voice_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE system_information
|
||||
(
|
||||
domain_name VARCHAR(255),
|
||||
sip_domain_name VARCHAR(255),
|
||||
monitoring_domain_name VARCHAR(255),
|
||||
private_network_cidr VARCHAR(8192),
|
||||
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -305,6 +385,7 @@ email_activation_code VARCHAR(16),
|
||||
email_validated BOOLEAN NOT NULL DEFAULT false,
|
||||
phone_validated 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)
|
||||
);
|
||||
|
||||
@@ -332,9 +413,24 @@ smpp_password VARCHAR(64),
|
||||
smpp_enquire_link_interval INTEGER DEFAULT 0,
|
||||
smpp_inbound_system_id VARCHAR(255),
|
||||
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,
|
||||
register_status VARCHAR(4096),
|
||||
dtmf_type ENUM('rfc2833','tones','info') NOT NULL DEFAULT 'rfc2833',
|
||||
outbound_sip_proxy VARCHAR(255),
|
||||
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
|
||||
PRIMARY KEY (voip_carrier_sid)
|
||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||
|
||||
CREATE TABLE user_permissions
|
||||
(
|
||||
user_permissions_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
user_sid CHAR(36) NOT NULL,
|
||||
permission_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (user_permissions_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE smpp_gateways
|
||||
(
|
||||
smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -352,7 +448,7 @@ PRIMARY KEY (smpp_gateway_sid)
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(32) NOT NULL UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL,
|
||||
voip_carrier_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
@@ -365,11 +461,15 @@ CREATE TABLE sip_gateways
|
||||
sip_gateway_sid CHAR(36),
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
|
||||
port INTEGER COMMENT 'sip signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||
voip_carrier_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
send_options_ping BOOLEAN NOT NULL DEFAULT 0,
|
||||
use_sips_scheme BOOLEAN NOT NULL DEFAULT 0,
|
||||
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
|
||||
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
|
||||
PRIMARY KEY (sip_gateway_sid)
|
||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||
|
||||
@@ -402,12 +502,25 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
|
||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
||||
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
||||
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||
app_json TEXT,
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
speech_synthesis_voice VARCHAR(256) DEFAULT 'en-US-Standard-C',
|
||||
speech_synthesis_label VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
speech_recognizer_label VARCHAR(64),
|
||||
use_for_fallback_speech BOOLEAN DEFAULT false,
|
||||
fallback_speech_synthesis_vendor VARCHAR(64),
|
||||
fallback_speech_synthesis_language VARCHAR(12),
|
||||
fallback_speech_synthesis_voice VARCHAR(256),
|
||||
fallback_speech_synthesis_label VARCHAR(64),
|
||||
fallback_speech_recognizer_vendor VARCHAR(64),
|
||||
fallback_speech_recognizer_language VARCHAR(64),
|
||||
fallback_speech_recognizer_label VARCHAR(64),
|
||||
env_vars TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (application_sid)
|
||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
@@ -445,6 +558,10 @@ subspace_client_secret VARCHAR(255),
|
||||
subspace_sip_teleport_id VARCHAR(255),
|
||||
subspace_sip_teleport_destinations VARCHAR(255),
|
||||
siprec_hook_sid CHAR(36),
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
|
||||
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
|
||||
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (account_sid)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
@@ -452,12 +569,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);
|
||||
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_sid_idx ON account_subscriptions (account_sid);
|
||||
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX account_sid_idx ON account_limits (account_sid);
|
||||
ALTER TABLE account_limits ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid) ON DELETE CASCADE;
|
||||
ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code);
|
||||
CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid);
|
||||
@@ -465,9 +582,21 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
|
||||
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX client_sid_idx ON clients (client_sid);
|
||||
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
|
||||
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
|
||||
|
||||
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
|
||||
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
|
||||
|
||||
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
|
||||
CREATE INDEX account_sid_idx ON lcr (account_sid);
|
||||
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
|
||||
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
|
||||
@@ -512,16 +641,14 @@ 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);
|
||||
|
||||
CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn);
|
||||
CREATE INDEX email_idx ON signup_history (email);
|
||||
CREATE INDEX service_provider_sid_idx ON service_provider_limits (service_provider_sid);
|
||||
ALTER TABLE service_provider_limits ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX email_idx ON signup_history (email);
|
||||
CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
|
||||
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
|
||||
|
||||
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
@@ -529,6 +656,10 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
|
||||
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
|
||||
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
|
||||
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX user_sid_idx ON users (user_sid);
|
||||
CREATE INDEX email_idx ON users (email);
|
||||
CREATE INDEX phone_idx ON users (phone);
|
||||
@@ -548,10 +679,18 @@ ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_
|
||||
|
||||
ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX user_permissions_sid_idx ON user_permissions (user_permissions_sid);
|
||||
CREATE INDEX user_sid_idx ON user_permissions (user_sid);
|
||||
ALTER TABLE user_permissions ADD FOREIGN KEY user_sid_idxfk (user_sid) REFERENCES users (user_sid) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE user_permissions ADD FOREIGN KEY permission_sid_idxfk (permission_sid) REFERENCES permissions (permission_sid);
|
||||
|
||||
CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
|
||||
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
|
||||
|
||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||
CREATE INDEX number_idx ON phone_numbers (number);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
@@ -566,6 +705,12 @@ ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_
|
||||
|
||||
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask);
|
||||
|
||||
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
|
||||
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
@@ -607,4 +752,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
values ('ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'test account', 'jambonz.org', '90dda62e-0ea2-47d1-8164-5bd49003476c', 'foobar');
|
||||
@@ -18,7 +19,8 @@ values ('f4e1848d-3ff8-40eb-b9c1-30e1ef053f94','ed649e33-e771-403a-8c99-1780eabb
|
||||
insert into account_products(account_product_sid, account_subscription_sid, product_sid,quantity)
|
||||
values ('f23ff996-6534-4aba-8666-4b347391eca2', 'f4e1848d-3ff8-40eb-b9c1-30e1ef053f94', 'c4403cdb-8e75-4b27-9726-7d8315e3216d', 10);
|
||||
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid) values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco', 'ed649e33-e771-403a-8c99-1780eabbc803');
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid, service_provider_sid)
|
||||
values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco', 'ed649e33-e771-403a-8c99-1780eabbc803', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.20', true, true);
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound)
|
||||
@@ -41,7 +43,8 @@ insert into account_products(account_product_sid, account_subscription_sid, prod
|
||||
values ('92f137f7-4bc3-4157-b096-6817e54b1874', '73bbcc5d-512f-4cea-8535-9a6e3d2bd19d', 'c4403cdb-8e75-4b27-9726-7d8315e3216d', 0);
|
||||
insert into account_limits(account_limits_sid, account_sid, category, quantity) values('a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d', 'd7cc37cb-d152-49ef-a51b-485f6e917089', 'voice_call_session', 0);
|
||||
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid) values ('9b1abdc7-0220-4964-bc66-32b5c70cd9ab', 'westco', 'd7cc37cb-d152-49ef-a51b-485f6e917089');
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid, service_provider_sid)
|
||||
values ('9b1abdc7-0220-4964-bc66-32b5c70cd9ab', 'westco', 'd7cc37cb-d152-49ef-a51b-485f6e917089', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('12f401d9-cbb1-49e5-bd33-cefbca0badc3', '9b1abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.20', true, true);
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound)
|
||||
@@ -51,7 +54,8 @@ values ('1401eb72-0daf-4471-aba6-038a0a2587b3', '9b1abdc7-0220-4964-bc66-32b5c70
|
||||
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
|
||||
values ('3b43e39f-4346-4218-8434-a53130e8be49', 'test', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '4d7ce0aa-5ead-4e61-9a6b-3daa732218b1');
|
||||
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid, application_sid) values ('999c1452-620d-4195-9f19-c9814ef90d78', 'customer PBX', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '3b43e39f-4346-4218-8434-a53130e8be49');
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid, application_sid, service_provider_sid)
|
||||
values ('999c1452-620d-4195-9f19-c9814ef90d78', 'customer PBX', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '3b43e39f-4346-4218-8434-a53130e8be49', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('888a5339-c62c-4075-9e19-f4de70a96597', '999c1452-620d-4195-9f19-c9814ef90d78', '172.38.0.21', true, false);
|
||||
|
||||
@@ -66,4 +70,73 @@ 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');
|
||||
|
||||
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');
|
||||
|
||||
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
|
||||
values ('c17d5a7d-9328-4663-92c0-f65aa8381264', '12125551212', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803', '3b43e39f-4346-4218-8434-a53130e8be49');
|
||||
|
||||
-- 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);
|
||||
|
||||
-- voip carrier belonging to all accounts
|
||||
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid)
|
||||
values ('voip100', 'test-voip-carrier', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('sip100', 'voip100', '172.38.0.50', true, false);
|
||||
|
||||
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid)
|
||||
values ('voip101', 'test-voip-carrier-101', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('sip101', 'voip101', '172.38.0.50', true, false);
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('sip102', 'voip101', '172.38.0.51', true, false);
|
||||
|
||||
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
|
||||
values ('app100', 'app100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '90dda62e-0ea2-47d1-8164-5bd49003476c');
|
||||
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
|
||||
values ('app101', 'app101', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '90dda62e-0ea2-47d1-8164-5bd49003476c');
|
||||
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
|
||||
values ('phone100', '^100', 'voip101', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app100');
|
||||
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
|
||||
values ('phone101', '^10012', 'voip100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app101');
|
||||
-- insert an invalid regex pattern, the below pattern should be ignored during pattern
|
||||
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
|
||||
values ('phone102', '\\dkjfhmdf\\', 'voip100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app101');
|
||||
|
||||
-- account with a sip realm that is not associated with any voip carriers
|
||||
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
|
||||
values ('acct-100', 'Account 100', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'ram.sip.jambonz.org');
|
||||
|
||||
-- registration trunk carrier for ephemeral gateway testing
|
||||
insert into voip_carriers (voip_carrier_sid, name, account_sid, service_provider_sid, trunk_type,
|
||||
requires_register, register_username, register_sip_realm, register_password, is_active)
|
||||
values ('4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', 'test-registration-trunk', 'ed649e33-e771-403a-8c99-1780eabbc803',
|
||||
'3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'reg', true, 'testuser',
|
||||
'sip.carrier.example.com', 'testpass', true);
|
||||
|
||||
-- sip_gateway for outbound only (inbound will use ephemeral gateway from Redis)
|
||||
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
|
||||
values ('8b3e5f9a-2c1d-4e7b-9a6c-3d8f1e5a7b2c', '4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', '3.3.3.3', false, true);
|
||||
|
||||
-- phone number for ephemeral gateway test
|
||||
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
|
||||
values ('7c2d4e6f-8a1b-4c9d-7e5f-2a8b3c6d9e1f', '16175551000', '4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', 'ed649e33-e771-403a-8c99-1780eabbc803',
|
||||
'3b43e39f-4346-4218-8434-a53130e8be49');
|
||||
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
sbc-inbound:
|
||||
ipv4_address: 172.38.0.2
|
||||
sbc:
|
||||
image: drachtio/drachtio-server:latest
|
||||
image: drachtio/drachtio-server:0.8.26
|
||||
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
|
||||
ports:
|
||||
- "9060:9022/tcp"
|
||||
|
||||
@@ -2,3 +2,4 @@ require('./docker_start');
|
||||
require('./create-test-db');
|
||||
require('./sip-tests');
|
||||
require('./docker_stop');
|
||||
require('./utils');
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
<scenario name="UAC with x-application-sid header">
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
INVITE sip:+12125551212@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:12125551212@jambonz.org>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
X-Application-Sid: 3b43e39f-4346-4218-8434-a53130e8be49
|
||||
|
||||
v=0
|
||||
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
||||
s=-
|
||||
c=IN IP[media_ip_type] [media_ip]
|
||||
t=0 0
|
||||
m=audio [media_port] RTP/AVP 0
|
||||
a=rtpmap:0 PCMU/8000
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="100" optional="true">
|
||||
</recv>
|
||||
|
||||
<recv response="482" rtd="true" >
|
||||
</recv>
|
||||
|
||||
</scenario>
|
||||
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<!-- This program is free software; you can redistribute it and/or -->
|
||||
<!-- modify it under the terms of the GNU General Public License as -->
|
||||
<!-- published by the Free Software Foundation; either version 2 of the -->
|
||||
<!-- License, or (at your option) any later version. -->
|
||||
<!-- -->
|
||||
<!-- This program is distributed in the hope that it will be useful, -->
|
||||
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||
<!-- GNU General Public License for more details. -->
|
||||
<!-- -->
|
||||
<!-- You should have received a copy of the GNU General Public License -->
|
||||
<!-- along with this program; if not, write to the -->
|
||||
<!-- Free Software Foundation, Inc., -->
|
||||
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
||||
<!-- -->
|
||||
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
|
||||
<!-- -->
|
||||
|
||||
<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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-success
|
||||
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>
|
||||
|
||||
<recv response="180" 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="200" 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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 ACK
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<!-- Play a pre-recorded PCAP file (RTP stream) -->
|
||||
<nop>
|
||||
<action>
|
||||
<exec play_pcap_audio="pcap/g711a.pcap"/>
|
||||
</action>
|
||||
</nop>
|
||||
|
||||
<!-- Pause briefly -->
|
||||
<pause milliseconds="3000"/>
|
||||
|
||||
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
BYE sip:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 2 BYE
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="200" crlf="true">
|
||||
</recv>
|
||||
|
||||
<!-- 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>
|
||||
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<!-- This program is free software; you can redistribute it and/or -->
|
||||
<!-- modify it under the terms of the GNU General Public License as -->
|
||||
<!-- published by the Free Software Foundation; either version 2 of the -->
|
||||
<!-- License, or (at your option) any later version. -->
|
||||
<!-- -->
|
||||
<!-- This program is distributed in the hope that it will be useful, -->
|
||||
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||
<!-- GNU General Public License for more details. -->
|
||||
<!-- -->
|
||||
<!-- You should have received a copy of the GNU General Public License -->
|
||||
<!-- along with this program; if not, write to the -->
|
||||
<!-- Free Software Foundation, Inc., -->
|
||||
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
||||
<!-- -->
|
||||
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
|
||||
<!-- -->
|
||||
|
||||
<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:+100678@ram.sip.jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:100678@ram.sip.jambonz.org>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-success
|
||||
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>
|
||||
|
||||
<recv response="180" 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="200" 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:100678@ram.sip.jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:100678@ram.sip.jambonz.org>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 ACK
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<!-- Play a pre-recorded PCAP file (RTP stream) -->
|
||||
<nop>
|
||||
<action>
|
||||
<exec play_pcap_audio="pcap/g711a.pcap"/>
|
||||
</action>
|
||||
</nop>
|
||||
|
||||
<!-- Pause briefly -->
|
||||
<pause milliseconds="3000"/>
|
||||
|
||||
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
BYE sip:100678@ram.sip.jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:100678@ram.sip.jambonz.org>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 2 BYE
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="200" crlf="true">
|
||||
</recv>
|
||||
|
||||
<!-- 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>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<!-- This program is free software; you can redistribute it and/or -->
|
||||
<!-- modify it under the terms of the GNU General Public License as -->
|
||||
<!-- published by the Free Software Foundation; either version 2 of the -->
|
||||
<!-- License, or (at your option) any later version. -->
|
||||
<!-- -->
|
||||
<!-- This program is distributed in the hope that it will be useful, -->
|
||||
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||
<!-- GNU General Public License for more details. -->
|
||||
<!-- -->
|
||||
<!-- You should have received a copy of the GNU General Public License -->
|
||||
<!-- along with this program; if not, write to the -->
|
||||
<!-- Free Software Foundation, Inc., -->
|
||||
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
||||
<!-- -->
|
||||
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
|
||||
<!-- -->
|
||||
|
||||
<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:+16173333456@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16173333456@jambonz.org>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-late-media
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="100" optional="true">
|
||||
</recv>
|
||||
|
||||
<recv response="180" 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="200" 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:16173333456@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16173333456@jambonz.org>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 ACK
|
||||
Max-Forwards: 70
|
||||
Subject: uac-late-media
|
||||
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>
|
||||
|
||||
<!-- Play a pre-recorded PCAP file (RTP stream) -->
|
||||
<nop>
|
||||
<action>
|
||||
<exec play_pcap_audio="pcap/g711a.pcap"/>
|
||||
</action>
|
||||
</nop>
|
||||
|
||||
<!-- Pause briefly -->
|
||||
<pause milliseconds="3000"/>
|
||||
|
||||
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
BYE sip:16173333456@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16173333456@jambonz.org>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 2 BYE
|
||||
Subject: uac-late-media
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="200" crlf="true">
|
||||
</recv>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE scenario SYSTEM "sipp.dtd">
|
||||
|
||||
<!-- This program is free software; you can redistribute it and/or -->
|
||||
<!-- modify it under the terms of the GNU General Public License as -->
|
||||
<!-- published by the Free Software Foundation; either version 2 of the -->
|
||||
<!-- License, or (at your option) any later version. -->
|
||||
<!-- -->
|
||||
<!-- This program is distributed in the hope that it will be useful, -->
|
||||
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||
<!-- GNU General Public License for more details. -->
|
||||
<!-- -->
|
||||
<!-- You should have received a copy of the GNU General Public License -->
|
||||
<!-- along with this program; if not, write to the -->
|
||||
<!-- Free Software Foundation, Inc., -->
|
||||
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
|
||||
<!-- -->
|
||||
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
|
||||
<!-- -->
|
||||
|
||||
<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:+16175551000@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16175551000@jambonz.org>
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 INVITE
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-ephemeral-gateway-success
|
||||
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>
|
||||
|
||||
<recv response="180" 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="200" 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:16175551000@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16175551000@jambonz.org>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 1 ACK
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-ephemeral-gateway-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<!-- Play a pre-recorded PCAP file (RTP stream) -->
|
||||
<nop>
|
||||
<action>
|
||||
<exec play_pcap_audio="pcap/g711a.pcap"/>
|
||||
</action>
|
||||
</nop>
|
||||
|
||||
<!-- Pause briefly -->
|
||||
<pause milliseconds="3000"/>
|
||||
|
||||
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
|
||||
<send retrans="500">
|
||||
<![CDATA[
|
||||
|
||||
BYE sip:16175551000@jambonz.org SIP/2.0
|
||||
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
|
||||
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
|
||||
To: <sip:16175551000@jambonz.org>[peer_tag_param]
|
||||
Call-ID: [call_id]
|
||||
CSeq: 2 BYE
|
||||
Subject: uac-pcap-ephemeral-gateway-success
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
</send>
|
||||
|
||||
<recv response="200" crlf="true">
|
||||
</recv>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
X-Authenticated-User: xhoaluu@jambonz.org
|
||||
X-Application-Sid: APP_ID_1
|
||||
|
||||
v=0
|
||||
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
||||
|
||||
+30
-3
@@ -34,9 +34,21 @@ test('incoming call tests', async(t) => {
|
||||
obj = await getJSON('http://127.0.0.1:3050/system-health');
|
||||
t.ok(obj.calls === 0, 'HTTP GET /system-health works (health check)')
|
||||
|
||||
await sippUac('uac-late-media.xml', '172.38.0.20');
|
||||
t.pass('incoming call with no SDP packet is rejected with a 488');
|
||||
|
||||
await sippUac('uac-did-applicationsid-loop.xml', '172.38.0.20');
|
||||
t.pass('incoming call with x-application-sid header is rejected with 482');
|
||||
|
||||
await sippUac('uac-pcap-carrier-success.xml', '172.38.0.20');
|
||||
t.pass('incoming call from carrier completed successfully');
|
||||
|
||||
// Test ephemeral gateway (registration trunk)
|
||||
const { createEphemeralGateway } = srf.locals.realtimeDbHelpers;
|
||||
await createEphemeralGateway('172.38.0.60', '4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', 3600);
|
||||
await sippUac('uac-pcap-ephemeral-gateway-success.xml', '172.38.0.60');
|
||||
t.pass('incoming call from ephemeral gateway (registration trunk) completed successfully');
|
||||
|
||||
await sippUac('uac-pcap-pbx-success.xml', '172.38.0.21');
|
||||
t.pass('incoming call from account-level carrier completed successfully');
|
||||
|
||||
@@ -59,12 +71,27 @@ test('incoming call tests', async(t) => {
|
||||
t.pass('handles in-dialog requests');
|
||||
|
||||
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');
|
||||
|
||||
await sippUac('uac-did-regex-match-vc-all-accts.xml', '172.38.0.50');
|
||||
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, with sip realm');
|
||||
|
||||
await sippUac('uac-did-regex-match-vc-all-accts-nosiprealm.xml', '172.38.0.51');
|
||||
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, without sip realm');
|
||||
|
||||
await sippUac('uac-did-regex-match-vc-all-accts-nosiprealm.xml', '172.38.0.50');
|
||||
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, without sip realm');
|
||||
|
||||
/* 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);
|
||||
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
|
||||
console.log(`cdrs: ${JSON.stringify(res)}`);
|
||||
t.ok(7 === res.total, 'successfully wrote 7 cdrs for calls');
|
||||
console.log(`cdrs res.total: ${res.total}`);
|
||||
//console.log(`cdrs: ${JSON.stringify(res)}`);
|
||||
t.ok(8 === res.total, 'successfully wrote 8 cdrs for calls (including ephemeral gateway)');
|
||||
|
||||
srf.disconnect();
|
||||
t.end();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
const test = require('tape');
|
||||
const { parseHostPorts } = require('../lib/utils');
|
||||
const { parseUri } = require('drachtio-srf');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
const hostports = "tls/3.70.141.74:5061,wss/3.70.141.74:8443,tcp/3.70.141.74:5060,udp/3.70.141.74:5060";
|
||||
const hostportsNoTls = "wss/3.70.141.74:8443,tcp/3.70.141.74:5060,udp/3.70.141.74:5060";
|
||||
const logger = { info: (args) => console.log(args) };
|
||||
|
||||
const srf = {
|
||||
locals: {
|
||||
sipAddress: '127.0.0.1'
|
||||
}
|
||||
};
|
||||
|
||||
test('utils tests - parseHostPorts', async (t) => {
|
||||
try {
|
||||
let obj = parseHostPorts(logger, hostports, srf);
|
||||
|
||||
const expected = {
|
||||
tls: '3.70.141.74:5061',
|
||||
wss: '3.70.141.74:8443',
|
||||
tcp: '3.70.141.74:5060',
|
||||
udp: '3.70.141.74:5060'
|
||||
};
|
||||
|
||||
t.ok(obj.tls === expected.tls, 'sip endpoint tls');
|
||||
t.ok(obj.wss === expected.wss, 'sip endpoint wss');
|
||||
t.ok(obj.tcp === expected.tcp, 'sip endpoint tcp');
|
||||
t.ok(obj.udp === expected.udp, 'sip endpoint udp');
|
||||
|
||||
obj = parseHostPorts(logger, hostportsNoTls.split(','), srf);
|
||||
|
||||
t.ok(obj.tls === '127.0.0.1:5061', 'sip endpoint tls');
|
||||
|
||||
t.end();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('utils tests - parse URI user', async (t) => {
|
||||
try {
|
||||
let invalidUri = "sip:@202660.tenios.com";
|
||||
const req = {
|
||||
getParsedHeader: () => ({ uri: invalidUri })
|
||||
};
|
||||
|
||||
const referTo = req.getParsedHeader('Refer-To');
|
||||
let uri = parseUri(referTo.uri);
|
||||
|
||||
const expected = {
|
||||
family: "ipv4",
|
||||
scheme: "sip",
|
||||
user: "",
|
||||
password: undefined,
|
||||
host: "202660.tenios.com",
|
||||
port: NaN,
|
||||
params: {},
|
||||
headers: {},
|
||||
}
|
||||
|
||||
t.ok(uri.family === expected.family, 'family eq ipv4');
|
||||
t.ok(uri.scheme === expected.scheme, 'scheme eq sip');
|
||||
t.ok(uri.password === expected.password, 'pw eq undefined');
|
||||
t.ok(uri.host === expected.host, 'host eq 202660.tenios.com');
|
||||
t.ok(uri.user === "", 'user eq empty string');
|
||||
t.ok(isNaN(uri.port), 'port eq NaN');
|
||||
t.ok(typeof uri.params === 'object', 'params eq object');
|
||||
t.ok(typeof uri.headers === 'object', 'headers eq object');
|
||||
|
||||
invalidUri = "<sip:@202660.tenios.com>";
|
||||
uri = parseUri(invalidUri);
|
||||
/* TODO: uri can be undefined - check these conditions in call-session */
|
||||
t.ok(uri === undefined, 'uri is undefined');
|
||||
|
||||
const validUri = "<sip:+49221578952870@202660.tenios.com>";
|
||||
|
||||
t.end();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user