From 847f73ad8888e86826dc4cbfa3fea9b74a2da49d Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Tue, 23 Jul 2024 18:45:08 -0400 Subject: [PATCH] wip --- lib/call-session.js | 205 +++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 86 deletions(-) diff --git a/lib/call-session.js b/lib/call-session.js index 1778e21..2ad9440 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -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'; @@ -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; @@ -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 @@ -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 { @@ -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) { @@ -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 @@ -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), @@ -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, @@ -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({ @@ -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); @@ -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 = `;reason=unknown;counter=1;privacy=off`; - } - else div = `;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('+') ? + `;reason=unknown;counter=1;privacy=off` : + `;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, @@ -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) => { @@ -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; @@ -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); @@ -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 } });