mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2026-10-04 02:04:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea3033ae0 | ||
|
|
9b9eaa37fb | ||
|
|
56e0e3c0f2 | ||
|
|
6a6b4dd26f | ||
|
|
a3799576a2 | ||
|
|
63e72fbfce | ||
|
|
e5dce35bab | ||
|
|
b6675de2bd | ||
|
|
66e73d8353 | ||
|
|
a25aa08e17 | ||
|
|
b1296c8e89 | ||
|
|
17df3a4ca8 | ||
|
|
f2e820915d |
@@ -33,7 +33,10 @@ const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
|
||||
}),
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
|
||||
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
|
||||
})
|
||||
}),
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD && {
|
||||
sentinelPassword: process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD
|
||||
}),
|
||||
} : null;
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
|
||||
+52
-33
@@ -6,7 +6,8 @@ const {
|
||||
SdpWantsSDES,
|
||||
nudgeCallCounts,
|
||||
roundTripTime,
|
||||
parseConnectionIp
|
||||
parseConnectionIp,
|
||||
isPrivateVoipNetwork
|
||||
} = require('./utils');
|
||||
|
||||
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
|
||||
@@ -64,6 +65,7 @@ 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 = [];
|
||||
}
|
||||
|
||||
get isFromMSTeams() {
|
||||
@@ -176,7 +178,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();
|
||||
@@ -247,7 +249,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 +320,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 +329,13 @@ class CallSession extends Emitter {
|
||||
dlg.on('modify', this._onReinvite.bind(this, dlg));
|
||||
}
|
||||
|
||||
_stopRecording() {
|
||||
if (this.srsClients.length) {
|
||||
this.srsClients.forEach((c) => c.stop());
|
||||
this.srsClients = [];
|
||||
}
|
||||
}
|
||||
|
||||
_setHandlers({uas, uac}) {
|
||||
this.emit('connected');
|
||||
const callStart = Date.now();
|
||||
@@ -412,10 +419,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);
|
||||
@@ -689,6 +693,7 @@ Duration=${payload.duration} `
|
||||
const to = this.req.getParsedHeader('To');
|
||||
const aorFrom = from.uri;
|
||||
const aorTo = to.uri;
|
||||
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
|
||||
this.logger.info({to, from}, 'startCallRecording request for a call');
|
||||
|
||||
const srsUrl = req.get('X-Srs-Url');
|
||||
@@ -696,7 +701,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;
|
||||
@@ -706,13 +711,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,
|
||||
@@ -727,42 +733,51 @@ 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();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.start())
|
||||
)).every((r) => r);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error starting SipRec call recording');
|
||||
}
|
||||
}
|
||||
else if (reason === 'stopCallRecording') {
|
||||
if (!this.srsClient) {
|
||||
if (!this.srsClients.length) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding stopCallRecording request because we are not recording');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
succeeded = await this.srsClient.stop();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.stop())
|
||||
)).every((r) => r);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error stopping SipRec call recording');
|
||||
}
|
||||
this.srsClient = null;
|
||||
this.srsClients = [];
|
||||
}
|
||||
else if (reason === 'pauseCallRecording') {
|
||||
if (!this.srsClient || this.srsClient.paused) {
|
||||
if (!this.srsClients.length || this.srsClients.every((c) => c.paused)) {
|
||||
this.logger.info('discarding invalid pauseCallRecording request');
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.pause();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.pause())
|
||||
)).every((r) => r);
|
||||
}
|
||||
else if (reason === 'resumeCallRecording') {
|
||||
if (!this.srsClient || !this.srsClient.paused) {
|
||||
if (!this.srsClients.length || !this.srsClients.every((c) => c.paused)) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding invalid resumeCallRecording request');
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.resume();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.resume())
|
||||
)).every((r) => r);
|
||||
}
|
||||
res.send(succeeded ? 200 : 503);
|
||||
}
|
||||
@@ -815,8 +830,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`);
|
||||
@@ -891,6 +906,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', () => {
|
||||
@@ -900,19 +916,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');
|
||||
}
|
||||
}
|
||||
|
||||
+38
-18
@@ -45,7 +45,7 @@ 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 = ?';
|
||||
|
||||
@@ -153,24 +153,41 @@ 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, logger, 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) {
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: selected,
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
account_sid: a[0].account_sid,
|
||||
application_sid: selected.application_sid,
|
||||
account: a[0]
|
||||
};
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
|
||||
/* no match, so let's look for case #2 */
|
||||
try {
|
||||
logger.info({
|
||||
@@ -178,7 +195,9 @@ 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, logger, 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
|
||||
@@ -229,6 +248,7 @@ 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
|
||||
.sort((a, b) => b.netmask - a.netmask)
|
||||
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address))
|
||||
.map((gw) => {
|
||||
return {
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
const debug = require('debug')('jambonz:sbc-inbound');
|
||||
const assert = require('assert');
|
||||
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()) :
|
||||
@@ -129,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) {
|
||||
@@ -155,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);
|
||||
@@ -178,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) {
|
||||
|
||||
+28
-2
@@ -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) => {
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Generated
+636
-994
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -29,8 +29,8 @@
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||
"@jambonz/rtpengine-utils": "^0.4.3",
|
||||
"@jambonz/siprec-client-utils": "^0.2.5",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/siprec-client-utils": "^0.2.6",
|
||||
"@jambonz/stats-collector": "^0.1.9",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@jambonz/digest-utils": "^0.0.3",
|
||||
"@aws-sdk/client-sns": "^3.360.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||
"drachtio-srf": "^4.5.21",
|
||||
"drachtio-srf": "^4.5.31",
|
||||
"express": "^4.18.1",
|
||||
"pino": "^7.11.0",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user