Compare commits

...
10 Commits
Author SHA1 Message Date
Dave Horton 2d350d4850 bugfix: regression - adding custom headers to refer caused preferred hostname on Refer-To to be lost 2022-07-27 11:31:29 +01:00
Dave Horton baad125924 when releasing media, use asymetric flag so that rtpengine does react to a spurious final packet from freeswitch by incorrectly sending rtp there 2022-07-26 12:26:17 +01:00
Dave Horton 73528f5ce2 bugfix #131: pass on custom headers in REFER 2022-07-18 14:58:12 +02:00
Snyk bot c8329a94f3 fix: upgrade drachtio-srf from 4.5.0 to 4.5.1 (#39)
Snyk has created this PR to upgrade drachtio-srf from 4.5.0 to 4.5.1.

See this package in npm:
https://www.npmjs.com/package/drachtio-srf

See this project in Snyk:
https://app.snyk.io/org/davehorton/project/be6a10dd-83a0-4fef-a6b9-7dea4a956a5a?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-07-13 09:17:33 +02:00
Dave Horton dceeef5549 update to latest verify-aws-sns-signature 2022-07-06 18:27:24 +02:00
Paulo Tellesandp.souza b5551fffba improve dockerfile to fix snyk security issues (#38)
Co-authored-by: p.souza <p.souza@cognigy.com>
2022-07-06 18:17:20 +02:00
Dave Horton d2b5597571 initial support for siprec recording (#36)
* initial support for siprec recording

* handle pause/resume siprec recording
2022-06-23 16:23:09 -04:00
Dave Horton c9401ab3c8 update to azure 1.22.0 2022-06-11 16:22:26 -04:00
Dave Horton cc5c712a5b update deps 2022-06-11 11:40:44 -04:00
Dave Horton a923227e4a fix test scenario 2022-05-15 10:05:57 -04:00
6 changed files with 1799 additions and 2745 deletions
+17 -4
View File
@@ -1,10 +1,23 @@
FROM node:slim
FROM --platform=linux/amd64 node:16.15.1-alpine as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/
FROM base as build
COPY package.json package-lock.json ./
RUN npm ci
RUN npm prune
COPY . /opt/app
COPY . .
FROM base
COPY --from=build /opt/app /opt/app/
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
CMD [ "npm", "start" ]
CMD [ "node", "app.js" ]
+119 -4
View File
@@ -1,4 +1,5 @@
const Emitter = require('events');
const SrsClient = require('./srs-client');
const {makeRtpEngineOpts, SdpWantsSrtp, makeCallCountKey} = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {parseUri, stringifyUri, SipError} = require('drachtio-srf');
@@ -62,7 +63,10 @@ class CallSession extends Emitter {
blockDTMF,
unblockDTMF,
subscribeDTMF,
unsubscribeDTMF
unsubscribeDTMF,
subscribeRequest,
subscribeAnswer,
unsubscribe
} = engine;
this.offer = offer;
this.answer = answer;
@@ -73,6 +77,9 @@ class CallSession extends Emitter {
this.unblockDTMF = unblockDTMF;
this.subscribeDTMF = subscribeDTMF;
this.unsubscribeDTMF = unsubscribeDTMF;
this.subscribeRequest = subscribeRequest;
this.subscribeAnswer = subscribeAnswer;
this.unsubscribe = unsubscribe;
const featureServer = await this.getFeatureServer();
if (!featureServer) {
@@ -224,6 +231,12 @@ class CallSession extends Emitter {
this.rtpEngineResource.destroy().catch((err) => {});
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.srf.endSession(this.req);
});
@@ -288,6 +301,11 @@ class CallSession extends Emitter {
dlg.other = null;
other.other = null;
if (this.srsClient) {
this.srsClient.stop();
this.srsClient = null;
}
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
this.srf.endSession(this.req);
});
@@ -413,7 +431,6 @@ Duration=${payload.duration} `
direction,
sdp: req.body,
};
//if (reason && opts.flags && !opts.flags.includes('reset')) opts.flags.push('reset');
let response = await this.offer(opts);
if ('ok' !== response.result) {
@@ -426,6 +443,7 @@ Duration=${payload.duration} `
if (reason && dlg.type === 'uac' && ['release-media', 'anchor-media'].includes(reason)) {
this.logger.info({response}, `got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp;
answerMedia.flags = ['asymmetric', 'port latching'];
}
else {
sdp = await dlg.other.modify(response.sdp);
@@ -450,6 +468,7 @@ Duration=${payload.duration} `
async _onInfo(dlg, req, res) {
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;
try {
if (dlg.type === 'uac' && req.has('X-Reason')) {
const reason = req.get('X-Reason');
@@ -459,16 +478,100 @@ Duration=${payload.duration} `
'from-tag': fromTag
};
this.logger.info(`_onInfo: got request ${reason}`);
res.send(200);
if (reason.startsWith('mute')) {
const response = Promise.all([this.blockMedia(opts), this.blockDTMF(opts)]);
res.send(200);
this.logger.info({response}, `_onInfo: response to rtpengine command for ${reason}`);
}
else if (reason.startsWith('unmute')) {
const response = Promise.all([this.unblockMedia(opts), this.unblockDTMF(opts)]);
res.send(200);
this.logger.info({response}, `_onInfo: response to rtpengine command for ${reason}`);
}
else if (reason.includes('CallRecording')) {
let succeeded = false;
if (reason === 'startCallRecording') {
const from = this.req.getParsedHeader('From');
const to = this.req.getParsedHeader('To');
const aorFrom = from.uri;
const aorTo = to.uri;
this.logger.info({to, from}, 'startCallRecording request for a call');
const srsUrl = req.get('X-Srs-Url');
const srsRecordingId = req.get('X-Srs-Recording-ID');
const callSid = req.get('X-Call-Sid');
const accountSid = req.get('X-Account-Sid');
const applicationSid = req.get('X-Application-Sid');
if (this.srsClient) {
res.send(400);
this.logger.info('discarding duplicate startCallRecording request for a call');
return;
}
if (!srsUrl) {
this.logger.info('startCallRecording request is missing X-Srs-Url header');
res.send(400);
return;
}
this.srsClient = new SrsClient(this.logger, {
srf: dlg.srf,
originalInvite: this.req,
callingNumber: this.req.callingNumber,
calledNumber: this.req.calledNumber,
srsUrl,
srsRecordingId,
callSid,
accountSid,
applicationSid,
rtpEngineOpts: this.rtpEngineOpts,
fromTag,
toTag,
aorFrom,
aorTo,
subscribeRequest: this.subscribeRequest,
subscribeAnswer: this.subscribeAnswer,
del: this.del,
blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe
});
try {
succeeded = await this.srsClient.start();
} catch (err) {
this.logger.error({err}, 'Error starting SipRec call recording');
}
}
else if (reason === 'stopCallRecording') {
if (!this.srsClient) {
res.send(400);
this.logger.info('discarding stopCallRecording request because we are not recording');
return;
}
try {
succeeded = await this.srsClient.stop();
} catch (err) {
this.logger.error({err}, 'Error stopping SipRec call recording');
}
this.srsClient = null;
}
else if (reason === 'pauseCallRecording') {
if (!this.srsClient || this.srsClient.paused) {
this.logger.info('discarding invalid pauseCallRecording request');
res.send(400);
return;
}
succeeded = await this.srsClient.pause();
}
else if (reason === 'resumeCallRecording') {
if (!this.srsClient || !this.srsClient.paused) {
res.send(400);
this.logger.info('discarding invalid resumeCallRecording request');
return;
}
succeeded = await this.srsClient.resume();
}
res.send(succeeded ? 200 : 503);
}
}
else {
const immutableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
@@ -484,6 +587,10 @@ Duration=${payload.duration} `
res.send(response.status, {headers: responseHeaders, body: response.body});
}
} catch (err) {
if (this.srsClient) {
this.srsClient = null;
}
res.send(500);
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
}
}
@@ -520,11 +627,19 @@ Duration=${payload.duration} `
if (e164 && !uri.user.startsWith('+')) {
uri.user = `+${uri.user}`;
}
// eslint-disable-next-line no-unused-vars
const {via, from, to, 'call-id':callid, cseq, 'max-forwards':maxforwards,
// eslint-disable-next-line no-unused-vars
'content-length':contentlength, 'refer-to':_referto, 'referred-by':_referredby,
...customHeaders
} = req.headers;
const response = await this.uas.request({
method: 'REFER',
headers: {
'Refer-To': stringifyUri(uri),
'Referred-By': stringifyUri(u)
'Referred-By': stringifyUri(u),
...customHeaders
}
});
return res.send(response.status);
+249
View File
@@ -0,0 +1,249 @@
const Emitter = require('events');
const assert = require('assert');
const transform = require('sdp-transform');
const { v4: uuidv4 } = require('uuid');
const createMultipartSdp = (sdp, {
originalInvite,
srsRecordingId,
callSid,
accountSid,
applicationSid,
sipCallId,
aorFrom,
aorTo,
callingNumber,
calledNumber
}) => {
const sessionId = uuidv4();
const uuidStream1 = uuidv4();
const uuidStream2 = uuidv4();
const participant1 = uuidv4();
const participant2 = uuidv4();
const sipSessionId = originalInvite.get('Call-ID');
const {originator = 'unknown', carrier = 'unknown'} = originalInvite.locals;
const x = `--uniqueBoundary
Content-Disposition: session;handling=required
Content-Type: application/sdp
--sdp-placeholder--
--uniqueBoundary
Content-Disposition: recording-session
Content-Type: application/rs-metadata+xml
<?xml version="1.0" encoding="UTF-8"?>
<recording xmlns="urn:ietf:params:xml:ns:recording:1">
<datamode>complete</datamode>
<session session_id="${sessionId}">
<sipSessionID>${sipSessionId}</sipSessionID>
</session>
<extensiondata xmlns:jb="http://jambonz.org/siprec">
<jb:callsid>${callSid}</jb:callsid>
<jb:accountsid>${accountSid}</jb:accountsid>
<jb:applicationsid>${applicationSid}</jb:applicationsid>
<jb:recordingid>${srsRecordingId}</jb:recordingid>
<jb:originationsource>${originator}</jb:originationsource>
<jb:carrier>${carrier}</jb:carrier>
</extensiondata>
<participant participant_id="${participant1}">
<nameID aor="${aorFrom}">
<name>${callingNumber}</name>
</nameID>
</participant>
<participantsessionassoc participant_id="${participant1}" session_id="${sessionId}">
</participantsessionassoc>
<stream stream_id="${uuidStream1}" session_id="${sessionId}">
<label>1</label>
</stream>
<participant participant_id="${participant2}">
<nameID aor="${aorTo}">
<name>${calledNumber}</name>
</nameID>
</participant>
<participantsessionassoc participant_id="${participant2}" session_id="${sessionId}">
</participantsessionassoc>
<stream stream_id="${uuidStream2}" session_id="${sessionId}">
<label>2</label>
</stream>
<participantstreamassoc participant_id="${participant1}">
<send>${uuidStream1}</send>
<recv>${uuidStream2}</recv>
</participantstreamassoc>
<participantstreamassoc participant_id="${participant2}">
<send>${uuidStream2}</send>
<recv>${uuidStream1}</recv>
</participantstreamassoc>
</recording>`
.replace(/\n/g, '\r\n')
.replace('--sdp-placeholder--', sdp);
return `${x}\r\n`;
};
class SrsClient extends Emitter {
constructor(logger, opts) {
super();
const {
srf,
originalInvite,
calledNumber,
callingNumber,
srsUrl,
srsRecordingId,
callSid,
accountSid,
applicationSid,
srsDestUserName,
rtpEngineOpts,
//fromTag,
toTag,
aorFrom,
aorTo,
subscribeRequest,
subscribeAnswer,
del,
blockMedia,
unblockMedia,
unsubscribe
} = opts;
this.logger = logger;
this.srf = srf;
this.originalInvite = originalInvite;
this.callingNumber = callingNumber;
this.calledNumber = calledNumber;
this.subscribeRequest = subscribeRequest;
this.subscribeAnswer = subscribeAnswer;
this.del = del;
this.blockMedia = blockMedia;
this.unblockMedia = unblockMedia;
this.unsubscribe = unsubscribe;
this.srsUrl = srsUrl;
this.srsRecordingId = srsRecordingId;
this.callSid = callSid;
this.accountSid = accountSid;
this.applicationSid = applicationSid;
this.srsDestUserName = srsDestUserName;
this.rtpEngineOpts = rtpEngineOpts;
this.sipRecFromTag = toTag;
this.aorFrom = aorFrom;
this.aorTo = aorTo;
/* state */
this.activated = false;
this.paused = false;
}
async start() {
assert(!this.activated);
const opts = {
'call-id': this.rtpEngineOpts.common['call-id'],
'from-tag': this.sipRecFromTag
};
let response = await this.subscribeRequest({...opts, label: '1', flags: ['all'], interface: 'public'});
if (response.result !== 'ok') {
this.logger.error({response}, 'SrsClient:start error calling subscribe request');
throw new Error('error calling subscribe request');
}
this.siprecToTag = response['to-tag'];
const parsed = transform.parse(response.sdp);
parsed.name = 'jambonz SRS';
parsed.media[0].label = '1';
parsed.media[1].label = '2';
this.sdpOffer = transform.write(parsed);
const sdp = createMultipartSdp(this.sdpOffer, {
originalInvite: this.originalInvite,
srsRecordingId: this.srsRecordingId,
callSid: this.callSid,
accountSid: this.accountSid,
applicationSid: this.applicationSid,
calledNumber: this.calledNumber,
callingNumber: this.callingNumber,
aorFrom: this.aorFrom,
aorTo: this.aorTo
});
this.logger.info({response}, `SrsClient: sending SDP ${sdp}`);
/* */
try {
this.uac = await this.srf.createUAC(this.srsUrl, {
headers: {
'Content-Type': 'multipart/mixed;boundary=uniqueBoundary',
},
localSdp: sdp
});
} catch (err) {
this.logger.info({err}, `Error sending SIPREC INVITE to ${this.srsUrl}`);
throw err;
}
this.logger.info({sdp: this.uac.remote.sdp}, `SrsClient:start - successfully connected to SRS ${this.srsUrl}`);
response = await this.subscribeAnswer({
...opts,
sdp: this.uac.remote.sdp,
'to-tag': response['to-tag'],
label: '2'
});
if (response.result !== 'ok') {
this.logger.error({response}, 'SrsClient:start error calling subscribe answer');
throw new Error('error calling subscribe answer');
}
this.activated = true;
this.logger.info('successfully established siprec connection');
return true;
}
async stop() {
assert(this.activated);
const opts = {
'call-id': this.rtpEngineOpts.common['call-id'],
'from-tag': this.sipRecFromTag
};
this.del(opts)
//.then((response) => this.logger.debug({response}, 'Successfully stopped siprec media'))
.catch((err) => this.logger.info({err}, 'Error deleting siprec media session'));
this.uac.destroy().catch(() => {});
this.activated = false;
return true;
}
async pause() {
assert(!this.paused);
const opts = {
'call-id': this.rtpEngineOpts.common['call-id'],
'from-tag': this.sipRecFromTag
};
try {
await this.blockMedia(opts);
await this.uac.modify(this.sdpOffer.replace(/sendonly/g, 'inactive'));
this.paused = true;
return true;
} catch (err) {
this.logger.info({err}, 'Error pausing siprec media session');
}
return false;
}
async resume() {
assert(this.paused);
const opts = {
'call-id': this.rtpEngineOpts.common['call-id'],
'from-tag': this.sipRecFromTag
};
try {
await this.blockMedia(opts);
await this.uac.modify(this.sdpOffer);
} catch (err) {
this.logger.info({err}, 'Error resuming siprec media session');
}
return true;
}
}
module.exports = SrsClient;
+1385 -2724
View File
File diff suppressed because it is too large Load Diff
+11 -12
View File
@@ -26,30 +26,29 @@
},
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
"@jambonz/http-authenticator": "^0.2.0",
"@jambonz/http-authenticator": "^0.2.1",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.4.24",
"@jambonz/realtimedb-helpers": "^0.4.29",
"@jambonz/rtpengine-utils": "^0.3.1",
"@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.9",
"aws-sdk": "^2.1036.0",
"aws-sdk": "^2.1152.0",
"bent": "^7.3.12",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.3",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "0.0.12",
"drachtio-srf": "^4.4.59",
"express": "^4.17.3",
"husky": "^7.0.4",
"pino": "^7.4.1",
"rtpengine-client": "^0.2.0",
"verify-aws-sns-signature": "^0.0.6",
"drachtio-srf": "^4.5.1",
"express": "^4.18.1",
"pino": "^7.11.0",
"sdp-transform": "^2.14.1",
"uuid": "^8.3.2",
"verify-aws-sns-signature": "^0.0.7",
"xml2js": "^0.4.23"
},
"devDependencies": {
"clear-module": "^4.1.1",
"eslint": "^7.32.0",
"eslint-plugin-promise": "^4.3.1",
"nyc": "^15.1.0",
"tape": "^4.13.3"
"tape": "^4.15.1"
}
}
+18 -1
View File
@@ -124,7 +124,24 @@
]]>
</send>
<recv request="BYE">
</recv>
<send next="2">
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
<label id="2"/>
</scenario>