mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
48
Commits
v0.6.3
...
v0.7.4-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98ecfa20aa | ||
|
|
56efe50aec | ||
|
|
891d0ff38b | ||
|
|
78d6cb5f22 | ||
|
|
23255a71db | ||
|
|
4cb34ad784 | ||
|
|
f096a31a95 | ||
|
|
f671ef56bb | ||
|
|
3a2fcf5976 | ||
|
|
dd85ec762d | ||
|
|
61ec935ab8 | ||
|
|
cd7c425f7e | ||
|
|
e9b8cd595a | ||
|
|
fc7cdae22a | ||
|
|
2ca44c4f5a | ||
|
|
50ff35c582 | ||
|
|
a318b07f61 | ||
|
|
be354cde10 | ||
|
|
594e8983e8 | ||
|
|
13f7d583df | ||
|
|
6085452469 | ||
|
|
64ac043656 | ||
|
|
7e9d48ee5b | ||
|
|
fa7de71650 | ||
|
|
9d228d4805 | ||
|
|
607fb3d127 | ||
|
|
8da9c4ffa1 | ||
|
|
97131be582 | ||
|
|
3144cfd9c9 | ||
|
|
a1810e577a | ||
|
|
bd6c39d9ac | ||
|
|
24a974f48d | ||
|
|
1dfad575b9 | ||
|
|
dc30075665 | ||
|
|
e2bcc16568 | ||
|
|
a9ee51d671 | ||
|
|
9e45de261c | ||
|
|
5eb209354a | ||
|
|
4c3c167fe0 | ||
|
|
a018b03310 | ||
|
|
a61740556d | ||
|
|
07f3a55ec3 | ||
|
|
2ea516be8a | ||
|
|
d9d2fa9918 | ||
|
|
16573632fc | ||
|
|
d0f1f16d9a | ||
|
|
98886cfdbf | ||
|
|
2ec513b1b0 |
@@ -11,7 +11,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 14.x
|
||||
- run: npm ci
|
||||
- run: npm run jslint
|
||||
- run: npm test
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
IMAGE_NAME: sbc-outbound
|
||||
|
||||
jobs:
|
||||
push:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- 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
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
|
||||
# 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 "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
|
||||
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run jslint
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
FROM node:alpine as builder
|
||||
FROM node:17.4-slim
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune --production
|
||||
|
||||
FROM node:alpine as app
|
||||
WORKDIR /opt/app
|
||||
RUN npm prune
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 jambonz
|
||||
Copyright (c) 2021 Drachtio Communications Services, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -6,9 +6,12 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
|
||||
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 var');
|
||||
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET 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-outbound');
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const {pingMsTeamsGateways, equalsIgnoreOrder} = require('./lib/utils');
|
||||
const opts = Object.assign({
|
||||
timestamp: () => {return `, "time": "${new Date().toISOString()}"`;}
|
||||
}, {level: process.env.JAMBONES_LOGLEVEL || 'info'});
|
||||
@@ -33,7 +36,9 @@ const {
|
||||
lookupAllTeamsFQDNs,
|
||||
lookupAccountBySipRealm,
|
||||
lookupAccountBySid,
|
||||
lookupAccountCapacitiesBySid
|
||||
lookupAccountCapacitiesBySid,
|
||||
lookupSipGatewaysByCarrier,
|
||||
lookupCarrierBySid
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: process.env.JAMBONES_MYSQL_HOST,
|
||||
user: process.env.JAMBONES_MYSQL_USER,
|
||||
@@ -41,7 +46,14 @@ const {
|
||||
database: process.env.JAMBONES_MYSQL_DATABASE,
|
||||
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||
}, logger);
|
||||
const {createHash, retrieveHash, incrKey, decrKey, retrieveSet} = require('@jambonz/realtimedb-helpers')({
|
||||
const {
|
||||
createHash,
|
||||
retrieveHash,
|
||||
incrKey,
|
||||
decrKey,
|
||||
retrieveSet,
|
||||
isMemberOfSet
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
@@ -60,33 +72,57 @@ srf.locals = {...srf.locals,
|
||||
lookupAllTeamsFQDNs,
|
||||
lookupAccountBySipRealm,
|
||||
lookupAccountBySid,
|
||||
lookupAccountCapacitiesBySid
|
||||
lookupAccountCapacitiesBySid,
|
||||
lookupSipGatewaysByCarrier,
|
||||
lookupCarrierBySid
|
||||
},
|
||||
realtimeDbHelpers: {
|
||||
createHash,
|
||||
retrieveHash,
|
||||
incrKey,
|
||||
decrKey
|
||||
decrKey,
|
||||
isMemberOfSet
|
||||
}
|
||||
};
|
||||
const {initLocals, checkLimits, route} = require('./lib/middleware')(srf, logger, {
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
});
|
||||
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {emitter: stats});
|
||||
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {
|
||||
emitter: stats,
|
||||
dtmfListenPort: process.env.DTMF_LISTEN_PORT || 22225,
|
||||
protocol: 'udp'
|
||||
});
|
||||
srf.locals.getRtpEngine = getRtpEngine;
|
||||
|
||||
if (process.env.DRACHTIO_HOST) {
|
||||
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
const cidrs = process.env.JAMBONES_NETWORK_CIDR
|
||||
.split(',')
|
||||
.map((s) => s.trim());
|
||||
logger.info({cidrs}, 'internal network CIDRs');
|
||||
const matcher = new CIDRMatcher(cidrs);
|
||||
|
||||
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
|
||||
srf.on('connect', (err, hp) => {
|
||||
logger.info(`connected to drachtio listening on ${hp}`);
|
||||
const last = hp.split(',').pop();
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(last);
|
||||
logger.info(`connected to drachtio listening on ${hp}: adding ${arr[2]} to sbc_addresses table`);
|
||||
srf.locals.sipAddress = arr[2];
|
||||
|
||||
const hostports = hp.split(',');
|
||||
for (const hp of hostports) {
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
|
||||
if (arr && 'udp' === arr[1] && !matcher.contains(arr[2])) {
|
||||
logger.info(`sbc public address: ${arr[2]}`);
|
||||
srf.locals.sipAddress = arr[2];
|
||||
}
|
||||
else if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
const hostport = `${arr[2]}:${arr[3]}`;
|
||||
logger.info(`sbc private address: ${hostport}`);
|
||||
srf.locals.privateSipAddress = hostport;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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});
|
||||
}
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
@@ -101,33 +137,59 @@ srf.invite((req, res) => {
|
||||
session.connect();
|
||||
});
|
||||
|
||||
/* update call stats periodically */
|
||||
setInterval(() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound']);
|
||||
}, 5000);
|
||||
if (process.env.K8S) {
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
const getCount = () => activeCallIds.size;
|
||||
const healthCheck = require('@jambonz/http-health-check');
|
||||
healthCheck({port: PORT, logger, path: '/', fn: getCount});
|
||||
}
|
||||
|
||||
const arrayCompare = (a, b) => {
|
||||
if (a.length !== b.length) return false;
|
||||
const uniqueValues = new Set([...a, ...b]);
|
||||
for (const v of uniqueValues) {
|
||||
const aCount = a.filter((e) => e === v).length;
|
||||
const bCount = b.filter((e) => e === v).length;
|
||||
if (aCount !== bCount) return false;
|
||||
}
|
||||
return true;
|
||||
if ('test' !== process.env.NODE_ENV) {
|
||||
/* update call stats periodically */
|
||||
setInterval(() => {
|
||||
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound']);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const lookupRtpServiceEndpoints = (lookup, serviceName) => {
|
||||
logger.debug(`dns lookup for ${serviceName}..`);
|
||||
lookup(serviceName, {family: 4, all: true}, (err, addresses) => {
|
||||
if (err) {
|
||||
logger.error({err}, `Error looking up ${serviceName}`);
|
||||
return;
|
||||
}
|
||||
logger.debug({addresses, rtpServers}, `dns lookup for ${serviceName} returned`);
|
||||
const addrs = addresses.map((a) => a.address);
|
||||
if (!equalsIgnoreOrder(addrs, rtpServers)) {
|
||||
rtpServers.length = 0;
|
||||
Array.prototype.push.apply(rtpServers, addrs);
|
||||
logger.info({rtpServers}, 'rtpserver endpoints have been updated');
|
||||
setRtpEngines(rtpServers.map((a) => `${a}:${process.env.RTPENGINE_PORT || 22222}`));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* update rtpengines periodically */
|
||||
if (process.env.JAMBONES_RTPENGINES) {
|
||||
if (process.env.K8S_RTPENGINE_SERVICE_NAME) {
|
||||
/* poll dns for endpoints every so often */
|
||||
const arr = /^(.*):(\d+)$/.exec(process.env.K8S_RTPENGINE_SERVICE_NAME);
|
||||
const svc = arr[1];
|
||||
logger.info(`rtpengine(s) will be found at dns name: ${svc}`);
|
||||
const {lookup} = require('dns');
|
||||
lookupRtpServiceEndpoints(lookup, svc);
|
||||
setInterval(lookupRtpServiceEndpoints.bind(null, lookup, svc), process.env.RTPENGINE_DNS_POLL_INTERVAL || 10000);
|
||||
}
|
||||
else if (process.env.JAMBONES_RTPENGINES) {
|
||||
/* static list of rtpengines */
|
||||
setRtpEngines([process.env.JAMBONES_RTPENGINES]);
|
||||
}
|
||||
else {
|
||||
/* poll redis periodically for rtpengines that have registered via OPTIONS ping */
|
||||
const getActiveRtpServers = async() => {
|
||||
try {
|
||||
const set = await retrieveSet(setNameRtp);
|
||||
const newArray = Array.from(set);
|
||||
logger.debug({newArray, rtpServers}, 'getActiveRtpServers');
|
||||
if (!arrayCompare(newArray, rtpServers)) {
|
||||
if (!equalsIgnoreOrder(newArray, rtpServers)) {
|
||||
logger.info({newArray}, 'resetting active rtpengines');
|
||||
setRtpEngines(newArray.map((a) => `${a}:${process.env.RTPENGINE_PORT || 22222}`));
|
||||
rtpServers.length = 0;
|
||||
@@ -137,14 +199,12 @@ else {
|
||||
logger.error({err}, 'Error setting new rtpengines');
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
getActiveRtpServers();
|
||||
}, 30000);
|
||||
getActiveRtpServers();
|
||||
}
|
||||
|
||||
const {pingMsTeamsGateways} = require('./lib/utils');
|
||||
pingMsTeamsGateways(logger, srf);
|
||||
|
||||
module.exports = {srf};
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json');
|
||||
const PORT = process.env.HTTP_PORT || 3000;
|
||||
|
||||
const sleep = (ms) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
(async function() {
|
||||
|
||||
try {
|
||||
do {
|
||||
const obj = await getJSON(`http://127.0.0.1:${PORT}/`);
|
||||
const {calls} = obj;
|
||||
if (calls === 0) {
|
||||
console.log('no calls on the system, we can exit');
|
||||
process.exit(0);
|
||||
}
|
||||
else {
|
||||
console.log(`waiting for ${calls} to exit..`);
|
||||
}
|
||||
await sleep(10000);
|
||||
} while (1);
|
||||
} catch (err) {
|
||||
console.error(err, 'Error querying health endpoint');
|
||||
process.exit(-1);
|
||||
}
|
||||
})();
|
||||
@@ -3,5 +3,6 @@
|
||||
"DTLS": "off",
|
||||
"SDES": "off",
|
||||
"ICE": "remove",
|
||||
"flags": ["media handover", "port latching"],
|
||||
"rtcp-mux": ["demux"]
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"transport-protocol": "UDP/TLS/RTP/SAVPF",
|
||||
"ICE": "force",
|
||||
"SDES": "off",
|
||||
"flags": ["generate mid", "SDES-no", "media handover"],
|
||||
"flags": ["generate mid", "SDES-no", "media handover", "port latching"],
|
||||
"rtcp-mux": ["require"]
|
||||
},
|
||||
"teams": {
|
||||
|
||||
+242
-37
@@ -1,8 +1,7 @@
|
||||
const Emitter = require('events');
|
||||
const {makeRtpEngineOpts, makeCallCountKey} = require('./utils');
|
||||
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
|
||||
const {SipError} = require('drachtio-srf');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
const {SipError, stringifyUri, parseUri} = require('drachtio-srf');
|
||||
const debug = require('debug')('jambonz:sbc-outbound');
|
||||
|
||||
const makeInviteInProgressKey = (callid) => `sbc-out-iip${callid}`;
|
||||
@@ -54,7 +53,6 @@ class CallSession extends Emitter {
|
||||
this.req = req;
|
||||
this.res = res;
|
||||
this.srf = req.srf;
|
||||
this.performLcr = this.srf.locals.dbHelpers.performLcr;
|
||||
this.logger = logger.child({callId: req.get('Call-ID')});
|
||||
this.useWss = req.locals.registration && req.locals.registration.protocol === 'wss';
|
||||
this.stats = this.srf.locals.stats;
|
||||
@@ -64,12 +62,21 @@ class CallSession extends Emitter {
|
||||
this.incrKey = req.srf.locals.realtimeDbHelpers.incrKey;
|
||||
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
|
||||
this.callCountKey = makeCallCountKey(req.locals.account_sid);
|
||||
|
||||
const {performLcr, lookupCarrierBySid, lookupSipGatewaysByCarrier} = this.srf.locals.dbHelpers;
|
||||
this.performLcr = performLcr;
|
||||
this.lookupCarrierBySid = lookupCarrierBySid;
|
||||
this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier;
|
||||
}
|
||||
|
||||
get account_sid() {
|
||||
return this.req.locals.account_sid;
|
||||
}
|
||||
|
||||
get privateSipAddress() {
|
||||
return this.srf.locals.privateSipAddress;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const teams = this.teams = this.req.locals.target === 'teams';
|
||||
const engine = this.srf.locals.getRtpEngine();
|
||||
@@ -78,11 +85,27 @@ class CallSession extends Emitter {
|
||||
return this.res.send(480);
|
||||
}
|
||||
debug(`got engine: ${JSON.stringify(engine)}`);
|
||||
const {offer, answer, del} = engine;
|
||||
const {
|
||||
offer,
|
||||
answer,
|
||||
del,
|
||||
blockMedia,
|
||||
unblockMedia,
|
||||
blockDTMF,
|
||||
unblockDTMF,
|
||||
subscribeDTMF,
|
||||
unsubscribeDTMF
|
||||
} = engine;
|
||||
const {createHash, retrieveHash} = this.srf.locals.realtimeDbHelpers;
|
||||
this.offer = offer;
|
||||
this.answer = answer;
|
||||
this.del = del;
|
||||
this.blockMedia = blockMedia;
|
||||
this.unblockMedia = unblockMedia;
|
||||
this.blockDTMF = blockDTMF;
|
||||
this.unblockDTMF = unblockDTMF;
|
||||
this.subscribeDTMF = subscribeDTMF;
|
||||
this.unsubscribeDTMF = unsubscribeDTMF;
|
||||
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, teams);
|
||||
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
|
||||
@@ -101,14 +124,19 @@ class CallSession extends Emitter {
|
||||
};
|
||||
|
||||
if (this.req.locals.registration) {
|
||||
debug(`sending call to user ${JSON.stringify(this.req.locals.registration)}`);
|
||||
debug(`sending call to registered user ${JSON.stringify(this.req.locals.registration)}`);
|
||||
const contact = this.req.locals.registration.contact;
|
||||
if (contact.includes('transport=ws')) {
|
||||
uris = [contact];
|
||||
let destUri = contact;
|
||||
if (this.req.has('X-Override-To')) {
|
||||
const dest = this.req.get('X-Override-To');
|
||||
const uri = parseUri(contact);
|
||||
uri.user = dest;
|
||||
destUri = stringifyUri(uri);
|
||||
this.logger.info(`overriding destination user with ${dest}, so final uri is ${destUri}`);
|
||||
}
|
||||
else {
|
||||
uris = [destUri];
|
||||
if (!contact.includes('transport=ws')) {
|
||||
proxy = this.req.locals.registration.proxy;
|
||||
uris = [this.req.uri];
|
||||
}
|
||||
}
|
||||
else if (this.req.locals.target === 'forward') {
|
||||
@@ -129,27 +157,64 @@ class CallSession extends Emitter {
|
||||
else {
|
||||
debug('calling lcr');
|
||||
try {
|
||||
/**
|
||||
* We normalize the called number by removing a leading + before sending it to LCR..
|
||||
* but LCR will return us an array of sip uris, with leading + for carriers that require it
|
||||
*/
|
||||
const routableNumber = this.req.calledNumber.startsWith('+') ?
|
||||
this.req.calledNumber.slice(1) :
|
||||
this.req.calledNumber;
|
||||
const gateways = await this.performLcr(routableNumber, this.account_sid);
|
||||
if (!gateways || gateways.length === 0) throw new Error('no routes found');
|
||||
debug(`got gateways: ${JSON.stringify(gateways)}`);
|
||||
gateways.forEach((gw) => mapGateways.set(gw.uri, {
|
||||
name: gw.name,
|
||||
auth: gw.auth,
|
||||
diversion: gw.diversion,
|
||||
hostport: gw.hostport
|
||||
}));
|
||||
uris = gateways.map((gw) => gw.uri);
|
||||
/* was a specific carrier requested */
|
||||
const voip_carrier_sid = this.req.get('X-Requested-Carrier-Sid');
|
||||
if (voip_carrier_sid) {
|
||||
const vc = await this.lookupCarrierBySid(voip_carrier_sid);
|
||||
const gateways = await this.lookupSipGatewaysByCarrier(voip_carrier_sid);
|
||||
const gws = (gateways || [])
|
||||
.filter((gw) => gw.outbound);
|
||||
if (gws.length) {
|
||||
uris = [];
|
||||
gws.forEach((o) => {
|
||||
const prefix = vc.tech_prefix;
|
||||
const hostport = !o.port || 5060 === o.port ? o.ipv4 : `${o.ipv4}:${o.port}`;
|
||||
const prependPlus = vc.e164_leading_plus && !this.req.calledNumber.startsWith('0');
|
||||
const u = `sip:${prefix ? prefix : ''}${prependPlus ? '+' : ''}${this.req.calledNumber}@${hostport}`;
|
||||
const obj = {
|
||||
name: vc.name,
|
||||
diversion: vc.diversion,
|
||||
hostport
|
||||
};
|
||||
if (vc.register_username && vc.register_password) {
|
||||
obj.auth = {
|
||||
username: vc.register_username,
|
||||
password: vc.register_password
|
||||
};
|
||||
}
|
||||
mapGateways.set(u, obj);
|
||||
uris.push(u);
|
||||
});
|
||||
this.logger.debug({uris, voip_carrier_sid}, 'selected outbound gateways for requested carrier');
|
||||
}
|
||||
else {
|
||||
this.logger.info({voip_carrier_sid}, 'no outbound gateways found for requested carrier');
|
||||
}
|
||||
}
|
||||
if (mapGateways.size === 0) {
|
||||
/**
|
||||
* We normalize the called number by removing a leading + before sending it to LCR..
|
||||
* but LCR will return us an array of sip uris, with leading + for carriers that require it
|
||||
*/
|
||||
const routableNumber = this.req.calledNumber.startsWith('+') ?
|
||||
this.req.calledNumber.slice(1) :
|
||||
this.req.calledNumber;
|
||||
const gateways = await this.performLcr(routableNumber, this.account_sid);
|
||||
if (!gateways || gateways.length === 0) throw new Error('no routes found');
|
||||
debug(`got gateways: ${JSON.stringify(gateways)}`);
|
||||
gateways.forEach((gw) => mapGateways.set(gw.uri, {
|
||||
name: gw.name,
|
||||
auth: gw.auth,
|
||||
diversion: gw.diversion,
|
||||
hostport: gw.hostport
|
||||
}));
|
||||
uris = gateways.map((gw) => gw.uri);
|
||||
}
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
this.logger.error(err, 'Error performing lcr');
|
||||
return this.res.send(488);
|
||||
this.res.send(488);
|
||||
return this.srf.endSession(this.req);
|
||||
}
|
||||
debug(`sending call to PSTN ${uris}`);
|
||||
}
|
||||
@@ -212,13 +277,30 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
|
||||
try {
|
||||
const responseHeaders = this.privateSipAddress ? {Contact: `<sip:${this.privateSipAddress}>`} : {};
|
||||
|
||||
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
|
||||
proxy,
|
||||
passFailure,
|
||||
proxyRequestHeaders: ['all', '-X-MS-Teams-FQDN', '-X-MS-Teams-Tenant-FQDN', 'X-CID', '-Allow',
|
||||
'-Session-Expires', 'Min-SE'],
|
||||
proxyResponseHeaders: ['all', '-Allow', '-Session-Expires'],
|
||||
proxyRequestHeaders: [
|
||||
'all',
|
||||
'-X-MS-Teams-FQDN',
|
||||
'-X-MS-Teams-Tenant-FQDN',
|
||||
'X-CID',
|
||||
'-Allow',
|
||||
'-Session-Expires',
|
||||
'-X-Requested-Carrier-Sid',
|
||||
'-X-Jambonz-Routing',
|
||||
'-X-Jambonz-FS-UUID',
|
||||
'Min-SE'
|
||||
],
|
||||
proxyResponseHeaders: [
|
||||
'all',
|
||||
'-Allow',
|
||||
'-Session-Expires'
|
||||
],
|
||||
headers: hdrs,
|
||||
responseHeaders,
|
||||
auth: gw ? gw.auth : undefined,
|
||||
localSdpB: response.sdp,
|
||||
localSdpA: async(sdp, res) => {
|
||||
@@ -228,6 +310,7 @@ class CallSession extends Emitter {
|
||||
...this.rtpEngineOpts.uas.mediaOpts,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
'to-tag': this.rtpEngineOpts.uac.tag,
|
||||
flags: ['single codec'],
|
||||
sdp
|
||||
};
|
||||
const response = await this.answer(opts);
|
||||
@@ -241,6 +324,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}, {
|
||||
cbRequest: async(err, inv) => {
|
||||
if (err) return this.logger.info({err}, 'CallSession error sending INVITE');
|
||||
let trunk = gw ? gw.name : null;
|
||||
if (!trunk) {
|
||||
if (teams) trunk = 'Microsoft Teams';
|
||||
@@ -272,6 +356,7 @@ class CallSession extends Emitter {
|
||||
|
||||
// successfully connected
|
||||
this.logger.info(`call connected to ${uri}`);
|
||||
this.connectedUri = uri;
|
||||
debug('call connected');
|
||||
this.emit('connected', uri);
|
||||
|
||||
@@ -285,14 +370,15 @@ class CallSession extends Emitter {
|
||||
!(err instanceof SipError) || // unexpected error
|
||||
err.status === 487) { // caller hung up
|
||||
|
||||
const abandoned = err.message.includes('rtpengine failed: Unknown call-id');
|
||||
const abandoned = err.message && err.message.includes('rtpengine failed: Unknown call-id');
|
||||
const status = err.status || (abandoned ? 487 : 500);
|
||||
if (err instanceof SipError) this.logger.info(`final call failure ${status}`);
|
||||
else if (!abandoned) this.logger.error(err, 'unexpected call failure');
|
||||
debug(`got final outdial error: ${err}`);
|
||||
if (!passFailure) this.res.send(status);
|
||||
this.emit('failed');
|
||||
this.rtpEngineResource.destroy();
|
||||
this.rtpEngineResource.destroy()
|
||||
.catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure'));
|
||||
const tags = ['accepted:no', `sipStatus:${status}`];
|
||||
this.stats.increment('sbc.originations', tags);
|
||||
|
||||
@@ -310,8 +396,9 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if ('abandonded' !== err.message) this.logger.error(err, `Error setting up outbonund call to: ${uris}`);
|
||||
if ('abandonded' !== err.message) this.logger.error(err, `Error setting up outbound call to: ${uris}`);
|
||||
this.emit('failed');
|
||||
this.srf.endSession(this.req);
|
||||
this.rtpEngineResource.destroy();
|
||||
}
|
||||
}
|
||||
@@ -335,6 +422,7 @@ class CallSession extends Emitter {
|
||||
this.logger.info('call ended');
|
||||
this.rtpEngineResource.destroy();
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uac.tag);
|
||||
dlg.other.destroy();
|
||||
|
||||
this.decrKey(this.callCountKey)
|
||||
@@ -356,21 +444,74 @@ class CallSession extends Emitter {
|
||||
duration: Math.floor((now - callStart) / 1000)
|
||||
}).catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
|
||||
}
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
});
|
||||
|
||||
this.subscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uac.tag,
|
||||
this._onDTMF.bind(this, uas));
|
||||
|
||||
uas.on('modify', this._onReinvite.bind(this, uas));
|
||||
uac.on('modify', this._onReinvite.bind(this, uac));
|
||||
|
||||
uas.on('refer', this._onFeatureServerTransfer.bind(this, uas));
|
||||
uac.on('refer', this._onRefer.bind(this, uac));
|
||||
|
||||
uas.on('info', this._onInfo.bind(this, uas));
|
||||
uac.on('info', this._onInfo.bind(this, uac));
|
||||
|
||||
// default forwarding of other request types
|
||||
forwardInDialogRequests(uac, ['info', 'notify', 'options', 'message']);
|
||||
forwardInDialogRequests(uac, ['notify', 'options', 'message']);
|
||||
}
|
||||
|
||||
async _onRefer(dlg, req, res) {
|
||||
/* REFER coming in from a sip device, forward to feature server */
|
||||
try {
|
||||
const response = await dlg.other.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
'Refer-To': req.get('Refer-To'),
|
||||
'Referred-By': req.get('Referred-By'),
|
||||
'User-Agent': req.get('User-Agent')
|
||||
}
|
||||
});
|
||||
res.send(response.status, response.reason);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'CallSession:_onRefer: error handling incoming REFER');
|
||||
}
|
||||
}
|
||||
|
||||
async _onDTMF(dlg, payload) {
|
||||
this.logger.info({payload}, '_onDTMF');
|
||||
try {
|
||||
let dtmf;
|
||||
switch (payload.event) {
|
||||
case 10:
|
||||
dtmf = '*';
|
||||
break;
|
||||
case 11:
|
||||
dtmf = '#';
|
||||
break;
|
||||
default:
|
||||
dtmf = '' + payload.event;
|
||||
break;
|
||||
}
|
||||
await dlg.request({
|
||||
method: 'INFO',
|
||||
headers: {
|
||||
'Content-Type': 'application/dtmf-relay'
|
||||
},
|
||||
body: `Signal=${dtmf}
|
||||
Duration=${payload.duration} `
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Error sending INFO application/dtmf-relay');
|
||||
}
|
||||
}
|
||||
|
||||
async _onReinvite(dlg, req, res) {
|
||||
try {
|
||||
const reason = req.get('X-Reason');
|
||||
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
|
||||
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
|
||||
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
|
||||
@@ -384,13 +525,25 @@ class CallSession extends Emitter {
|
||||
direction,
|
||||
sdp: req.body,
|
||||
};
|
||||
if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
|
||||
|
||||
|
||||
let response = await this.offer(opts);
|
||||
if ('ok' !== response.result) {
|
||||
res.send(488);
|
||||
throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`);
|
||||
}
|
||||
const sdp = await dlg.other.modify(response.sdp);
|
||||
|
||||
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
|
||||
let sdp;
|
||||
if (reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason)) {
|
||||
this.logger.info(`got a reinvite from FS to ${reason}`);
|
||||
sdp = dlg.other.remote.sdp;
|
||||
}
|
||||
else {
|
||||
sdp = await dlg.other.modify(response.sdp);
|
||||
this.logger.info({sdp}, 'CallSession:_onReinvite: got sdp from 200 OK to invite we sent');
|
||||
}
|
||||
opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...answerMedia,
|
||||
@@ -403,12 +556,49 @@ class CallSession extends Emitter {
|
||||
res.send(488);
|
||||
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
|
||||
}
|
||||
this.logger.info({sdp: response.sdp}, 'CallSession:_onReinvite: sending back upstream');
|
||||
res.send(200, {body: response.sdp});
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
}
|
||||
|
||||
async _onInfo(dlg, req, res) {
|
||||
try {
|
||||
if (dlg.type === 'uas' && req.has('X-Reason')) {
|
||||
const toTag = this.rtpEngineOpts.uac.tag;
|
||||
const reason = req.get('X-Reason');
|
||||
const opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
'from-tag': toTag
|
||||
};
|
||||
this.logger.info(`_onInfo: got request ${reason}`);
|
||||
res.send(200);
|
||||
|
||||
if (reason.startsWith('mute')) {
|
||||
const response = Promise.all([this.blockMedia(opts), this.blockDTMF(opts)]);
|
||||
this.logger.info({response}, `_onInfo: response to rtpengine command for ${reason}`);
|
||||
}
|
||||
else if (reason.startsWith('unmute')) {
|
||||
const response = Promise.all([this.unblockMedia(opts), this.unblockDTMF(opts)]);
|
||||
this.logger.info({response}, `_onInfo: response to rtpengine command for ${reason}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const response = await dlg.other.request({
|
||||
method: 'INFO',
|
||||
headers: req.headers,
|
||||
body: req.body
|
||||
});
|
||||
res.send(response.status, {
|
||||
headers: response.headers,
|
||||
body: response.body
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
|
||||
}
|
||||
}
|
||||
|
||||
async _onFeatureServerTransfer(dlg, req, res) {
|
||||
try {
|
||||
@@ -417,8 +607,22 @@ class CallSession extends Emitter {
|
||||
this.logger.info({uri, referTo}, 'received REFER from feature server');
|
||||
const arr = /context-(.*)/.exec(uri.user);
|
||||
if (!arr) {
|
||||
this.logger.info(`invalid Refer-To header: ${referTo.uri}`);
|
||||
return res.send(501);
|
||||
/* call transfer requested */
|
||||
const referredBy = req.getParsedHeader('Referred-By');
|
||||
if (!referredBy) return res.send(400);
|
||||
const u = parseUri(referredBy.uri);
|
||||
const farEnd = parseUri(this.connectedUri);
|
||||
uri.host = farEnd.host;
|
||||
uri.port = farEnd.port;
|
||||
|
||||
const response = await this.uac.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
'Refer-To': stringifyUri(uri),
|
||||
'Referred-By': stringifyUri(u)
|
||||
}
|
||||
});
|
||||
return res.send(response.status);
|
||||
}
|
||||
res.send(202);
|
||||
|
||||
@@ -438,6 +642,7 @@ class CallSession extends Emitter {
|
||||
this.rtpEngineResource.destroy();
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
this.uas.other.destroy();
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
|
||||
// modify rtpengine to stream to new feature server
|
||||
|
||||
+59
-39
@@ -2,15 +2,10 @@ const debug = require('debug')('jambonz:sbc-outbound');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const Registrar = require('@jambonz/mw-registrar');
|
||||
const {selectHostPort, makeCallCountKey} = require('./utils');
|
||||
|
||||
const isLocalUri = (host, req) => {
|
||||
debug({hostport: req.server.hostport}, `is ${host} local?`);
|
||||
return req.server.hostport.includes(host);
|
||||
};
|
||||
|
||||
const FS_UUID_SET_NAME = 'fsUUIDs';
|
||||
|
||||
module.exports = (srf, logger, opts) => {
|
||||
const {incrKey, decrKey} = srf.locals.realtimeDbHelpers;
|
||||
const {incrKey, decrKey, isMemberOfSet} = srf.locals.realtimeDbHelpers;
|
||||
const {stats} = srf.locals;
|
||||
const registrar = new Registrar(opts);
|
||||
const {
|
||||
@@ -26,13 +21,44 @@ module.exports = (srf, logger, opts) => {
|
||||
|
||||
if (!req.locals.account_sid) {
|
||||
logger.info('missing X-Account-Sid on outbound call');
|
||||
return res.send(403, {
|
||||
res.send(403, {
|
||||
headers: {
|
||||
'X-Reason': 'missing X-Account-Sid'
|
||||
}
|
||||
});
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
|
||||
/* must come from a valid FS */
|
||||
if (!req.has('X-Jambonz-Routing')) {
|
||||
logger.info({msg: req.msg}, 'missing X-Jambonz-Routing header');
|
||||
res.send(403, {
|
||||
headers: {
|
||||
'X-Reason': 'missing required jambonz headers'
|
||||
}
|
||||
});
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
if (process.env.K8S) {
|
||||
/* for K8S we do not use JAMBONES_CIDR so we must validate the sender by uuid FS creates */
|
||||
const fsUUID = req.get('X-Jambonz-FS-UUID');
|
||||
try {
|
||||
const exists = await isMemberOfSet(FS_UUID_SET_NAME, fsUUID);
|
||||
if (!exists || !fsUUID) {
|
||||
res.send(403, {
|
||||
headers: {
|
||||
'X-Reason': `missing or invalid FS-UUID ${fsUUID}`
|
||||
}
|
||||
});
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
} catch (err) {
|
||||
res.send(500);
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stats.increment('sbc.invites', ['direction:outbound']);
|
||||
|
||||
req.on('cancel', () => {
|
||||
@@ -40,14 +66,15 @@ module.exports = (srf, logger, opts) => {
|
||||
req.canceled = true;
|
||||
const tags = ['canceled:yes', 'sipStatus:487'];
|
||||
if (req.locals.originator) tags.push(`originator:${req.locals.originator}`);
|
||||
stats.increment('sbc.terminations', tags);
|
||||
stats.increment('sbc.origination', tags);
|
||||
});
|
||||
|
||||
try {
|
||||
req.locals.account = await lookupAccountBySid(req.locals.account_sid);
|
||||
} catch (err) {
|
||||
req.locals.logger.error({err}, `Error looking up account sid ${req.locals.account_sid}`);
|
||||
return res.send(500);
|
||||
res.send(500);
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
next();
|
||||
};
|
||||
@@ -90,7 +117,10 @@ module.exports = (srf, logger, opts) => {
|
||||
|
||||
const capacities = await lookupAccountCapacitiesBySid(account_sid);
|
||||
const limit = capacities.find((c) => c.category == 'voice_call_session');
|
||||
if (!limit) throw new Error('no account_capacities found');
|
||||
if (!limit) {
|
||||
logger.debug('checkLimits: no call limits specified');
|
||||
return next();
|
||||
}
|
||||
const limit_sessions = limit.quantity;
|
||||
|
||||
if (calls > limit_sessions) {
|
||||
@@ -103,6 +133,7 @@ module.exports = (srf, logger, opts) => {
|
||||
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
|
||||
|
||||
res.send(503, 'Maximum Calls In Progress');
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
@@ -116,31 +147,26 @@ module.exports = (srf, logger, opts) => {
|
||||
const {lookupAccountBySipRealm} = req.srf.locals.dbHelpers;
|
||||
logger.info(`received outbound INVITE to ${req.uri} from server at ${req.server.hostport}`);
|
||||
const uri = parseUri(req.uri);
|
||||
const desiredRouting = req.get('X-Jambonz-Routing');
|
||||
|
||||
if (!uri.user || !uri.host) {
|
||||
if (!uri || !uri.user || !uri.host) {
|
||||
logger.info({uri: req.uri}, 'invalid request-uri on outbound call, rejecting');
|
||||
res.send(404, {
|
||||
res.send(400, {
|
||||
headers: {
|
||||
'X-Reason': 'invalid request-uri'
|
||||
}
|
||||
});
|
||||
return;
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
const aor = `${uri.user}@${uri.host}`;
|
||||
let reg;
|
||||
const dotDecimalHost = /^[0-9\.]+$/.test(uri.host);
|
||||
|
||||
debug(`received outbound INVITE to ${req.calledNumber} from server at ${req.server.hostport}`);
|
||||
|
||||
if (req.has('X-MS-Teams-FQDN') && req.has('X-MS-Teams-Tenant-FQDN')) {
|
||||
if ('teams' === desiredRouting) {
|
||||
logger.debug('This is a call to ms teams');
|
||||
req.locals.target = 'teams';
|
||||
return next();
|
||||
}
|
||||
else if (!dotDecimalHost) {
|
||||
// uri host is not a dot-decimal address, so try to look up user
|
||||
logger.debug(`searching for registered user ${aor}`);
|
||||
reg = await registrar.query(aor);
|
||||
else if ('user' === desiredRouting) {
|
||||
const aor = `${uri.user}@${uri.host}`;
|
||||
const reg = await registrar.query(aor);
|
||||
if (reg) {
|
||||
// user is registered..find out which sbc is handling it
|
||||
// us => we can put the call through
|
||||
@@ -155,34 +181,28 @@ module.exports = (srf, logger, opts) => {
|
||||
}
|
||||
req.locals.registration = reg;
|
||||
req.locals.target = 'user';
|
||||
return next();
|
||||
}
|
||||
else {
|
||||
// if the sip domain is one of ours return 404
|
||||
const account = await lookupAccountBySipRealm(uri.host);
|
||||
if (account) {
|
||||
logger.info({host: uri.host, account}, `returning 404 to unregistered user in valid domain: ${req.uri}`);
|
||||
res.send(404);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
logger.info({host: uri.host, account}, `returning 404 to user in invalid domain: ${req.uri}`);
|
||||
}
|
||||
res.send(404);
|
||||
return req.srf.endSession(req);
|
||||
}
|
||||
}
|
||||
if (!dotDecimalHost || !isLocalUri(uri.host, req)) {
|
||||
else if ('sip' === desiredRouting) {
|
||||
// call that needs to be forwarded to a sip endpoint
|
||||
logger.info(`forwarding call to sip endpoint ${req.uri}`);
|
||||
req.locals.target = 'forward';
|
||||
return next();
|
||||
}
|
||||
|
||||
// if the called number is digits only (after possible leading plus sign) and long enough, do lcr
|
||||
if (!/^\d+$/.test(req.calledNumber.slice(1)) || req.calledNumber.length < 8) {
|
||||
debug(`unable to route call to ${aor}; no registered user found`);
|
||||
logger.info(`unable to route call to ${aor}; no registered user found`);
|
||||
return res.send(404);
|
||||
else if ('phone' === desiredRouting) {
|
||||
debug('sending call to LCR');
|
||||
req.locals.target = 'lcr';
|
||||
}
|
||||
|
||||
debug('sending call to LCR');
|
||||
req.locals.target = 'lcr';
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
+13
-1
@@ -73,9 +73,21 @@ const pingMsTeamsGateways = (logger, srf) => {
|
||||
|
||||
const makeCallCountKey = (sid) => `${sid}:outcalls`;
|
||||
|
||||
const equalsIgnoreOrder = (a, b) => {
|
||||
if (a.length !== b.length) return false;
|
||||
const uniqueValues = new Set([...a, ...b]);
|
||||
for (const v of uniqueValues) {
|
||||
const aCount = a.filter((e) => e === v).length;
|
||||
const bCount = b.filter((e) => e === v).length;
|
||||
if (aCount !== bCount) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
makeRtpEngineOpts,
|
||||
selectHostPort,
|
||||
pingMsTeamsGateways,
|
||||
makeCallCountKey
|
||||
makeCallCountKey,
|
||||
equalsIgnoreOrder
|
||||
};
|
||||
|
||||
Generated
+5924
-3697
File diff suppressed because it is too large
Load Diff
+18
-15
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.4.6",
|
||||
"version": "v0.7.3",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"sip",
|
||||
@@ -22,26 +22,29 @@
|
||||
"description": "jambonz session border controller application for outbound calls",
|
||||
"scripts": {
|
||||
"start": "node app",
|
||||
"test": "NODE_ENV=test JAMBONZ_HOSTING=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_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ",
|
||||
"test": "NODE_ENV=test JAMBONZ_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 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_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 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.6.12",
|
||||
"@jambonz/mw-registrar": "0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.3",
|
||||
"@jambonz/rtpengine-utils": "^0.1.12",
|
||||
"@jambonz/stats-collector": "^0.1.5",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"debug": "^4.3.1",
|
||||
"@jambonz/db-helpers": "^0.6.17",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "0.2.2",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.24",
|
||||
"@jambonz/rtpengine-utils": "^0.3.1",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.1.6",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.3",
|
||||
"drachtio-fn-b2b-sugar": "^0.0.12",
|
||||
"drachtio-srf": "^4.4.49",
|
||||
"pino": "^6.11.2"
|
||||
"drachtio-srf": "^4.4.59",
|
||||
"husky": "^7.0.4",
|
||||
"pino": "^7.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.2.2"
|
||||
"tape": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ networks:
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
platform: linux/x86_64
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
@@ -118,7 +119,7 @@ services:
|
||||
ipv4_address: 172.39.0.24
|
||||
|
||||
influxdb:
|
||||
image: influxdb:1.8-alpine
|
||||
image: influxdb:1.8
|
||||
ports:
|
||||
- "8086:8086"
|
||||
networks:
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
Subject: uac-pcap-carrier-success
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: phone
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
@@ -103,4 +104,3 @@
|
||||
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
|
||||
|
||||
</scenario>
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-device-unknown-user
|
||||
X-Jambonz-Routing: user
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-device-unknown-user
|
||||
X-Jambonz-Routing: user
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-device-unknown-user
|
||||
X-Jambonz-Routing: user
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
Max-Forwards: 70
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: ff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: phone
|
||||
Subject: uac-pcap-carrier-fail-limits
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
Subject: uac-pcap-carrier-success
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: phone
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
Max-Forwards: 70
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: phone
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
Subject: uac-pcap-device-success
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: ff649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: user
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-device-success
|
||||
X-Jambonz-Routing: user
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
Contact: sip:sipp@[local_ip]:[local_port]
|
||||
Max-Forwards: 70
|
||||
Subject: uac-pcap-device-success
|
||||
X-Jambonz-Routing: user
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
Subject: uac-sip-uri-auth-success
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: sip
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
@@ -90,6 +91,7 @@
|
||||
Subject: uac-sip-uri-auth-success
|
||||
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
|
||||
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
|
||||
X-Jambonz-Routing: sip
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
|
||||
@@ -126,7 +128,7 @@
|
||||
To: <sip:sip:john@jambonz.org>[peer_tag_param]
|
||||
[last_Call-ID:]
|
||||
CSeq: 2 ACK
|
||||
Subject: uac-sip-uri-auth-success
|
||||
Subject:
|
||||
Content-Length: 0
|
||||
|
||||
]]>
|
||||
|
||||
Reference in New Issue
Block a user