From cac13c5863b14c9a07145af8f59ba7d847d946a9 Mon Sep 17 00:00:00 2001 From: Rhonda Hollis Date: Tue, 6 Sep 2022 17:55:10 -0700 Subject: [PATCH 1/2] fix handling of late offer scenarios --- lib/call-session.js | 193 +++++++++++++++++++++++++------------------- lib/middleware.js | 4 + package.json | 4 +- 3 files changed, 117 insertions(+), 84 deletions(-) diff --git a/lib/call-session.js b/lib/call-session.js index 3fd5d78..ecdb361 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -75,13 +75,14 @@ class CallSession extends Emitter { this.subscribeDTMF = subscribeDTMF; this.unsubscribeDTMF = unsubscribeDTMF; - const { callDirection, remoteUri, callid } = this.req.locals; + const { callDirection, remoteUri, callid, is3pcc } = this.req.locals; const parsedUri = parseUri(uri); const trunk = parsedUri.host; let inviteSent; //outbound is a call from webrtc (public) toward the SBC (private). const rtpDirection = 'outbound' === callDirection ? ['public', 'private'] : ['private', 'public']; + const direction3pcc = 'outbound' === callDirection ? ['private', 'public'] : ['public', 'private']; this.rtpEngineOpts = makeRtpEngineOpts(this.req, ('outbound' === callDirection), ('inbound' === callDirection)); this.rtpEngineResource = { destroy: this.del.bind(null, this.rtpEngineOpts.common) }; const opts = { @@ -93,17 +94,19 @@ class CallSession extends Emitter { }; try { - const response = await this.offer(opts); - this.logger.debug({ opts, response }, 'response from rtpengine to offer'); - if ('ok' !== response.result) { - this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(response)}`); - throw new Error(`failed allocating endpoint for callID ${callid} from rtpengine: ${JSON.stringify(response)}`); - } + let offerResponse; + if (!is3pcc) { + offerResponse = await this.offer(opts); + this.logger.debug({ opts, offerResponse }, 'response from rtpengine to offer'); + if ('ok' !== offerResponse.result) { + this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(offerResponse)}`); + throw new Error(`failed allocating endpoint for callID ${callid} from rtpengine: ${JSON.stringify(offerResponse)}`); + } - if ('outbound' === callDirection && response.sdp) { - response.sdp = removeWebrtcAttributes(response.sdp); + if ('outbound' === callDirection) { + offerResponse.sdp = removeWebrtcAttributes(offerResponse.sdp); + } } - const headers = createHeaders(this.registrar, callid); // check to see if we are sending to a trunk that we hold sip credentials for @@ -116,7 +119,6 @@ class CallSession extends Emitter { const callOpts = { headers, ...(t && { auth: t.auth }), - localSdpB: response.sdp, proxyRequestHeaders: [ 'from', 'to', @@ -140,6 +142,7 @@ class CallSession extends Emitter { this.logger.info({ callOpts }, 'sending INVITE to B'); const { uas, uac } = await this.srf.createB2BUA(this.req, this.res, remoteUri, { ...callOpts, + noAck: is3pcc, localSdpA: async (sdp, res) => { this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag; const opts = { @@ -149,13 +152,37 @@ class CallSession extends Emitter { 'to-tag': this.rtpEngineOpts.uac.tag, sdp }; - const response = await this.answer(opts); + const opts3pcc = { + ...this.rtpEngineOpts.common, + ...this.rtpEngineOpts.uas.mediaOpts, + 'from-tag': this.rtpEngineOpts.uac.tag, + 'to-tag': this.rtpEngineOpts.uas.tag, + 'direction': direction3pcc, + sdp + } + const response = is3pcc ? await this.offer(opts3pcc) : await this.answer(opts) if ('ok' !== response.result) { this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`); throw new Error('rtpengine failed: answer'); } return response.sdp; - } + }, + localSdpB: is3pcc ? async (sdp) => { + this.logger.info('sending ACK to B'); + opts.sdp = sdp; + Object.assign(opts, { + 'to-tag': this.rtpEngineOpts.uas.tag, + 'from-tag': this.rtpEngineOpts.uac.tag + }); + const response = await this.answer(opts); + this.logger.debug({ opts, response }, 'response from rtpengine to offer with sdp from ACK'); + if ('ok' !== response.result) { + this.logger.error({}, `rtpengine (re)offer failed with ${JSON.stringify(response)}`); + throw new Error( + `failed allocating endpoint for callID ${callid} from rtpengine: ${JSON.stringify(response)}`); + } + return response.sdp; + } : offerResponse.sdp }, { cbRequest: (err, req) => inviteSent = req }); @@ -234,65 +261,80 @@ class CallSession extends Emitter { async _handleReinvite(dlg, req, res) { try { this.logger.info(`received reinvite on ${dlg.type} leg`); - 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; - const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts; - let opts = { - ...this.rtpEngineOpts.common, - ...offerMedia, - 'from-tag': fromTag, - 'to-tag': toTag, - sdp: req.body, - }; - let response = await this.offer(opts); - if ('ok' !== response.result) { - res.send(488); - throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`); - } - this.logger.debug({ opts, response }, 'sent offer for reinvite to rtpengine'); - if (JSON.stringify(offerMedia).includes('ICE\":\"remove')) { - response.sdp = removeWebrtcAttributes(response.sdp); - } - - let ackFunc; - let optsSdp; if (!req.body) { + const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag; + const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag; + const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts; + + //reINVITE has no sdp. Send the INVITE on without passing it to rtengine first. const modifyOpts = makeModifyDialogOpts(req, true); this.logger.info({ modifyOpts }, 'calling dlg.modify with opts'); - const { sdp, ack } = await dlg.other.modify(response.sdp, modifyOpts); + const { sdp, ack } = await dlg.other.modify(req.sdp, modifyOpts); this.logger.info({ sdp }, 'return from dlg.modify with sdp'); - optsSdp = sdp; - ackFunc = ack - } - else { - const modifyOpts = makeModifyDialogOpts(req, false); - optsSdp = await dlg.other.modify(response.sdp, modifyOpts); - } - opts = { - ...this.rtpEngineOpts.common, - ...answerMedia, - 'from-tag': fromTag, - 'to-tag': toTag, - sdp: optsSdp - }; - response = await this.answer(opts); - if ('ok' !== response.result) { - res.send(488); - throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`); + let opts = { + ...this.rtpEngineOpts.common, + ...offerMedia, + 'from-tag': fromTag, + 'to-tag': toTag, + sdp + }; + //Pass the sdp from the response to rtpengine as an Offer + const response = await this.offer(opts); + if ('ok' !== response.result) { + res.send(488); + throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`); + } + res.send(200, { body: response.sdp }); + // set listener for ACK, so that we can use that SDP to create the ACK for the other leg. + dlg.once('ack', this._handleAck.bind(this, dlg, ack, sdp)); } + else { + 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; + const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts; + + //reINVITE has sdp + let opts = { + ...this.rtpEngineOpts.common, + ...offerMedia, + 'from-tag': fromTag, + 'to-tag': toTag, + sdp: req.body, + }; + let response = await this.offer(opts); + if ('ok' !== response.result) { + res.send(488); + throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`); + } + this.logger.debug({ opts, response }, 'sent offer for reinvite to rtpengine'); + if (JSON.stringify(offerMedia).includes('ICE\":\"remove')) { + response.sdp = removeWebrtcAttributes(response.sdp); + } - if (JSON.stringify(answerMedia).includes('ICE\":\"remove')) { - response.sdp = removeWebrtcAttributes(response.sdp); - } - res.send(200, { body: response.sdp }); + const modifyOpts = makeModifyDialogOpts(req, false); + const optsSdp = await dlg.other.modify(response.sdp, modifyOpts); + + opts = { + ...this.rtpEngineOpts.common, + ...answerMedia, + 'from-tag': fromTag, + 'to-tag': toTag, + sdp: optsSdp + }; + response = await this.answer(opts); + if ('ok' !== response.result) { + res.send(488); + throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`); + } - if (ackFunc) { - // set listener for ACK, so that we can use that SDP to create the ACK for the other leg. - dlg.once('ack', this._handleAck.bind(this, dlg, ackFunc, optsSdp)); - } + if (JSON.stringify(answerMedia).includes('ICE\":\"remove')) { + response.sdp = removeWebrtcAttributes(response.sdp); + } + res.send(200, { body: response.sdp }); + } } catch (err) { this.logger.error({ err }, 'Error handling reinvite'); } @@ -374,28 +416,15 @@ class CallSession extends Emitter { fromTag = dlg.sip.localTag; toTag = dlg.sip.remoteTag; } - const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts; let answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts; + const mediaStringified = JSON.stringify(answerMedia); //if uas is webrtc facing, we need to keep that side as the active ssl role, so use passive in the ACK sdp - if (dlg.type === 'uas' && JSON.stringify(answerMedia).includes('SAVPF')) { - let mediaStringified = JSON.stringify(answerMedia); - mediaStringified = mediaStringified.replace('SAVPF\"', 'SAVPF\",\"DTLS\":\"passive\"'); - answerMedia = JSON.parse(mediaStringified); - } - - const optsOffer = { - ...this.rtpEngineOpts.common, - ...offerMedia, - 'from-tag': fromTag, - 'to-tag': toTag, - sdp: offerSdp - }; - //send an offer first so that rtpEngine knows that DTLS fingerprint needs to be in the answer sdp. - const response = await this.offer(optsOffer); - if ('ok' !== response.result) { - throw new Error(`_handleAck: rtpengine failed: offer: ${JSON.stringify(response)}`); + if (!this.req.locals.is3pcc && dlg.type === 'uas' && mediaStringified.includes('SAVPF')) { + const updatedMedia = mediaStringified.replace('SAVPF\"', 'SAVPF\",\"DTLS\":\"passive\"'); + answerMedia = JSON.parse(updatedMedia); } + //Offer was already sent in _handleReInvite const optsAnswer = { ...this.rtpEngineOpts.common, ...answerMedia, @@ -407,7 +436,7 @@ class CallSession extends Emitter { if ('ok' !== ackResponse.result) { throw new Error(`_handleAck ${req.get('Call-Id')}: rtpengine failed: answer: ${JSON.stringify(ackResponse)}`); } - if (JSON.stringify(answerMedia).includes('ICE\":\"remove')) { + if (mediaStringified.includes('ICE\":\"remove')) { ackResponse.sdp = removeWebrtcAttributes(ackResponse.sdp); } //send the ACK with sdp diff --git a/lib/middleware.js b/lib/middleware.js index 915ff9f..9e04de9 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -6,6 +6,10 @@ module.exports = function(srf, logger) { const callid = req.get('Call-Id'); const from = req.getParsedHeader('From'); req.locals = {logger: logger.child({callid}), from, callid}; + if (!req.body) { + req.locals.logger.info('incoming INVITE has no SDP'); + req.locals.is3pcc = true; + } next(); }; diff --git a/package.json b/package.json index a6266eb..05f0cc7 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,11 @@ }, "homepage": "https://github.com/voxbone/drachtio-rtpengine-webrtcproxy#readme", "dependencies": { - "@jambonz/rtpengine-utils": "^0.3.1", + "@jambonz/rtpengine-utils": "^0.3.2", "config": "^3.3.7", "drachtio-fn-b2b-sugar": "0.0.12", "drachtio-mw-registration-parser": "0.0.2", - "drachtio-srf": "^4.5.13", + "drachtio-srf": "^4.5.17", "pino": "^4.17.6", "rtpengine-client": "^0.2.0", "uuid": "^3.4.0" From 2e0e4a9c09e08df5fae458ef97f6dae00df6e615 Mon Sep 17 00:00:00 2001 From: Rhonda Hollis Date: Thu, 3 Nov 2022 11:43:40 -0700 Subject: [PATCH 2/2] handle to-tag change on Sequential Ring --- lib/call-session.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/call-session.js b/lib/call-session.js index ecdb361..c174cbd 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -144,7 +144,12 @@ class CallSession extends Emitter { ...callOpts, noAck: is3pcc, localSdpA: async (sdp, res) => { - this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag; + //if the To tag changes on the final response (as in a Sequential Ring scenario), the rtpengine does not support this scenario, + // and the rtcp-mux attribute ends up missing from the sdp on the final response. Forcing the To-tag to remain the same for the rtpengine + // is the workaround. + if (null == this.rtpEngineOpts.uac.tag) { + this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag; + } const opts = { ...this.rtpEngineOpts.common, ...this.rtpEngineOpts.uas.mediaOpts, @@ -410,12 +415,8 @@ class CallSession extends Emitter { this.logger.info('Received ACK with late offer: ', offerSdp); try { - let fromTag = dlg.other.sip.remoteTag; - let toTag = dlg.other.sip.localTag; - if (dlg.type === 'uac') { - fromTag = dlg.sip.localTag; - toTag = dlg.sip.remoteTag; - } + const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag; + const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag; let answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts; const mediaStringified = JSON.stringify(answerMedia); //if uas is webrtc facing, we need to keep that side as the active ssl role, so use passive in the ACK sdp