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