Compare commits

..
Author SHA1 Message Date
Quan HL 14552c0ef0 feat: outbound protocol 2023-05-10 09:41:52 +07:00
Quan HL d5895c24fe fix typo 2023-05-08 20:01:53 +07:00
Quan HL a1fdf56e73 fix typo 2023-05-08 20:00:22 +07:00
Quan HL 93d6a30160 fix typo 2023-05-08 19:59:41 +07:00
Quan HL 20ac00973a fix: add lookupCarrierByAccountLcr to locals 2023-05-05 09:25:53 +07:00
Quan HL 22312c50a4 fix: await for async 2023-04-25 18:27:57 +07:00
Quan HL 4020f84847 feat: lcr added 2023-04-25 15:57:45 +07:00
15 changed files with 474 additions and 1269 deletions
+1 -3
View File
@@ -2,8 +2,6 @@ name: Docker
on:
push:
branches:
- main
tags:
- '*'
@@ -20,7 +18,7 @@ jobs:
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=jambonz/sbc-outbound
IMAGE_ID=$GITHUB_REPOSITORY
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
+12 -11
View File
@@ -3,12 +3,7 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST &&
process.env.JAMBONES_MYSQL_USER &&
process.env.JAMBONES_MYSQL_PASSWORD &&
process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
if (process.env.JAMBONES_REDIS_SENTINELS) {
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
} else {
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
}
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_NETWORK_CIDR env var');
@@ -42,7 +37,7 @@ const setNameRtp = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-rtp
const rtpServers = [];
const {
ping,
lookupOutboundCarrierForAccount,
performLcr,
lookupAllTeamsFQDNs,
lookupAccountBySipRealm,
lookupAccountBySid,
@@ -66,8 +61,11 @@ const {
incrKey,
decrKey,
retrieveSet,
isMemberOfSet,
} = require('@jambonz/realtimedb-helpers')({}, logger);
isMemberOfSet
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
const activeCallIds = new Map();
const Emitter = require('events');
@@ -86,7 +84,7 @@ srf.locals = {...srf.locals,
idleEmitter,
dbHelpers: {
ping,
lookupOutboundCarrierForAccount,
performLcr,
lookupAllTeamsFQDNs,
lookupAccountBySipRealm,
lookupAccountBySid,
@@ -104,7 +102,10 @@ srf.locals = {...srf.locals,
isMemberOfSet
}
};
const {initLocals, checkLimits, route} = require('./lib/middleware')(srf, logger, redisClient);
const {initLocals, checkLimits, route} = require('./lib/middleware')(srf, logger, {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
});
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], logger, {
+120 -297
View File
@@ -1,18 +1,17 @@
const Emitter = require('events');
const sdpTransform = require('sdp-transform');
const SrsClient = require('@jambonz/siprec-client-utils');
const {makeRtpEngineOpts, nudgeCallCounts, isPrivateVoipNetwork} = require('./utils');
const {makeRtpEngineOpts, nudgeCallCounts} = require('./utils');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {SipError, stringifyUri, parseUri} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-outbound');
const makeInviteInProgressKey = (callid) => `sbc-out-iip${callid}`;
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
* and not the incoming PAI
*/
const createBLegFromHeader = (req, teams, register_from_domain = null) => {
const createBLegFromHeader = (req, teams) => {
const from = req.getParsedHeader('From');
const uri = parseUri(from.uri);
let user = uri.user || 'anonymous';
@@ -23,11 +22,6 @@ const createBLegFromHeader = (req, teams, register_from_domain = null) => {
else if (req.has('X-Preferred-From-User') || req.has('X-Preferred-From-Host')) {
user = req.get('X-Preferred-From-User') || user;
host = req.get('X-Preferred-From-Host') || host;
} else if (register_from_domain) {
host = register_from_domain;
}
if (from.name) {
return `${from.name} <sip:${user}@${host}>`;
}
return `sip:${user}@${host}`;
};
@@ -39,21 +33,19 @@ const createBLegToHeader = (req, teams) => {
return `sip:anonymous@${host}`;
};
const initCdr = (req, invite) => {
const {srf} = req;
const {trace_id} = req.locals;
const uri = parseUri(invite.uri);
const initCdr = (srf, req) => {
const uri = parseUri(req.uri);
const regex = /^\+(\d+)$/;
let arr = regex.exec(invite.calledNumber);
const to = arr ? arr[1] : invite.calledNumber;
arr = regex.exec(invite.callingNumber);
const from = arr ? arr[1] : invite.callingNumber;
let arr = regex.exec(req.calledNumber);
const to = arr ? arr[1] : req.calledNumber;
arr = regex.exec(req.callingNumber);
const from = arr ? arr[1] : req.callingNumber;
const applicationSid = req.get('X-Application-Sid');
return {
account_sid: req.get('X-Account-Sid'),
call_sid: req.get('X-Call-Sid'),
sip_callid: invite.get('Call-ID'),
sip_callid: req.get('Call-ID'),
...(applicationSid && {application_sid: applicationSid}),
from,
to,
@@ -63,7 +55,7 @@ const initCdr = (req, invite) => {
direction: 'outbound',
host: srf.locals.sipAddress,
remote_host: uri.host,
trace_id: trace_id || '00000000000000000000000000000000'
trace_id: req.get('X-Trace-ID') || '00000000000000000000000000000000'
};
};
@@ -91,23 +83,17 @@ class CallSession extends Emitter {
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
const {
lookupOutboundCarrierForAccount,
const {performLcr,
lookupCarrierBySid,
lookupSipGatewaysByCarrier,
lookupCarrierByAccountLcr
} = this.srf.locals.dbHelpers;
this.lookupOutboundCarrierForAccount = lookupOutboundCarrierForAccount;
this.performLcr = performLcr;
this.lookupCarrierBySid = lookupCarrierBySid;
this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier;
this.lookupCarrierByAccountLcr = lookupCarrierByAccountLcr;
this._mediaReleased = false;
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
}
get service_provider_sid() {
return this.req.locals.service_provider_sid;
}
get account_sid() {
@@ -182,9 +168,8 @@ class CallSession extends Emitter {
this.subscribeRequest = subscribeRequest;
this.subscribeAnswer = subscribeAnswer;
this.unsubscribe = unsubscribe;
this.srsClients = [];
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, false, teams);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, teams);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
let proxy, uris;
const mapGateways = new Map();
@@ -211,37 +196,21 @@ class CallSession extends Emitter {
destUri = stringifyUri(uri);
this.logger.info(`overriding destination user with ${dest}, so final uri is ${destUri}`);
}
uris = [{
private_network: contact.includes('transport=ws') ? false : await isPrivateVoipNetwork(destUri),
uri: destUri
}];
uris = [destUri];
if (!contact.includes('transport=ws')) {
proxy = this.req.locals.registration.proxy;
}
}
else if (this.req.locals.target === 'forward') {
uris = [{
private_network: await isPrivateVoipNetwork(this.req.uri),
uri: this.req.uri
}];
if (this.req.has('X-SIP-Proxy')) {
proxy = this.req.get('X-SIP-Proxy');
if (!proxy.startsWith('sip:') && !proxy.startsWith('sips:')) proxy = `sip:${proxy}`;
}
uris = [this.req.uri];
}
else if (teams) {
const vmailParam = 'opaque=app:voicemail';
proxy = `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com:5061;transport=tls`;
if (this.req.uri.includes(vmailParam)) {
uris = [{
private_network: false,
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`
}];
uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`];
}
else uris = [{
private_network: false,
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`
}];
else uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`];
headers = {
...headers,
Contact: `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
@@ -253,85 +222,60 @@ class CallSession extends Emitter {
let voip_carrier_sid = this.req.get('X-Requested-Carrier-Sid');
const account_sid = this.req.get('X-Account-Sid');
if (!voip_carrier_sid && account_sid) {
/* search for an LCR table for this account or service provider */
debug('calling lcr');
voip_carrier_sid = await this.lookupCarrierByAccountLcr(account_sid, this.req.calledNumber);
}
if (!voip_carrier_sid) {
/* no LCR for this account/SP - try with inbound carrier */
const inbound_carrier_sid = this.req.get('X-Voip-Carrier-Sid');
if (inbound_carrier_sid) {
const gateways = await this.lookupSipGatewaysByCarrier(inbound_carrier_sid);
const gws = (gateways || [])
.filter((gw) => gw.outbound);
if (gws.length) {
voip_carrier_sid = inbound_carrier_sid;
}
if (voip_carrier_sid) {
const vc = await this.lookupCarrierBySid(voip_carrier_sid);
const gateways = await this.lookupSipGatewaysByCarrier(voip_carrier_sid);
const gws = (gateways || [])
.filter((gw) => gw.outbound);
if (gws.length) {
uris = [];
gws.forEach((o) => {
const prefix = vc.tech_prefix;
const hostport = !o.port || 5060 === o.port ? o.ipv4 : `${o.ipv4}:${o.port}`;
const prependPlus = vc.e164_leading_plus && !this.req.calledNumber.startsWith('0');
const transport = o.protocol || 'udp';
const u = `sip:${prefix ? prefix : ''}${prependPlus ? '+' : ''}${this.req.calledNumber}@${hostport};transport=${transport}`;
const obj = {
name: vc.name,
diversion: vc.diversion,
hostport
};
if (vc.register_username && vc.register_password) {
obj.auth = {
username: vc.register_username,
password: vc.register_password
};
}
mapGateways.set(u, obj);
uris.push(u);
});
this.logger.debug({uris, voip_carrier_sid}, 'selected outbound gateways for requested carrier');
}
else {
this.logger.info({voip_carrier_sid}, 'no outbound gateways found for requested carrier');
}
}
if (!voip_carrier_sid) {
/* no LCR/ inbound carrier for this account/SP - at this point its a random shuffle of outbound carriers */
voip_carrier_sid = await this.lookupOutboundCarrierForAccount(this.account_sid);
}
if (!voip_carrier_sid) {
/* no outbound carriers exist for this account/SP */
this.logger.info(`no outbound carriers found for account_sid ${account_sid}`);
this.res.send(603);
return this.srf.endSession(this.req);
}
const vc = await this.lookupCarrierBySid(voip_carrier_sid);
const gateways = await this.lookupSipGatewaysByCarrier(voip_carrier_sid);
const gws = (gateways || [])
.filter((gw) => gw.outbound);
if (gws.length) {
uris = [];
gws.forEach((o) => {
const calledNumber = this.req.calledNumber.startsWith('+') ?
this.req.calledNumber.slice(1) :
this.req.calledNumber;
const prefix = vc.tech_prefix || '';
const protocol = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
const hostport = !o.port || 5060 === o.port ? o.ipv4 : `${o.ipv4}:${o.port}`;
const prependPlus = vc.e164_leading_plus && !this.req.calledNumber.startsWith('0') ? '+' : '';
const transport = `transport=${protocol}`;
const scheme = (protocol === 'tls' && !process.env.JAMBONES_USE_BEST_EFFORT_TLS) ? 'sips' : 'sip';
const u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};${transport}`;
const obj = {
name: vc.name,
diversion: vc.diversion,
hostport,
protocol: o.protocol,
register_from_domain: vc.register_from_domain
};
if (vc.register_username && vc.register_password) {
obj.auth = {
username: vc.register_username,
password: vc.register_password
};
}
mapGateways.set(u, obj);
uris.push(u);
if (o.protocol === 'tls/srtp') {
/** TODO: this is a bit of a hack in the sense that we are not
* supporting a scenario where you have a carrier with several outbound
* gateways, some requiring encrypted media and some not. This should be rectified
* but it will require more significant changes and right now it seems
* like a rare use case -- encryption is usually an all or nothing requirement.
*/
this.logger.info({u}, `using SRTP for outbound call, pad crypto: ${o.pad_crypto ? 'yes' : 'no'}`);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true);
}
});
// Check private network for each gw
uris = await Promise.all(uris.map(async(u) => {
return {
private_network: await isPrivateVoipNetwork(u),
uri: u
};
if (mapGateways.size === 0) {
/**
* We normalize the called number by removing a leading + before sending it to LCR..
* but LCR will return us an array of sip uris, with leading + for carriers that require it
*/
const routableNumber = this.req.calledNumber.startsWith('+') ?
this.req.calledNumber.slice(1) :
this.req.calledNumber;
const gateways = await this.performLcr(routableNumber, this.account_sid);
if (!gateways || gateways.length === 0) throw new Error('no routes found');
debug(`got gateways: ${JSON.stringify(gateways)}`);
gateways.forEach((gw) => mapGateways.set(gw.uri, {
name: gw.name,
auth: gw.auth,
diversion: gw.diversion,
hostport: gw.hostport
}));
this.logger.debug({uris, voip_carrier_sid}, 'selected outbound gateways for requested carrier');
}
else {
this.logger.info({voip_carrier_sid}, 'no outbound gateways found for requested carrier');
uris = gateways.map((gw) => gw.uri);
}
} catch (err) {
debug(err);
@@ -342,22 +286,15 @@ class CallSession extends Emitter {
debug(`sending call to PSTN ${uris}`);
}
// private_network should be called at last
uris = uris.sort((a, b) => a.private_network - b.private_network);
const toPrivate = uris.some((u) => u.private_network === true);
const toPublic = uris.some((u) => u.private_network === false);
let isOfferUpdatedToPrivate = toPrivate && !toPublic;
// rtpengine 'offer'
const opts = updateRtpEngineFlags(this.req.body, {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['private', toPublic ? 'public' : 'private'],
direction: ['private', 'public'],
sdp: this.req.body
});
let response = await this.offer(opts);
const response = await this.offer(opts);
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
if ('ok' !== response.result) {
@@ -372,17 +309,7 @@ class CallSession extends Emitter {
let earlyMedia = false;
while (uris.length) {
let hdrs = { ...headers};
const {private_network, uri} = uris.shift();
if (private_network && !isOfferUpdatedToPrivate) {
// Cannot make call to all public Uris, now come to talk with private network Uris
this.rtpEngineResource.destroy()
.catch((err) => this.logger.info({err}, 'Error destroying rtpe to re-connect to private network'));
response = await this.offer({
...opts,
direction: ['private', 'private']
});
isOfferUpdatedToPrivate = true;
}
const uri = uris.shift();
const gw = mapGateways.get(uri);
const passFailure = 0 === uris.length; // only a single target
if (0 === uris.length) {
@@ -400,10 +327,9 @@ class CallSession extends Emitter {
this.logger.info({err}, 'Error retrieving iip key');
}
}
// INVITE request line and To header should be the same.
hdrs = {...hdrs, 'To': uri};
if (gw) {
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
hdrs = {...hdrs, 'To': uri};
if (gw.diversion) {
let div = gw.diversion;
if (div.startsWith('+')) {
@@ -415,12 +341,6 @@ class CallSession extends Emitter {
'Diversion': div
};
}
if (gw.register_from_domain) {
hdrs = {
...hdrs,
'From': createBLegFromHeader(this.req, teams, gw.register_from_domain)
};
}
}
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
try {
@@ -440,10 +360,8 @@ class CallSession extends Emitter {
'-X-Jambonz-Routing',
'-X-Jambonz-FS-UUID',
'-X-Preferred-From-User',
'-X-Preferred-From-Host',
'X-Preferred-From-Host',
'-X-Jambonz-FS-UUID',
'-X-Voip-Carrier-Sid',
'-X-SIP-Proxy'
],
proxyResponseHeaders: [
'all',
@@ -461,7 +379,7 @@ class CallSession extends Emitter {
...this.rtpEngineOpts.uas.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag,
flags: ['single codec', 'inject DTMF'],
flags: ['single codec'],
sdp
};
const response = await this.answer(opts);
@@ -487,7 +405,7 @@ class CallSession extends Emitter {
}
else {
this.req.locals.cdr = {
...initCdr(this.req, inv),
...initCdr(this.req.srf, inv),
service_provider_sid: this.req.locals.service_provider_sid,
account_sid: this.req.locals.account_sid,
...(this.req.locals.application_sid && {application_sid: this.req.locals.application_sid}),
@@ -576,18 +494,13 @@ class CallSession extends Emitter {
this.uas = uas;
this.uac = uac;
[uas, uac].forEach((dlg) => {
dlg.on('destroy', async(bye) => {
dlg.on('destroy', async() => {
const other = dlg.other;
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
this.unsubscribeForDTMF();
//this.unsubscribeDTMF(this.logger, this.req.get('Call-ID'), this.rtpEngineOpts.uac.tag);
try {
const headers = {};
Object.keys(bye.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
await other.destroy();
} catch (err) {}
@@ -608,18 +521,12 @@ class CallSession extends Emitter {
/* write cdr for connected call */
if (this.req.locals.cdr) {
const now = Date.now();
const day = new Date();
const recordAllCalls = this.req.locals.record_all_calls;
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${this.req.locals.cdr.call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${recordAllCalls}`;
this.writeCdrs({...this.req.locals.cdr,
terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
sip_status: 200,
answered: true,
duration: Math.floor((now - callStart) / 1000),
...(recordAllCalls && {recording_url})
duration: Math.floor((now - callStart) / 1000)
}).catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
}
/* de-link the 2 Dialogs for GC */
@@ -628,7 +535,10 @@ class CallSession extends Emitter {
dlg.other = null;
other.other = null;
this._stopRecording();
if (this.srsClient) {
this.srsClient.stop();
this.srsClient = null;
}
this.logger.info(`call ended with normal termination, there are ${this.activeCallIds.size} active`);
if (this.activeCallIds.size === 0) this.idleEmitter.emit('idle');
@@ -653,29 +563,6 @@ class CallSession extends Emitter {
forwardInDialogRequests(uac, ['notify', 'options', 'message']);
}
_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 = [];
}
}
async _onRefer(dlg, req, res) {
/* REFER coming in from a sip device, forward to feature server */
try {
@@ -782,7 +669,6 @@ Duration=${payload.duration} `
this.logger.debug({opts, sdp: response.sdp}, 'CallSession:_onReinvite: (answer) sending back upstream');
res.send(200, {body: response.sdp});
} catch (err) {
res.send(err.status || 500);
this.logger.error(err, 'Error handling reinvite');
}
}
@@ -810,7 +696,6 @@ 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');
@@ -823,7 +708,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.srsClients.length) {
if (this.srsClient) {
res.send(400);
this.logger.info('discarding duplicate startCallRecording request for a call');
return;
@@ -833,14 +718,13 @@ Duration=${payload.duration} `
res.send(400);
return;
}
const arr = srsUrl.split(',');
this.srsClients = arr.map((url) => new SrsClient(this.logger, {
this.srsClient = new SrsClient(this.logger, {
srf: dlg.srf,
direction: 'outbound',
originalInvite: this.req,
callingNumber: this.req.callingNumber,
calledNumber: this.req.calledNumber,
srsUrl: url,
srsUrl,
srsRecordingId,
callSid,
accountSid,
@@ -854,90 +738,44 @@ Duration=${payload.duration} `
del: this.del,
blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe,
headers
}));
unsubscribe: this.unsubscribe
});
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.start()));
// Only take who success accept request.
this.srsClients = this.srsClients.filter((c) => c.activated);
succeeded = true;
succeeded = await this.srsClient.start();
} catch (err) {
this.logger.error({err}, 'Error starting SipRec call recording');
succeeded = false;
}
}
else if (reason === 'stopCallRecording') {
if (!this.srsClients.length) {
if (!this.srsClient) {
res.send(400);
this.logger.info('discarding stopCallRecording request because we are not recording');
return;
}
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.stop()));
succeeded = true;
succeeded = await this.srsClient.stop();
} catch (err) {
this.logger.error({err}, 'Error stopping SipRec call recording');
succeeded = false;
}
this.srsClients = [];
this.srsClient = null;
}
else if (reason === 'pauseCallRecording') {
if (!this.srsClients.length || this.srsClients.every((c) => c.paused)) {
if (!this.srsClient || this.srsClient.paused) {
this.logger.info('discarding invalid pauseCallRecording request');
res.send(400);
return;
}
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.pause({headers})));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error pausing SipRec call recording');
succeeded = false;
}
succeeded = await this.srsClient.pause();
}
else if (reason === 'resumeCallRecording') {
if (!this.srsClients.length || !this.srsClients.every((c) => c.paused)) {
if (!this.srsClient || !this.srsClient.paused) {
res.send(400);
this.logger.info('discarding invalid resumeCallRecording request');
return;
}
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.resume({headers})));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error resuming SipRec call recording');
succeeded = false;
}
}
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.uas.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');
succeeded = await this.srsClient.resume();
}
res.send(succeeded ? 200 : 503);
}
}
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
@@ -975,16 +813,15 @@ Duration=${payload.duration} `
}
}
else {
const headers = {};
Object.keys(req.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = req.headers[h];
const response = await dlg.other.request({
method: 'INFO',
headers: req.headers,
body: req.body
});
res.send(response.status, {
headers: response.headers,
body: response.body
});
const response = await dlg.other.request({method: 'INFO', headers, body: req.body});
const responseHeaders = {};
if (response.has('Content-Type')) {
Object.assign(responseHeaders, {'Content-Type': response.get('Content-Type')});
}
res.send(response.status, {headers: responseHeaders, body: response.body});
}
} catch (err) {
this.logger.info({err}, `Error handing INFO request on ${dlg.type} leg`);
@@ -993,16 +830,6 @@ Duration=${payload.duration} `
async _onFeatureServerTransfer(dlg, req, res) {
try {
// the following properties are ignored in the REFER headers
// 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,
// eslint-disable-next-line no-unused-vars
'X-Refer-To-Leave-Untouched': _leave,
...customHeaders
} = req.headers;
const referTo = req.getParsedHeader('Refer-To');
const uri = parseUri(referTo.uri);
this.logger.info({uri, referTo}, 'received REFER from feature server');
@@ -1020,8 +847,7 @@ Duration=${payload.duration} `
method: 'REFER',
headers: {
'Refer-To': stringifyUri(uri),
'Referred-By': stringifyUri(u),
...customHeaders
'Referred-By': stringifyUri(u)
}
});
return res.send(response.status);
@@ -1033,38 +859,35 @@ Duration=${payload.duration} `
...(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 uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uas = uac;
uac.type = 'uas';
uac.other = this.uac;
this.uac.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', () => {
const dlg = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uas = dlg;
this.uas.other = this.uac;
this.uac.other = this.uas;
this.uas.on('modify', this._onReinvite.bind(this, this.uas));
this.uas.on('refer', this._onFeatureServerTransfer.bind(this, this.uas));
this.uas.on('destroy', () => {
this.logger.info('call ended with normal termination');
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
if (this.activeCallIds.size === 0) this.idleEmitter.emit('idle');
uac.other.destroy();
this.uas.other.destroy();
this.srf.endSession(this.req);
});
const opts = {
...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uas.tag,
sdp: uac.remote.sdp,
flags: ['port latching']
};
const response = await this.offer(opts);
// modify rtpengine to stream to new feature server
let response = await this.offer(Object.assign(this.rtpEngineOpts.offer, {sdp: this.uas.remote.sdp}));
if ('ok' !== response.result) {
throw new Error(`_onFeatureServerTransfer: rtpengine offer failed: ${JSON.stringify(response)}`);
throw new Error(`_onReinvite: rtpengine failed: offer: ${JSON.stringify(response)}`);
}
const sdp = await this.uas.other.modify(response.sdp);
const opts = Object.assign({sdp, 'to-tag': res.getParsedHeader('To').params.tag},
this.rtpEngineOpts.answer);
response = await this.answer(opts);
if ('ok' !== response.result) {
throw new Error(`_onReinvite: rtpengine 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');
}
}
+2 -3
View File
@@ -4,10 +4,10 @@ const Registrar = require('@jambonz/mw-registrar');
const {selectHostPort, nudgeCallCounts} = require('./utils');
const FS_UUID_SET_NAME = 'fsUUIDs';
module.exports = (srf, logger, redisClient) => {
module.exports = (srf, logger, opts) => {
const {incrKey, decrKey, isMemberOfSet} = srf.locals.realtimeDbHelpers;
const {stats} = srf.locals;
const registrar = new Registrar(logger, redisClient);
const registrar = new Registrar(opts);
const {
lookupAccountCapacitiesBySid,
lookupAccountBySid,
@@ -19,7 +19,6 @@ module.exports = (srf, logger, redisClient) => {
const callId = req.get('Call-ID');
req.locals.account_sid = req.get('X-Account-Sid');
req.locals.application_sid = req.get('X-Application-Sid');
req.locals.record_all_calls = req.get('X-Record-All-Calls');
const traceId = req.locals.trace_id = req.get('X-Trace-ID');
req.locals.logger = logger.child({
callId,
+6 -45
View File
@@ -1,34 +1,19 @@
const rtpCharacteristics = require('../data/rtp-transcoding');
const srtpCharacteristics = require('../data/srtp-transcoding');
const debug = require('debug')('jambonz:sbc-outbound');
const CIDRMatcher = require('cidr-matcher');
const dns = require('dns');
const cidrMatcher = process.env.PRIVATE_VOIP_NETWORK_CIDR ?
new CIDRMatcher(process.env.PRIVATE_VOIP_NETWORK_CIDR.split(',')) : null;
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams) {
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
const from = req.getParsedHeader('from');
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
if (padCrypto) {
srtpCopy['default'].flags.push('SDES-pad');
srtpCopy['teams'].flags.push('SDES-pad');
}
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
/** Allow feature server to send DTMF to the call excepts call from/to teams */
if (!teams) {
if (!dstOpts.flags.includes('inject DTMF')) {
dstOpts.flags.push('inject DTMF');
}
if (!srcOpts.flags.includes('inject DTMF')) {
srcOpts.flags.push('inject DTMF');
}
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
dstOpts.flags.push('inject DTMF');
srcOpts.flags.push('inject DTMF');
}
const common = {
'call-id': req.get('Call-ID'),
@@ -190,29 +175,6 @@ const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
return {callsSP: null, calls: null, callsApp: null};
};
const isPrivateVoipNetwork = async(uri) => {
if (cidrMatcher) {
try {
const arr = /sips?:.*@(.*?)(:\d+)?(;.*)$/.exec(uri);
if (arr) {
const input = arr[1];
let addresses;
if (input.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
addresses = [input];
} else {
addresses = await dns.resolve4(input);
}
for (const ip of addresses) {
if (cidrMatcher.contains(ip)) {
return true;
}
}
}
} catch (err) {}
}
return false;
};
module.exports = {
makeRtpEngineOpts,
selectHostPort,
@@ -222,6 +184,5 @@ module.exports = {
equalsIgnoreOrder,
systemHealth,
createHealthCheckApp,
nudgeCallCounts,
isPrivateVoipNetwork
nudgeCallCounts
};
+291 -564
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "sbc-outbound",
"version": "0.8.5",
"version": "v0.8.2",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
@@ -24,21 +24,21 @@
"start": "node app",
"test": "NODE_ENV=test HTTP_PORT=3050 JAMBONES_HOSTING=1 JAMBONES_NETWORK_CIDR=127.0.0.1/32 JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error DRACHTIO_SECRET=cymru DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 JAMBONES_RTPENGINES=127.0.0.1:12222 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib --fix"
"jslint": "eslint app.js lib"
},
"dependencies": {
"@jambonz/db-helpers": "^0.9.3",
"@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/db-helpers": "^0.7.8",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "0.2.4",
"@jambonz/mw-registrar": "0.2.2",
"@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/siprec-client-utils": "^0.2.7",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/siprec-client-utils": "^0.2.4",
"@jambonz/stats-collector": "^0.1.8",
"@jambonz/time-series": "^0.2.5",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.5.31",
"drachtio-srf": "^4.5.21",
"express": "^4.18.1",
"pino": "^7.11.0",
"sdp-transform": "^2.14.1"
+2 -2
View File
@@ -429,7 +429,7 @@ inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound ca
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
protocol ENUM('udp','tcp','tls') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -683,4 +683,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
SET FOREIGN_KEY_CHECKS=1;
+5 -16
View File
@@ -27,23 +27,12 @@ insert into voip_carriers (voip_carrier_sid, name, service_provider_sid) values
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('e71519ff-4494-4c98-a06a-324e2712d94b', '1d8ef351-062a-4487-94f8-7698d5a40d24', '172.39.0.21', true, true);
-- lcr route: default eastco, but 1617 matches westco (503)
insert into lcr(lcr_sid, account_sid)
values('c0b0b6a0-0b0a-4b0b-8b0b-0b0b0b0b0b0b', 'ed649e33-e771-403a-8c99-1780eabbc803');
insert into lcr_routes(lcr_route_sid, lcr_sid, regex, priority)
values ('9eba4204-b036-4388-8f47-724c4cfb3d4e', 'c0b0b6a0-0b0a-4b0b-8b0b-0b0b0b0b0b0b', '.*', 999);
-- lcr route: first try eastco (503) then westco (200)
insert into lcr(lcr_sid, name, is_active, service_provider_sid) values ('0eba4204-b036-4388-8f47-724c4cfb3d4f', 'default LCR', 1, '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into lcr_routes(lcr_route_sid, lcr_sid, regex, priority) values ('0eba4204-b036-4388-8f47-724c4cfb3d4e', '0eba4204-b036-4388-8f47-724c4cfb3d4f', '.*', 1);
insert into lcr_carrier_set_entry(lcr_carrier_set_entry_sid, lcr_route_sid, voip_carrier_sid, priority)
values ('13e344a0-8a4c-4f95-8a19-ccbfc7ab053e', '9eba4204-b036-4388-8f47-724c4cfb3d4e', '1d8ef351-062a-4487-94f8-7698d5a40d24', 1);
-- attach default route to lcr
update lcr set default_carrier_set_entry_sid = '13e344a0-8a4c-4f95-8a19-ccbfc7ab053e';
-- add a route based on digit match
insert into lcr_routes(lcr_route_sid, lcr_sid, regex, priority)
values ('3eba4204-b036-4388-8f47-724c4cfb3d4e', 'c0b0b6a0-0b0a-4b0b-8b0b-0b0b0b0b0b0b', '1617', 1);
values ('b677a7b5-bec6-4045-ae4a-a67a5ccb3448', '0eba4204-b036-4388-8f47-724c4cfb3d4e', '1d8ef351-062a-4487-94f8-7698d5a40d24', 2);
insert into lcr_carrier_set_entry(lcr_carrier_set_entry_sid, lcr_route_sid, voip_carrier_sid, priority)
values ('b677a7b5-bec6-4045-ae4a-a67a5ccb3448', '3eba4204-b036-4388-8f47-724c4cfb3d4e', '287c1452-620d-4195-9f19-c9814ef90d78', 1);
values ('13e344a0-8a4c-4f95-8a19-ccbfc7ab053e', '0eba4204-b036-4388-8f47-724c4cfb3d4e', '287c1452-620d-4195-9f19-c9814ef90d78', 1);
+22
View File
@@ -17,7 +17,29 @@ values ('f4e1848d-3ff8-40eb-b9c1-30e1ef053f94','ed649e33-e771-403a-8c99-1780eabb
insert into account_products(account_product_sid, account_subscription_sid, product_sid,quantity)
values ('f23ff996-6534-4aba-8666-4b347391eca2', 'f4e1848d-3ff8-40eb-b9c1-30e1ef053f94', 'c4403cdb-8e75-4b27-9726-7d8315e3216d', 10);
-- "good" carrier - "westco" at 172.39.0.20
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid) values ('287c1452-620d-4195-9f19-c9814ef90d78', 'westco', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('124a5339-c62c-4075-9e19-f4de70a96597', '287c1452-620d-4195-9f19-c9814ef90d78', '172.39.0.20', true, true);
-- "bad" carrier - "eastco" at 172.39.0.21 (returns 503)
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid) values ('1d8ef351-062a-4487-94f8-7698d5a40d24', 'eastco', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('e71519ff-4494-4c98-a06a-324e2712d94b', '1d8ef351-062a-4487-94f8-7698d5a40d24', '172.39.0.21', true, true);
-- "bad" carrier - "northco" at 172.39.0.22 (returns 100 Trying and never answers)
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid) values ('7b4b9c56-4d81-4f31-9d70-62cd7d82193b', 'northco', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('fcbb2109-582c-4d55-b38a-b6bc7cc4be73', '7b4b9c56-4d81-4f31-9d70-62cd7d82193b', '172.39.0.22', true, true);
-- lcr route: first try eastco (503) then northco (cancel), then westco (200)
insert into lcr(lcr_sid, name, is_active, service_provider_sid) values ('0eba4204-b036-4388-8f47-724c4cfb3d4f', 'default LCR', 1, '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into lcr_routes(lcr_route_sid, lcr_sid, regex, priority) values ('0eba4204-b036-4388-8f47-724c4cfb3d4e', '0eba4204-b036-4388-8f47-724c4cfb3d4f', '.*', 1);
insert into lcr_carrier_set_entry(lcr_carrier_set_entry_sid, lcr_route_sid, voip_carrier_sid, priority)
values ('b677a7b5-bec6-4045-ae4a-a67a5ccb3448', '0eba4204-b036-4388-8f47-724c4cfb3d4e', '1d8ef351-062a-4487-94f8-7698d5a40d24', 3);
insert into lcr_carrier_set_entry(lcr_carrier_set_entry_sid, lcr_route_sid, voip_carrier_sid, priority)
values ('b417ce66-a805-46b3-b296-697a6c2ca249', '0eba4204-b036-4388-8f47-724c4cfb3d4e', '7b4b9c56-4d81-4f31-9d70-62cd7d82193b', 1);
insert into lcr_carrier_set_entry(lcr_carrier_set_entry_sid, lcr_route_sid, voip_carrier_sid, priority)
values ('13e344a0-8a4c-4f95-8a19-ccbfc7ab053e', '0eba4204-b036-4388-8f47-724c4cfb3d4e', '287c1452-620d-4195-9f19-c9814ef90d78', 2);
+2 -2
View File
@@ -33,7 +33,7 @@
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-cancel
Subject: uac-pcap-carrier-success
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
X-Jambonz-Routing: phone
@@ -60,7 +60,7 @@
<recv response="100" response_txn="invite"/>
<pause milliseconds="1000"/>
<pause milliseconds="3000"/>
<send>
<![CDATA[
@@ -1,92 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC with media">
<send retrans="500">
<![CDATA[
INVITE sip:16173333456@127.0.0.1 SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:16173333456@127.0.0.1>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
X-Jambonz-Routing: phone
X-Voip-Carrier-Sid: 287c1452-620d-4195-9f19-c9814ef90d78
Subject: uac-pcap-carrier-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[local_ip_type] [local_ip]
t=0 0
m=audio [auto_media_port] RTP/AVP 8 101
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-11,16
]]>
</send>
<recv response="100" optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="200" rtd="true" crlf="true">
</recv>
<send>
<![CDATA[
ACK sip:sip:+16173333456@127.0.0.1 SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:sip:+16173333456@127.0.0.1>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</send>
<!-- Play a pre-recorded PCAP file (RTP stream) -->
<nop>
<action>
<exec play_pcap_audio="pcap/g711a.pcap"/>
</action>
</nop>
<!-- Pause briefly -->
<pause milliseconds="2000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:+16173333456@127.0.0.1 SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:sip:+16173333456@127.0.0.1>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>
@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC with media">
<send retrans="500">
<![CDATA[
INVITE sip:16173333456@172.39.0.20 SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:16173333456@172.39.0.20>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
X-Jambonz-Routing: sip
Subject: uac-pcap-carrier-success
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[local_ip_type] [local_ip]
t=0 0
m=audio [auto_media_port] RTP/AVP 8 101
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-11,16
]]>
</send>
<recv response="100" optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="200" rtd="true" crlf="true">
</recv>
<send>
<![CDATA[
ACK sip:sip:+16173333456@127.0.0.1 SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:sip:+16173333456@127.0.0.1>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</send>
<!-- Play a pre-recorded PCAP file (RTP stream) -->
<nop>
<action>
<exec play_pcap_audio="pcap/g711a.pcap"/>
</action>
</nop>
<!-- Pause briefly -->
<pause milliseconds="2000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:sip:+16173333456@127.0.0.1 SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:sip:+16173333456@127.0.0.1>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Max-Forwards: 70
Subject: uac-pcap-carrier-success
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
</scenario>
-120
View File
@@ -1,120 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<scenario name="UAC with media">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:john@foo.bar SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag09[call_number]
To: <sip:john@foo.bar>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-sip-uri-proxy
X-Account-Sid: ed649e33-e771-403a-8c99-1780eabbc803
X-Call-Sid: fff49e33-e771-403a-8c99-1780eabbc803
X-Jambonz-Routing: sip
X-SIP-Proxy: 172.39.0.20:5060
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[local_ip_type] [local_ip]
t=0 0
m=audio [auto_media_port] RTP/AVP 8 101
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-11,16
]]>
</send>
<recv response="100" optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="200">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:john@foo.bar SIP/2.0
[last_Via:]
[last_From:]
To: <sip:sip:john@jambonz.org>[peer_tag_param]
[last_Call-ID:]
CSeq: 2 ACK
Subject:
Content-Length: 0
]]>
</send>
<!-- Play a pre-recorded PCAP file (RTP stream) -->
<nop>
<action>
<exec play_pcap_audio="pcap/g711a.pcap"/>
</action>
</nop>
<!-- Pause briefly -->
<pause milliseconds="2000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:john@foo.bar SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
[last_From:]
[last_To:]
[last_Call-ID:]
CSeq: 3 BYE
Max-Forwards: 70
Subject: uac-sip-uri-proxy
Content-Length: 0
]]>
</send>
<recv response="200" crlf="true">
</recv>
<!-- definition of the response time repartition table (unit is ms) -->
<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
<!-- definition of the call length repartition table (unit is ms) -->
<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
</scenario>
+3 -15
View File
@@ -44,14 +44,6 @@ test('sbc-outbound tests', async(t) => {
await sippUac('uac-pcap-carrier-success.xml');
t.pass('successfully completed outbound call to sip trunk');
/* call to Sip URI with no lcr configured */
await sippUac('uac-pcap-sip-routing-success.xml');
t.pass('successfully completed outbound call to sip routing trunk');
/* call to PSTN with no lcr configured */
await sippUac('uac-pcap-inbound-carrier-success.xml');
t.pass('successfully completed outbound call to sip trunk');
/* call to PSTN with request uri we see in kubernetes */
await sippUac('uac-pcap-carrier-success-k8s.xml');
t.pass('successfully completed outbound call to sip trunk (k8S req uri)');
@@ -62,7 +54,7 @@ test('sbc-outbound tests', async(t) => {
/* call to PSTN with lcr configured */
await sippUac('uac-pcap-carrier-success.xml');
t.pass('successfully completed outbound call using LCR');
t.pass('successfully completed outbound lcr carrier with crankback after failure');
// re-rack test data
execSync(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/jambones-sql.sql`);
@@ -84,10 +76,6 @@ test('sbc-outbound tests', async(t) => {
await sippUac('uac-sip-uri-auth-success.xml');
t.pass('successfully connected to sip uri that requires auth');
/* invite to sipUri through proxy */
await sippUac('uac-sip-uri-proxy.xml');
t.pass('successfully connected to sip uri through proxy');
// re-rack test data
execSync(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/jambones-sql.sql`);
execSync(`mysql -h 127.0.0.1 -u root --protocol=tcp -D jambones_test < ${__dirname}/db/populate-test-data5.sql`);
@@ -99,8 +87,8 @@ test('sbc-outbound tests', async(t) => {
await waitFor(25);
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
console.log(`${res.total} cdrs: ${JSON.stringify(res)}`);
t.ok(res.total === 9, 'wrote 9 cdrs');
console.log(`cdrs: ${JSON.stringify(res)}`);
t.ok(res.total === 6, 'wrote 6 cdrs');
srf.disconnect();
} catch (err) {