mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2026-10-04 02:04:22 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c2225758 |
+130
-126
@@ -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: `<sip:${this.privateSipAddress}>`};
|
||||
|
||||
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: `<sip:${this.privateSipAddress}>`};
|
||||
|
||||
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) {
|
||||
@@ -508,16 +517,11 @@ Duration=${payload.duration} `
|
||||
req.body;
|
||||
|
||||
const reason = req.get('X-Reason');
|
||||
const isReleasingMedia = reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason);
|
||||
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;
|
||||
const direction = dlg.type === 'uas' ? ['public', 'private'] : ['private', 'public'];
|
||||
if (isReleasingMedia) {
|
||||
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
|
||||
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
|
||||
}
|
||||
let opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...offerMedia,
|
||||
@@ -536,11 +540,11 @@ Duration=${payload.duration} `
|
||||
|
||||
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
|
||||
let sdp;
|
||||
if (isReleasingMedia && !this.callerIsUsingSrtp) {
|
||||
if (reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason) &&
|
||||
!this.callerIsUsingSrtp) {
|
||||
this.logger.info({response}, `got a reinvite from FS to ${reason}`);
|
||||
sdp = dlg.other.remote.sdp;
|
||||
if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
|
||||
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
|
||||
answerMedia.flags = ['asymmetric', 'port latching'];
|
||||
this._mediaReleased = 'release-media' === reason;
|
||||
}
|
||||
else {
|
||||
@@ -781,10 +785,10 @@ Duration=${payload.duration} `
|
||||
res.send(202);
|
||||
|
||||
// invite to new fs
|
||||
const headers = {
|
||||
...(req.has('X-Retain-Call-Sid') && {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')}),
|
||||
...(req.has('X-Account-Sid') && {'X-Account-Sid': req.get('X-Account-Sid')})
|
||||
};
|
||||
const headers = {};
|
||||
if (req.has('X-Retain-Call-Sid')) {
|
||||
Object.assign(headers, {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')});
|
||||
}
|
||||
const uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
|
||||
this.uac = uac;
|
||||
uac.other = this.uas;
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ module.exports = (srf, logger) => {
|
||||
/* we have one or more carriers that match. Now we need to find one with a provisioned phone number */
|
||||
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const did = normalizeDID(req.calledNumber);
|
||||
const sql = `SELECT * FROM phone_numbers WHERE number = '${did}' AND voip_carrier_sid IN (${vc_sids})`;
|
||||
const sql = `SELECT * FROM phone_numbers WHERE number = ${did} AND voip_carrier_sid IN (${vc_sids})`;
|
||||
logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
|
||||
|
||||
const [r] = await pp.query(sql);
|
||||
|
||||
+3
-5
@@ -12,12 +12,10 @@ const getAppserver = (srf) => {
|
||||
};
|
||||
|
||||
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
|
||||
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
|
||||
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
|
||||
const from = req.getParsedHeader('from');
|
||||
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
|
||||
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
|
||||
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
|
||||
const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default'];
|
||||
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics;
|
||||
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics;
|
||||
|
||||
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
|
||||
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
|
||||
|
||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sbc-inbound",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.6.19",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@jambonz/rtpengine-utils": "^0.3.6",
|
||||
"@jambonz/siprec-client-utils": "^0.1.4",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@jambonz/time-series": "^0.2.4",
|
||||
"aws-sdk": "^2.1152.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
@@ -680,9 +680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/time-series": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.5.tgz",
|
||||
"integrity": "sha512-WrVenxY4ZgW0YYNIXdAz3ZKao8IQy612RVDAnYq7oWvHHqzgLmLrbIvTXKhycd27iYvwFAPCcUDk1EMuUoPLPA==",
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.4.tgz",
|
||||
"integrity": "sha512-0rvELi9V/qdyPpM/2LidTd2EDN34iCU6U8ccuLUgqV05FaH/EW0qN0RFaVXVlFK6wCDZqEOBynd7x05De3dncw==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"influx": "^5.9.3"
|
||||
@@ -5818,9 +5818,9 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/time-series": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.5.tgz",
|
||||
"integrity": "sha512-WrVenxY4ZgW0YYNIXdAz3ZKao8IQy612RVDAnYq7oWvHHqzgLmLrbIvTXKhycd27iYvwFAPCcUDk1EMuUoPLPA==",
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.2.4.tgz",
|
||||
"integrity": "sha512-0rvELi9V/qdyPpM/2LidTd2EDN34iCU6U8ccuLUgqV05FaH/EW0qN0RFaVXVlFK6wCDZqEOBynd7x05De3dncw==",
|
||||
"requires": {
|
||||
"debug": "^4.3.1",
|
||||
"influx": "^5.9.3"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.6",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -32,7 +32,7 @@
|
||||
"@jambonz/rtpengine-utils": "^0.3.6",
|
||||
"@jambonz/siprec-client-utils": "^0.1.4",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@jambonz/time-series": "^0.2.4",
|
||||
"aws-sdk": "^2.1152.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
|
||||
Reference in New Issue
Block a user