Compare commits

...
7 Commits
Author SHA1 Message Date
Dave Horton 2747bc15d6 update deps, comment out questionable code 2024-01-17 13:37:07 -05:00
Dave Horton cdfbf384fe update db-helpers 2024-01-17 13:26:37 -05:00
Hoan Luu Huu 96a83619c7 fix sbc to forward BYE header (#129)
* fix sbc to forward BYE header

* fix sbc to forward BYE header
2023-12-28 20:45:38 -05:00
Hoan Luu Huu b8a67019a1 support sending DTMF to caller (#125)
* support sending DTMF to caller

* wip
2023-12-13 09:14:27 -05:00
Hoan Luu Huu 0e1627ba82 phone number's application should override voice carrier default application (#128)
* phone number's application should override voice carrier default application

* fix review comments

* fix review comments
2023-12-13 08:25:26 -05:00
Hoan Luu Huu bb857a72ff let realtimedb-utils build configuration from env vars (#122) 2023-11-14 09:13:08 -05:00
Hoan Luu Huu aea3033ae0 support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation (#124)
* support PRIVATE_VOIP_NETWORK_CIDR for SDP negotiation

* support multi PRIVATE_VOIP_NETWORK_CIDR
2023-11-13 15:24:41 -05:00
6 changed files with 108 additions and 75 deletions
+1 -29
View File
@@ -14,31 +14,6 @@ assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
let host, port = 26379;
if (sentinel.includes(':')) {
const arr = sentinel.split(':');
host = arr[0];
port = parseInt(arr[1], 10);
} else {
host = sentinel;
}
return {host, port};
}),
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
}),
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
}),
...(process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD && {
sentinelPassword: process.env.JAMBONES_REDIS_SENTINEL_SENTINAL_PASSWORD
}),
} : null;
const Srf = require('drachtio-srf');
const srf = new Srf('sbc-inbound');
const opts = Object.assign({
@@ -96,10 +71,7 @@ const {
addToSet,
removeFromSet,
incrKey,
decrKey} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
decrKey} = require('@jambonz/realtimedb-helpers')({}, logger);
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
+46 -7
View File
@@ -6,7 +6,8 @@ const {
SdpWantsSDES,
nudgeCallCounts,
roundTripTime,
parseConnectionIp
parseConnectionIp,
isPrivateVoipNetwork
} = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
@@ -14,6 +15,7 @@ const {parseUri, stringifyUri, SipError} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-inbound');
const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy';
const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
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
@@ -117,6 +119,7 @@ class CallSession extends Emitter {
offer,
answer,
del,
query,
blockMedia,
unblockMedia,
blockDTMF,
@@ -131,6 +134,7 @@ class CallSession extends Emitter {
this.offer = offer;
this.answer = answer;
this.del = del;
this.query = query;
this.blockMedia = blockMedia;
this.unblockMedia = unblockMedia;
this.blockDTMF = blockDTMF;
@@ -154,7 +158,10 @@ class CallSession extends Emitter {
const wantsSrtp = this.req.locals.possibleWebRtcClient = SdpWantsSrtp(sdp);
const wantsSDES = SdpWantsSDES(sdp);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, wantsSrtp, false, this.isFromMSTeams || wantsSDES);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
this.rtpEngineResource = {
destroy: this.del.bind(null, this.rtpEngineOpts.common),
query: this.query.bind(null, this.rtpEngineOpts.common),
};
const obj = parseUri(this.req.uri);
let proxy, host, uri;
@@ -177,7 +184,7 @@ class CallSession extends Emitter {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['public', 'private'],
direction: [isPrivateVoipNetwork(this.req.source_address) ? 'private' : 'public', 'private'],
sdp
};
const startAt = process.hrtime();
@@ -356,12 +363,24 @@ class CallSession extends Emitter {
this.uas = uas;
this.uac = uac;
[uas, uac].forEach((dlg) => {
dlg.on('destroy', async() => {
dlg.on('destroy', async(bye) => {
const other = dlg.other;
this.rtpEngineResource.destroy().catch((err) => {});
/* DH: need a better understanding of why query before delete is a good idea
this.rtpEngineResource.query()
.then((results) => {
this.logger.info({results}, 'rtpengine query results');
return this.rtpEngineResource.destroy();
})
.catch((err) => {});
*/
this.activeCallIds.delete(this.req.get('Call-ID'));
try {
await other.destroy();
const headers = {};
Object.keys(bye.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
} catch (err) {}
this.unsubscribeForDTMF();
@@ -779,6 +798,27 @@ Duration=${payload.duration} `
)).every((r) => r);
}
res.send(succeeded ? 200 : 503);
} else if (reason.includes('Dtmf')) {
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
if (!arr) {
this.logger.info({body: req.body}, '_onInfo: invalid INFO Dtmf');
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
}
const code = arr[1];
const arr2 = /Duration=\s*(\d+)/.exec(req.body);
const duration = arr2 ? arr2[1] : 250;
const dtmfOpts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uac.tag,
code,
duration
};
const response = await this.playDTMF(dtmfOpts);
if ('ok' !== response.result) {
this.logger.info({response}, `rtpengine play Dtmf failed with ${JSON.stringify(response)}`);
throw new Error('rtpengine failed: answer');
}
res.send(200);
}
}
else if (dlg.type === 'uas' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
@@ -816,10 +856,9 @@ Duration=${payload.duration} `
}
}
else {
const immutableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
const headers = {};
Object.keys(req.headers).forEach((h) => {
if (!immutableHdrs.includes(h)) headers[h] = req.headers[h];
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = req.headers[h];
});
const response = await dlg.other.request({method: 'INFO', headers, body: req.body});
const responseHeaders = {};
+17 -4
View File
@@ -142,7 +142,7 @@ module.exports = (srf, logger) => {
const failure = {fromCarrier: false};
const uri = parseUri(req.uri);
const isDotDecimal = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host);
const did = normalizeDID(req.calledNumber) || 'anonymous';
if (!isDotDecimal) {
/**
* The host part of the SIP URI is not a dot-decimal IP address,
@@ -175,12 +175,19 @@ module.exports = (srf, logger) => {
.sort((a, b) => b.netmask - a.netmask);
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
if (selected) {
const sql =
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${selected.voip_carrier_sid}'
AND account_sid = '${a[0].account_sid}'`;
logger.debug({selected, sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
return {
fromCarrier: true,
gateway: selected,
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: selected.application_sid,
application_sid: r[0]?.application_sid || selected.application_sid,
account: a[0]
};
}
@@ -207,12 +214,19 @@ module.exports = (srf, logger) => {
const [a] = await pp.query(sqlAccountBySid, [[matches[0].account_sid]]);
if (0 === a.length) return failure;
logger.debug({matches}, `found registration carrier using ${uri.host} and ${uri.user}`);
const sql =
`SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${matches[0].voip_carrier_sid}'
AND account_sid = '${matches[0].account_sid}'`;
logger.debug({matches: matches[0], sql, did}, 'looking up DID');
const [r] = await pp.query(sql);
return {
fromCarrier: true,
gateway: matches[0],
service_provider_sid: a[0].service_provider_sid,
account_sid: a[0].account_sid,
application_sid: matches[0].application_sid,
application_sid: r[0]?.application_sid || matches[0].application_sid,
account: a[0]
};
}
@@ -264,7 +278,6 @@ module.exports = (srf, logger) => {
if (matches.length) {
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const did = normalizeDID(req.calledNumber) || 'anonymous';
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');
+12 -3
View File
@@ -22,8 +22,8 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
/* Allow Feature server to inject DTMF to both leg except call from Teams*/
if (!teams) {
dstOpts.flags.push('inject DTMF');
srcOpts.flags.push('inject DTMF');
}
@@ -195,6 +195,14 @@ const isMSTeamsCIDR = (ip) => {
return matcher.contains(ip);
};
const isPrivateVoipNetwork = (ip) => {
if (process.env.PRIVATE_VOIP_NETWORK_CIDR) {
const matcher = new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(','));
return matcher.contains(ip);
}
return false;
};
module.exports = {
isWSS,
SdpWantsSrtp,
@@ -211,5 +219,6 @@ module.exports = {
nudgeCallCounts,
roundTripTime,
parseConnectionIp,
isMSTeamsCIDR
isMSTeamsCIDR,
isPrivateVoipNetwork
};
+29 -29
View File
@@ -11,11 +11,11 @@
"dependencies": {
"@aws-sdk/client-auto-scaling": "^3.360.0",
"@aws-sdk/client-sns": "^3.360.0",
"@jambonz/db-helpers": "^0.9.1",
"@jambonz/db-helpers": "^0.9.3",
"@jambonz/digest-utils": "^0.0.3",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.6",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.5",
@@ -1797,9 +1797,9 @@
}
},
"node_modules/@jambonz/db-helpers": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.1.tgz",
"integrity": "sha512-asQQdeQEl1jCyQAxp2kMljZzExQbcG/mBxVYA2Jf0E1ReZctC206LWMWmY/rvbSHHXNZBzJlNxhD0dQB+FtJYA==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.3.tgz",
"integrity": "sha512-3XFs7NC7J7Q/eb1CwG1YJHa6N4elh8IP/4hMMDgoM9U5Loplx61XI4nZ58FIrY3C/F6gEF4UdjqKvbusEVw7cQ==",
"dependencies": {
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
@@ -1831,21 +1831,21 @@
}
},
"node_modules/@jambonz/realtimedb-helpers": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.8.6.tgz",
"integrity": "sha512-ODKVVat2VAQheY13DjiAxiBBOmffMoeu3xviTjsTODr3ySZkqQZvSWU7N80pAIZvoiWG1UUEy9yI8D8mE4Jgiw==",
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.8.7.tgz",
"integrity": "sha512-gDq56YUd/SIx8Q9mJ+WEIfLCUcGNQ33A0iWKhhwMFwzqTESlhHjy97oQGr7BZVQGB8c0ZnqKIxuAv4ruI3ew7w==",
"dependencies": {
"debug": "^4.3.4",
"ioredis": "^5.3.2"
}
},
"node_modules/@jambonz/rtpengine-utils": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.4.3.tgz",
"integrity": "sha512-Aca2QHINtYfFTORag7I7WCw0IqTzx7ZNnboj99XshVF+kcP+AN7qbzUCMYMVC36QXCkHNP7ExhVaBWB2e3Cahg==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.4.4.tgz",
"integrity": "sha512-Kta5MlCPAnb4Z9yxzbPiRThI3yYbeBEslh00/g0rXSEK3voMNpQFPVMy9d83EAOoMEIMouas7/D2gVwV+2hfHA==",
"dependencies": {
"debug": "^4.3.1",
"rtpengine-client": "^0.4.11",
"rtpengine-client": "^0.4.12",
"ws": "^8.5.0"
}
},
@@ -5128,9 +5128,9 @@
}
},
"node_modules/rtpengine-client": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.4.11.tgz",
"integrity": "sha512-upWU+aDQ9SWzyL8gSoCWelInnkELR8c2Qlym9iCupwAmJb6SztZDP/wwZDWVKhusHI4o0ihx077AZrgv3fWwAg==",
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.4.12.tgz",
"integrity": "sha512-Ax7Xf0ct4ZsjX+BzW/t3l19Lux4Ia6q+9BwWWcnelqi8IF0vts9OGcC3EV1/oBlVUCKnbjSYyG6kmUU5xC65vw==",
"dependencies": {
"bencode": "^2.0.3",
"uuid-random": "^1.3.2",
@@ -7498,9 +7498,9 @@
"dev": true
},
"@jambonz/db-helpers": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.1.tgz",
"integrity": "sha512-asQQdeQEl1jCyQAxp2kMljZzExQbcG/mBxVYA2Jf0E1ReZctC206LWMWmY/rvbSHHXNZBzJlNxhD0dQB+FtJYA==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/@jambonz/db-helpers/-/db-helpers-0.9.3.tgz",
"integrity": "sha512-3XFs7NC7J7Q/eb1CwG1YJHa6N4elh8IP/4hMMDgoM9U5Loplx61XI4nZ58FIrY3C/F6gEF4UdjqKvbusEVw7cQ==",
"requires": {
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
@@ -7529,21 +7529,21 @@
}
},
"@jambonz/realtimedb-helpers": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.8.6.tgz",
"integrity": "sha512-ODKVVat2VAQheY13DjiAxiBBOmffMoeu3xviTjsTODr3ySZkqQZvSWU7N80pAIZvoiWG1UUEy9yI8D8mE4Jgiw==",
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.8.7.tgz",
"integrity": "sha512-gDq56YUd/SIx8Q9mJ+WEIfLCUcGNQ33A0iWKhhwMFwzqTESlhHjy97oQGr7BZVQGB8c0ZnqKIxuAv4ruI3ew7w==",
"requires": {
"debug": "^4.3.4",
"ioredis": "^5.3.2"
}
},
"@jambonz/rtpengine-utils": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.4.3.tgz",
"integrity": "sha512-Aca2QHINtYfFTORag7I7WCw0IqTzx7ZNnboj99XshVF+kcP+AN7qbzUCMYMVC36QXCkHNP7ExhVaBWB2e3Cahg==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@jambonz/rtpengine-utils/-/rtpengine-utils-0.4.4.tgz",
"integrity": "sha512-Kta5MlCPAnb4Z9yxzbPiRThI3yYbeBEslh00/g0rXSEK3voMNpQFPVMy9d83EAOoMEIMouas7/D2gVwV+2hfHA==",
"requires": {
"debug": "^4.3.1",
"rtpengine-client": "^0.4.11",
"rtpengine-client": "^0.4.12",
"ws": "^8.5.0"
}
},
@@ -10022,9 +10022,9 @@
}
},
"rtpengine-client": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.4.11.tgz",
"integrity": "sha512-upWU+aDQ9SWzyL8gSoCWelInnkELR8c2Qlym9iCupwAmJb6SztZDP/wwZDWVKhusHI4o0ihx077AZrgv3fWwAg==",
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/rtpengine-client/-/rtpengine-client-0.4.12.tgz",
"integrity": "sha512-Ax7Xf0ct4ZsjX+BzW/t3l19Lux4Ia6q+9BwWWcnelqi8IF0vts9OGcC3EV1/oBlVUCKnbjSYyG6kmUU5xC65vw==",
"requires": {
"bencode": "^2.0.3",
"uuid-random": "^1.3.2",
+3 -3
View File
@@ -25,10 +25,10 @@
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.9.1",
"@jambonz/db-helpers": "^0.9.3",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.6",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.5",