mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d70a01578d | ||
|
|
eb0de5b0ee | ||
|
|
d684fbb59b | ||
|
|
d86d03c792 |
+79
-11
@@ -1,7 +1,7 @@
|
||||
const Emitter = require('events');
|
||||
const sdpTransform = require('sdp-transform');
|
||||
const SrsClient = require('@jambonz/siprec-client-utils');
|
||||
const {makeRtpEngineOpts, nudgeCallCounts} = require('./utils');
|
||||
const {makeRtpEngineOpts, nudgeCallCounts, isPrivateVoipNetwork} = require('./utils');
|
||||
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
|
||||
const {SipError, stringifyUri, parseUri} = require('drachtio-srf');
|
||||
const debug = require('debug')('jambonz:sbc-outbound');
|
||||
@@ -11,7 +11,7 @@ const makeInviteInProgressKey = (callid) => `sbc-out-iip${callid}`;
|
||||
* this is to make sure the outgoing From has the number in the incoming From
|
||||
* and not the incoming PAI
|
||||
*/
|
||||
const createBLegFromHeader = (req, teams) => {
|
||||
const createBLegFromHeader = (req, teams, register_from_domain = null) => {
|
||||
const from = req.getParsedHeader('From');
|
||||
const uri = parseUri(from.uri);
|
||||
let user = uri.user || 'anonymous';
|
||||
@@ -22,6 +22,11 @@ const createBLegFromHeader = (req, teams) => {
|
||||
else if (req.has('X-Preferred-From-User') || req.has('X-Preferred-From-Host')) {
|
||||
user = req.get('X-Preferred-From-User') || user;
|
||||
host = req.get('X-Preferred-From-Host') || host;
|
||||
} else if (register_from_domain) {
|
||||
host = register_from_domain;
|
||||
}
|
||||
if (from.name) {
|
||||
return `${from.name} <sip:${user}@${host}>`;
|
||||
}
|
||||
return `sip:${user}@${host}`;
|
||||
};
|
||||
@@ -204,13 +209,19 @@ class CallSession extends Emitter {
|
||||
destUri = stringifyUri(uri);
|
||||
this.logger.info(`overriding destination user with ${dest}, so final uri is ${destUri}`);
|
||||
}
|
||||
uris = [destUri];
|
||||
uris = [{
|
||||
private_network: contact.includes('transport=ws') ? false : await isPrivateVoipNetwork(destUri),
|
||||
uri: destUri
|
||||
}];
|
||||
if (!contact.includes('transport=ws')) {
|
||||
proxy = this.req.locals.registration.proxy;
|
||||
}
|
||||
}
|
||||
else if (this.req.locals.target === 'forward') {
|
||||
uris = [this.req.uri];
|
||||
uris = [{
|
||||
private_network: await isPrivateVoipNetwork(this.req.uri),
|
||||
uri: this.req.uri
|
||||
}];
|
||||
if (this.req.has('X-SIP-Proxy')) {
|
||||
proxy = this.req.get('X-SIP-Proxy');
|
||||
if (!proxy.startsWith('sip:') && !proxy.startsWith('sips:')) proxy = `sip:${proxy}`;
|
||||
@@ -220,9 +231,15 @@ class CallSession extends Emitter {
|
||||
const vmailParam = 'opaque=app:voicemail';
|
||||
proxy = `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com:5061;transport=tls`;
|
||||
if (this.req.uri.includes(vmailParam)) {
|
||||
uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`];
|
||||
uris = [{
|
||||
private_network: false,
|
||||
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`
|
||||
}];
|
||||
}
|
||||
else uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`];
|
||||
else uris = [{
|
||||
private_network: false,
|
||||
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`
|
||||
}];
|
||||
headers = {
|
||||
...headers,
|
||||
Contact: `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
|
||||
@@ -280,7 +297,8 @@ class CallSession extends Emitter {
|
||||
name: vc.name,
|
||||
diversion: vc.diversion,
|
||||
hostport,
|
||||
protocol: o.protocol
|
||||
protocol: o.protocol,
|
||||
register_from_domain: vc.register_from_domain
|
||||
};
|
||||
if (vc.register_username && vc.register_password) {
|
||||
obj.auth = {
|
||||
@@ -301,6 +319,13 @@ class CallSession extends Emitter {
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true);
|
||||
}
|
||||
});
|
||||
// Check private network for each gw
|
||||
uris = await Promise.all(uris.map(async(u) => {
|
||||
return {
|
||||
private_network: await isPrivateVoipNetwork(u),
|
||||
uri: u
|
||||
};
|
||||
}));
|
||||
this.logger.debug({uris, voip_carrier_sid}, 'selected outbound gateways for requested carrier');
|
||||
}
|
||||
else {
|
||||
@@ -315,15 +340,22 @@ class CallSession extends Emitter {
|
||||
debug(`sending call to PSTN ${uris}`);
|
||||
}
|
||||
|
||||
// private_network should be called at last
|
||||
uris = uris.sort((a, b) => a.private_network - b.private_network);
|
||||
const toPrivate = uris.some((u) => u.private_network === true);
|
||||
const toPublic = uris.some((u) => u.private_network === false);
|
||||
let isOfferUpdatedToPrivate = toPrivate && !toPublic;
|
||||
|
||||
|
||||
// rtpengine 'offer'
|
||||
const opts = updateRtpEngineFlags(this.req.body, {
|
||||
...this.rtpEngineOpts.common,
|
||||
...this.rtpEngineOpts.uac.mediaOpts,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
direction: ['private', 'public'],
|
||||
direction: ['private', toPublic ? 'public' : 'private'],
|
||||
sdp: this.req.body
|
||||
});
|
||||
const response = await this.offer(opts);
|
||||
let response = await this.offer(opts);
|
||||
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
|
||||
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
|
||||
if ('ok' !== response.result) {
|
||||
@@ -338,7 +370,17 @@ class CallSession extends Emitter {
|
||||
let earlyMedia = false;
|
||||
while (uris.length) {
|
||||
let hdrs = { ...headers};
|
||||
const uri = uris.shift();
|
||||
const {private_network, uri} = uris.shift();
|
||||
if (private_network && !isOfferUpdatedToPrivate) {
|
||||
// Cannot make call to all public Uris, now come to talk with private network Uris
|
||||
this.rtpEngineResource.destroy()
|
||||
.catch((err) => this.logger.info({err}, 'Error destroying rtpe to re-connect to private network'));
|
||||
response = await this.offer({
|
||||
...opts,
|
||||
direction: ['private', 'private']
|
||||
});
|
||||
isOfferUpdatedToPrivate = true;
|
||||
}
|
||||
const gw = mapGateways.get(uri);
|
||||
const passFailure = 0 === uris.length; // only a single target
|
||||
if (0 === uris.length) {
|
||||
@@ -371,6 +413,12 @@ class CallSession extends Emitter {
|
||||
'Diversion': div
|
||||
};
|
||||
}
|
||||
if (gw.register_from_domain) {
|
||||
hdrs = {
|
||||
...hdrs,
|
||||
'From': createBLegFromHeader(this.req, teams, gw.register_from_domain)
|
||||
};
|
||||
}
|
||||
}
|
||||
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
|
||||
try {
|
||||
@@ -411,7 +459,7 @@ class CallSession extends Emitter {
|
||||
...this.rtpEngineOpts.uas.mediaOpts,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
'to-tag': this.rtpEngineOpts.uac.tag,
|
||||
flags: ['single codec'],
|
||||
flags: ['single codec', 'inject DTMF'],
|
||||
sdp
|
||||
};
|
||||
const response = await this.answer(opts);
|
||||
@@ -829,6 +877,26 @@ Duration=${payload.duration} `
|
||||
)).every((r) => r);
|
||||
}
|
||||
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.uas.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
|
||||
|
||||
+38
-5
@@ -1,6 +1,11 @@
|
||||
const rtpCharacteristics = require('../data/rtp-transcoding');
|
||||
const srtpCharacteristics = require('../data/srtp-transcoding');
|
||||
const debug = require('debug')('jambonz:sbc-outbound');
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const dns = require('dns');
|
||||
|
||||
const cidrMatcher = process.env.PRIVATE_VOIP_NETWORK_CIDR ?
|
||||
new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(',')) : null;
|
||||
|
||||
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams) {
|
||||
const from = req.getParsedHeader('from');
|
||||
@@ -16,10 +21,14 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
|
||||
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
|
||||
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
|
||||
|
||||
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
|
||||
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
|
||||
dstOpts.flags.push('inject DTMF');
|
||||
srcOpts.flags.push('inject DTMF');
|
||||
/** Allow feature server to send DTMF to the call excepts call from/to teams */
|
||||
if (!teams) {
|
||||
if (!dstOpts.flags.includes('inject DTMF')) {
|
||||
dstOpts.flags.push('inject DTMF');
|
||||
}
|
||||
if (!srcOpts.flags.includes('inject DTMF')) {
|
||||
srcOpts.flags.push('inject DTMF');
|
||||
}
|
||||
}
|
||||
const common = {
|
||||
'call-id': req.get('Call-ID'),
|
||||
@@ -181,6 +190,29 @@ const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
|
||||
return {callsSP: null, calls: null, callsApp: null};
|
||||
};
|
||||
|
||||
const isPrivateVoipNetwork = async(uri) => {
|
||||
if (cidrMatcher) {
|
||||
try {
|
||||
const arr = /sips?:.*@(.*?)(:\d+)?(;.*)$/.exec(uri);
|
||||
if (arr) {
|
||||
const input = arr[1];
|
||||
let addresses;
|
||||
if (input.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||
addresses = [input];
|
||||
} else {
|
||||
addresses = await dns.resolve4(input);
|
||||
}
|
||||
for (const ip of addresses) {
|
||||
if (cidrMatcher.contains(ip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
makeRtpEngineOpts,
|
||||
selectHostPort,
|
||||
@@ -190,5 +222,6 @@ module.exports = {
|
||||
equalsIgnoreOrder,
|
||||
systemHealth,
|
||||
createHealthCheckApp,
|
||||
nudgeCallCounts
|
||||
nudgeCallCounts,
|
||||
isPrivateVoipNetwork
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user