Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix handling of late offer scenarios #20

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 119 additions & 89 deletions lib/call-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -116,7 +119,6 @@ class CallSession extends Emitter {
const callOpts = {
headers,
...(t && { auth: t.auth }),
localSdpB: response.sdp,
proxyRequestHeaders: [
'from',
'to',
Expand All @@ -140,22 +142,52 @@ 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;
//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,
'from-tag': this.rtpEngineOpts.uas.tag,
'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
});
Expand Down Expand Up @@ -234,65 +266,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');
}
Expand Down Expand Up @@ -368,34 +415,17 @@ 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 offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
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
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,
Expand All @@ -407,7 +437,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
Expand Down
4 changes: 4 additions & 0 deletions lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down