mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc3e2334ed | ||
|
|
d70a01578d | ||
|
|
eb0de5b0ee | ||
|
|
d684fbb59b | ||
|
|
d86d03c792 | ||
|
|
b93e220fef | ||
|
|
43ee944023 | ||
|
|
5a6a9b34d3 | ||
|
|
1b215ed77b | ||
|
|
0b141c55bf | ||
|
|
f7bd1f2bf5 | ||
|
|
454986edcd | ||
|
|
114d34def4 | ||
|
|
6cf59732fb | ||
|
|
7f2bce927d | ||
|
|
41eaa4286f | ||
|
|
9fe731ab65 | ||
|
|
0977dd16b3 | ||
|
|
33971ed6d8 | ||
|
|
6200f2b50a | ||
|
|
7e13c5559f | ||
|
|
4343b01505 | ||
|
|
81d7a9f1e6 | ||
|
|
695e44da29 | ||
|
|
9b1a70d4a5 | ||
|
|
db38269a77 | ||
|
|
337c24df41 | ||
|
|
554cdbcacf | ||
|
|
d50272d7da | ||
|
|
b2c4199c04 | ||
|
|
f79f29bd7e | ||
|
|
738429a46f |
@@ -2,6 +2,8 @@ name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
@@ -18,7 +20,7 @@ jobs:
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
run: |
|
||||
IMAGE_ID=$GITHUB_REPOSITORY
|
||||
IMAGE_ID=jambonz/sbc-outbound
|
||||
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
|
||||
@@ -3,7 +3,12 @@ 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');
|
||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||
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.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');
|
||||
@@ -61,11 +66,8 @@ const {
|
||||
incrKey,
|
||||
decrKey,
|
||||
retrieveSet,
|
||||
isMemberOfSet
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
}, logger);
|
||||
isMemberOfSet,
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
const activeCallIds = new Map();
|
||||
const Emitter = require('events');
|
||||
@@ -102,10 +104,7 @@ srf.locals = {...srf.locals,
|
||||
isMemberOfSet
|
||||
}
|
||||
};
|
||||
const {initLocals, checkLimits, route} = require('./lib/middleware')(srf, logger, {
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
});
|
||||
const {initLocals, checkLimits, route} = require('./lib/middleware')(srf, logger, redisClient);
|
||||
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, {
|
||||
|
||||
+196
-71
@@ -1,17 +1,18 @@
|
||||
const Emitter = require('events');
|
||||
const sdpTransform = require('sdp-transform');
|
||||
const SrsClient = require('@jambonz/siprec-client-utils');
|
||||
const {makeRtpEngineOpts, nudgeCallCounts} = require('./utils');
|
||||
const {makeRtpEngineOpts, nudgeCallCounts, isPrivateVoipNetwork} = 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) => {
|
||||
const createBLegFromHeader = (req, teams, register_from_domain = null) => {
|
||||
const from = req.getParsedHeader('From');
|
||||
const uri = parseUri(from.uri);
|
||||
let user = uri.user || 'anonymous';
|
||||
@@ -22,6 +23,11 @@ const createBLegFromHeader = (req, teams) => {
|
||||
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}`;
|
||||
};
|
||||
@@ -33,19 +39,21 @@ const createBLegToHeader = (req, teams) => {
|
||||
return `sip:anonymous@${host}`;
|
||||
};
|
||||
|
||||
const initCdr = (srf, req) => {
|
||||
const uri = parseUri(req.uri);
|
||||
const initCdr = (req, invite) => {
|
||||
const {srf} = req;
|
||||
const {trace_id} = req.locals;
|
||||
const uri = parseUri(invite.uri);
|
||||
const regex = /^\+(\d+)$/;
|
||||
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;
|
||||
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;
|
||||
const applicationSid = req.get('X-Application-Sid');
|
||||
|
||||
return {
|
||||
account_sid: req.get('X-Account-Sid'),
|
||||
call_sid: req.get('X-Call-Sid'),
|
||||
sip_callid: req.get('Call-ID'),
|
||||
sip_callid: invite.get('Call-ID'),
|
||||
...(applicationSid && {application_sid: applicationSid}),
|
||||
from,
|
||||
to,
|
||||
@@ -55,7 +63,7 @@ const initCdr = (srf, req) => {
|
||||
direction: 'outbound',
|
||||
host: srf.locals.sipAddress,
|
||||
remote_host: uri.host,
|
||||
trace_id: req.get('X-Trace-ID') || '00000000000000000000000000000000'
|
||||
trace_id: trace_id || '00000000000000000000000000000000'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -97,6 +105,10 @@ class CallSession extends Emitter {
|
||||
this._mediaReleased = false;
|
||||
}
|
||||
|
||||
get service_provider_sid() {
|
||||
return this.req.locals.service_provider_sid;
|
||||
}
|
||||
|
||||
get account_sid() {
|
||||
return this.req.locals.account_sid;
|
||||
}
|
||||
@@ -169,8 +181,9 @@ class CallSession extends Emitter {
|
||||
this.subscribeRequest = subscribeRequest;
|
||||
this.subscribeAnswer = subscribeAnswer;
|
||||
this.unsubscribe = unsubscribe;
|
||||
this.srsClients = [];
|
||||
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, teams);
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, false, teams);
|
||||
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
|
||||
let proxy, uris;
|
||||
const mapGateways = new Map();
|
||||
@@ -197,21 +210,37 @@ class CallSession extends Emitter {
|
||||
destUri = stringifyUri(uri);
|
||||
this.logger.info(`overriding destination user with ${dest}, so final uri is ${destUri}`);
|
||||
}
|
||||
uris = [destUri];
|
||||
uris = [{
|
||||
private_network: contact.includes('transport=ws') ? false : await isPrivateVoipNetwork(destUri),
|
||||
uri: destUri
|
||||
}];
|
||||
if (!contact.includes('transport=ws')) {
|
||||
proxy = this.req.locals.registration.proxy;
|
||||
}
|
||||
}
|
||||
else if (this.req.locals.target === 'forward') {
|
||||
uris = [this.req.uri];
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
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 = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`];
|
||||
uris = [{
|
||||
private_network: false,
|
||||
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com;${vmailParam}`
|
||||
}];
|
||||
}
|
||||
else uris = [`sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`];
|
||||
else uris = [{
|
||||
private_network: false,
|
||||
uri: `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`
|
||||
@@ -227,7 +256,19 @@ class CallSession extends Emitter {
|
||||
voip_carrier_sid = await this.lookupCarrierByAccountLcr(account_sid, this.req.calledNumber);
|
||||
}
|
||||
if (!voip_carrier_sid) {
|
||||
/* no LCR for this account/SP - at this point its a random shuffle of outbound carriers */
|
||||
/* 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) {
|
||||
/* 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) {
|
||||
@@ -257,7 +298,8 @@ class CallSession extends Emitter {
|
||||
name: vc.name,
|
||||
diversion: vc.diversion,
|
||||
hostport,
|
||||
protocol: o.protocol
|
||||
protocol: o.protocol,
|
||||
register_from_domain: vc.register_from_domain
|
||||
};
|
||||
if (vc.register_username && vc.register_password) {
|
||||
obj.auth = {
|
||||
@@ -274,10 +316,17 @@ class CallSession extends Emitter {
|
||||
* 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');
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, true);
|
||||
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
|
||||
};
|
||||
}));
|
||||
this.logger.debug({uris, voip_carrier_sid}, 'selected outbound gateways for requested carrier');
|
||||
}
|
||||
else {
|
||||
@@ -292,15 +341,22 @@ 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', 'public'],
|
||||
direction: ['private', toPublic ? 'public' : 'private'],
|
||||
sdp: this.req.body
|
||||
});
|
||||
const response = await this.offer(opts);
|
||||
let 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) {
|
||||
@@ -315,7 +371,17 @@ class CallSession extends Emitter {
|
||||
let earlyMedia = false;
|
||||
while (uris.length) {
|
||||
let hdrs = { ...headers};
|
||||
const uri = uris.shift();
|
||||
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 gw = mapGateways.get(uri);
|
||||
const passFailure = 0 === uris.length; // only a single target
|
||||
if (0 === uris.length) {
|
||||
@@ -333,9 +399,10 @@ 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('+')) {
|
||||
@@ -347,6 +414,12 @@ 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 {
|
||||
@@ -366,8 +439,10 @@ 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',
|
||||
@@ -385,7 +460,7 @@ class CallSession extends Emitter {
|
||||
...this.rtpEngineOpts.uas.mediaOpts,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
'to-tag': this.rtpEngineOpts.uac.tag,
|
||||
flags: ['single codec'],
|
||||
flags: ['single codec', 'inject DTMF'],
|
||||
sdp
|
||||
};
|
||||
const response = await this.answer(opts);
|
||||
@@ -411,7 +486,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else {
|
||||
this.req.locals.cdr = {
|
||||
...initCdr(this.req.srf, inv),
|
||||
...initCdr(this.req, 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}),
|
||||
@@ -500,13 +575,18 @@ 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();
|
||||
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) {}
|
||||
|
||||
@@ -527,12 +607,18 @@ 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)
|
||||
duration: Math.floor((now - callStart) / 1000),
|
||||
...(recordAllCalls && {recording_url})
|
||||
}).catch((err) => this.logger.error({err}, 'Error writing cdr for completed call'));
|
||||
}
|
||||
/* de-link the 2 Dialogs for GC */
|
||||
@@ -541,10 +627,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`);
|
||||
if (this.activeCallIds.size === 0) this.idleEmitter.emit('idle');
|
||||
@@ -569,6 +652,13 @@ class CallSession extends Emitter {
|
||||
forwardInDialogRequests(uac, ['notify', 'options', 'message']);
|
||||
}
|
||||
|
||||
_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 {
|
||||
@@ -707,6 +797,7 @@ Duration=${payload.duration} `
|
||||
const to = this.req.getParsedHeader('To');
|
||||
const aorFrom = from.uri;
|
||||
const aorTo = to.uri;
|
||||
const headers = contentType === 'application/json' && req.body ? JSON.parse(req.body) : {};
|
||||
this.logger.info({to, from}, 'startCallRecording request for an outbound call');
|
||||
|
||||
const srsUrl = req.get('X-Srs-Url');
|
||||
@@ -714,7 +805,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;
|
||||
@@ -724,13 +815,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: 'outbound',
|
||||
originalInvite: this.req,
|
||||
callingNumber: this.req.callingNumber,
|
||||
calledNumber: this.req.calledNumber,
|
||||
srsUrl,
|
||||
srsUrl: url,
|
||||
srsRecordingId,
|
||||
callSid,
|
||||
accountSid,
|
||||
@@ -744,44 +836,73 @@ 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();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.start())
|
||||
)).every((r) => r);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error starting SipRec call recording');
|
||||
}
|
||||
}
|
||||
else if (reason === 'stopCallRecording') {
|
||||
if (!this.srsClient) {
|
||||
if (!this.srsClients.length) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding stopCallRecording request because we are not recording');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
succeeded = await this.srsClient.stop();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.stop())
|
||||
)).every((r) => r);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Error stopping SipRec call recording');
|
||||
}
|
||||
this.srsClient = null;
|
||||
this.srsClients = [];
|
||||
}
|
||||
else if (reason === 'pauseCallRecording') {
|
||||
if (!this.srsClient || this.srsClient.paused) {
|
||||
if (!this.srsClients.length || this.srsClients.every((c) => c.paused)) {
|
||||
this.logger.info('discarding invalid pauseCallRecording request');
|
||||
res.send(400);
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.pause();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.pause())
|
||||
)).every((r) => r);
|
||||
}
|
||||
else if (reason === 'resumeCallRecording') {
|
||||
if (!this.srsClient || !this.srsClient.paused) {
|
||||
if (!this.srsClients.length || !this.srsClients.every((c) => c.paused)) {
|
||||
res.send(400);
|
||||
this.logger.info('discarding invalid resumeCallRecording request');
|
||||
return;
|
||||
}
|
||||
succeeded = await this.srsClient.resume();
|
||||
succeeded = (await Promise.all(
|
||||
this.srsClients.map((c) => c.resume())
|
||||
)).every((r) => r);
|
||||
}
|
||||
res.send(succeeded ? 200 : 503);
|
||||
} else if (reason.includes('Dtmf')) {
|
||||
const arr = /Signal=\s*([0-9#*])/.exec(req.body);
|
||||
if (!arr) {
|
||||
this.logger.info({body: req.body}, '_onInfo: invalid INFO Dtmf');
|
||||
throw new Error(`_onInfo: no dtmf in body for ${contentType}`);
|
||||
}
|
||||
const code = arr[1];
|
||||
const arr2 = /Duration=\s*(\d+)/.exec(req.body);
|
||||
const duration = arr2 ? arr2[1] : 250;
|
||||
const dtmfOpts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
'from-tag': this.rtpEngineOpts.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
|
||||
@@ -819,15 +940,16 @@ Duration=${payload.duration} `
|
||||
}
|
||||
}
|
||||
else {
|
||||
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 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, 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`);
|
||||
@@ -865,35 +987,38 @@ 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 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', () => {
|
||||
|
||||
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', () => {
|
||||
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');
|
||||
this.uas.other.destroy();
|
||||
uac.other.destroy();
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
|
||||
// modify rtpengine to stream to new feature server
|
||||
let response = await this.offer(Object.assign(this.rtpEngineOpts.offer, {sdp: this.uas.remote.sdp}));
|
||||
const opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
'from-tag': this.rtpEngineOpts.uas.tag,
|
||||
sdp: uac.remote.sdp,
|
||||
flags: ['port latching']
|
||||
};
|
||||
const response = await this.offer(opts);
|
||||
if ('ok' !== response.result) {
|
||||
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)}`);
|
||||
throw new Error(`_onFeatureServerTransfer: rtpengine offer 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');
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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, opts) => {
|
||||
module.exports = (srf, logger, redisClient) => {
|
||||
const {incrKey, decrKey, isMemberOfSet} = srf.locals.realtimeDbHelpers;
|
||||
const {stats} = srf.locals;
|
||||
const registrar = new Registrar(opts);
|
||||
const registrar = new Registrar(logger, redisClient);
|
||||
const {
|
||||
lookupAccountCapacitiesBySid,
|
||||
lookupAccountBySid,
|
||||
@@ -19,6 +19,7 @@ module.exports = (srf, logger, opts) => {
|
||||
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,
|
||||
|
||||
+45
-6
@@ -1,19 +1,34 @@
|
||||
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');
|
||||
|
||||
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
|
||||
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) {
|
||||
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;
|
||||
|
||||
/* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */
|
||||
if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) {
|
||||
dstOpts.flags.push('inject DTMF');
|
||||
srcOpts.flags.push('inject DTMF');
|
||||
/** 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');
|
||||
}
|
||||
}
|
||||
const common = {
|
||||
'call-id': req.get('Call-ID'),
|
||||
@@ -175,6 +190,29 @@ 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,
|
||||
@@ -184,5 +222,6 @@ module.exports = {
|
||||
equalsIgnoreOrder,
|
||||
systemHealth,
|
||||
createHealthCheckApp,
|
||||
nudgeCallCounts
|
||||
nudgeCallCounts,
|
||||
isPrivateVoipNetwork
|
||||
};
|
||||
|
||||
Generated
+566
-293
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.8.3",
|
||||
"version": "0.8.5",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
@@ -27,18 +27,18 @@
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.8.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.7.2",
|
||||
"@jambonz/db-helpers": "^0.9.0",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.7",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "0.2.2",
|
||||
"@jambonz/mw-registrar": "0.2.4",
|
||||
"@jambonz/rtpengine-utils": "^0.4.3",
|
||||
"@jambonz/siprec-client-utils": "^0.2.4",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/siprec-client-utils": "^0.2.6",
|
||||
"@jambonz/stats-collector": "^0.1.9",
|
||||
"@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.21",
|
||||
"drachtio-srf": "^4.5.31",
|
||||
"express": "^4.18.1",
|
||||
"pino": "^7.11.0",
|
||||
"sdp-transform": "^2.14.1"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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>
|
||||
+13
-1
@@ -44,6 +44,14 @@ 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)');
|
||||
@@ -76,6 +84,10 @@ 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`);
|
||||
@@ -88,7 +100,7 @@ test('sbc-outbound tests', async(t) => {
|
||||
|
||||
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
|
||||
console.log(`${res.total} cdrs: ${JSON.stringify(res)}`);
|
||||
t.ok(res.total === 6, 'wrote 6 cdrs');
|
||||
t.ok(res.total === 9, 'wrote 9 cdrs');
|
||||
|
||||
srf.disconnect();
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user