mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2026-10-04 02:04:22 +00:00
Compare commits
28
Commits
v0.8.4
...
v0.9.1-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e2285c4b8 | ||
|
|
f9ae466709 | ||
|
|
831005f7a1 | ||
|
|
a46ad934a8 | ||
|
|
d200799997 | ||
|
|
2fef169e92 | ||
|
|
5829ecdf4c | ||
|
|
03d3a9695d | ||
|
|
f5be95006c | ||
|
|
2747bc15d6 | ||
|
|
cdfbf384fe | ||
|
|
96a83619c7 | ||
|
|
b8a67019a1 | ||
|
|
0e1627ba82 | ||
|
|
bb857a72ff | ||
|
|
aea3033ae0 | ||
|
|
9b9eaa37fb | ||
|
|
56e0e3c0f2 | ||
|
|
6a6b4dd26f | ||
|
|
a3799576a2 | ||
|
|
63e72fbfce | ||
|
|
e5dce35bab | ||
|
|
b6675de2bd | ||
|
|
66e73d8353 | ||
|
|
a25aa08e17 | ||
|
|
b1296c8e89 | ||
|
|
17df3a4ca8 | ||
|
|
f2e820915d |
@@ -14,28 +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
|
||||
})
|
||||
} : null;
|
||||
|
||||
const Srf = require('drachtio-srf');
|
||||
const srf = new Srf('sbc-inbound');
|
||||
const opts = Object.assign({
|
||||
@@ -93,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);
|
||||
@@ -170,11 +145,28 @@ if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
const matcher = new CIDRMatcher(cidrs);
|
||||
|
||||
srf.connect({host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
|
||||
srf.on('connect', (err, hp) => {
|
||||
srf.on('connect', (err, hp, version, localHostports) => {
|
||||
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
|
||||
logger.info(`connected to drachtio listening on ${hp}`);
|
||||
let addedPrivateIp = false;
|
||||
logger.info(`connected to drachtio ${version} listening on ${hp}, local hostports: ${localHostports}`);
|
||||
|
||||
const hostports = hp.split(',');
|
||||
|
||||
if (localHostports) {
|
||||
const locals = localHostports.split(',');
|
||||
for (const hp of locals) {
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
|
||||
if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
const hostport = `${arr[2]}:${arr[3]}`;
|
||||
logger.info(`adding sbc private address to redis: ${hostport}`);
|
||||
srf.locals.privateSipAddress = hostport;
|
||||
srf.locals.addToRedis = () => addToSet(setName, hostport);
|
||||
srf.locals.removeFromRedis = () => removeFromSet(setName, hostport);
|
||||
srf.locals.addToRedis();
|
||||
addedPrivateIp = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const hp of hostports) {
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
|
||||
if (arr && 'udp' === arr[1] && !matcher.contains(arr[2])) {
|
||||
@@ -182,7 +174,7 @@ if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
srf.locals.sipAddress = arr[2];
|
||||
if (!process.env.SBC_ACCOUNT_SID) addSbcAddress(arr[2]);
|
||||
}
|
||||
else if (arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
else if (!addedPrivateIp && arr && 'tcp' === arr[1] && matcher.contains(arr[2])) {
|
||||
const hostport = `${arr[2]}:${arr[3]}`;
|
||||
logger.info(`adding sbc private address to redis: ${hostport}`);
|
||||
srf.locals.privateSipAddress = hostport;
|
||||
@@ -198,13 +190,13 @@ else {
|
||||
logger.info(`listening in outbound mode on port ${process.env.DRACHTIO_PORT}`);
|
||||
});
|
||||
srf.listen({port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET});
|
||||
srf.on('connect', (err, hp) => {
|
||||
srf.on('connect', (err, hp, version, localHostports) => {
|
||||
if (err) return this.logger.error({err}, 'Error connecting to drachtio server');
|
||||
logger.info(`connected to drachtio listening on ${hp}`);
|
||||
logger.info(`connected to drachtio ${version} listening on ${hp}, local hostports: ${localHostports}`);
|
||||
|
||||
if (process.env.K8S_FEATURE_SERVER_TRANSPORT === 'tcp') {
|
||||
const matcher = new CIDRMatcher(['192.168.0.0/24', '172.16.0.0/16', '10.0.0.0/8']);
|
||||
const hostports = hp.split(',');
|
||||
const hostports = localHostports ? localHostports.split(',') : hp.split(',');
|
||||
for (const hp of hostports) {
|
||||
const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
|
||||
if (arr && matcher.contains(arr[2])) {
|
||||
|
||||
+146
-41
@@ -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
|
||||
@@ -64,6 +66,8 @@ class CallSession extends Emitter {
|
||||
this.application_sid = req.locals.application_sid;
|
||||
this.account_sid = req.locals.account_sid;
|
||||
this.service_provider_sid = req.locals.service_provider_sid;
|
||||
this.srsClients = [];
|
||||
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
|
||||
}
|
||||
|
||||
get isFromMSTeams() {
|
||||
@@ -116,6 +120,7 @@ class CallSession extends Emitter {
|
||||
offer,
|
||||
answer,
|
||||
del,
|
||||
query,
|
||||
blockMedia,
|
||||
unblockMedia,
|
||||
blockDTMF,
|
||||
@@ -130,6 +135,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;
|
||||
@@ -153,7 +159,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;
|
||||
|
||||
@@ -176,7 +185,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();
|
||||
@@ -205,6 +214,9 @@ class CallSession extends Emitter {
|
||||
response.sdp;
|
||||
|
||||
const responseHeaders = {};
|
||||
if (isPrivateVoipNetwork(this.req.source_address)) {
|
||||
Object.assign(responseHeaders, {'Contact':`<sip:${this.privateSipAddress}>`});
|
||||
}
|
||||
if (this.req.locals.carrier) {
|
||||
Object.assign(headers, {
|
||||
'X-Originating-Carrier': this.req.locals.carrier,
|
||||
@@ -247,7 +259,8 @@ class CallSession extends Emitter {
|
||||
'-Max-Forwards',
|
||||
'-Record-Route',
|
||||
'-Session-Expires',
|
||||
'-X-Subspace-Forwarded-For'
|
||||
'-X-Application-Sid',
|
||||
'-X-Authenticated-User'
|
||||
],
|
||||
proxyResponseHeaders: ['all', '-X-Trace-ID'],
|
||||
localSdpB: spdOfferB,
|
||||
@@ -317,10 +330,7 @@ class CallSession extends Emitter {
|
||||
this.activeCallIds.delete(callId);
|
||||
if (dlg.other && dlg.other.connected) dlg.other.destroy().catch((e) => {});
|
||||
|
||||
if (this.srsClient) {
|
||||
this.srsClient.stop();
|
||||
this.srsClient = null;
|
||||
}
|
||||
this._stopRecording();
|
||||
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
@@ -329,6 +339,29 @@ class CallSession extends Emitter {
|
||||
dlg.on('modify', this._onReinvite.bind(this, dlg));
|
||||
}
|
||||
|
||||
_startRecordingNoAnswerTimer(res) {
|
||||
this._clearRecordingNoAnswerTimer();
|
||||
this.recordingNoAnswerTimer = setTimeout(() => {
|
||||
this.logger.info('No response from SipRec server, return error to feature server');
|
||||
this.isRecordingNoAnswerResponded = true;
|
||||
res.send(400);
|
||||
}, this.recordingNoAnswerTimeout);
|
||||
}
|
||||
|
||||
_clearRecordingNoAnswerTimer() {
|
||||
if (this.recordingNoAnswerTimer) {
|
||||
clearTimeout(this.recordingNoAnswerTimer);
|
||||
this.recordingNoAnswerTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
_stopRecording() {
|
||||
if (this.srsClients.length) {
|
||||
this.srsClients.forEach((c) => c.stop());
|
||||
this.srsClients = [];
|
||||
}
|
||||
}
|
||||
|
||||
_setHandlers({uas, uac}) {
|
||||
this.emit('connected');
|
||||
const callStart = Date.now();
|
||||
@@ -350,12 +383,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();
|
||||
|
||||
@@ -412,10 +457,7 @@ class CallSession extends Emitter {
|
||||
dlg.other = null;
|
||||
other.other = null;
|
||||
|
||||
if (this.srsClient) {
|
||||
this.srsClient.stop();
|
||||
this.srsClient = null;
|
||||
}
|
||||
this._stopRecording();
|
||||
|
||||
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
|
||||
this.srf.endSession(this.req);
|
||||
@@ -654,6 +696,7 @@ Duration=${payload.duration} `
|
||||
}
|
||||
res.send(200, {body: response.sdp});
|
||||
} catch (err) {
|
||||
res.send(err.status || 500);
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
}
|
||||
@@ -684,6 +727,7 @@ Duration=${payload.duration} `
|
||||
}
|
||||
else if (reason.includes('CallRecording')) {
|
||||
let succeeded = false;
|
||||
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
|
||||
if (reason === 'startCallRecording') {
|
||||
const from = this.req.getParsedHeader('From');
|
||||
const to = this.req.getParsedHeader('To');
|
||||
@@ -696,7 +740,7 @@ Duration=${payload.duration} `
|
||||
const callSid = req.get('X-Call-Sid');
|
||||
const accountSid = req.get('X-Account-Sid');
|
||||
const applicationSid = req.get('X-Application-Sid');
|
||||
if (this.srsClient) {
|
||||
if (this.srsClients.length) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding duplicate startCallRecording request for a call');
|
||||
return;
|
||||
@@ -706,13 +750,14 @@ Duration=${payload.duration} `
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
this.srsClient = new SrsClient(this.logger, {
|
||||
const arr = srsUrl.split(',');
|
||||
this.srsClients = arr.map((url) => new SrsClient(this.logger, {
|
||||
srf: dlg.srf,
|
||||
direction: 'inbound',
|
||||
originalInvite: this.req,
|
||||
callingNumber: this.req.callingNumber,
|
||||
calledNumber: this.req.calledNumber,
|
||||
srsUrl,
|
||||
srsUrl: url,
|
||||
srsRecordingId,
|
||||
callSid,
|
||||
accountSid,
|
||||
@@ -727,44 +772,101 @@ Duration=${payload.duration} `
|
||||
del: this.del,
|
||||
blockMedia: this.blockMedia,
|
||||
unblockMedia: this.unblockMedia,
|
||||
unsubscribe: this.unsubscribe
|
||||
});
|
||||
unsubscribe: this.unsubscribe,
|
||||
headers
|
||||
}));
|
||||
try {
|
||||
succeeded = await this.srsClient.start();
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => c.start()));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error starting SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
else if (reason === 'stopCallRecording') {
|
||||
if (!this.srsClient) {
|
||||
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated)) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding stopCallRecording request because we are not recording');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
succeeded = await this.srsClient.stop();
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => {
|
||||
if (c.activated) {
|
||||
c.stop();
|
||||
}
|
||||
}));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error stopping SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
this.srsClient = null;
|
||||
this.srsClients = [];
|
||||
}
|
||||
else if (reason === 'pauseCallRecording') {
|
||||
if (!this.srsClient || this.srsClient.paused) {
|
||||
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && !c.paused)) {
|
||||
this.logger.info('discarding invalid pauseCallRecording request');
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.pause();
|
||||
try {
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => {
|
||||
if (c.activated && !c.paused) {
|
||||
c.pause({headers});
|
||||
}
|
||||
}));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error pausing SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
else if (reason === 'resumeCallRecording') {
|
||||
if (!this.srsClient || !this.srsClient.paused) {
|
||||
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && c.paused)) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding invalid resumeCallRecording request');
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.resume();
|
||||
try {
|
||||
this._startRecordingNoAnswerTimer(res);
|
||||
await Promise.any(this.srsClients.map((c) => {
|
||||
if (c.activated && c.paused) {
|
||||
c.resume({headers});
|
||||
}
|
||||
}));
|
||||
succeeded = true;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error resuming SipRec call recording');
|
||||
succeeded = false;
|
||||
}
|
||||
}
|
||||
res.send(succeeded ? 200 : 503);
|
||||
if (!this.isRecordingNoAnswerResponded) {
|
||||
this._clearRecordingNoAnswerTimer();
|
||||
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)) {
|
||||
@@ -802,10 +904,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 = {};
|
||||
@@ -815,8 +916,8 @@ Duration=${payload.duration} `
|
||||
res.send(response.status, {headers: responseHeaders, body: response.body});
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.srsClient) {
|
||||
this.srsClient = null;
|
||||
if (this.srsClients.length) {
|
||||
this.srsClients = [];
|
||||
}
|
||||
res.send(500);
|
||||
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
|
||||
@@ -873,8 +974,8 @@ Duration=${payload.duration} `
|
||||
const response = await this.uas.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
'Refer-To': stringifyUri(uri),
|
||||
'Referred-By': stringifyUri(u),
|
||||
'Refer-To': `<${stringifyUri(uri)}>`,
|
||||
'Referred-By': `<${stringifyUri(u)}>`,
|
||||
...customHeaders
|
||||
}
|
||||
});
|
||||
@@ -891,6 +992,7 @@ Duration=${payload.duration} `
|
||||
this.uac = uac;
|
||||
uac.other = this.uas;
|
||||
this.uas.other = uac;
|
||||
uac.on('info', this._onInfo.bind(this, uac));
|
||||
uac.on('modify', this._onReinvite.bind(this, uac));
|
||||
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
|
||||
uac.on('destroy', () => {
|
||||
@@ -900,19 +1002,22 @@ Duration=${payload.duration} `
|
||||
uac.other.destroy();
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
// now we can destroy the old dialog
|
||||
dlg.destroy().catch(() => {});
|
||||
|
||||
// modify rtpengine to stream to new feature server
|
||||
const opts = Object.assign({sdp: uac.remote.sdp, 'to-tag': res.getParsedHeader('To').params.tag},
|
||||
this.rtpEngineOpts.answer);
|
||||
const opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
'to-tag': this.rtpEngineOpts.uac.tag,
|
||||
sdp: uac.remote.sdp,
|
||||
flags: ['port latching']
|
||||
};
|
||||
const response = await this.answer(opts);
|
||||
if ('ok' !== response.result) {
|
||||
res.send(488);
|
||||
throw new Error(`_onFeatureServerTransfer: rtpengine failed: ${JSON.stringify(response)}`);
|
||||
throw new Error(`_onFeatureServerTransfer: rtpengine answer failed: ${JSON.stringify(response)}`);
|
||||
}
|
||||
dlg.destroy().catch(() => {});
|
||||
this.logger.info('successfully moved call to new feature server');
|
||||
} catch (err) {
|
||||
res.send(488);
|
||||
this.logger.error(err, 'Error handling refer from feature server');
|
||||
}
|
||||
}
|
||||
|
||||
+54
-21
@@ -45,7 +45,7 @@ AND vc.is_active = 1
|
||||
AND sg.inbound = 1
|
||||
AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
|
||||
|
||||
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ?';
|
||||
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ? AND is_active = 1';
|
||||
const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?';
|
||||
const sqlApplicationBySid = 'SELECT * from applications WHERE application_sid = ?';
|
||||
|
||||
@@ -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,
|
||||
@@ -153,24 +153,48 @@ module.exports = (srf, logger) => {
|
||||
* Let's look for case #1 first...
|
||||
*/
|
||||
|
||||
/* get all the carriers and gateways for the account owning this sip realm */
|
||||
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]);
|
||||
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
|
||||
const gw = gwAcc.concat(gwSP);
|
||||
const selected = gw.find(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
|
||||
if (selected) {
|
||||
const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
|
||||
if (0 === a.length) return failure;
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: selected,
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
account_sid: a[0].account_sid,
|
||||
application_sid: selected.application_sid,
|
||||
account: a[0]
|
||||
};
|
||||
/* does anyone own this sip realm? */
|
||||
const [a] = await pp.query(sqlAccountByRealm, [uri.host]);
|
||||
if (a.length) {
|
||||
assert(a.length === 1);
|
||||
|
||||
/* yes they do */
|
||||
logger.debug(`sip realm is associated with account_sid: ${a[0].account_sid}`);
|
||||
|
||||
/**
|
||||
* We have one of two cases:
|
||||
* (1a). The user configured his or her carrier to send to their sip realm, or
|
||||
* (1b). The user is making a call from a sip device.
|
||||
*/
|
||||
|
||||
/* get all the carriers and gateways for the account owning this sip realm */
|
||||
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]);
|
||||
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
|
||||
const gw = gwAcc
|
||||
.concat(gwSP)
|
||||
.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: r[0]?.application_sid || selected.application_sid,
|
||||
account: a[0]
|
||||
};
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
|
||||
/* no match, so let's look for case #2 */
|
||||
try {
|
||||
logger.info({
|
||||
@@ -178,7 +202,9 @@ module.exports = (srf, logger) => {
|
||||
user: uri.user
|
||||
}, 'sip realm is not associated with an account, checking carriers');
|
||||
const [gw] = await pp.query(sqlSelectCarrierRequiringRegistration, [uri.host, uri.user]);
|
||||
const matches = gw.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
|
||||
const matches = gw
|
||||
.sort((a, b) => b.netmask - a.netmask)
|
||||
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address));
|
||||
if (1 === matches.length) {
|
||||
// bingo
|
||||
//TODO: this assumes the carrier is associate to an account, not an SP
|
||||
@@ -188,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]
|
||||
};
|
||||
}
|
||||
@@ -229,6 +262,7 @@ module.exports = (srf, logger) => {
|
||||
/* find all carrier entries that have an inbound gateway matching the source IP */
|
||||
const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
|
||||
let matches = gw
|
||||
.sort((a, b) => b.netmask - a.netmask)
|
||||
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address))
|
||||
.map((gw) => {
|
||||
return {
|
||||
@@ -244,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');
|
||||
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
const debug = require('debug')('jambonz:sbc-inbound');
|
||||
const assert = require('assert');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const {nudgeCallCounts, roundTripTime} = require('./utils');
|
||||
const {nudgeCallCounts, roundTripTime, isMSTeamsCIDR} = require('./utils');
|
||||
const digestChallenge = require('@jambonz/digest-utils');
|
||||
const msProxyIps = process.env.MS_TEAMS_SIP_PROXY_IPS ?
|
||||
process.env.MS_TEAMS_SIP_PROXY_IPS.split(',').map((i) => i.trim()) :
|
||||
@@ -129,7 +129,7 @@ module.exports = function(srf, logger) {
|
||||
logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned');
|
||||
return res.send(404, 'Number Not Provisioned');
|
||||
}
|
||||
logger.debug({gateway}, 'identifyAccount: incoming call from gateway');
|
||||
logger.info({gateway}, 'identifyAccount: incoming call from gateway');
|
||||
|
||||
let sid;
|
||||
if (siprec) {
|
||||
@@ -155,8 +155,8 @@ module.exports = function(srf, logger) {
|
||||
...req.locals
|
||||
};
|
||||
}
|
||||
else if (msProxyIps.includes(req.source_address)) {
|
||||
logger.debug({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
|
||||
else if (msProxyIps.includes(req.source_address) || isMSTeamsCIDR(req.source_address)) {
|
||||
logger.info({source_address: req.source_address}, 'identifyAccount: incoming call from Microsoft Teams');
|
||||
const uri = parseUri(req.uri);
|
||||
|
||||
const app = await lookupAppByTeamsTenant(uri.host);
|
||||
@@ -178,7 +178,7 @@ module.exports = function(srf, logger) {
|
||||
else {
|
||||
req.locals.originator = 'user';
|
||||
const uri = parseUri(req.uri);
|
||||
logger.debug({source_address: req.source_address, realm: uri.host},
|
||||
logger.info({source_address: req.source_address, realm: uri.host},
|
||||
'identifyAccount: incoming user call');
|
||||
const account = await lookupAccountBySipRealm(uri.host);
|
||||
if (!account) {
|
||||
|
||||
+32
-5
@@ -1,5 +1,8 @@
|
||||
const CIDRMatcher = require('cidr-matcher');
|
||||
const express = require('express');
|
||||
const rtpCharacteristics = require('../data/rtp-transcoding');
|
||||
const srtpCharacteristics = require('../data/srtp-transcoding');
|
||||
|
||||
let idx = 0;
|
||||
|
||||
const isWSS = (req) => {
|
||||
@@ -19,15 +22,16 @@ 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');
|
||||
}
|
||||
const common = {
|
||||
'call-id': req.get('Call-ID'),
|
||||
'replace': ['origin', 'session-connection'],
|
||||
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no'
|
||||
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no',
|
||||
...(process.env.JAMBONES_ACCEPT_G729 && { codec: { mask: 'g729', transcode: 'pcmu'}})
|
||||
};
|
||||
return {
|
||||
common,
|
||||
@@ -96,7 +100,6 @@ const handleErrors = (logger, app, resolve, reject, e) => {
|
||||
|
||||
|
||||
const createHealthCheckApp = (port, logger) => {
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -178,6 +181,28 @@ const parseConnectionIp = (sdp) => {
|
||||
return arr ? arr[1] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if ip is one of MS Teams sip signalling ips
|
||||
* https://learn.microsoft.com/en-us/azure/communication-services/concepts
|
||||
* /telephony/direct-routing-infrastructure#sip-signaling-fqdns
|
||||
* @param ip IP address, example 172.31.0.1
|
||||
* */
|
||||
const isMSTeamsCIDR = (ip) => {
|
||||
const cidrs = [
|
||||
'52.112.0.0/14',
|
||||
'52.120.0.0/14'
|
||||
];
|
||||
const matcher = new CIDRMatcher(cidrs);
|
||||
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,
|
||||
@@ -194,5 +219,7 @@ module.exports = {
|
||||
createHealthCheckApp,
|
||||
nudgeCallCounts,
|
||||
roundTripTime,
|
||||
parseConnectionIp
|
||||
parseConnectionIp,
|
||||
isMSTeamsCIDR,
|
||||
isPrivateVoipNetwork
|
||||
};
|
||||
|
||||
Generated
+3903
-3183
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "0.8.4",
|
||||
"version": "0.9.0",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -25,30 +25,30 @@
|
||||
"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/siprec-client-utils": "^0.2.5",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@jambonz/digest-utils": "^0.0.3",
|
||||
"@aws-sdk/client-sns": "^3.360.0",
|
||||
"@aws-sdk/client-auto-scaling": "^3.360.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||
"@jambonz/rtpengine-utils": "^0.4.4",
|
||||
"@jambonz/siprec-client-utils": "^0.2.7",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/digest-utils": "^0.0.5",
|
||||
"@aws-sdk/client-sns": "^3.549.0",
|
||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||
"bent": "^7.3.12",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||
"drachtio-srf": "^4.5.21",
|
||||
"express": "^4.18.1",
|
||||
"pino": "^7.11.0",
|
||||
"drachtio-fn-b2b-sugar": "0.1.0",
|
||||
"drachtio-srf": "^4.5.38",
|
||||
"express": "^4.19.2",
|
||||
"pino": "^8.20.0",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"xml2js": "^0.4.23"
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^4.15.1"
|
||||
"tape": "^5.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
Subject: uac-pcap-carrier-success
|
||||
Content-Type: application/sdp
|
||||
Content-Length: [len]
|
||||
X-Authenticated-User: xhoaluu@jambonz.org
|
||||
X-Application-Sid: APP_ID_1
|
||||
|
||||
v=0
|
||||
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
|
||||
|
||||
Reference in New Issue
Block a user