Compare commits

...
42 Commits
Author SHA1 Message Date
Dave Horton 2fef169e92 update deps (#138) 2024-04-07 18:33:28 -04:00
Hoan Luu Huu 5829ecdf4c fix cannot clear siprec sessions (#136) 2024-03-27 07:25:07 -04:00
Hoan Luu Huu 03d3a9695d forward reinvite error response to other destination (#133)
* forward reinvite error response to other destination

* update comment review
2024-03-07 07:48:33 -05:00
Hoan Luu Huu f5be95006c start/stop/pause/resume recording success when one of siprec server success. (#131)
* start/stop/pause/resume recording success when one of siprec server success.

* wip

* wip

* update customer header for pause and resume

* allow timeout for siprec action

* wip

* wip

* wip

* update siprec client utils
2024-03-05 13:05:42 -05:00
Dave Horton 2747bc15d6 update deps, comment out questionable code 2024-01-17 13:37:07 -05:00
Dave Horton cdfbf384fe update db-helpers 2024-01-17 13:26:37 -05:00
Hoan Luu Huu 96a83619c7 fix sbc to forward BYE header (#129)
* fix sbc to forward BYE header

* fix sbc to forward BYE header
2023-12-28 20:45:38 -05:00
Hoan Luu Huu b8a67019a1 support sending DTMF to caller (#125)
* support sending DTMF to caller

* wip
2023-12-13 09:14:27 -05:00
Hoan Luu Huu 0e1627ba82 phone number's application should override voice carrier default application (#128)
* phone number's application should override voice carrier default application

* fix review comments

* fix review comments
2023-12-13 08:25:26 -05:00
Hoan Luu Huu bb857a72ff let realtimedb-utils build configuration from env vars (#122) 2023-11-14 09:13:08 -05:00
Hoan Luu Huu aea3033ae0 support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation (#124)
* support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation

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

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

* middleware add ms teams cidr check

* use cidr-matcher lib

* removed redundant require

* moved cidr-matcher require to top of file

* moved express require to top of file

---------

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

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

* Add INFO listener

---------

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

* siprec custom headers

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

* multi srs

* multi srs

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

* fix: update aws sdk v3

* fix jslint issue

* fix jslint issue

* fix parse aws response

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

* fix client encrypted password

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

* authenticate user

* update db helper

* update db helper

* fix jslint

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

* wip: add cdr

* wip: add cdr

* fix

* fix jslint

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

* redis sentinel configuration

* update redis version

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

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

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

* README updated with environment variables
2023-04-10 15:36:36 -04:00
Dave Horton 0b0e37020c push to docker 2023-04-10 09:43:00 -04:00
Antony Jukes 01cde07117 if else made authorization check unreachable (#90) 2023-04-06 09:34:37 -04:00
Hoan Luu Huu 31b4534878 fix: update stat collector version (#89) 2023-04-05 12:03:36 -04:00
Hoan Luu Huu 0c6365a8df fix: update stat collector version (#89) 2023-04-05 12:03:17 -04:00
11 changed files with 4924 additions and 2160 deletions
+31 -28
View File
@@ -2,16 +2,10 @@ name: Docker
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: sbc-inbound
- '*'
jobs:
push:
@@ -20,32 +14,41 @@ jobs:
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v3
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
IMAGE_ID=jambonz/sbc-inbound
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention
[ "$VERSION" == "main" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
- name: Login to Docker Hub
uses: docker/login-action@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
+33 -2
View File
@@ -1,10 +1,41 @@
# sbc-inbound ![Build Status](https://github.com/jambonz/sbc-inbound/workflows/CI/badge.svg)
This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
This application provides a part of the SBC (Session Border Controller) functionality of jambonz platform. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
## Configuration
Configuration 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
```
{
+18 -9
View File
@@ -3,10 +3,17 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER &&
process.env.JAMBONES_MYSQL_PASSWORD &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
if (process.env.JAMBONES_REDIS_SENTINELS) {
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
} else {
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
}
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env vars');
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
const Srf = require('drachtio-srf');
const srf = new Srf('sbc-inbound');
const opts = Object.assign({
@@ -47,9 +54,11 @@ const {
lookupAccountBySipRealm,
lookupAccountBySid,
lookupAccountCapacitiesBySid,
queryCallLimits
queryCallLimits,
lookupClientByAccountAndUsername
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
user: process.env.JAMBONES_MYSQL_USER,
password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE,
@@ -62,10 +71,7 @@ const {
addToSet,
removeFromSet,
incrKey,
decrKey} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
decrKey} = require('@jambonz/realtimedb-helpers')({}, logger);
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
@@ -95,7 +101,8 @@ srf.locals = {...srf.locals,
lookupAccountBySid,
lookupAccountBySipRealm,
lookupAccountCapacitiesBySid,
queryCallLimits
queryCallLimits,
lookupClientByAccountAndUsername
},
realtimeDbHelpers: {
createSet,
@@ -108,7 +115,8 @@ const {
getSPForAccount,
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer
getOutboundGatewayForRefer,
getApplicationBySid
} = require('./lib/db-utils')(srf, logger);
srf.locals = {
...srf.locals,
@@ -116,7 +124,8 @@ srf.locals = {
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer,
getFeatureServer: require('./lib/fs-tracking')(srf, logger)
getFeatureServer: require('./lib/fs-tracking')(srf, logger),
getApplicationBySid
};
const activeCallIds = srf.locals.activeCallIds;
+33 -23
View File
@@ -6,15 +6,20 @@ const {LifeCycleEvents} = require('./constants');
const express = require('express');
const app = express();
const getString = bent('string');
const AWS = require('aws-sdk');
const sns = new AWS.SNS({apiVersion: '2010-03-31'});
const autoscaling = new AWS.AutoScaling({apiVersion: '2011-01-01'});
const {
SNSClient,
SubscribeCommand,
UnsubscribeCommand } = require('@aws-sdk/client-sns');
const snsClient = new SNSClient({ region: process.env.AWS_REGION, apiVersion: '2010-03-31' });
const {
AutoScalingClient,
DescribeAutoScalingGroupsCommand,
CompleteLifecycleActionCommand } = require('@aws-sdk/client-auto-scaling');
const autoScalingClient = new AutoScalingClient({ region: process.env.AWS_REGION, apiVersion: '2011-01-01' });
const {Parser} = require('xml2js');
const parser = new Parser();
const {validatePayload} = require('verify-aws-sns-signature');
AWS.config.update({region: process.env.AWS_REGION});
class SnsNotifier extends Emitter {
constructor(logger) {
super();
@@ -64,7 +69,7 @@ class SnsNotifier extends Emitter {
subscriptionRequestId: this.subscriptionRequestId
}, 'response from SNS SubscribeURL');
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;
@@ -130,11 +135,12 @@ class SnsNotifier extends Emitter {
async subscribe() {
try {
const response = await sns.subscribe({
const params = {
Protocol: 'http',
TopicArn: process.env.AWS_SNS_TOPIC_ARM,
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}`);
} catch (err) {
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
@@ -144,9 +150,10 @@ class SnsNotifier extends Emitter {
async unsubscribe() {
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
try {
const response = await sns.unsubscribe({
const params = {
SubscriptionArn: this.subscriptionArn
}).promise();
};
const response = await snsClient.send(new UnsubscribeCommand(params));
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) {
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
@@ -155,26 +162,29 @@ class SnsNotifier extends Emitter {
completeScaleIn() {
assert(this.scaleInParams);
autoscaling.completeLifecycleAction(this.scaleInParams, (err, response) => {
if (err) return this.logger.error({err}, 'Error completing scale-in');
this.logger.info({response}, 'Successfully completed scale-in action');
});
autoScalingClient.send(new CompleteLifecycleActionCommand(this.scaleInParams))
.then((data) => {
return this.logger.info({data}, 'Successfully completed scale-in action');
})
.catch((err) => {
this.logger.error({err}, 'Error completing scale-in');
});
}
describeInstance() {
return new Promise((resolve, reject) => {
if (!this.instanceId) return reject('instance-id unknown');
autoscaling.describeAutoScalingInstances({
autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
InstanceIds: [this.instanceId]
}, (err, data) => {
if (err) {
}))
.then((data) => {
this.logger.info({data}, 'SnsNotifier: describeInstance');
return resolve(data);
})
.catch((err) => {
this.logger.error({err}, 'Error describing instances');
reject(err);
} else {
this.logger.info({data}, 'SnsNotifier: describeInstance');
resolve(data);
}
});
});
});
}
@@ -188,7 +198,7 @@ module.exports = async function(logger) {
process.on('SIGHUP', async() => {
try {
const data = await notifier.describeInstance();
const state = data.AutoScalingInstances[0].LifecycleState;
const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
if (state !== notifier.lifecycleState) {
notifier.lifecycleState = state;
switch (state) {
+154 -43
View File
@@ -6,7 +6,8 @@ const {
SdpWantsSDES,
nudgeCallCounts,
roundTripTime,
parseConnectionIp
parseConnectionIp,
isPrivateVoipNetwork
} = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
@@ -14,6 +15,7 @@ const {parseUri, stringifyUri, SipError} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-inbound');
const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy';
const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
/**
* this is to make sure the outgoing From has the number in the incoming From
@@ -64,6 +66,8 @@ class CallSession extends Emitter {
this.application_sid = req.locals.application_sid;
this.account_sid = req.locals.account_sid;
this.service_provider_sid = req.locals.service_provider_sid;
this.srsClients = [];
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
}
get isFromMSTeams() {
@@ -116,6 +120,7 @@ class CallSession extends Emitter {
offer,
answer,
del,
query,
blockMedia,
unblockMedia,
blockDTMF,
@@ -130,6 +135,7 @@ class CallSession extends Emitter {
this.offer = offer;
this.answer = answer;
this.del = del;
this.query = query;
this.blockMedia = blockMedia;
this.unblockMedia = unblockMedia;
this.blockDTMF = blockDTMF;
@@ -153,7 +159,10 @@ class CallSession extends Emitter {
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),
query: this.query.bind(null, this.rtpEngineOpts.common),
};
const obj = parseUri(this.req.uri);
let proxy, host, uri;
@@ -176,7 +185,7 @@ class CallSession extends Emitter {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['public', 'private'],
direction: [isPrivateVoipNetwork(this.req.source_address) ? 'private' : 'public', 'private'],
sdp
};
const startAt = process.hrtime();
@@ -223,7 +232,7 @@ class CallSession extends Emitter {
if (this.req.locals.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid});
}
else if (this.req.authorization) {
if (this.req.authorization) {
if (this.req.authorization.grant && this.req.authorization.grant.application_sid) {
Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid});
}
@@ -247,7 +256,8 @@ class CallSession extends Emitter {
'-Max-Forwards',
'-Record-Route',
'-Session-Expires',
'-X-Subspace-Forwarded-For'
'-X-Application-Sid',
'-X-Authenticated-User'
],
proxyResponseHeaders: ['all', '-X-Trace-ID'],
localSdpB: spdOfferB,
@@ -317,10 +327,7 @@ class CallSession extends Emitter {
this.activeCallIds.delete(callId);
if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {});
if (this.srsClient) {
this.srsClient.stop();
this.srsClient = null;
}
this._stopRecording();
this.srf.endSession(this.req);
});
@@ -329,6 +336,29 @@ class CallSession extends Emitter {
dlg.on('modify', this._onReinvite.bind(this, dlg));
}
_startRecordingNoAnswerTimer(res) {
this._clearRecordingNoAnswerTimer();
this.recordingNoAnswerTimer = setTimeout(() => {
this.logger.info('No response from SipRec server, return error to feature server');
this.isRecordingNoAnswerResponded = true;
res.send(400);
}, this.recordingNoAnswerTimeout);
}
_clearRecordingNoAnswerTimer() {
if (this.recordingNoAnswerTimer) {
clearTimeout(this.recordingNoAnswerTimer);
this.recordingNoAnswerTimer = null;
}
}
_stopRecording() {
if (this.srsClients.length) {
this.srsClients.forEach((c) => c.stop());
this.srsClients = [];
}
}
_setHandlers({uas, uac}) {
this.emit('connected');
const callStart = Date.now();
@@ -350,12 +380,24 @@ class CallSession extends Emitter {
this.uas = uas;
this.uac = uac;
[uas, uac].forEach((dlg) => {
dlg.on('destroy', async() => {
dlg.on('destroy', async(bye) => {
const other = dlg.other;
this.rtpEngineResource.destroy().catch((err) => {});
/* DH: need a better understanding of why query before delete is a good idea
this.rtpEngineResource.query()
.then((results) => {
this.logger.info({results}, 'rtpengine query results');
return this.rtpEngineResource.destroy();
})
.catch((err) => {});
*/
this.activeCallIds.delete(this.req.get('Call-ID'));
try {
await other.destroy();
const headers = {};
Object.keys(bye.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
} catch (err) {}
this.unsubscribeForDTMF();
@@ -380,12 +422,19 @@ class CallSession extends Emitter {
const trunk = ['trunk', 'teams'].includes(this.req.locals.originator) ?
this.req.locals.carrier :
this.req.locals.originator;
const application = await this.srf.locals.getApplicationBySid(application_sid);
const 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,
terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200,
duration: Math.floor((now - callStart) / 1000),
trunk
trunk,
...(isRecording && {recording_url})
};
this.logger.info({cdr}, 'going to write a cdr now..');
this.writeCdrs({...this.req.locals.cdr,
@@ -393,7 +442,8 @@ class CallSession extends Emitter {
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200,
duration: Math.floor((now - callStart) / 1000),
trunk
trunk,
...(isRecording && {recording_url})
})
.then(() => this.logger.debug('successfully wrote cdr'))
.catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
@@ -404,10 +454,7 @@ class CallSession extends Emitter {
dlg.other = null;
other.other = null;
if (this.srsClient) {
this.srsClient.stop();
this.srsClient = null;
}
this._stopRecording();
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
this.srf.endSession(this.req);
@@ -611,7 +658,8 @@ Duration=${payload.duration} `
direction,
sdp: offeredSdp,
};
if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
// Dont reset ICE - causes audiocodes webrtrc to fail with "missing ice-ufrag and ice-pwd in re-invite"
// if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
let response = await this.offer(opts);
if ('ok' !== response.result) {
@@ -645,6 +693,7 @@ Duration=${payload.duration} `
}
res.send(200, {body: response.sdp});
} catch (err) {
res.send(err.status || 500);
this.logger.error(err, 'Error handling reinvite');
}
}
@@ -675,6 +724,7 @@ Duration=${payload.duration} `
}
else if (reason.includes('CallRecording')) {
let succeeded = false;
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
if (reason === 'startCallRecording') {
const from = this.req.getParsedHeader('From');
const to = this.req.getParsedHeader('To');
@@ -687,7 +737,7 @@ Duration=${payload.duration} `
const callSid = req.get('X-Call-Sid');
const accountSid = req.get('X-Account-Sid');
const applicationSid = req.get('X-Application-Sid');
if (this.srsClient) {
if (this.srsClients.length) {
res.send(400);
this.logger.info('discarding duplicate startCallRecording request for a call');
return;
@@ -697,13 +747,14 @@ Duration=${payload.duration} `
res.send(400);
return;
}
this.srsClient = new SrsClient(this.logger, {
const arr = srsUrl.split(',');
this.srsClients = arr.map((url) => new SrsClient(this.logger, {
srf: dlg.srf,
direction: 'inbound',
originalInvite: this.req,
callingNumber: this.req.callingNumber,
calledNumber: this.req.calledNumber,
srsUrl,
srsUrl: url,
srsRecordingId,
callSid,
accountSid,
@@ -718,44 +769,101 @@ Duration=${payload.duration} `
del: this.del,
blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe
});
unsubscribe: this.unsubscribe,
headers
}));
try {
succeeded = await this.srsClient.start();
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.start()));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error starting SipRec call recording');
succeeded = false;
}
}
else if (reason === 'stopCallRecording') {
if (!this.srsClient) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated)) {
res.send(400);
this.logger.info('discarding stopCallRecording request because we are not recording');
return;
}
try {
succeeded = await this.srsClient.stop();
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated) {
c.stop();
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error stopping SipRec call recording');
succeeded = false;
}
this.srsClient = null;
this.srsClients = [];
}
else if (reason === 'pauseCallRecording') {
if (!this.srsClient || this.srsClient.paused) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && !c.paused)) {
this.logger.info('discarding invalid pauseCallRecording request');
res.send(400);
return;
}
succeeded = await this.srsClient.pause();
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated && !c.paused) {
c.pause({headers});
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error pausing SipRec call recording');
succeeded = false;
}
}
else if (reason === 'resumeCallRecording') {
if (!this.srsClient || !this.srsClient.paused) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && c.paused)) {
res.send(400);
this.logger.info('discarding invalid resumeCallRecording request');
return;
}
succeeded = await this.srsClient.resume();
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated && c.paused) {
c.resume({headers});
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error resuming SipRec call recording');
succeeded = false;
}
}
res.send(succeeded ? 200 : 503);
if (!this.isRecordingNoAnswerResponded) {
this._clearRecordingNoAnswerTimer();
res.send(succeeded ? 200 : 503);
}
} else if (reason.includes('Dtmf')) {
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
if (!arr) {
this.logger.info({body: req.body}, '_onInfo: invalid INFO Dtmf');
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
}
const code = arr[1];
const arr2 = /Duration=\s*(\d+)/.exec(req.body);
const duration = arr2 ? arr2[1] : 250;
const dtmfOpts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uac.tag,
code,
duration
};
const response = await this.playDTMF(dtmfOpts);
if ('ok' !== response.result) {
this.logger.info({response}, `rtpengine play Dtmf failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer');
}
res.send(200);
}
}
else if (dlg.type === 'uas' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
@@ -793,10 +901,9 @@ Duration=${payload.duration} `
}
}
else {
const immutableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
const headers = {};
Object.keys(req.headers).forEach((h) => {
if (!immutableHdrs.includes(h)) headers[h] = req.headers[h];
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = req.headers[h];
});
const response = await dlg.other.request({method: 'INFO', headers, body: req.body});
const responseHeaders = {};
@@ -806,8 +913,8 @@ Duration=${payload.duration} `
res.send(response.status, {headers: responseHeaders, body: response.body});
}
} catch (err) {
if (this.srsClient) {
this.srsClient = null;
if (this.srsClients.length) {
this.srsClients = [];
}
res.send(500);
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
@@ -882,6 +989,7 @@ Duration=${payload.duration} `
this.uac = uac;
uac.other = this.uas;
this.uas.other = uac;
uac.on('info', this._onInfo.bind(this, uac));
uac.on('modify', this._onReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uac.on('destroy', () => {
@@ -891,19 +999,22 @@ Duration=${payload.duration} `
uac.other.destroy();
this.srf.endSession(this.req);
});
// now we can destroy the old dialog
dlg.destroy().catch(() => {});
// modify rtpengine to stream to new feature server
const opts = Object.assign({sdp: uac.remote.sdp, 'to-tag': res.getParsedHeader('To').params.tag},
this.rtpEngineOpts.answer);
const opts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
sdp: uac.remote.sdp,
flags: ['port latching']
};
const response = await this.answer(opts);
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onFeatureServerTransfer: rtpengine failed: ${JSON.stringify(response)}`);
throw new Error(`_onFeatureServerTransfer: rtpengine answer failed: ${JSON.stringify(response)}`);
}
dlg.destroy().catch(() => {});
this.logger.info('successfully moved call to new feature server');
} catch (err) {
res.send(488);
this.logger.error(err, 'Error handling refer from feature server');
}
}
+82 -31
View File
@@ -11,6 +11,8 @@ vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask
FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ?
AND vc.account_sid = acc.account_sid
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllCarriersForSPByRealm =
@@ -20,6 +22,8 @@ FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ?
AND vc.service_provider_sid = acc.service_provider_sid
AND vc.account_sid IS NULL
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllGatewaysForSP =
@@ -37,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
WHERE 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`;
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ?';
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ? AND is_active = 1';
const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?';
const sqlApplicationBySid = 'SELECT * from applications WHERE application_sid = ?';
const sqlQueryApplicationByDid = `
SELECT * FROM phone_numbers
@@ -68,11 +75,15 @@ AND vc.is_active = 1
AND vc.register_sip_realm = ?
AND vc.register_username = ?`;
const gatewayMatchesSourceAddress = (source_address, gw) => {
const gatewayMatchesSourceAddress = (logger, source_address, gw) => {
if (32 === gw.netmask && gw.ipv4 === source_address) return true;
if (gw.netmask < 32) {
const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]);
return matcher.contains(source_address);
try {
const matcher = new CIDRMatcher([`${gw.ipv4}/${gw.netmask}`]);
return matcher.contains(source_address);
} catch (err) {
logger.info({err, gw}, 'gatewayMatchesSourceAddress: Error parsing netmask');
}
}
return false;
};
@@ -81,6 +92,12 @@ module.exports = (srf, logger) => {
const {pool} = srf.locals.dbHelpers;
const pp = pool.promise();
const getApplicationBySid = async(application_sid) => {
const [r] = await pp.query(sqlApplicationBySid, [application_sid]);
if (0 === r.length) return null;
return r[0];
};
const getSPForAccount = async(account_sid) => {
const [r] = await pp.query(sqlSelectSPForAccount, [account_sid]);
if (0 === r.length) return null;
@@ -125,7 +142,7 @@ module.exports = (srf, logger) => {
const failure = {fromCarrier: false};
const uri = parseUri(req.uri);
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
const did = normalizeDID(req.calledNumber) || 'anonymous';
if (!isDotDecimal) {
/**
* The host part of the SIP URI is not a dot-decimal IP address,
@@ -136,24 +153,48 @@ module.exports = (srf, logger) => {
* Let's look for case #1 first...
*/
/* get all the carriers and gateways for the account owning this sip realm */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, uri.host);
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
const gw = gwAcc.concat(gwSP);
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, req.source_address));
if (selected) {
const [a] = await pp.query(sqlAccountByRealm, uri.host);
if (0 === a.length) return failure;
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]
};
/* does anyone own this sip realm? */
const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
if (a.length) {
assert(a.length === 1);
/* yes they do */
logger.debug(`sip realm is associated with account_sid: ${a[0].account_sid}`);
/**
* We have one of two cases:
* (1a). The user configured his or her carrier to send to their sip realm, or
* (1b). The user is making a call from a sip device.
*/
/* get all the carriers and gateways for the account owning this sip realm */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]);
const [gwSP] = 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) {
const sql =
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${selected.voip_carrier_sid}'
AND account_sid = '${a[0].account_sid}'`;
logger.debug({selected, sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
return {
fromCarrier: true,
gateway: selected,
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: r[0]?.application_sid || selected.application_sid,
account: a[0]
};
}
return failure;
}
/* no match, so let's look for case #2 */
try {
logger.info({
@@ -161,22 +202,31 @@ module.exports = (srf, logger) => {
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.filter(gatewayMatchesSourceAddress.bind(null, req.source_address));
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);
const [a] = await pp.query(sqlAccountBySid, [[matches[0].account_sid]]);
if (0 === a.length) return failure;
logger.debug({matches}, `found registration carrier using ${uri.host} and ${uri.user}`);
const sql =
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${matches[0].voip_carrier_sid}'
AND account_sid = '${matches[0].account_sid}'`;
logger.debug({matches: matches[0], sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
return {
fromCarrier: true,
gateway: matches[0],
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: matches[0].application_sid,
application_sid: r[0]?.application_sid || matches[0].application_sid,
account: a[0]
};
}
@@ -212,7 +262,8 @@ module.exports = (srf, logger) => {
/* find all carrier entries that have an inbound gateway matching the source IP */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
let matches = gw
.filter(gatewayMatchesSourceAddress.bind(null, req.source_address))
.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,
@@ -227,7 +278,6 @@ module.exports = (srf, logger) => {
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');
@@ -247,7 +297,7 @@ module.exports = (srf, logger) => {
}
else if (accountLevelGateways.length === 1) {
const [accounts] = await pp.query('SELECT * from accounts where account_sid = ?',
accountLevelGateways[0].account_sid);
[accountLevelGateways[0].account_sid]);
return {
fromCarrier: true,
gateway: accountLevelGateways[0],
@@ -262,11 +312,11 @@ module.exports = (srf, logger) => {
- way of knowing which account this is for
*/
const [r] = await pp.query('SELECT count(*) as count from accounts where service_provider_sid = ?',
matches[0].service_provider_sid);
[matches[0].service_provider_sid]);
if (r[0].count === 0 || r[0].count > 1) return {fromCarrier: true};
else {
const [accounts] = await pp.query('SELECT * from accounts where service_provider_sid = ?',
matches[0].service_provider_sid);
[matches[0].service_provider_sid]);
return {
fromCarrier: true,
gateway: matches[0],
@@ -288,7 +338,7 @@ module.exports = (srf, logger) => {
/* we have a route for this phone number and carrier combination */
const gateway = matches.find((m) => m.voip_carrier_sid === r[0].voip_carrier_sid);
const [accounts] = await pp.query(sqlAccountBySid, r[0].account_sid);
const [accounts] = await pp.query(sqlAccountBySid, [r[0].account_sid]);
assert(accounts.length);
return {
fromCarrier: true,
@@ -307,6 +357,7 @@ module.exports = (srf, logger) => {
wasOriginatedFromCarrier,
getApplicationForDidAndCarrier,
getOutboundGatewayForRefer,
getSPForAccount
getSPForAccount,
getApplicationBySid
};
};
+14 -45
View File
@@ -1,8 +1,8 @@
const debug = require('debug')('jambonz:sbc-inbound');
const assert = require('assert');
const Emitter = require('events');
const parseUri = require('drachtio-srf').parseUri;
const {nudgeCallCounts, roundTripTime} = require('./utils');
const {nudgeCallCounts, roundTripTime, isMSTeamsCIDR} = require('./utils');
const digestChallenge = require('@jambonz/digest-utils');
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ?
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
[];
@@ -22,42 +22,8 @@ const initCdr = (req) => {
};
module.exports = function(srf, logger) {
class AuthOutcomeReporter extends Emitter {
constructor(stats) {
super();
this
.on('regHookOutcome', ({rtt, status}) => {
stats.histogram('app.hook.response_time', rtt, ['hook_type:auth', `status:${status}`]);
})
.on('error', async(err, req) => {
const {account_sid, account} = req.locals;
const {writeAlerts, AlertType} = req.srf.locals;
if (account_sid) {
let opts = {account_sid, service_provider_sid: account.service_provider_sid};
if (err.code === 'ECONNREFUSED') {
opts = {...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook};
}
else if (err.code === 'ENOTFOUND') {
opts = {...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook};
}
else if (err.name === 'StatusError') {
opts = {...opts, alert_type: AlertType.WEBHOOK_STATUS_FAILURE, url: err.hook, status: err.statusCode};
}
if (opts.alert_type) {
try {
await writeAlerts(opts);
} catch (err) {
logger.error({err, opts}, 'Error writing alert');
}
}
}
});
}
}
const {
lookupAuthHook,
lookupAppByTeamsTenant,
lookupAccountBySipRealm,
lookupAccountBySid,
@@ -65,10 +31,6 @@ module.exports = function(srf, logger) {
queryCallLimits
} = srf.locals.dbHelpers;
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 callId = req.get('Call-ID');
@@ -167,7 +129,7 @@ module.exports = function(srf, logger) {
logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned');
return res.send(404, 'Number Not Provisioned');
}
logger.debug({gateway}, 'identifyAccount: incoming call from gateway');
logger.info({gateway}, 'identifyAccount: incoming call from gateway');
let sid;
if (siprec) {
@@ -193,8 +155,8 @@ module.exports = function(srf, logger) {
...req.locals
};
}
else if (msProxyIps.includes(req.source_address)) {
logger.debug({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
else if (msProxyIps.includes(req.source_address) || isMSTeamsCIDR(req.source_address)) {
logger.info({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
const uri = parseUri(req.uri);
const app = await lookupAppByTeamsTenant(uri.host);
@@ -216,7 +178,7 @@ module.exports = function(srf, logger) {
else {
req.locals.originator = 'user';
const uri = parseUri(req.uri);
logger.debug({source_address: req.source_address, realm: uri.host},
logger.info({source_address: req.source_address, realm: uri.host},
'identifyAccount: incoming user call');
const account = await lookupAccountBySipRealm(uri.host);
if (!account) {
@@ -240,6 +202,13 @@ module.exports = function(srf, logger) {
account,
application_sid: account.device_calling_application_sid,
webhook_secret: account.webhook_secret,
realm: uri.host,
...(account.registration_hook && {
registration_hook_url: account.registration_hook.url,
registration_hook_method: account.registration_hook.method,
registration_hook_username: account.registration_hook.username,
registration_hook_password: account.registration_hook.password
}),
...req.locals
};
}
@@ -382,7 +351,7 @@ module.exports = function(srf, logger) {
try {
/* TODO: check if this is a gateway that we have an ACL for */
if (req.locals.originator !== 'user') return next();
return authenticator(req, res, next);
return digestChallenge(req, res, next);
} catch (err) {
stats.increment('sbc.terminations', ['sipStatus:500']);
logger.error(err, `${req.get('Call-ID')} Error looking up related info for inbound call`);
+30 -4
View File
@@ -1,5 +1,8 @@
const CIDRMatcher = require('cidr-matcher');
const express = require('express');
const rtpCharacteristics = require('../data/rtp-transcoding');
const srtpCharacteristics = require('../data/srtp-transcoding');
let idx = 0;
const isWSS = (req) => {
@@ -19,8 +22,8 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
/* Allow Feature server to inject DTMF to both leg except call from Teams*/
if (!teams) {
dstOpts.flags.push('inject DTMF');
srcOpts.flags.push('inject DTMF');
}
@@ -96,7 +99,6 @@ const handleErrors = (logger, app, resolve, reject, e) => {
const createHealthCheckApp = (port, logger) => {
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
@@ -178,6 +180,28 @@ const parseConnectionIp = (sdp) => {
return arr ? arr[1] : null;
};
/**
* Checks if ip is one of MS Teams sip signalling ips
* https://learn.microsoft.com/en-us/azure/communication-services/concepts
* /telephony/direct-routing-infrastructure#sip-signaling-fqdns
* @param ip IP address, example 172.31.0.1
* */
const isMSTeamsCIDR = (ip) => {
const cidrs = [
'52.112.0.0/14',
'52.120.0.0/14'
];
const matcher = new CIDRMatcher(cidrs);
return matcher.contains(ip);
};
const isPrivateVoipNetwork = (ip) => {
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 = {
isWSS,
@@ -194,5 +218,7 @@ module.exports = {
createHealthCheckApp,
nudgeCallCounts,
roundTripTime,
parseConnectionIp
parseConnectionIp,
isMSTeamsCIDR,
isPrivateVoipNetwork
};
+4509 -1958
View File
File diff suppressed because it is too large Load Diff
+18 -17
View File
@@ -1,6 +1,6 @@
{
"name": "sbc-inbound",
"version": "v0.8.2",
"version": "0.9.0",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
@@ -20,34 +20,35 @@
},
"scripts": {
"start": "node app",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_NETWORK_CIDR='127.0.0.1/32' JAMBONES_HOSTING=1 SBC_ACCOUNT_SID=ed649e33-e771-403a-8c99-1780eabbc803 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=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",
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.7.4",
"@jambonz/http-authenticator": "^0.2.2",
"@jambonz/db-helpers": "^0.9.3",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/siprec-client-utils": "^0.2.4",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.2.5",
"aws-sdk": "^2.1261.0",
"@jambonz/realtimedb-helpers": "^0.8.8",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.7",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.8",
"@jambonz/digest-utils": "^0.0.5",
"@aws-sdk/client-sns": "^3.549.0",
"@aws-sdk/client-auto-scaling": "^3.549.0",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.5.21",
"express": "^4.18.1",
"pino": "^7.11.0",
"drachtio-fn-b2b-sugar": "0.1.0",
"drachtio-srf": "^4.5.31",
"express": "^4.19.2",
"pino": "^8.20.0",
"verify-aws-sns-signature": "^0.1.0",
"xml2js": "^0.4.23"
"xml2js": "^0.6.2"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-promise": "^6.1.1",
"nyc": "^15.1.0",
"tape": "^4.15.1"
"tape": "^5.7.5"
}
}
+2
View File
@@ -36,6 +36,8 @@
Subject: uac-pcap-carrier-success
Content-Type: application/sdp
Content-Length: [len]
X-Authenticated-User: xhoaluu@jambonz.org
X-Application-Sid: APP_ID_1
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]