diff --git a/lib/call-session.js b/lib/call-session.js index 610a4e8..79fddbf 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -148,128 +148,137 @@ class CallSession extends Emitter { else uri = `${obj.scheme}:${host}`; this.logger.info(`uri will be: ${uri}, proxy ${proxy}`); - try { - const opts = { - ...this.rtpEngineOpts.common, - ...this.rtpEngineOpts.uac.mediaOpts, - 'from-tag': this.rtpEngineOpts.uas.tag, - direction: ['public', 'private'], - sdp - }; - const startAt = process.hrtime(); - const response = await this.offer(opts); - this.rtpengineIp = opts.sdp ? parseConnectionIp(opts.sdp) : 'undefined'; - const rtt = roundTripTime(startAt); - this.stats.histogram('app.rtpengine.response_time', rtt, [ - 'direction:inbound', 'command:offer', `rtpengine:${this.rtpengineIp}`]); - this.logger.debug({opts, response, rtt, rtpengine: this.rtpengineIp}, 'response from rtpengine to offer'); - if ('ok' !== response.result) { - this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(response)}`); - throw new Error('rtpengine failed: answer'); - } - - let headers = { - 'From': createBLegFromHeader(this.req), - 'To': this.req.get('To'), - 'X-Account-Sid': this.req.locals.account_sid, - 'X-CID': this.req.get('Call-ID'), - 'X-Forwarded-For': `${this.req.source_address}` - }; - if (this.privateSipAddress) headers = {...headers, Contact: ``}; - - const spdOfferB = this.siprec && this.xml ? - createSiprecBody(headers, response.sdp, this.xml.type, this.xml.content) : - response.sdp; - - const responseHeaders = {}; - if (this.req.locals.carrier) { - Object.assign(headers, { - 'X-Originating-Carrier': this.req.locals.carrier, - 'X-Voip-Carrier-Sid': this.req.locals.voip_carrier_sid - }); - } - if (this.req.locals.msTeamsTenantFqdn) { - Object.assign(headers, {'X-MS-Teams-Tenant-FQDN': this.req.locals.msTeamsTenantFqdn}); - - // for Microsoft Teams the Contact header must include the tenant FQDN - Object.assign(responseHeaders, { - Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK', - Contact: `sip:${this.req.locals.msTeamsTenantFqdn}` - }); - } - if (this.req.locals.application_sid) { - Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid}); - } - else if (this.req.authorization) { - if (this.req.authorization.grant && this.req.authorization.grant.application_sid) { - Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid}); + const opts = { + ...this.rtpEngineOpts.common, + ...this.rtpEngineOpts.uac.mediaOpts, + 'from-tag': this.rtpEngineOpts.uas.tag, + direction: ['public', 'private'], + sdp + }; + const startAt = process.hrtime(); + this.offer(opts, async(err, response) => { + try { + if (err || 'ok' !== response.result) { + this.logger.error({}, `rtpengine offer failed with ${JSON.stringify(response)}`); + return this.res.send(480, { + headers: { + 'X-Reason': 'RTPENGINE_OFFER_FAILED' + } + }); } - if (this.req.authorization.challengeResponse) { - const {username, realm} = this.req.authorization.challengeResponse; - Object.assign(headers, {'X-Authenticated-User': `${username}@${realm}`}); + this.rtpengineIp = opts.sdp ? parseConnectionIp(opts.sdp) : 'undefined'; + const rtt = roundTripTime(startAt); + this.stats.histogram('app.rtpengine.response_time', rtt, [ + 'direction:inbound', 'command:offer', `rtpengine:${this.rtpengineIp}`]); + this.logger.debug({opts, response, rtt, rtpengine: this.rtpengineIp}, 'response from rtpengine to offer'); + if ('ok' !== response.result) { } - } + let headers = { + 'From': createBLegFromHeader(this.req), + 'To': this.req.get('To'), + 'X-Account-Sid': this.req.locals.account_sid, + 'X-CID': this.req.get('Call-ID'), + 'X-Forwarded-For': `${this.req.source_address}` + }; + if (this.privateSipAddress) headers = {...headers, Contact: ``}; - if (this.req.canceled) throw new Error('call canceled'); + const spdOfferB = this.siprec && this.xml ? + createSiprecBody(headers, response.sdp, this.xml.type, this.xml.content) : + response.sdp; - // now send the INVITE in towards the feature servers - debug(`sending INVITE to ${proxy} with ${uri}`); - const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, { - proxy, - headers, - responseHeaders, - proxyRequestHeaders: [ - 'all', - '-Authorization', - '-Max-Forwards', - '-Record-Route', - '-Session-Expires', - '-X-Subspace-Forwarded-For' - ], - proxyResponseHeaders: ['all', '-X-Trace-ID'], - localSdpB: spdOfferB, - localSdpA: async(sdp, res) => { - 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 startAt = process.hrtime(); - const response = await this.answer(opts); - const rtt = roundTripTime(startAt); - this.stats.histogram('app.rtpengine.response_time', rtt, [ - 'direction:inbound', 'command:answer', `rtpengine:${this.rtpengineIp}`]); - if ('ok' !== response.result) { - this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`); - throw new Error('rtpengine failed: answer'); + const responseHeaders = {}; + if (this.req.locals.carrier) { + Object.assign(headers, { + 'X-Originating-Carrier': this.req.locals.carrier, + 'X-Voip-Carrier-Sid': this.req.locals.voip_carrier_sid + }); + } + if (this.req.locals.msTeamsTenantFqdn) { + Object.assign(headers, {'X-MS-Teams-Tenant-FQDN': this.req.locals.msTeamsTenantFqdn}); + + // for Microsoft Teams the Contact header must include the tenant FQDN + Object.assign(responseHeaders, { + Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK', + Contact: `sip:${this.req.locals.msTeamsTenantFqdn}` + }); + } + if (this.req.locals.application_sid) { + Object.assign(headers, {'X-Application-Sid': this.req.locals.application_sid}); + } + else if (this.req.authorization) { + if (this.req.authorization.grant && this.req.authorization.grant.application_sid) { + Object.assign(headers, {'X-Application-Sid': this.req.authorization.grant.application_sid}); + } + if (this.req.authorization.challengeResponse) { + const {username, realm} = this.req.authorization.challengeResponse; + Object.assign(headers, {'X-Authenticated-User': `${username}@${realm}`}); } - return response.sdp; } - }); - // successfully connected - this.logger.info('call connected successfully to feature server'); - debug('call connected successfully to feature server'); - this._setHandlers({uas, uac}); - return; - } catch (err) { - this.rtpEngineResource.destroy().catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure')); - this.activeCallIds.delete(this.req.get('Call-ID')); - this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size); - if (err instanceof SipError) { - const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`]; - this.stats.increment('sbc.terminations', tags); - this.logger.info(`call failed to connect to feature server with ${err.status}`); - this.emit('failed'); + if (this.req.canceled) throw new Error('call canceled'); + + // now send the INVITE in towards the feature servers + debug(`sending INVITE to ${proxy} with ${uri}`); + const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, { + proxy, + headers, + responseHeaders, + proxyRequestHeaders: [ + 'all', + '-Authorization', + '-Max-Forwards', + '-Record-Route', + '-Session-Expires', + '-X-Subspace-Forwarded-For' + ], + proxyResponseHeaders: ['all', '-X-Trace-ID'], + localSdpB: spdOfferB, + localSdpA: async(sdp, res) => { + 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 startAt = process.hrtime(); + const response = await this.answer(opts); + const rtt = roundTripTime(startAt); + this.stats.histogram('app.rtpengine.response_time', rtt, [ + 'direction:inbound', 'command:answer', `rtpengine:${this.rtpengineIp}`]); + if ('ok' !== response.result) { + this.logger.error(`rtpengine answer failed with ${JSON.stringify(response)}`); + throw new Error('rtpengine failed: answer'); + } + return response.sdp; + } + }); + + // successfully connected + this.logger.info('call connected successfully to feature server'); + debug('call connected successfully to feature server'); + this._setHandlers({uas, uac}); + + + } catch (err) { + // eslint-disable-next-line promise/no-promise-in-callback + this.rtpEngineResource.destroy() + .catch((err) => this.logger.info({err}, 'Error destroying rtpe after failure')); + this.activeCallIds.delete(this.req.get('Call-ID')); + this.stats.gauge('sbc.sip.calls.count', this.activeCallIds.size); + if (err instanceof SipError) { + const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`]; + this.stats.increment('sbc.terminations', tags); + this.logger.info(`call failed to connect to feature server with ${err.status}`); + this.emit('failed'); + } + else if (err.message !== 'call canceled') { + this.logger.error(err, 'unexpected error routing inbound call'); + } + this.srf.endSession(this.req); } - else if (err.message !== 'call canceled') { - this.logger.error(err, 'unexpected error routing inbound call'); - } - this.srf.endSession(this.req); - } + }); } _setDlgHandlers(dlg) {