mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2026-10-04 02:04:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
769526be24 | ||
|
|
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 |
@@ -2,16 +2,10 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `master` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: sbc-inbound
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -20,32 +14,41 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@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
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: npm ci
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/amd64 node:18.12.1-alpine3.16 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
|
||||
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
# sbc-inbound 
|
||||
|
||||
This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
|
||||
This application provides a part of the SBC (Session Border Controller) functionality of jambonz platform. It handles incoming INVITE requests from carrier sip trunks or from sip devices and webrtc applications. SIP INVITEs from known carriers are allowed in, while INVITEs from sip devices are challenged to authenticate. SIP traffic that is allowed in is sent on to a jambonz application server in a private subnet.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is provided via the [npmjs config](https://www.npmjs.com/package/config) package. The following elements make up the configuration for the application:
|
||||
Configuration is provided via environment variables:
|
||||
|
||||
| variable | meaning | required?|
|
||||
|----------|----------|---------|
|
||||
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|
||||
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
||||
|DRACHTIO_SECRET| shared secret|yes|
|
||||
|HTTP_PORT| http listen port |no|
|
||||
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|
|
||||
|JAMBONES_MYSQL_HOST| mysql host|yes|
|
||||
|JAMBONES_MYSQL_PORT| mysql port |no|
|
||||
|JAMBONES_MYSQL_USER| mysql username|yes|
|
||||
|JAMBONES_MYSQL_PASSWORD| mysql password|yes|
|
||||
|JAMBONES_MYSQL_DATABASE| mysql data|yes|
|
||||
|JAMBONES_MYSQL_CONNECTION_LIMIT| mysql connection limit |no|
|
||||
|DTMF_LISTEN_PORT| DTMF listening port |no|
|
||||
|JAMBONES_NG_PROTOCOL| rtpengine NG protocol |no|
|
||||
|RTPENGINE_PORT| rtpengine port |no|
|
||||
|JAMBONES_CLUSTER_ID| cluster id |no|
|
||||
|JAMBONES_NETWORK_CIDR| CIDR of private network that feature server is running in (e.g. '172.31.0.0/16')|yes|
|
||||
|JAMBONES_REDIS_HOST| redis host|yes|
|
||||
|JAMBONES_REDIS_PORT|redis port|no|
|
||||
|JAMBONES_RTPENGINES| commas-separated list of ip:ng-port for rtpengines (e.g. '172.31.32.10:22222')|no|
|
||||
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|
||||
|JAMBONES_TIME_SERIES_PORT| influxdb port |no|
|
||||
|JAMBONES_RECORD_ALL_CALLS| enable auto record calls, 'yes' or 'no' |no|
|
||||
|K8S| service running as kubernetes service |no|
|
||||
|K8S_RTPENGINE_SERVICE_NAME| rtpengine service name(required for K8S) |no|
|
||||
|K8S_FEATURE_SERVER_SERVICE_NAME| feature server service name(required for K8S) |no|
|
||||
|
||||
##### drachtio server location
|
||||
```
|
||||
{
|
||||
|
||||
@@ -3,10 +3,12 @@ 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');
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env vars');
|
||||
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
||||
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
|
||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
const srf = new Srf('sbc-inbound');
|
||||
const opts = Object.assign({
|
||||
@@ -28,6 +30,7 @@ const {
|
||||
commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
|
||||
});
|
||||
const StatsCollector = require('@jambonz/stats-collector');
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const stats = new StatsCollector(logger);
|
||||
const {equalsIgnoreOrder, createHealthCheckApp, systemHealth} = require('./lib/utils');
|
||||
const {LifeCycleEvents} = require('./lib/constants');
|
||||
@@ -49,6 +52,7 @@ const {
|
||||
queryCallLimits
|
||||
} = 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,7 +66,7 @@ const {
|
||||
removeFromSet,
|
||||
incrKey,
|
||||
decrKey} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
|
||||
@@ -129,7 +133,6 @@ const {
|
||||
const CallSession = require('./lib/call-session');
|
||||
|
||||
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const cidrs = process.env.JAMBONES_NETWORK_CIDR
|
||||
.split(',')
|
||||
.map((s) => s.trim());
|
||||
@@ -164,6 +167,23 @@ else {
|
||||
logger.info(`listening in outbound mode on port ${process.env.DRACHTIO_PORT}`);
|
||||
});
|
||||
srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET});
|
||||
srf.on('connect', (err, hp) => {
|
||||
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') {
|
||||
srf.on('error', (err) => {
|
||||
@@ -230,7 +250,8 @@ if (process.env.K8S || process.env.HTTP_PORT) {
|
||||
if ('test' !== process.env.NODE_ENV) {
|
||||
/* update call stats periodically */
|
||||
setInterval(() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:inbound']);
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size,
|
||||
['direction:inbound', `instance_id:${process.env.INSTANCE_ID || 0}`]);
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
@@ -304,8 +325,18 @@ process.on('SIGUSR2', handle.bind(null, removeFromSet, setName));
|
||||
process.on('SIGTERM', handle.bind(null, removeFromSet, setName));
|
||||
|
||||
function handle(removeFromSet, setName, signal) {
|
||||
logger.info(`got signal ${signal}, removing ${srf.locals.privateSipAddress} from set ${setName}`);
|
||||
removeFromSet(setName, srf.locals.privateSipAddress);
|
||||
logger.info(`got signal ${signal}`);
|
||||
if (srf.locals.privateSipAddress && setName) {
|
||||
logger.info(`removing ${srf.locals.privateSipAddress} from set ${setName}`);
|
||||
removeFromSet(setName, srf.locals.privateSipAddress);
|
||||
}
|
||||
if (process.env.K8S) {
|
||||
lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
|
||||
if (0 === activeCallIds.size) {
|
||||
logger.info('exiting immediately since we have no calls in progress');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {srf, logger};
|
||||
|
||||
@@ -57,6 +57,9 @@ module.exports = (logger) => {
|
||||
}
|
||||
})();
|
||||
}
|
||||
else if (process.env.K8S) {
|
||||
lifecycleEmitter.scaleIn = () => process.exit(0);
|
||||
}
|
||||
|
||||
return {lifecycleEmitter};
|
||||
};
|
||||
|
||||
+72
-22
@@ -22,8 +22,10 @@ const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
|
||||
const createBLegFromHeader = (req) => {
|
||||
const from = req.getParsedHeader('From');
|
||||
const uri = parseUri(from.uri);
|
||||
if (uri && uri.user) return `<sip:${uri.user}@localhost>`;
|
||||
return '<sip:anonymous@localhost>';
|
||||
const name = from.name;
|
||||
const displayName = name ? `${name} ` : '';
|
||||
if (uri && uri.user) return `${displayName}<sip:${uri.user}@localhost>`;
|
||||
else return `${displayName}<sip:anonymous@localhost>`;
|
||||
};
|
||||
|
||||
const createSiprecBody = (headers, sdp, type, content) => {
|
||||
@@ -85,10 +87,6 @@ class CallSession extends Emitter {
|
||||
return this.req.has('X-Five9-StreamingPairId');
|
||||
}
|
||||
|
||||
get isPossibleWebRtcClient() {
|
||||
return this.req.locals.isPossibleWebRtcClient;
|
||||
}
|
||||
|
||||
subscribeForDTMF(dlg) {
|
||||
if (!this._subscribedForDTMF) {
|
||||
this._subscribedForDTMF = true;
|
||||
@@ -225,7 +223,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});
|
||||
}
|
||||
@@ -295,7 +293,6 @@ class CallSession extends Emitter {
|
||||
} catch (err) {
|
||||
this.rtpEngineResource.destroy().catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure'));
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size);
|
||||
if (err instanceof SipError) {
|
||||
const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`];
|
||||
this.stats.increment('sbc.terminations', tags);
|
||||
@@ -312,7 +309,7 @@ class CallSession extends Emitter {
|
||||
_setDlgHandlers(dlg) {
|
||||
const {callId} = dlg.sip;
|
||||
this.activeCallIds.set(callId, this);
|
||||
if (this.isPossibleWebRtcClient) this.subscribeForDTMF(dlg);
|
||||
this.subscribeForDTMF(this.uac);
|
||||
dlg.on('destroy', () => {
|
||||
debug('call ended with normal termination');
|
||||
this.logger.info('call ended with normal termination');
|
||||
@@ -335,11 +332,11 @@ class CallSession extends Emitter {
|
||||
_setHandlers({uas, uac}) {
|
||||
this.emit('connected');
|
||||
const callStart = Date.now();
|
||||
const call_sid = uac.res?.get('X-Call-Sid');
|
||||
const application_sid = this.application_sid || uac.res?.get('X-Application-Sid');
|
||||
const tags = ['accepted:yes', 'sipStatus:200', `originator:${this.req.locals.originator}`];
|
||||
this.stats.increment('sbc.terminations', tags);
|
||||
this.activeCallIds.set(this.req.get('Call-ID'), this);
|
||||
const call_sid = uac.res?.get('X-Call-Sid');
|
||||
const application_sid = this.application_sid || uac.res?.get('X-Application-Sid');
|
||||
if (this.req.locals.cdr) {
|
||||
this.req.locals.cdr = {
|
||||
...this.req.locals.cdr,
|
||||
@@ -417,7 +414,7 @@ class CallSession extends Emitter {
|
||||
});
|
||||
});
|
||||
|
||||
if (this.isPossibleWebRtcClient) this.subscribeForDTMF(uac);
|
||||
this.subscribeForDTMF(uac);
|
||||
|
||||
uas.on('modify', this._onReinvite.bind(this, uas));
|
||||
uac.on('modify', this._onReinvite.bind(this, uac));
|
||||
@@ -430,6 +427,12 @@ class CallSession extends Emitter {
|
||||
|
||||
// default forwarding of other request types
|
||||
forwardInDialogRequests(uas, ['notify', 'options', 'message']);
|
||||
|
||||
// we need special handling for invite with null sdp followed by 3pcc re-invite
|
||||
if (uas.local.sdp.includes('a=recvonly') || uas.local.sdp.includes('a=inactive')) {
|
||||
this.logger.info('incoming call is recvonly or inactive, waiting for re-invite');
|
||||
this._recvonly = true;
|
||||
}
|
||||
}
|
||||
|
||||
async _onDTMF(dlg, payload) {
|
||||
@@ -532,24 +535,70 @@ Duration=${payload.duration} `
|
||||
}
|
||||
|
||||
async _onReinvite(dlg, req, res) {
|
||||
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
|
||||
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
|
||||
const reason = req.get('X-Reason');
|
||||
const isReleasingMedia = reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason);
|
||||
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
|
||||
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
|
||||
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
|
||||
|
||||
try {
|
||||
/* check for re-invite with no SDP -- seen that from BT when they provide UUI info */
|
||||
if (!req.body) {
|
||||
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
|
||||
res.send(200, {body: dlg.local.sdp});
|
||||
if (dlg.type === 'uas' && this._recvonly) {
|
||||
/* seen this from Broadworks - initial INVITE has no SDP, then reINVITE with SDP */
|
||||
this._recvonly = false; //one-time only
|
||||
const myMungedSdp = dlg.local.sdp.replace('a=recvonly', 'a=sendrecv').replace('a=inactive', 'a=sendrecv');
|
||||
this.logger.info({myMungedSdp}, '_onReinvite (3gpp): got a reINVITE with no SDP while in recvonly mode');
|
||||
res.send(200,
|
||||
{
|
||||
body: myMungedSdp
|
||||
},
|
||||
(err, req) => {},
|
||||
async(ack) => {
|
||||
const remoteOffer = ack.body;
|
||||
this.logger.info({remoteOffer}, '_onReinvite (3gpp): got ACK for reINVITE with SDP');
|
||||
let opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...offerMedia,
|
||||
'from-tag': fromTag,
|
||||
'to-tag': toTag,
|
||||
direction,
|
||||
sdp: remoteOffer,
|
||||
};
|
||||
let response = await this.offer(opts);
|
||||
if ('ok' !== response.result) {
|
||||
res.send(488);
|
||||
throw new Error(`_onReinvite (3gpp): rtpengine failed: offer: ${JSON.stringify(response)}`);
|
||||
}
|
||||
this.logger.info({response}, '_onReinvite (3gpp): response from rtpengine for offer');
|
||||
const fsSdp = await dlg.other.modify(response.sdp);
|
||||
opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...answerMedia,
|
||||
'from-tag': fromTag,
|
||||
'to-tag': toTag,
|
||||
sdp: fsSdp
|
||||
};
|
||||
response = await this.answer(opts);
|
||||
if ('ok' !== response.result) {
|
||||
res.send(488);
|
||||
throw new Error(`_onReinvite(3gpp): rtpengine failed answer: ${JSON.stringify(response)}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
this.logger.info('got a reINVITE with no SDP; just respond with our current offer');
|
||||
res.send(200, {body: dlg.local.sdp});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const offeredSdp = Array.isArray(req.payload) && req.payload.length > 1 ?
|
||||
req.payload.find((p) => p.type === 'application/sdp').content :
|
||||
req.body;
|
||||
|
||||
const reason = req.get('X-Reason');
|
||||
const isReleasingMedia = reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason);
|
||||
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
|
||||
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
|
||||
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
|
||||
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
|
||||
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
|
||||
if (isReleasingMedia) {
|
||||
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
|
||||
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
|
||||
@@ -562,7 +611,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) {
|
||||
|
||||
+24
-11
@@ -28,7 +28,8 @@ vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ip
|
||||
FROM sip_gateways sg, voip_carriers vc
|
||||
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
|
||||
AND vc.service_provider_sid IS NOT NULL
|
||||
AND vc.is_active = 1`;
|
||||
AND vc.is_active = 1
|
||||
AND sg.inbound = 1`;
|
||||
|
||||
const sqlCarriersForAccountBySid =
|
||||
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
|
||||
@@ -100,7 +101,7 @@ module.exports = (srf, logger) => {
|
||||
};
|
||||
|
||||
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
|
||||
const did = normalizeDID(req.calledNumber);
|
||||
const did = normalizeDID(req.calledNumber) || 'anonymous';
|
||||
|
||||
try {
|
||||
/* straight DID match */
|
||||
@@ -136,12 +137,12 @@ module.exports = (srf, logger) => {
|
||||
*/
|
||||
|
||||
/* get all the carriers and gateways for the account owning this sip realm */
|
||||
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, uri.host);
|
||||
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);
|
||||
const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
|
||||
if (0 === a.length) return failure;
|
||||
return {
|
||||
fromCarrier: true,
|
||||
@@ -167,7 +168,7 @@ module.exports = (srf, logger) => {
|
||||
//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}`);
|
||||
return {
|
||||
@@ -210,11 +211,23 @@ module.exports = (srf, logger) => {
|
||||
else {
|
||||
/* find all carrier entries that have an inbound gateway matching the source IP */
|
||||
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
|
||||
const matches = gw.filter(gatewayMatchesSourceAddress.bind(null, req.source_address));
|
||||
let matches = gw
|
||||
.filter(gatewayMatchesSourceAddress.bind(null, 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);
|
||||
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');
|
||||
|
||||
@@ -234,7 +247,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],
|
||||
@@ -249,11 +262,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],
|
||||
@@ -275,7 +288,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,
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@ module.exports = (srf, logger) => {
|
||||
return async() => {
|
||||
try {
|
||||
if (process.env.K8S) {
|
||||
return process.env.K8S_FEATURE_SERVER_SERVICE_NAME;
|
||||
return process.env.K8S_FEATURE_SERVER_TRANSPORT ?
|
||||
`${process.env.K8S_FEATURE_SERVER_SERVICE_NAME};transport=${process.env.K8S_FEATURE_SERVER_TRANSPORT}` :
|
||||
process.env.K8S_FEATURE_SERVER_SERVICE_NAME;
|
||||
}
|
||||
else {
|
||||
const fs = await retrieveSet(setName);
|
||||
|
||||
Generated
+580
-2392
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "v0.7.8",
|
||||
"version": "0.8.3",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -25,20 +25,20 @@
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.7.3",
|
||||
"@jambonz/db-helpers": "^0.8.1",
|
||||
"@jambonz/http-authenticator": "^0.2.2",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.5.7",
|
||||
"@jambonz/rtpengine-utils": "^0.4.0",
|
||||
"@jambonz/siprec-client-utils": "^0.2.0",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/realtimedb-helpers": "^0.7.2",
|
||||
"@jambonz/rtpengine-utils": "^0.4.3",
|
||||
"@jambonz/siprec-client-utils": "^0.2.5",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"aws-sdk": "^2.1261.0",
|
||||
"aws-sdk": "^2.1354.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.19",
|
||||
"drachtio-srf": "^4.5.21",
|
||||
"express": "^4.18.1",
|
||||
"pino": "^7.11.0",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user