Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
davehorton committed Jul 23, 2024
1 parent 6f6f26d commit 847f73a
Showing 1 changed file with 119 additions and 86 deletions.
205 changes: 119 additions & 86 deletions lib/call-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,45 @@ const debug = require('debug')('jambonz:sbc-outbound');

const makeInviteInProgressKey = (callid) => `sbc-out-iip${callid}`;
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
/**
* this is to make sure the outgoing From has the number in the incoming From
* and not the incoming PAI
*/
const createBLegFromHeader = (req, teams, register_from_domain = null) => {

const createBLegFromHeader = ({
logger,
req,
host,
register_from_domain,
transport,
teams = false,
scheme = 'sip'
}) => {
const from = req.getParsedHeader('From');
const uri = parseUri(from.uri);
let user = uri.user || 'anonymous';
this.scheme = uri.scheme;
let host = 'localhost';
if (teams) {
host = req.get('X-MS-Teams-Tenant-FQDN');
}
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;
const transportParam = transport ? `;transport=${transport}` : '';

logger.debug({from, uri, host, scheme, transport, teams}, 'createBLegFromHeader');
/* user */
const user = req.get('X-Preferred-From-User') || uri.user || 'anonymous';

/* host */
if (!host) {
if (teams) {
host = req.get('X-MS-Teams-Tenant-FQDN');
}
else if (req.has('X-Preferred-From-User')) {
host = req.get('X-Preferred-From-Host');
} else if (register_from_domain) {
host = register_from_domain;
}
else {
host = 'localhost';
}
}

if (from.name) {
return `${from.name} <${this.scheme}:${user}@${host}>`;
return `${from.name} <${scheme}:${user}@${host}>${transportParam}`;
}
return `${this.scheme}:${user}@${host}`;
return `<${scheme}:${user}@${host}>${transportParam}`;
};

const createBLegToHeader = (req, teams) => {
const to = req.getParsedHeader('To');
const host = teams ? req.get('X-MS-Teams-Tenant-FQDN') : 'localhost';
Expand Down Expand Up @@ -89,7 +104,6 @@ class CallSession extends Emitter {
this.idleEmitter = this.srf.locals.idleEmitter;
this.activeCallIds = this.srf.locals.activeCallIds;
this.writeCdrs = this.srf.locals.writeCdrs;
this.scheme = 'sip';

this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;

Expand Down Expand Up @@ -193,12 +207,9 @@ class CallSession extends Emitter {
let encryptedMedia = false;

try {
this.contactHeader = createBLegFromHeader(this.req, teams);
// determine where to send the call
debug(`connecting call: ${JSON.stringify(this.req.locals)}`);
let headers = {
'From': createBLegFromHeader(this.req, teams),
'Contact': this.contactHeader,
const headers = {
'To': createBLegToHeader(this.req, teams),
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
'X-Account-Sid': this.account_sid
Expand Down Expand Up @@ -246,10 +257,6 @@ class CallSession extends Emitter {
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`
};
}
else {
try {
Expand Down Expand Up @@ -299,23 +306,18 @@ class CallSession extends Emitter {
this.req.calledNumber.slice(1) :
this.req.calledNumber;
const prefix = vc.tech_prefix || '';
const protocol = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
const transport = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
const hostport = !o.port || 5060 === o.port ? o.ipv4 : `${o.ipv4}:${o.port}`;
const prependPlus = vc.e164_leading_plus && !this.req.calledNumber.startsWith('0') ? '+' : '';
this.transport = `transport=${protocol}`;
const useSipsScheme = protocol === 'tls' &&
!process.env.JAMBONES_USE_BEST_EFFORT_TLS &&
o.use_sips_scheme;

if (useSipsScheme) {
this.scheme = 'sips';
}
const u = `${this.scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};${this.transport}`;
const scheme = transport === 'tls' && !process.env.JAMBONES_USE_BEST_EFFORT_TLS && o.use_sips_scheme ?
'sips' : 'sip';
const u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};transport=${transport}`;
const obj = {
name: vc.name,
diversion: vc.diversion,
hostport,
protocol: o.protocol,
transport,
scheme,
register_from_domain: vc.register_from_domain
};
if (vc.register_username && vc.register_password) {
Expand All @@ -326,7 +328,8 @@ class CallSession extends Emitter {
}
mapGateways.set(u, obj);
uris.push(u);
if (o.protocol === 'tls/srtp') {
this.logger.debug({gateway: o}, `pushed uri ${u}`);
if (transport === 'tls/srtp') {
/** TODO: this is a bit of a hack in the sense that we are not
* supporting a scenario where you have a carrier with several outbound
* gateways, some requiring encrypted media and some not. This should be rectified
Expand All @@ -338,7 +341,7 @@ class CallSession extends Emitter {
encryptedMedia = true;
}
});
// Check private network for each gw
/* Check private network for each gw */
uris = await Promise.all(uris.map(async(u) => {
return {
private_network: await isPrivateVoipNetwork(u),
Expand All @@ -360,14 +363,12 @@ class CallSession extends Emitter {
debug(`sending call to PSTN ${uris}`);
}

// private_network should be called at last
/* private_network should be called at last - try public first */
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,
Expand All @@ -388,11 +389,13 @@ class CallSession extends Emitter {

// crank through the list of gateways until connected, exhausted or caller hangs up
let earlyMedia = false;
let attempts = 0;
while (uris.length) {
let hdrs = { ...headers};
let hdrs = { ...headers };
const {private_network, uri} = uris.shift();

/* if we've exhausted attempts to public endpoints and are switching to trying private, we need new rtp */
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({
Expand All @@ -401,9 +404,9 @@ class CallSession extends Emitter {
});
isOfferUpdatedToPrivate = true;
}
const gw = mapGateways.get(uri);
const passFailure = 0 === uris.length; // only a single target
if (0 === uris.length) {

/* on the second and subsequent attempts, use the same Call-ID and CSeq from the first attempt */
if (attempts++ > 0) {
try {
const key = makeInviteInProgressKey(this.req.get('Call-ID'));
const obj = await retrieveHash(key);
Expand All @@ -418,35 +421,74 @@ class CallSession extends Emitter {
this.logger.info({err}, 'Error retrieving iip key');
}
}
// INVITE request line and To header should be the same.

/* INVITE request line and To header should be the same. */
hdrs = {...hdrs, 'To': uri};

/* only now can we set Contact & From header since they depend on transport and scheme of gw */
const gw = mapGateways.get(uri);
if (gw) {
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
if (gw.diversion) {
let div = gw.diversion;
if (div.startsWith('+')) {
div = `<sip:${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
}
else div = `<sip:+${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
hdrs = {
...hdrs,
'Diversion': div
};
}
if (gw.register_from_domain) {
hdrs = {
...hdrs,
'From': createBLegFromHeader(this.req, teams, gw.register_from_domain)
};
}
hdrs = {
...hdrs,
From: gw.register_from_domain ?
createBLegFromHeader({
logger: this.logger,
req: this.req,
register_from_domain: gw.register_from_domain,
scheme: gw.scheme,
transport: gw.transport,
...(private_network && {host: this.privateSipAddress})
}) :
createBLegFromHeader({
logger: this.logger,
req: this.req,
scheme: gw.scheme,
transport: gw.transport,
...(private_network && {host: this.privateSipAddress})
}),
Contact: createBLegFromHeader({
logger: this.logger,
req: this.req,
scheme: gw.scheme,
transport: gw.transport,
...(private_network && {host: this.privateSipAddress})
}),
...(gw.diversion && {
Diversion: gw.diversion.startsWith('+') ?
`<sip:${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off` :
`<sip:+${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`
})
};
}
else if (teams) {
hdrs = {
...hdrs,
'From': createBLegFromHeader({logger: this.logger, req: this.req, teams: true, transport: 'tls'}),
'Contact': `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
};
}
else {
hdrs = {
...hdrs,
'From': createBLegFromHeader({
logger: this.logger,
req: this.req,
...(private_network && {host: this.privateSipAddress})
}),
'Contact': createBLegFromHeader({
logger: this.logger,
req: this.req,
...(private_network && {host: this.privateSipAddress})
})
};
const p = proxy ? ` via ${proxy}` : '';
this.logger.info(`sending INVITE to ${uri}${p})`);
}
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
try {
if (this.privateSipAddress) {
this.contactHeader = `<${this.scheme}:${this.privateSipAddress}>`;
}
const responseHeaders = this.privateSipAddress ? {Contact: this.contactHeader} : {};

/* now launch an outbound call attempt */
const passFailure = 0 === uris.length; // only propagate failure on last attempt
try {
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
proxy,
passFailure,
Expand All @@ -472,7 +514,6 @@ class CallSession extends Emitter {
'-Session-Expires'
],
headers: hdrs,
responseHeaders,
auth: gw ? gw.auth : undefined,
localSdpB: response.sdp,
localSdpA: async(sdp, res) => {
Expand Down Expand Up @@ -525,6 +566,9 @@ class CallSession extends Emitter {
} catch (err) {
this.logger.error({err}, 'Error saving Call-ID/CSeq');
}

this.contactHeader = inv.get('Contact');
this.logger.info(`outbound call attempt to ${uri} has contact header ${this.contactHeader}`);
},
cbProvisional: (response) => {
if (!earlyMedia && [180, 183].includes(response.status) && response.body) earlyMedia = true;
Expand Down Expand Up @@ -797,7 +841,7 @@ Duration=${payload.duration} `
sdp
};
response = await this.answer(opts);
/* now remove asymeetric as B party (looking at you Genesys ring group) may need port re-learning on invites */
/* now remove asymmetric as B party (looking at you Genesys ring group) may need port re-learning on invites */
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'asymmetric');
if ('ok' !== response.result) {
res.send(488);
Expand Down Expand Up @@ -1051,26 +1095,15 @@ Duration=${payload.duration} `
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;
this.scheme = farEnd.scheme;

if (farEnd.params?.transport) {
this.transport = `transport=${farEnd.params.transport}`;
}

/* we receive 'contact' in lowercase from feature-server */
/* delete contact if it was there from feature server */
delete customHeaders['contact'];

this.referContactHeader = `<${this.scheme}:${uri.user}@${uri.host}:${uri.port}>;${this.transport}`;
const response = await this.uac.request({
method: 'REFER',
headers: {
// Make sure the uri is protected by <> if uri is complex form
'Refer-To': `<${stringifyUri(uri)}>`,
'Referred-By': `<${stringifyUri(u)}>`,
'Contact': this.referContactHeader,
...customHeaders
}
});
Expand Down

0 comments on commit 847f73a

Please sign in to comment.