Compare commits

...
56 Commits
Author SHA1 Message Date
Hoan Luu Huu dd198105e3 update drachtio srf 5.0.4 (#176) 2025-04-18 07:50:21 -04:00
rammohan-y a34491a356 Updated db-helper to 0.9.11 (#175)
https://github.com/jambonz/jambonz-feature-server/issues/1151
2025-04-14 08:22:09 -04:00
Dave Horton 3a467921a6 return 482 loop detected if call to a sip uri would loop back to us (#173)
* return 482 loop detected if call to a sip uri would loop back to us

* this pr fixes #172

* typo

* wip

* wip
2025-03-20 11:25:08 -04:00
rammohan-y 9cb75a0b6b updated realtimedb-helper to 0.8.13 (#169) 2025-03-10 09:53:49 -04:00
Dave Horton 28dae50202 REFER should have sips contact if far end is using sips (#168) 2025-02-24 09:47:47 -05:00
Dave Horton 0b45e766b9 update drachtio-srf 2025-02-20 10:16:59 -05:00
Dave Horton 7380457b5a reject calls on hosted jambonz with no activ (#166)
e subscriptions
2025-02-19 13:04:44 -05:00
Hoan Luu Huu 9feb6f3c8f support voip carrier sip proxy (#165)
* support voiip carrier sip proxy

* wip

* wip

* wip
2025-02-17 09:47:59 -05:00
Hoan Luu Huu c449feeb9c support sip recording from siprec call (#164)
* support sip recording from siprec call

* update srsclient version
2025-02-12 09:24:33 -05:00
Dave Horton b2abe9891e refined the method for syncing call count updates and added debugging (#163)
* refined the method for syncing call count updates and added debugging

* wip

* include callId in debug key
2025-02-05 13:01:04 -05:00
Dave Horton a89b54b19e update deps 2025-01-14 10:50:08 -05:00
Dave Horton ab0f3ed2b5 update dractio-srf and mrf to main branch releases (#162) 2025-01-14 10:06:27 -05:00
Vinod Dharashive d8ae824559 Fresh/466 (#160)
* Hold and unhold does not resume transcript for outbound call to webrtc

* WIP

* jslint

* jslint

* handle null

* Added direction

* re-invite with opus codec need to change pcmu 

on reinvite on unhold opus codec was been sent from rtpegine to freeswitch due to which transcripts were not getting generated , hence it needs to be changed to pcmu

* jslint

jslint
2024-12-23 07:20:04 -05:00
Hoan Luu Huu 334db6f84f support referby display name (#161) 2024-12-11 10:34:27 -05:00
Hoan Luu Huu c762accce8 forward all refer to feature server (#156)
* forward all refer to feature server

* forwawrd extra custom header
2024-12-05 21:16:12 -05:00
Dave Horton c6c63e26da fix duplicate attempt to destroy dialog 2024-11-22 10:46:24 -05:00
Dave Horton 916d577b75 wip (#155) 2024-11-19 09:37:53 -05:00
Dave Horton b976a62a60 when forcing PCMU or PCMA we must also include telephone-event (#153) 2024-11-14 08:37:40 -05:00
Hoan Luu Huu 5e2369e5e8 support opus transcode (#150)
* support opus transcode

* dont resend reinvite when release media for srtp

* wip

* fix review comment
2024-11-13 07:41:43 -05:00
Dave Horton 41528db630 fix call count race condition #151 (#152)
* fix call count race condition #151

* logging fix
2024-11-12 14:57:04 -05:00
Hoan Luu Huu c22104fbe2 allow outbound cdr has sip_parent_callid (#149)
* allow outbound cdr has sip_parent_callid

* update time series version
2024-10-17 08:05:25 -04:00
Hoan Luu Huu 3701e20295 fix sbc crash while outbound calling to user (#148) 2024-10-15 07:28:40 -04:00
Hoan Luu Huu 821275cb67 support X-CID for feature server to detect sip callid (#147)
* support X-CID for feature server to detect sip callid

* fixed review comment

* fix review comment
2024-10-15 07:19:25 -04:00
Dave Horton f5ac51a8f7 minor logging 2024-10-13 10:12:55 -04:00
Hoan Luu Huu 0cf90f37b8 support change log level runtime (#146) 2024-10-07 09:52:22 -04:00
Dave Horton 62fb716236 bump version 2024-09-04 13:42:02 +01:00
Dave Horton b8fdf9f429 when sending out registered trunk use auth.username as userinfo in Contact header of INVITE 2024-09-04 13:40:59 +01:00
Dave Horton 503aa73bc6 Fix/invite use realm to registered trunk (#144)
* special header X-Preferred-From-Host was ignored

* when sending INVITE to registered trunk with a sip realm, use realm in the uri and send via proxy as defined in outbound gateway
2024-08-29 15:26:44 -04:00
Dave Horton a7406ddb8a use system_information.private_network_cidr (#143)
* use system_information.private_network_cidr

* make gh actions work

* fix: export logger
2024-08-18 12:50:19 -04:00
Dave HortonandMarkus Frindt bf1c670b40 Fix/tls transport (#141)
* fix scheme

* add missing initialization of scheme

* delete contact header explicitly

* wip

* wip

* wip

* fix bug with cseq

* wip

* deps

* wip

* wip

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-07-24 15:27:01 -04:00
Markus FrindtandMarkus Frindt b21eaad1ff Support contact header with sips or sip over tls (#139)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-07-23 08:53:38 -04:00
Dave Horton c671fbdbd0 after challenge register with creds should stay tls if it started that way (#137) 2024-06-24 15:08:43 -04:00
Hoan Luu Huu 73e3779eb1 only send Sips if it's enabled in outbound gateway configuration (#136)
* only send Sips if it's enabled in outbound gateway configuration

* update license
2024-06-15 09:12:02 -04:00
Dave Horton 9ad7dc76c7 fix for #133 (#134) 2024-05-17 07:21:19 -04:00
Hoan Luu Huu 1baf7fa824 Feat/refer support conplex uri for refer-to refered-by header (#131)
* respond to reinvite request incase error

* support complex refer-to refer-by uri

* wip
2024-05-07 08:28:08 -04:00
Hoan Luu Huu d2807c69ed Feat/stats version (#132)
* respond to reinvite request incase error

* update stats colector version
2024-05-06 20:06:23 -04:00
Dave Horton 3e6dc35e86 update deps (#130) 2024-04-07 18:33:39 -04:00
Hoan Luu Huu 16a4709b7d check if sip gateway is in blacklist before sending outbound call (#119)
* check if sip gateway is in blacklist before sending outbound call

* wip

* wip

* wip

* add testcase for blacklist

* wip
2024-03-30 11:10:13 -04:00
Hoan Luu Huu 01adb5cbf0 fix cannot clear siprec records (#127) 2024-03-27 07:50:50 -04:00
Dave Horton 49ba872f51 remove asymmetric flag on offer to B party reinvite as port learning should still happen (#128) 2024-03-26 12:13:01 -04:00
Hoan Luu Huu 591b8f2f8a respond to reinvite request incase error (#126) 2024-03-07 07:49:05 -05:00
Hoan Luu Huu 16ac56e50e start/stop/pause/resume recording success when one of siprec server success. (#125)
* start/stop/pause/resume recording success when one of siprec server success.

* wip

* allow send custom header on pause, resume recording

* allow timeout for siprec action

* wip

* wip

* update siprec-client-utils
2024-03-05 13:06:04 -05:00
Markus FrindtandMarkus Frindt 9412790366 add fix to lint script (#124)
Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-02-15 09:44:56 -05:00
Dave Horton 453b27d6e1 avoid sips scheme if best effort TLS is enabled via env (#122) 2024-01-30 20:09:58 -08:00
Dave Horton 4ba3d5b2b0 update db-helpers 2024-01-17 13:37:39 -05:00
Hoan Luu Huu bc3e2334ed fix hangup custom headers (#121) 2023-12-28 20:44:33 -05:00
Hoan Luu Huu d70a01578d INVITE from gw should have register_from_domain if it's available (#120) 2023-12-13 07:57:28 -05:00
Hoan Luu Huu eb0de5b0ee Feat support Outbound DTMF (#115)
* feat/support inband dtmf

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2023-12-11 10:40:24 -05:00
Anton Voylenko d684fbb59b Set display name (#117)
* JAMBONES_MYSQL_PORT env added

* set display name from header

* remove double quotes
2023-12-10 09:07:21 -05:00
Hoan Luu Huu d86d03c792 feat support invite private to private network (#111)
* feat support invite private to private network

* feat support invite private to private network

* support multi private cidrs

* fix review comment

* fix matching regex

* fix regex to extract IP from sip uri
2023-11-27 08:26:41 -05:00
Hoan Luu Huu b93e220fef let realtimedb-helper build configuration from env vars (#109) 2023-11-14 09:12:48 -05:00
Dave Horton 43ee944023 add support for sending calls to a sip uri through a proxy (#114) 2023-11-13 14:38:05 -05:00
Dave Horton 5a6a9b34d3 0.8.5 2023-11-09 12:39:49 -05:00
Dave Horton 1b215ed77b update drachtio-srf 2023-11-09 10:04:08 -05:00
Hoan Luu Huu 0b141c55bf fix outbound info (#107) 2023-10-10 08:18:43 -04:00
Hoan Luu Huu f7bd1f2bf5 add sentinelPassword option (#106) 2023-10-04 19:41:43 -04:00
15 changed files with 2279 additions and 4671 deletions
+8 -3
View File
@@ -6,12 +6,17 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 20.x
- run: npm ci
- run: npm run jslint
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- run: npm test
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run jslint
npm run jslint
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Drachtio Communications Services, LLC
Copyright (c) 2018-2024 FirstFive8, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+28 -30
View File
@@ -14,27 +14,6 @@ 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');
assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var');
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
let host, port = 26379;
if (sentinel.includes(':')) {
const arr = sentinel.split(':');
host = arr[0];
port = parseInt(arr[1], 10);
} else {
host = sentinel;
}
return {host, port};
}),
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
}),
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
})
} : null;
const Srf = require('drachtio-srf');
const srf = new Srf('sbc-outbound');
const CIDRMatcher = require('cidr-matcher');
@@ -71,7 +50,8 @@ const {
lookupSipGatewaysByCarrier,
lookupCarrierBySid,
queryCallLimits,
lookupCarrierByAccountLcr
lookupCarrierByAccountLcr,
lookupSystemInformation
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
port: process.env.JAMBONES_MYSQL_PORT || 3306,
@@ -88,10 +68,10 @@ const {
decrKey,
retrieveSet,
isMemberOfSet,
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
addKey,
deleteKey,
retrieveKey
} = require('@jambonz/realtimedb-helpers')({}, logger);
const activeCallIds = new Map();
const Emitter = require('events');
@@ -108,6 +88,7 @@ srf.locals = {...srf.locals,
queryCdrs,
activeCallIds,
idleEmitter,
privateNetworkCidr: process.env.PRIVATE_VOIP_NETWORK_CIDR || null,
dbHelpers: {
ping,
lookupOutboundCarrierForAccount,
@@ -118,9 +99,14 @@ srf.locals = {...srf.locals,
lookupSipGatewaysByCarrier,
lookupCarrierBySid,
queryCallLimits,
lookupCarrierByAccountLcr
lookupCarrierByAccountLcr,
lookupSystemInformation
},
realtimeDbHelpers: {
client: redisClient,
addKey,
deleteKey,
retrieveKey,
createHash,
retrieveHash,
incrKey,
@@ -207,10 +193,22 @@ if (process.env.K8S || process.env.HTTP_PORT) {
});
}
if ('test' !== process.env.NODE_ENV) {
/* update call stats periodically */
setInterval(() => {
/* update call stats periodically as well as definition of private network cidr */
setInterval(async() => {
stats.gauge('sbc.sip.calls.count', activeCallIds.size, ['direction:outbound',
`instance_id:${process.env.INSTANCE_ID || 0}`]);
const r = await lookupSystemInformation();
if (r) {
if (r.private_network_cidr !== srf.locals.privateNetworkCidr) {
logger.info(`updating private network cidr from ${srf.locals.privateNetworkCidr} to ${r.private_network_cidr}`);
srf.locals.privateNetworkCidr = r.private_network_cidr;
}
// Update system log level
if (r.log_level) {
logger.level = r.log_level;
}
}
}, 20000);
}
@@ -285,4 +283,4 @@ function handle(signal) {
}
}
module.exports = {srf};
module.exports = {srf, logger};
+416 -117
View File
@@ -1,30 +1,62 @@
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,
isBlackListedSipGateway,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey,
isValidDomainOrIP
} = require('./utils');
const { MediaPath } = require('./constants.json');
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}`;
/**
* 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 IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
const createBLegFromHeader = ({
logger,
req,
host,
fromUser,
register_from_domain,
transport,
teams = false,
scheme = 'sip'
}) => {
const from = req.getParsedHeader('From');
const uri = parseUri(from.uri);
let user = uri.user || 'anonymous';
let host = 'localhost';
if (teams) {
host = req.get('X-MS-Teams-Tenant-FQDN');
const transportParam = transport ? `;transport=${transport}` : '';
logger.debug({from, fromUser, uri, host, scheme, transport, teams}, 'createBLegFromHeader');
/* user */
const user = fromUser || req.get('X-Preferred-From-User') || uri.user || 'anonymous';
/* host */
if (!host) {
if (teams) {
host = req.get('X-MS-Teams-Tenant-FQDN');
}
else if (req.has('X-Preferred-From-Host')) {
host = req.get('X-Preferred-From-Host');
} else if (register_from_domain) {
host = register_from_domain;
}
else {
host = 'localhost';
}
}
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;
if (from.name) {
return `${from.name} <${scheme}:${user}@${host}${transportParam}>`;
}
return `sip:${user}@${host}`;
return `<${scheme}:${user}@${host}${transportParam}>`;
};
const createBLegToHeader = (req, teams) => {
const to = req.getParsedHeader('To');
const host = teams ? req.get('X-MS-Teams-Tenant-FQDN') : 'localhost';
@@ -48,6 +80,7 @@ const initCdr = (req, invite) => {
account_sid: req.get('X-Account-Sid'),
call_sid: req.get('X-Call-Sid'),
sip_callid: invite.get('Call-ID'),
...(req.has('X-CID') && {sip_parent_callid: req.get('X-CID')}),
...(applicationSid && {application_sid: applicationSid}),
from,
to,
@@ -65,7 +98,10 @@ const updateRtpEngineFlags = (sdp, opts) => {
try {
const parsed = sdpTransform.parse(sdp);
const codec = parsed.media[0].rtp[0].codec;
if (['PCMU', 'PCMA'].includes(codec)) opts.flags.push(`codec-accept-${codec}`);
if (['PCMU', 'PCMA'].includes(codec)) {
opts.flags.push(`codec-accept-${codec}`);
opts.flags.push('codec-accept-telephone-event');
}
} catch (err) {}
return opts;
};
@@ -84,6 +120,8 @@ class CallSession extends Emitter {
this.writeCdrs = this.srf.locals.writeCdrs;
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
const {
lookupOutboundCarrierForAccount,
@@ -96,7 +134,9 @@ class CallSession extends Emitter {
this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier;
this.lookupCarrierByAccountLcr = lookupCarrierByAccountLcr;
this._mediaReleased = false;
this._mediaPath = MediaPath.FullMedia;
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
}
get service_provider_sid() {
@@ -116,12 +156,7 @@ class CallSession extends Emitter {
}
get isMediaReleased() {
return this._mediaReleased;
}
get calleeIsUsingSrtp() {
const tp = this.rtpEngineOpts?.uac?.mediaOpts['transport-protocol'];
return tp && -1 !== tp.indexOf('SAVP');
return this._mediaPath !== MediaPath.FullMedia;
}
subscribeForDTMF(dlg) {
@@ -161,7 +196,7 @@ class CallSession extends Emitter {
subscribeAnswer,
unsubscribe
} = engine;
const {createHash, retrieveHash} = this.srf.locals.realtimeDbHelpers;
const {client, createHash, retrieveHash} = this.srf.locals.realtimeDbHelpers;
this.offer = offer;
this.answer = answer;
this.del = del;
@@ -179,15 +214,14 @@ class CallSession extends Emitter {
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, this.useWss || teams, false, teams);
this.rtpEngineResource = {destroy: this.del.bind(null, this.rtpEngineOpts.common)};
let proxy, uris;
let proxy, uris = [];
const mapGateways = new Map();
let encryptedMedia = false;
try {
// determine where to send the call
debug(`connecting call: ${JSON.stringify(this.req.locals)}`);
let headers = {
'From': createBLegFromHeader(this.req, teams),
'Contact': createBLegFromHeader(this.req, teams),
const headers = {
'To': createBLegToHeader(this.req, teams),
Allow: 'INVITE, ACK, OPTIONS, CANCEL, BYE, NOTIFY, UPDATE, PRACK',
'X-Account-Sid': this.account_sid
@@ -204,25 +238,38 @@ 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;
}
this.logger.info(`sending call to registered user ${destUri}`);
}
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`];
headers = {
...headers,
Contact: `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
};
else uris = [{
private_network: false,
uri: `sip:${this.req.calledNumber}@sip.pstnhub.microsoft.com`
}];
}
else {
try {
@@ -257,7 +304,13 @@ class CallSession extends Emitter {
}
const vc = await this.lookupCarrierBySid(voip_carrier_sid);
const gateways = await this.lookupSipGatewaysByCarrier(voip_carrier_sid);
const gws = (gateways || [])
const goodGateways = [];
for (const g of gateways) {
if (!await isBlackListedSipGateway(client, this.logger, g.sip_gateway_sid)) {
goodGateways.push(g);
}
}
const gws = (goodGateways || [])
.filter((gw) => gw.outbound);
if (gws.length) {
uris = [];
@@ -266,17 +319,19 @@ class CallSession extends Emitter {
this.req.calledNumber.slice(1) :
this.req.calledNumber;
const prefix = vc.tech_prefix || '';
const protocol = o.protocol?.startsWith('tls') ? 'tls' : (o.protocol || 'udp');
const transport = 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' ? 'sips' : 'sip';
const u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};${transport}`;
const scheme = transport === 'tls' && !process.env.JAMBONES_USE_BEST_EFFORT_TLS && o.use_sips_scheme ?
'sips' : 'sip';
let u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${hostport};transport=${transport}`;
const obj = {
name: vc.name,
diversion: vc.diversion,
hostport,
protocol: o.protocol
transport,
scheme,
register_from_domain: vc.register_from_domain
};
if (vc.register_username && vc.register_password) {
obj.auth = {
@@ -284,8 +339,22 @@ class CallSession extends Emitter {
password: vc.register_password
};
}
if (vc.requires_register && vc.register_sip_realm?.length > 0) {
proxy = u;
u = `${scheme}:${prefix}${prependPlus}${calledNumber}@${vc.register_sip_realm};transport=${transport}`;
this.logger.debug({uri: u}, `using outbound proxy for this registered trunk: ${proxy}`);
} else if (vc.outbound_sip_proxy && vc.outbound_sip_proxy.length > 0) {
if (vc.outbound_sip_proxy.includes('sip:') || vc.outbound_sip_proxy.includes('sips:')) {
proxy = vc.outbound_sip_proxy;
} else if (isValidDomainOrIP(vc.outbound_sip_proxy)) {
proxy =
`${scheme}:${prefix}${prependPlus}${calledNumber}@${vc.outbound_sip_proxy};transport=${transport}`;
this.logger.debug({uri: u}, `using outbound proxy for this trunk: ${proxy}`);
}
}
mapGateways.set(u, obj);
uris.push(u);
this.logger.debug({gateway: o}, `pushed uri ${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
@@ -295,12 +364,21 @@ class CallSession extends Emitter {
*/
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);
encryptedMedia = 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 {
this.logger.info({voip_carrier_sid}, 'no outbound gateways found for requested carrier');
this.res.send(603);
}
} catch (err) {
debug(err);
@@ -311,16 +389,20 @@ class CallSession extends Emitter {
debug(`sending call to PSTN ${uris}`);
}
// rtpengine 'offer'
/* private_network should be called at last - try public first */
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;
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);
debug(`response from rtpengine to offer ${JSON.stringify(response)}`);
let response = await this.offer(opts);
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
if ('ok' !== response.result) {
this.logger.error(`rtpengine offer failed with ${JSON.stringify(response)}`);
@@ -333,10 +415,22 @@ class CallSession extends Emitter {
// crank through the list of gateways until connected, exhausted or caller hangs up
let earlyMedia = false;
while (uris.length) {
let hdrs = { ...headers};
const uri = uris.shift();
const gw = mapGateways.get(uri);
const passFailure = 0 === uris.length; // only a single target
let hdrs = { ...headers };
const {private_network, uri} = uris.shift();
/* if we've exhausted attempts to public endpoints and are switching to trying private, we need new rtp */
if (private_network && !isOfferUpdatedToPrivate) {
this.logger.info('switching to attempt to deliver call via private network now..');
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;
}
/* on the second and subsequent attempts, use the same Call-ID and CSeq from the first attempt */
if (0 === uris.length) {
try {
const key = makeInviteInProgressKey(this.req.get('Call-ID'));
@@ -352,26 +446,76 @@ 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}`);
if (gw.diversion) {
let div = gw.diversion;
if (div.startsWith('+')) {
div = `<sip:${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
}
else div = `<sip:+${div}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`;
hdrs = {
...hdrs,
'Diversion': div
};
}
}
else this.logger.info(`sending INVITE to ${uri} via proxy ${proxy})`);
try {
const responseHeaders = this.privateSipAddress ? {Contact: `<sip:${this.privateSipAddress}>`} : {};
/* INVITE request line and To header should be the same. */
hdrs = {...hdrs, 'To': uri};
/* only now can we set Contact & From header since they depend on transport and scheme of gw */
const gw = mapGateways.get(uri);
if (gw) {
const {scheme, transport} = gw;
this.logger.info({gw}, `sending INVITE to ${uri} via carrier ${gw.name}`);
hdrs = {
...hdrs,
From: gw.register_from_domain ?
createBLegFromHeader({
logger: this.logger,
req: this.req,
register_from_domain: gw.register_from_domain,
scheme,
transport,
...(private_network && {host: this.privateSipAddress})
}) :
createBLegFromHeader({
logger: this.logger,
req: this.req,
scheme,
transport,
...(private_network && {host: this.privateSipAddress})
}),
Contact: createBLegFromHeader({
logger: this.logger,
req: this.req,
...(gw.auth?.username && {fromUser: gw.auth?.username}),
scheme,
transport,
...(private_network && {host: this.privateSipAddress})
}),
...(gw.diversion && {
Diversion: gw.diversion.startsWith('+') ?
`<sip:${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off` :
`<sip:+${gw.diversion}@${gw.hostport}>;reason=unknown;counter=1;privacy=off`
})
};
}
else if (teams) {
hdrs = {
...hdrs,
'From': createBLegFromHeader({logger: this.logger, req: this.req, teams: true, transport: 'tls'}),
'Contact': `sip:${this.req.calledNumber}@${this.req.get('X-MS-Teams-Tenant-FQDN')}:5061;transport=tls`
};
}
else {
hdrs = {
...hdrs,
'From': createBLegFromHeader({
logger: this.logger,
req: this.req,
...(private_network && {host: this.privateSipAddress})
}),
'Contact': createBLegFromHeader({
logger: this.logger,
req: this.req,
...(private_network && {host: this.privateSipAddress})
})
};
const p = proxy ? ` via ${proxy}` : '';
this.logger.info({uri, p}, `sending INVITE to ${uri}${p}`);
}
/* now launch an outbound call attempt */
const passFailure = 0 === uris.length; // only propagate failure on last attempt
try {
const {uas, uac} = await this.srf.createB2BUA(this.req, this.res, uri, {
proxy,
passFailure,
@@ -389,14 +533,19 @@ class CallSession extends Emitter {
'-X-Preferred-From-Host',
'-X-Jambonz-FS-UUID',
'-X-Voip-Carrier-Sid',
'-X-SIP-Proxy'
],
proxyResponseHeaders: [
'all',
'-Allow',
'-Session-Expires'
],
// Add X-CID header to feature server response.
// to allow calling/status hooks contains sbc_callid.
responseHeaders: (uacRes, headers) => {
headers['X-CID'] = uacRes.get('Call-ID');
},
headers: hdrs,
responseHeaders,
auth: gw ? gw.auth : undefined,
localSdpB: response.sdp,
localSdpA: async(sdp, res) => {
@@ -406,7 +555,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);
@@ -449,6 +598,9 @@ class CallSession extends Emitter {
} catch (err) {
this.logger.error({err}, 'Error saving Call-ID/CSeq');
}
this.contactHeader = inv.get('Contact');
this.logger.info(`outbound call attempt to ${uri} has contact header ${this.contactHeader}`);
},
cbProvisional: (response) => {
if (!earlyMedia && [180, 183].includes(response.status) && response.body) earlyMedia = true;
@@ -468,13 +620,24 @@ class CallSession extends Emitter {
// these are all final failure scenarios
if (uris.length === 0 || // exhausted all targets
earlyMedia || // failure after early media
encryptedMedia || // cant crank back when using encrypted media as keys have been exchanged
!(err instanceof SipError) || // unexpected error
err.status === 487) { // caller hung up
const abandoned = err.message && err.message.includes('rtpengine failed: Unknown call-id');
const status = err.status || (abandoned ? 487 : 500);
if (err instanceof SipError) this.logger.info(`final call failure ${status}`);
else if (!abandoned) this.logger.error(err, 'unexpected call failure');
else if (!abandoned) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
this.logger.error({err}, 'unexpected call failure');
nudgeCallCounts(this.req, 'failure', {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid,
callId: this.req.locals.callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
}
debug(`got final outdial error: ${err}`);
if (!passFailure) this.res.send(status);
this.emit('failed');
@@ -499,7 +662,7 @@ class CallSession extends Emitter {
}
}
} catch (err) {
if ('abandonded' !== err.message) this.logger.error(err, `Error setting up outbound call to: ${uris}`);
if ('abandonded' !== err.message) this.logger.error({err}, `Error setting up outbound call to: ${uris}`);
this.emit('failed');
this.srf.endSession(this.req);
this.rtpEngineResource.destroy();
@@ -518,17 +681,30 @@ class CallSession extends Emitter {
answered_at: callStart
};
}
/* save far end SDP for later use if we do a full media release */
if (process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE) {
const key = makeFullMediaReleaseKey(this.req.get('X-CID'));
const sdp = uac.remote.sdp;
this.logger.info({key, sdp}, 'saving far end sdp for full media release feature');
this.addKey(key, sdp, 3600).catch((err) => this.logger.error(err, 'Error saving far end sdp'));
}
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 {
await other.destroy();
const headers = {};
Object.keys(bye.headers).forEach((h) => {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
} catch (err) {}
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
@@ -537,10 +713,12 @@ class CallSession extends Emitter {
if (process.env.JAMBONES_HOSTING || trackingOn) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
await nudgeCallCounts(this.logger, {
this.logger.debug('decrementing call count at end of call');
await nudgeCallCounts(this.req, 'complete', {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid
application_sid: this.application_sid,
callId: this.req.locals.callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
}
@@ -593,6 +771,22 @@ 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());
@@ -603,12 +797,19 @@ class CallSession extends Emitter {
async _onRefer(dlg, req, res) {
/* REFER coming in from a sip device, forward to feature server */
try {
const customHeaders = Object.keys(req.headers)
.filter((h) => h.toLowerCase().startsWith('x-'))
.reduce((acc, h) => {
acc[h] = req.get(h);
return acc;
}, {});
const response = await dlg.other.request({
method: 'REFER',
headers: {
'Refer-To': req.get('Refer-To'),
'Referred-By': req.get('Referred-By'),
'User-Agent': req.get('User-Agent')
'User-Agent': req.get('User-Agent'),
...customHeaders
}
});
res.send(response.status, response.reason);
@@ -649,16 +850,44 @@ Duration=${payload.duration} `
try {
const reason = req.get('X-Reason');
const isReleasingMedia = reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason);
const isFullMediaRelease = reason === 'release-media-entirely' && process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE;
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;
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
let offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
let answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
const direction = dlg.type === 'uas' ? ['private', 'public'] : ['public', 'private'];
if (isFullMediaRelease) {
const a_sdp = await this.retrieveKey(makePartnerFullMediaReleaseKey(this.req.get('X-CID')));
this.logger.info({a_sdp}, 'reinvite ourselves out of the media path with this reinvite offer');
const answerSdp = await dlg.other.modify(a_sdp);
this.logger.info({answerSdp}, 'far end response to full media release');
res.send(200, {
body: dlg.local.sdp,
headers: {
'Contact': this.contactHeader
}
});
/* no media going through us now we can destroy the rtpengine resource */
this.rtpEngineResource.destroy().catch((err) => {
this.logger.info({err}, 'Error destroying rtpengine resource after full media release');
});
this._mediaPath = MediaPath.NoMedia;
return;
}
if (isReleasingMedia) {
if (!offerMedia.flags.includes('port latching')) offerMedia.flags.push('port latching');
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
}
const acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ?
process.env.JAMBONES_ACCEPT_AND_TRANSCODE :
process.env.JAMBONES_ACCEPT_G729 ? 'g729' : '';
offerMedia = { ...offerMedia, ...(acceptCodecs && dlg.type === 'uac' &&
{ codec: { mask: acceptCodecs, transcode: 'pcmu,pcma' } })};
answerMedia = { ...answerMedia, ...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER && dlg.type === 'uac' &&
{ codec: {offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' }})};
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
@@ -679,17 +908,20 @@ Duration=${payload.duration} `
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
let sdp;
if (isReleasingMedia && !this.calleeIsUsingSrtp) {
//HL 2024-11-13: previously forwarded re-invites to webrtc clients but further testing has shown to be unnecessary
//if (isReleasingMedia && !this.calleeIsUsingSrtp) {
//DH 2024-11- 18: if we are going from no-media to either partial or full media, we need reinvite the far end
if (isReleasingMedia && this._mediaPath !== MediaPath.NoMedia) {
this.logger.info(`got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp;
if (!answerMedia.flags.includes('port latching')) answerMedia.flags.push('port latching');
if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
this._mediaReleased = 'release-media' === reason;
this._mediaPath = 'release-media' === reason ? MediaPath.PartialMedia : MediaPath.FullMedia;
}
else {
sdp = await dlg.other.modify(response.sdp);
this.logger.info({sdp}, 'CallSession:_onReinvite: got sdp from 200 OK to invite we sent');
}
opts = {
...this.rtpEngineOpts.common,
@@ -699,13 +931,21 @@ Duration=${payload.duration} `
sdp
};
response = await this.answer(opts);
/* now remove asymmetric as B party (looking at you Genesys ring group) may need port re-learning on invites */
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'asymmetric');
if ('ok' !== response.result) {
res.send(488);
throw new Error(`_onReinvite: rtpengine failed: ${JSON.stringify(response)}`);
}
this.logger.debug({opts, sdp: response.sdp}, 'CallSession:_onReinvite: (answer) sending back upstream');
res.send(200, {body: response.sdp});
res.send(200, {
body: response.sdp,
headers: {
'Contact': this.contactHeader
}
});
} catch (err) {
res.send(err.status || 500);
this.logger.error(err, 'Error handling reinvite');
}
}
@@ -733,12 +973,12 @@ 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');
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');
@@ -778,52 +1018,100 @@ Duration=${payload.duration} `
blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe,
headers
headers,
isSipRecCall: Array.isArray(this.req.payload) && this.req.payload.length > 1
}));
try {
succeeded = (await Promise.all(
this.srsClients.map((c) => c.start())
)).every((r) => r);
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => c.start()));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error starting SipRec call recording');
succeeded = false;
}
}
else if (reason === 'stopCallRecording') {
if (!this.srsClients.length) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated)) {
res.send(400);
this.logger.info('discarding stopCallRecording request because we are not recording');
return;
}
try {
succeeded = (await Promise.all(
this.srsClients.map((c) => c.stop())
)).every((r) => r);
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated) {
c.stop();
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error stopping SipRec call recording');
succeeded = false;
}
this.srsClients = [];
}
else if (reason === 'pauseCallRecording') {
if (!this.srsClients.length || this.srsClients.every((c) => c.paused)) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && !c.paused)) {
this.logger.info('discarding invalid pauseCallRecording request');
res.send(400);
return;
}
succeeded = (await Promise.all(
this.srsClients.map((c) => c.pause())
)).every((r) => r);
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated && !c.paused) {
c.pause({headers});
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error pausing SipRec call recording');
succeeded = false;
}
}
else if (reason === 'resumeCallRecording') {
if (!this.srsClients.length || !this.srsClients.every((c) => c.paused)) {
if (!this.srsClients.length || !this.srsClients.some((c) => c.activated && c.paused)) {
res.send(400);
this.logger.info('discarding invalid resumeCallRecording request');
return;
}
succeeded = (await Promise.all(
this.srsClients.map((c) => c.resume())
)).every((r) => r);
try {
this._startRecordingNoAnswerTimer(res);
await Promise.any(this.srsClients.map((c) => {
if (c.activated && c.paused) {
c.resume({headers});
}
}));
succeeded = true;
} catch (err) {
this.logger.error({err}, 'Error resuming SipRec call recording');
succeeded = false;
}
}
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');
}
res.send(succeeded ? 200 : 503);
}
}
else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) {
@@ -861,15 +1149,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`);
@@ -878,24 +1167,34 @@ 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, contact: _contact,
...customHeaders
} = req.headers;
const referTo = req.getParsedHeader('Refer-To');
const uri = parseUri(referTo.uri);
this.logger.info({uri, referTo}, 'received REFER from feature server');
const arr = /context-(.*)/.exec(uri.user);
if (!arr) {
/* call transfer requested */
const referredBy = req.getParsedHeader('Referred-By');
if (!referredBy) return res.send(400);
const u = parseUri(referredBy.uri);
const farEnd = parseUri(this.connectedUri);
uri.host = farEnd.host;
uri.port = farEnd.port;
if (!req.has('Referred-By')) return res.send(400);
/* delete contact if it was there from feature server */
delete customHeaders['contact'];
const response = await this.uac.request({
method: 'REFER',
headers: {
'Refer-To': stringifyUri(uri),
'Referred-By': stringifyUri(u)
'Refer-To': `<${stringifyUri(uri)}>`,
'Referred-By': referredby,
'Contact': this.contactHeader,
...customHeaders
}
});
return res.send(response.status);
+7
View File
@@ -0,0 +1,7 @@
{
"MediaPath": {
"NoMedia": "no-media",
"PartialMedia": "partial-media",
"FullMedia": "full-media"
}
}
+37 -8
View File
@@ -17,6 +17,8 @@ module.exports = (srf, logger, redisClient) => {
const initLocals = async(req, res, next) => {
req.locals = req.locals || {};
const callId = req.get('Call-ID');
req.locals.nudge = 0;
req.locals.callId = callId;
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');
@@ -77,7 +79,11 @@ module.exports = (srf, logger, redisClient) => {
});
try {
req.locals.account = await lookupAccountBySid(req.locals.account_sid);
const account = await lookupAccountBySid(req.locals.account_sid);
req.locals.account = account;
if (account.enable_debug_log) {
req.locals.logger.level = 'debug';
}
req.locals.service_provider_sid = req.locals.account.service_provider_sid;
} catch (err) {
req.locals.logger.error({err}, `Error looking up account sid ${req.locals.account_sid}`);
@@ -103,10 +109,11 @@ module.exports = (srf, logger, redisClient) => {
/* decrement count if INVITE is later rejected */
res.once('end', async({status}) => {
if (status > 200) {
nudgeCallCounts(logger, {
nudgeCallCounts(req, 'failure', {
service_provider_sid,
account_sid,
application_sid
application_sid,
callId: req.locals.callId
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => logger.error(err, 'Error decrementing call counts'));
const tags = ['accepted:no', `sipStatus:${status}`];
@@ -119,10 +126,11 @@ module.exports = (srf, logger, redisClient) => {
});
/* increment the call count */
const {callsSP, calls} = await nudgeCallCounts(logger, {
const {callsSP, calls} = await nudgeCallCounts(req, 'init', {
service_provider_sid,
account_sid,
application_sid
application_sid,
callId: req.locals.callId
}, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp});
/* compare to account's limit, though avoid db hit when call count is low */
@@ -166,6 +174,11 @@ module.exports = (srf, logger, redisClient) => {
});
return req.srf.endSession(req);
}
if (!account_limit && !sp_limit && process.env.JAMBONES_HOSTING) {
logger.info(`checkLimits: no active subscription found for account ${account_sid}, rejecting call`);
res.send(503, 'No Active Subscription');
return req.srf.endSession(req);
}
if (process.env.JAMBONES_TRACK_SP_CALLS && sp_limit > 0 && callsSP > sp_limit) {
logger.info({callsSP, sp_limit}, 'checkLimits: service provider limits exceeded');
writeAlerts({
@@ -223,7 +236,10 @@ module.exports = (srf, logger, redisClient) => {
/* redirect to the correct SBC where this user is connected */
const proxyAddress = selectHostPort(reg.sbcAddress, 'tcp');
const redirectUri = `<sip:${proxyAddress[1]}>`;
logger.info(`redirecting call to SBC at ${redirectUri}`);
logger.info({
myHostPort: req.server.hostport,
registeredHostPort: reg.sbcAddress,
}, `redirecting call to SBC at ${redirectUri}`);
return res.send(302, {headers: {Contact: redirectUri}});
}
req.locals.registration = reg;
@@ -244,9 +260,22 @@ module.exports = (srf, logger, redisClient) => {
else if ('sip' === desiredRouting) {
// call that needs to be forwarded to a sip endpoint
logger.info(`forwarding call to sip endpoint ${req.uri}`);
// check if the domain is one of ours
if (process.env.JAMBONES_LOCAL_SIP_DOMAINS) {
const allowedDomains = process.env.JAMBONES_LOCAL_SIP_DOMAINS.split(',');
const domain = uri.host;
const isLoop = allowedDomains.some((allowed) =>
domain === allowed.trim() || domain.endsWith(`.${allowed.trim()}`)
);
if (isLoop) {
logger.info({ host: domain }, `returning 482 Loop Detected for attempt to send to: ${req.uri}`);
res.send(482, 'Loop Detected');
return req.srf.endSession(req);
}
}
req.locals.target = 'forward';
}
else if ('phone' === desiredRouting) {
} else if ('phone' === desiredRouting) {
debug('sending call to LCR');
req.locals.target = 'lcr';
}
+168 -9
View File
@@ -1,6 +1,8 @@
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, padCrypto, teams) {
const from = req.getParsedHeader('from');
@@ -16,10 +18,14 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
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'),
@@ -34,7 +40,11 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
},
uac: {
tag: null,
mediaOpts: dstOpts
mediaOpts: {
...dstOpts,
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
{ codec: { offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' } }),
}
}
};
}
@@ -121,13 +131,96 @@ const createHealthCheckApp = (port, logger) => {
});
};
const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
const {service_provider_sid, account_sid, application_sid} = sids;
/**
* nudgeCallCounts - increment or decrement call counts in redis
*
* current nudge value
* -----------------------------------------
* why | -1 | 0 | 1 |
* -----------------------------------------
* init | no-op | +1 | N/A |
* failure | N/A | -1 | -1 |
* complete| N/A | N/A | -1 |
*
*
*/
const shouldNudge = (why, req) => {
const {nudge, logger} = req.locals;
let modifyCount = false;
const originalNudge = nudge;
switch (why) {
case 'init':
if (nudge === 0) {
// normal case: new call, increment call count
req.locals.nudge = 1;
modifyCount = true;
}
else if (nudge === -1) {
// extremely quick cancel, don't increment call count
req.locals.nudge = 0;
}
else {
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
}
break;
case 'failure':
if (nudge === 1) {
// normal case of call failed for any reason, decrement call count
req.locals.nudge = 0;
modifyCount = true;
}
else if (nudge === 0) {
// very quick failure dont decrement call count
req.locals.nudge = -1;
}
else {
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
}
break;
case 'complete':
if (nudge === 1) {
// normal case of call completed, decrement call count
req.locals.nudge = 0;
modifyCount = true;
}
else {
logger.info(`shouldNudge: unexpected nudge value ${nudge} for ${why}`);
}
break;
default:
logger.info(`shouldNudge: unexpected why value ${why}`);
break;
}
logger.info(`shouldNudge: '${why}': updating count: ${modifyCount}, nudge: ${originalNudge} -> ${req.locals.nudge}`);
return modifyCount;
};
const nudgeCallCounts = async(req, why, sids, nudgeOperator, writers) => {
const {logger} = req.locals;
const {service_provider_sid, account_sid, application_sid, callId} = sids;
const {writeCallCount, writeCallCountSP, writeCallCountApp} = writers;
const nudges = [];
const writes = [];
logger.debug(sids, 'nudgeCallCounts');
if (!shouldNudge(why, req)) {
return {callsSP: null, calls: null, callsApp: null};
}
if (process.env.JAMBONES_DEBUG_CALL_COUNTS) {
const {srf} = require('..');
const {addKey, deleteKey} = srf.locals.realtimeDbHelpers;
if (why === 'init') {
// save for 3 days
await addKey(`debug:outcalls:${account_sid}:${callId}`, new Date().toISOString(), 259200);
}
else {
await deleteKey(`debug:outcalls:${account_sid}:${callId}`);
}
}
if (process.env.JAMBONES_TRACK_SP_CALLS) {
const key = makeSPCallCountKey(service_provider_sid);
@@ -181,6 +274,67 @@ const nudgeCallCounts = async(logger, sids, nudgeOperator, writers) => {
return {callsSP: null, calls: null, callsApp: null};
};
const isPrivateVoipNetwork = async(uri) => {
const {srf, logger} = require('..');
const {privateNetworkCidr} = srf.locals;
if (privateNetworkCidr) {
try {
const matcher = new CIDRMatcher(privateNetworkCidr.split(','));
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 (matcher.contains(ip)) {
return true;
}
}
}
} catch (err) {
logger.info({err, privateNetworkCidr},
'Error checking private network CIDR, probably misconfigured must be a comma separated list of CIDRs');
}
}
return false;
};
function makeBlacklistGatewayKey(key) {
return `blacklist-sip-gateway:${key}`;
}
async function isBlackListedSipGateway(client, logger, sip_gateway_sid) {
try {
return await client.exists(makeBlacklistGatewayKey(sip_gateway_sid)) === 1;
} catch (err) {
logger.error({err}, `isBlackListedSipGateway: error while checking blacklist for ${sip_gateway_sid}`);
}
}
const makeFullMediaReleaseKey = (callId) => {
return `b_sdp:${callId}`;
};
const makePartnerFullMediaReleaseKey = (callId) => {
return `a_sdp:${callId}`;
};
function isValidDomainOrIP(input) {
const domainRegex = /^(?!:\/\/)([a-zA-Z0-9.-]+)(:\d+)?$/;
// eslint-disable-next-line max-len
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(:\d+)?$/;
if (domainRegex.test(input) || ipRegex.test(input)) {
return true;
}
return false; // Invalid input
}
module.exports = {
makeRtpEngineOpts,
selectHostPort,
@@ -190,5 +344,10 @@ module.exports = {
equalsIgnoreOrder,
systemHealth,
createHealthCheckApp,
nudgeCallCounts
nudgeCallCounts,
isPrivateVoipNetwork,
isBlackListedSipGateway,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey,
isValidDomainOrIP
};
+1337 -4476
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,9 +1,9 @@
{
"name": "sbc-outbound",
"version": "0.8.4",
"version": "0.9.3",
"main": "app.js",
"engines": {
"node": ">= 12.0.0"
"node": ">= 18.0.0"
},
"keywords": [
"sip",
@@ -24,30 +24,30 @@
"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"
"jslint": "eslint app.js lib --fix"
},
"dependencies": {
"@jambonz/db-helpers": "^0.9.0",
"@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/db-helpers": "^0.9.11",
"@jambonz/realtimedb-helpers": "^0.8.13",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "0.2.4",
"@jambonz/rtpengine-utils": "^0.4.3",
"@jambonz/siprec-client-utils": "^0.2.6",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.5",
"@jambonz/mw-registrar": "0.2.7",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.10",
"@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.10",
"cidr-matcher": "^2.1.1",
"debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "^0.0.12",
"drachtio-srf": "^4.5.21",
"express": "^4.18.1",
"pino": "^7.11.0",
"sdp-transform": "^2.14.1"
"drachtio-fn-b2b-sugar": "^0.2.1",
"drachtio-srf": "^5.0.4",
"express": "^4.19.2",
"pino": "^8.20.0",
"sdp-transform": "^2.15.0"
},
"devDependencies": {
"bent": "^7.3.12",
"eslint": "^7.32.0",
"eslint-plugin-promise": "^5.2.0",
"eslint-plugin-promise": "^6.1.1",
"nyc": "^15.1.0",
"tape": "^5.5.3"
"tape": "^5.7.5"
}
}
+62 -6
View File
@@ -1,4 +1,5 @@
/* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips;
@@ -13,6 +14,8 @@ DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS clients;
DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS lcr;
@@ -51,6 +54,8 @@ DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS google_custom_voices;
DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information;
@@ -127,6 +132,19 @@ application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid)
) COMMENT='a regex-based pattern match for call routing';
CREATE TABLE clients
(
client_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (client_sid)
);
CREATE TABLE dns_records
(
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
@@ -322,14 +340,27 @@ last_tested DATETIME,
tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
label VARCHAR(64),
PRIMARY KEY (speech_credential_sid)
);
CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
PRIMARY KEY (google_custom_voice_sid)
);
CREATE TABLE system_information
(
domain_name VARCHAR(255),
sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255)
monitoring_domain_name VARCHAR(255),
private_network_cidr VARCHAR(8192),
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
);
CREATE TABLE users
@@ -411,7 +442,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers
(
phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL UNIQUE ,
number VARCHAR(132) NOT NULL,
voip_carrier_sid CHAR(36),
account_sid CHAR(36),
application_sid CHAR(36),
@@ -424,11 +455,14 @@ CREATE TABLE sip_gateways
sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
port INTEGER COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
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,
send_options_ping BOOLEAN NOT NULL DEFAULT 0,
use_sips_scheme BOOLEAN NOT NULL DEFAULT 0,
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -465,10 +499,21 @@ messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(64),
speech_synthesis_voice VARCHAR(256),
speech_synthesis_label VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
speech_recognizer_label VARCHAR(64),
use_for_fallback_speech BOOLEAN DEFAULT false,
fallback_speech_synthesis_vendor VARCHAR(64),
fallback_speech_synthesis_language VARCHAR(12),
fallback_speech_synthesis_voice VARCHAR(256),
fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid)
) COMMENT='A defined set of behaviors to be applied to phone calls ';
@@ -506,6 +551,10 @@ subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36),
record_all_calls BOOLEAN NOT NULL DEFAULT false,
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services';
@@ -526,6 +575,9 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX client_sid_idx ON clients (client_sid);
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
@@ -590,8 +642,6 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -599,6 +649,10 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone);
@@ -628,6 +682,8 @@ CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
+1 -1
View File
@@ -24,7 +24,7 @@ services:
ipv4_address: 172.39.0.2
sbc:
image: drachtio/drachtio-server:latest
image: drachtio/drachtio-server:0.8.26
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
ports:
- "9060:9022/tcp"
@@ -0,0 +1,59 @@
<?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: ff49e33-e771-403a-8c99-1780eabbc803
X-Jambonz-Routing: phone
Subject: uac-pcap-carrier-fail-limits
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="603" rtd="true" crlf="true">
</recv>
<send>
<![CDATA[
ACK sip:sip:+16173333456@127.0.0.1 SIP/2.0
[last_Via]
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-fail-blacklist
Content-Length: 0
]]>
</send>
</scenario>
+120
View File
@@ -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>
+17 -2
View File
@@ -26,6 +26,7 @@ function connect(connectable) {
test('sbc-outbound tests', async(t) => {
const {srf} = require('../app');
const { queryCdrs } = srf.locals;
const redisClient = srf.locals.realtimeDbHelpers.client;
try {
await connect(srf);
@@ -84,6 +85,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`);
@@ -91,12 +96,22 @@ test('sbc-outbound tests', async(t) => {
/* fails when session limit exceeded */
await sippUac('uac-pcap-carrier-fail-limits.xml');
t.pass('fails when max calls in progress');
// 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-data.sql`);
// Black list good carrier for some seconds
await redisClient.setex('blacklist-sip-gateway:124a5339-c62c-4075-9e19-f4de70a96597', 3, '');
await sippUac('uac-pcap-carrier-fail-blacklist.xml');
t.pass('fails when carrier is blacklisted');
await redisClient.del('blacklist-sip-gateway:124a5339-c62c-4075-9e19-f4de70a96597');
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 === 8, 'wrote 8 cdrs');
t.ok(res.total === 9, 'wrote 9 cdrs');
srf.disconnect();
} catch (err) {