Compare commits

...
Author SHA1 Message Date
rhonda hollis a5ea0740a9 refactor
- remove Redis
- handle proxing of non-success response
2026-05-15 17:28:17 -07:00
rhonda hollis 79b2a99fe8 ensure application_sid & traceId are on CDR 2026-05-14 09:14:43 -07:00
Dave Horton cd48675499 curtail massive log msg (#242) 2026-04-23 07:35:27 -04:00
Hoan Luu Huu cecdbdccef update drachtio srf 5.0.21 (#241) 2026-04-13 21:24:28 -04:00
Dave Horton 6586919c86 update deps 2026-03-30 21:07:53 -04:00
Dave Horton c919438af5 update to drachtio-srf latest (#239) 2026-03-05 09:24:49 -05:00
Dave Horton ab39525467 update drachtio-srf (#236) 2026-02-09 10:28:42 -05:00
Hoan Luu Huu 61a66ab181 update drachtio srf version (#231) 2026-01-22 08:13:11 -05:00
Dave Horton 48dd7ebfcd Parallelize independent exact IP and CIDR gateway database queries to reduce latency in inbound call routing (#234) 2026-01-18 09:34:48 -05:00
Dave Horton 678fe9d9a8 Fix/sql query optimize (#233)
* Add STRAIGHT_JOIN to CIDR gateway query to prevent MySQL optimizer from choosing inefficient full table scan on voip_carriers table.

* update tests with latest schema

* fix test data

* security issues

* update workflow actions
2026-01-16 08:53:22 -05:00
Dave Horton 39bd65fb97 improve slow sql query and fix support for readonly endpoints (#232) 2026-01-15 08:47:04 -05:00
Sam Machin 3f0fd794ce Recording update (#229)
* new hasRecording flag on call info for setting recording URL in cdr

* lint

* use nullish coalescing for null response
2026-01-02 10:29:00 -05:00
Dave Horton 876cb393f9 fix vulnerability 2025-12-09 09:58:46 -05:00
Sam Machin fa2259d6e5 fix build (#228)
* update dependenices

* Update package-lock.json

* fix buold
2025-12-08 10:43:47 -05:00
Sam Machin 2405190dcc update dependenices (#227) 2025-12-08 09:22:27 -05:00
Hoan Luu Huu e9a5921b19 allow uas leg can send re-invite with outbound gatway credential (#221)
* allow uas leg can send re-invite with outbound gatway credential

* fix cannot get register username/password

* fix cannot get register username/password

* fix cannot get register username/password

* fixed failing test cases
2025-11-24 20:16:28 -06:00
Dave Horton 6efe714d49 fix package lock 2025-11-14 07:36:42 -05:00
Sam Machin 4150454736 remove no SP gateways if Account Gateways (#219) 2025-11-11 08:30:30 -05:00
Anton Voylenko 73eb8e8d17 chore: bump node version (#220) 2025-11-04 18:07:01 -05:00
Hoan Luu Huu 9beba4330a support auth trunk for incoming call (#213)
* support auth trunk for incoming call

* wip

* wip

* wip

* update digest-utils version

* update sql file from api-server

* update sql file from api-server

* wip
2025-10-23 17:00:34 -04:00
Dave Horton b2868842ad support incoming calls from registration trunks with ephemeral gateways (#216)
* support incoming calls from registration trunks with ephemeral gateways

* fix bug with multiple ephemeral gateways

* update to pino 10.1.0

* update eslint
2025-10-21 07:33:21 -04:00
Hoan Luu Huu 4f1b4815c4 update digest utils 0.0.8 (#215) 2025-10-19 10:39:40 -04:00
Sam Machin 025853934e bump dbhelpers for cache change (#214) 2025-10-15 11:39:42 -04:00
Dave Horton 8ce531ec81 revert change (for now) that caused audio issues when reinviting to partial media (#212) 2025-08-18 12:46:13 -04:00
Sam Machin 7bc2a1a6f4 Check for loops (#211)
* check for matching x-application-sid header

and tests

* lint

* update test scenario

* rename file

* now with valid test data!
2025-08-18 07:24:45 -04:00
Sam Machin 83461f99e8 Set strict source on rtpengine (#210)
* Set strict source on rtpengine

RTPBleed

* use env var for strict source

* lint

* Update utils.js

* arghhhh

* clarification

* change
2025-08-03 19:44:47 -04:00
Dave Horton 45684aa337 bump version 2025-07-15 11:46:09 -04:00
Vinod Dharashive 9d0c829ae8 increase dtmf volume (#208)
https://github.com/jambonz/jambonz-feature-server/issues/1272
2025-07-09 08:23:17 -04:00
sathish kumar pasham 7a6551c141 Fix security vulnerabilities by upgrading to @babel/helpers version 7.26.10. (#207) 2025-06-27 08:14:26 -04:00
rammohan-y 5f267906b1 Fixed issue when wild cards or regex is used in phone number for multiple carriers (#202)
* Fixed issue when wild cards or regex is used in phone number for multiple carriers
https://github.com/jambonz/sbc-inbound/issues/201

* Implemanted the regex/pattern based logic when url is a sip_realm

* Refactored getApplicationForDidAndCarrier as it is failing to parse other regex if first regex is invalid
In gateway is defined at "all accounts" using the phone number's account_sid

* Performance improvements

* Exported getApplicationForDidAndCarriers method

* Added additional unit test cases
2025-06-18 21:11:05 +02:00
Dave Horton 680d32e9a2 update version 2025-05-14 15:42:00 -04:00
Dave Horton 71c437ee07 update db-helpers 2025-05-13 10:35:46 -04:00
rammohan-y 35c38685a9 Updated drachtio-srf version (#199)
https://github.com/drachtio/drachtio-server/issues/424
2025-04-22 11:05:30 -04:00
Hoan Luu Huu 8d15098b77 update drachtio srf 5.0.4 (#198) 2025-04-18 07:45:18 -04:00
Hoan Luu Huu 656ad3b6cb update digest-utils version (#197) 2025-04-17 19:45:39 -04:00
rammohan-y 7eccb6816d Updated db-helper to 0.9.11 (#196)
https://github.com/jambonz/jambonz-feature-server/issues/1151
2025-04-14 08:22:17 -04:00
21 changed files with 2723 additions and 6830 deletions
-1
View File
@@ -1 +0,0 @@
test/*
-126
View File
@@ -1,126 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": false,
"modules": false
},
"ecmaVersion": 2020
},
"plugins": ["promise"],
"rules": {
"promise/always-return": "error",
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/no-return-in-finally": "warn",
// Possible Errors
// http://eslint.org/docs/rules/#possible-errors
"comma-dangle": [2, "only-multiline"],
"no-control-regex": 2,
"no-debugger": 2,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast" : 2,
"no-extra-parens": [2, "functions"],
"no-extra-semi": 2,
"no-func-assign": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-proto": 2,
"no-unexpected-multiline": 2,
"no-unreachable": 2,
"use-isnan": 2,
"valid-typeof": 2,
// Best Practices
// http://eslint.org/docs/rules/#best-practices
"no-fallthrough": 2,
"no-octal": 2,
"no-redeclare": 2,
"no-self-assign": 2,
"no-unused-labels": 2,
// Strict Mode
// http://eslint.org/docs/rules/#strict-mode
"strict": [2, "never"],
// Variables
// http://eslint.org/docs/rules/#variables
"no-delete-var": 2,
"no-undef": 2,
"no-unused-vars": [2, {"args": "none"}],
// Node.js and CommonJS
// http://eslint.org/docs/rules/#nodejs-and-commonjs
"no-mixed-requires": 2,
"no-new-require": 2,
"no-path-concat": 2,
"no-restricted-modules": [2, "sys", "_linklist"],
// Stylistic Issues
// http://eslint.org/docs/rules/#stylistic-issues
"comma-spacing": 2,
"eol-last": 2,
"indent": [2, 2, {"SwitchCase": 1}],
"keyword-spacing": 2,
"max-len": [2, 120, 2],
"new-parens": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [2, {"max": 2}],
"no-trailing-spaces": [2, {"skipBlankLines": false }],
"quotes": [2, "single", "avoid-escape"],
"semi": 2,
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, "never"],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": 2,
// ECMAScript 6
// http://eslint.org/docs/rules/#ecmascript-6
"arrow-parens": [2, "always"],
"arrow-spacing": [2, {"before": true, "after": true}],
"constructor-super": 2,
"no-class-assign": 2,
"no-confusing-arrow": 2,
"no-const-assign": 2,
"no-dupe-class-members": 2,
"no-new-symbol": 2,
"no-this-before-super": 2,
"prefer-const": 2
},
"globals": {
"DTRACE_HTTP_CLIENT_REQUEST" : false,
"LTTNG_HTTP_CLIENT_REQUEST" : false,
"COUNTER_HTTP_CLIENT_REQUEST" : false,
"DTRACE_HTTP_CLIENT_RESPONSE" : false,
"LTTNG_HTTP_CLIENT_RESPONSE" : false,
"COUNTER_HTTP_CLIENT_RESPONSE" : false,
"DTRACE_HTTP_SERVER_REQUEST" : false,
"LTTNG_HTTP_SERVER_REQUEST" : false,
"COUNTER_HTTP_SERVER_REQUEST" : false,
"DTRACE_HTTP_SERVER_RESPONSE" : false,
"LTTNG_HTTP_SERVER_RESPONSE" : false,
"COUNTER_HTTP_SERVER_RESPONSE" : false,
"DTRACE_NET_STREAM_END" : false,
"LTTNG_NET_STREAM_END" : false,
"COUNTER_NET_SERVER_CONNECTION_CLOSE" : false,
"DTRACE_NET_SERVER_CONNECTION" : false,
"LTTNG_NET_SERVER_CONNECTION" : false,
"COUNTER_NET_SERVER_CONNECTION" : false
}
}
+4 -4
View File
@@ -15,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: prepare tag - name: prepare tag
id: prepare_tag id: prepare_tag
@@ -37,14 +37,14 @@ jobs:
echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT echo "image_id=$IMAGE_ID" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
+3 -3
View File
@@ -1,10 +1,10 @@
FROM --platform=linux/amd64 node:20.13.0-alpine3.18 as base FROM --platform=linux/amd64 node:24-alpine AS base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3 RUN apk --update --no-cache add --virtual .builds-deps build-base python3
WORKDIR /opt/app/ WORKDIR /opt/app/
FROM base as build FROM base AS build
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
ARG NODE_ENV ARG NODE_ENV
ENV NODE_ENV $NODE_ENV ENV NODE_ENV=$NODE_ENV
CMD [ "node", "app.js" ] CMD [ "node", "app.js" ]
+27 -7
View File
@@ -64,18 +64,30 @@ const {
password: process.env.JAMBONES_MYSQL_PASSWORD, password: process.env.JAMBONES_MYSQL_PASSWORD,
database: process.env.JAMBONES_MYSQL_DATABASE, database: process.env.JAMBONES_MYSQL_DATABASE,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10 connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger); }, logger, process.env.JAMBONES_MYSQL_WRITE_HOST && process.env.JAMBONES_MYSQL_WRITE_USER &&
process.env.JAMBONES_MYSQL_WRITE_PASSWORD && process.env.JAMBONES_MYSQL_WRITE_DATABASE ? {
host: process.env.JAMBONES_MYSQL_WRITE_HOST,
port: process.env.JAMBONES_MYSQL_WRITE_PORT || 3306,
user: process.env.JAMBONES_MYSQL_WRITE_USER,
password: process.env.JAMBONES_MYSQL_WRITE_PASSWORD,
database: process.env.JAMBONES_MYSQL_WRITE_DATABASE,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
} : null);
const { const {
client: redisClient, client: redisClient,
addKey, addKey,
deleteKey, deleteKey,
retrieveKey, retrieveKey,
retrieveHash,
createSet, createSet,
retrieveSet, retrieveSet,
addToSet, addToSet,
removeFromSet, removeFromSet,
incrKey, incrKey,
decrKey} = require('@jambonz/realtimedb-helpers')({}, logger); decrKey,
createEphemeralGateway,
queryEphemeralGateways
} = require('@jambonz/realtimedb-helpers')({}, logger);
const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp'; const ngProtocol = process.env.JAMBONES_NG_PROTOCOL || 'udp';
const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080); const ngPort = process.env.RTPENGINE_PORT || ('udp' === ngProtocol ? 22222 : 8080);
@@ -114,10 +126,13 @@ srf.locals = {...srf.locals,
addKey, addKey,
deleteKey, deleteKey,
retrieveKey, retrieveKey,
retrieveHash,
createSet, createSet,
incrKey, incrKey,
decrKey, decrKey,
retrieveSet retrieveSet,
createEphemeralGateway,
queryEphemeralGateways
} }
}; };
const { const {
@@ -125,7 +140,8 @@ const {
wasOriginatedFromCarrier, wasOriginatedFromCarrier,
getApplicationForDidAndCarrier, getApplicationForDidAndCarrier,
getOutboundGatewayForRefer, getOutboundGatewayForRefer,
getApplicationBySid getApplicationBySid,
lookupAuthCarriersForAccountAndSP
} = require('./lib/db-utils')(srf, logger); } = require('./lib/db-utils')(srf, logger);
srf.locals = { srf.locals = {
...srf.locals, ...srf.locals,
@@ -134,7 +150,8 @@ srf.locals = {
getApplicationForDidAndCarrier, getApplicationForDidAndCarrier,
getOutboundGatewayForRefer, getOutboundGatewayForRefer,
getFeatureServer: require('./lib/fs-tracking')(srf, logger), getFeatureServer: require('./lib/fs-tracking')(srf, logger),
getApplicationBySid getApplicationBySid,
lookupAuthCarriersForAccountAndSP
}; };
const activeCallIds = srf.locals.activeCallIds; const activeCallIds = srf.locals.activeCallIds;
@@ -143,7 +160,8 @@ const {
handleSipRec, handleSipRec,
identifyAccount, identifyAccount,
checkLimits, checkLimits,
challengeDeviceCalls challengeDeviceCalls,
identifyAuthTrunk
} = require('./lib/middleware')(srf, logger); } = require('./lib/middleware')(srf, logger);
const CallSession = require('./lib/call-session'); const CallSession = require('./lib/call-session');
@@ -231,7 +249,9 @@ srf.use('invite', [
handleSipRec, handleSipRec,
identifyAccount, identifyAccount,
checkLimits, checkLimits,
challengeDeviceCalls challengeDeviceCalls,
// challengeDeviceCalls will detect auth_trunk or device calls, identifyAuthTrunk have to be after that
identifyAuthTrunk
]); ]);
srf.invite((req, res) => { srf.invite((req, res) => {
+1 -1
View File
@@ -3,6 +3,6 @@
"DTLS": "off", "DTLS": "off",
"SDES": "off", "SDES": "off",
"ICE": "remove", "ICE": "remove",
"flags": ["media handover", "port latching"], "flags": ["port latching"],
"rtcp-mux": ["accept"] "rtcp-mux": ["accept"]
} }
+2 -2
View File
@@ -3,14 +3,14 @@
"transport-protocol": "UDP/TLS/RTP/SAVPF", "transport-protocol": "UDP/TLS/RTP/SAVPF",
"ICE": "default", "ICE": "default",
"SDES": "off", "SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"], "flags": ["generate mid", "SDES-no", "port latching"],
"rtcp-mux": ["require"] "rtcp-mux": ["require"]
}, },
"teams": { "teams": {
"transport-protocol": "RTP/SAVP", "transport-protocol": "RTP/SAVP",
"ICE": "default", "ICE": "default",
"SDES": "off", "SDES": "off",
"flags": ["generate mid", "SDES-no", "media handover", "port latching"], "flags": ["generate mid", "SDES-no", "port latching"],
"rtcp-mux": ["accept"] "rtcp-mux": ["accept"]
} }
} }
+137
View File
@@ -0,0 +1,137 @@
const promisePlugin = require('eslint-plugin-promise');
module.exports = [
{
ignores: ['test/*']
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'commonjs',
globals: {
// Node.js globals
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
// DTrace/LTTNG globals
DTRACE_HTTP_CLIENT_REQUEST: false,
LTTNG_HTTP_CLIENT_REQUEST: false,
COUNTER_HTTP_CLIENT_REQUEST: false,
DTRACE_HTTP_CLIENT_RESPONSE: false,
LTTNG_HTTP_CLIENT_RESPONSE: false,
COUNTER_HTTP_CLIENT_RESPONSE: false,
DTRACE_HTTP_SERVER_REQUEST: false,
LTTNG_HTTP_SERVER_REQUEST: false,
COUNTER_HTTP_SERVER_REQUEST: false,
DTRACE_HTTP_SERVER_RESPONSE: false,
LTTNG_HTTP_SERVER_RESPONSE: false,
COUNTER_HTTP_SERVER_RESPONSE: false,
DTRACE_NET_STREAM_END: false,
LTTNG_NET_STREAM_END: false,
COUNTER_NET_SERVER_CONNECTION_CLOSE: false,
DTRACE_NET_SERVER_CONNECTION: false,
LTTNG_NET_SERVER_CONNECTION: false,
COUNTER_NET_SERVER_CONNECTION: false
}
},
plugins: {
promise: promisePlugin
},
rules: {
// Promise plugin rules
'promise/always-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/catch-or-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/no-return-in-finally': 'warn',
// Possible Errors
'comma-dangle': [2, 'only-multiline'],
'no-control-regex': 2,
'no-debugger': 2,
'no-dupe-args': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-ex-assign': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-extra-semi': 2,
'no-func-assign': 2,
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-obj-calls': 2,
'no-proto': 2,
'no-unexpected-multiline': 2,
'no-unreachable': 2,
'use-isnan': 2,
'valid-typeof': 2,
// Best Practices
'no-fallthrough': 2,
'no-octal': 2,
'no-redeclare': 2,
'no-self-assign': 2,
'no-unused-labels': 2,
// Strict Mode
'strict': [2, 'never'],
// Variables
'no-delete-var': 2,
'no-undef': 2,
'no-unused-vars': [2, {args: 'none'}],
// Node.js and CommonJS
'no-mixed-requires': 2,
'no-new-require': 2,
'no-path-concat': 2,
// Stylistic Issues
'comma-spacing': 2,
'eol-last': 2,
'indent': [2, 2, {SwitchCase: 1}],
'keyword-spacing': 2,
'max-len': [2, 120, 2],
'new-parens': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multiple-empty-lines': [2, {max: 2}],
'no-trailing-spaces': [2, {skipBlankLines: false}],
'quotes': [2, 'single', 'avoid-escape'],
'semi': 2,
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': 2,
// ECMAScript 6
'arrow-parens': [2, 'always'],
'arrow-spacing': [2, {before: true, after: true}],
'constructor-super': 2,
'no-class-assign': 2,
'no-confusing-arrow': 2,
'no-const-assign': 2,
'no-dupe-class-members': 2,
'no-new-symbol': 2,
'no-this-before-super': 2,
'prefer-const': 2
}
}
];
+50 -3
View File
@@ -19,6 +19,9 @@ const debug = require('debug')('jambonz:sbc-inbound');
const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy'; const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy';
const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com'; const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com';
const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length']; const IMMUTABLE_HEADERS = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length'];
const NONCOPYABLE_RESPONSE_HEADERS = [
'via', 'from', 'to', 'call-id', 'cseq', 'contact', 'content-length', 'content-type'
];
/** /**
* this is to make sure the outgoing From has the number in the incoming From * this is to make sure the outgoing From has the number in the incoming From
@@ -66,6 +69,7 @@ class CallSession extends Emitter {
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey; this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.addKey = req.srf.locals.realtimeDbHelpers.addKey; this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey; this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
this.retrieveHash = req.srf.locals.realtimeDbHelpers.retrieveHash;
this._mediaPath = MediaPath.FullMedia; this._mediaPath = MediaPath.FullMedia;
@@ -280,6 +284,7 @@ class CallSession extends Emitter {
proxy, proxy,
headers, headers,
responseHeaders, responseHeaders,
passFailure: false,
proxyRequestHeaders: [ proxyRequestHeaders: [
'all', 'all',
'-Authorization', '-Authorization',
@@ -332,6 +337,15 @@ class CallSession extends Emitter {
}, },
}); });
// passing gateway outbound auth that sbc-inbound can send RE-INVITE with authentication process on UAS side
const {gateway} = this.req.locals;
if (gateway && gateway.register_username && gateway.register_password) {
this.logger.debug('passing outbound gateway auth to CallSession for reinvite processing');
uas.auth = {
username: gateway.register_username,
password: gateway.register_password
};
}
// successfully connected // successfully connected
this.logger.info('call connected successfully to feature server'); this.logger.info('call connected successfully to feature server');
debug('call connected successfully to feature server'); debug('call connected successfully to feature server');
@@ -344,6 +358,19 @@ class CallSession extends Emitter {
const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`]; const tags = ['accepted:no', `sipStatus:${err.status}`, `originator:${this.req.locals.originator}`];
this.stats.increment('sbc.terminations', tags); this.stats.increment('sbc.terminations', tags);
this.logger.info(`call failed to connect to feature server with ${err.status}`); this.logger.info(`call failed to connect to feature server with ${err.status}`);
/* capture trace_id and application_sid from the feature server's error response for the CDR. */
if (this.req.locals.cdr && err.res) {
const trace_id = err.res.get('X-Trace-ID');
if (trace_id) {
this.req.locals.cdr.trace_id = trace_id;
}
const application_sid = err.res.get('X-Application-Sid');
if (application_sid) {
this.req.locals.cdr.application_sid = application_sid;
}
}
this.emit('failed'); this.emit('failed');
} }
else if (err.message !== 'call canceled') { else if (err.message !== 'call canceled') {
@@ -358,6 +385,22 @@ class CallSession extends Emitter {
.catch((err) => this.logger.error(err, 'Error decrementing call counts')); .catch((err) => this.logger.error(err, 'Error decrementing call counts'));
} }
/* manually proxy the failure response to UAS so that the trace_id/application_sid capture above runs before
res.end fires (which is what triggers the failure-CDR writer). */
if (err.message !== 'call canceled' && !this.res.finalResponseSent) {
if (err instanceof SipError && err.res) {
const headers = {};
Object.keys(err.res.headers || {}).forEach((h) => {
if (!NONCOPYABLE_RESPONSE_HEADERS.includes(h)) headers[h] = err.res.headers[h];
});
this.res.send(err.status, err.reason, {headers});
}
else {
this.res.send(err.status || 500, err.reason);
}
}
this.srf.endSession(this.req); this.srf.endSession(this.req);
} }
} }
@@ -452,7 +495,7 @@ class CallSession extends Emitter {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h]; if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
}); });
await other.destroy({headers}); await other.destroy({headers});
} catch (err) {} } catch {}
this.unsubscribeForDTMF(); this.unsubscribeForDTMF();
@@ -482,7 +525,9 @@ class CallSession extends Emitter {
this.req.locals.carrier : this.req.locals.carrier :
this.req.locals.originator; this.req.locals.originator;
const application = await this.srf.locals.getApplicationBySid(application_sid); const application = await this.srf.locals.getApplicationBySid(application_sid);
const isRecording = this.req.locals.account.record_all_calls || (application && application.record_all_calls); const {hasRecording} = await this.retrieveHash(`call:${this.account_sid}:${call_sid}`) ?? {};
const isRecording = this.req.locals.account.record_all_calls ||
(application && application.record_all_calls) || hasRecording;
const day = new Date(); const day = new Date();
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${call_sid}/record`; let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`; recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
@@ -947,11 +992,13 @@ Duration=${payload.duration} `
const code = arr[1]; const code = arr[1];
const arr2 = /Duration=\s*(\d+)/.exec(req.body); const arr2 = /Duration=\s*(\d+)/.exec(req.body);
const duration = arr2 ? arr2[1] : 250; const duration = arr2 ? arr2[1] : 250;
const volume = 13;
const dtmfOpts = { const dtmfOpts = {
...this.rtpEngineOpts.common, ...this.rtpEngineOpts.common,
'from-tag': this.rtpEngineOpts.uac.tag, 'from-tag': this.rtpEngineOpts.uac.tag,
code, code,
duration duration,
volume
}; };
const response = await this.playDTMF(dtmfOpts); const response = await this.playDTMF(dtmfOpts);
if ('ok' !== response.result) { if ('ok' !== response.result) {
+329 -33
View File
@@ -7,7 +7,8 @@ const sqlSelectSPForAccount = 'SELECT service_provider_sid FROM accounts WHERE a
const sqlSelectAllCarriersForAccountByRealm = const sqlSelectAllCarriersForAccountByRealm =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid, `SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
vc.register_username, vc.register_password
FROM sip_gateways sg, voip_carriers vc, accounts acc FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ? WHERE acc.sip_realm = ?
AND vc.account_sid = acc.account_sid AND vc.account_sid = acc.account_sid
@@ -17,7 +18,8 @@ AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllCarriersForSPByRealm = const sqlSelectAllCarriersForSPByRealm =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid, `SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
vc.register_username, vc.register_password
FROM sip_gateways sg, voip_carriers vc, accounts acc FROM sip_gateways sg, voip_carriers vc, accounts acc
WHERE acc.sip_realm = ? WHERE acc.sip_realm = ?
AND vc.service_provider_sid = acc.service_provider_sid AND vc.service_provider_sid = acc.service_provider_sid
@@ -26,14 +28,30 @@ AND vc.is_active = 1
AND sg.inbound = 1 AND sg.inbound = 1
AND sg.voip_carrier_sid = vc.voip_carrier_sid`; AND sg.voip_carrier_sid = vc.voip_carrier_sid`;
const sqlSelectAllGatewaysForSP = const sqlSelectExactGatewayForSP =
`SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid, `SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid,
vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
vc.register_username, vc.register_password
FROM sip_gateways sg, voip_carriers vc FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.service_provider_sid IS NOT NULL AND vc.service_provider_sid IS NOT NULL
AND vc.is_active = 1 AND vc.is_active = 1
AND sg.inbound = 1`; AND sg.inbound = 1
AND sg.netmask = 32
AND sg.ipv4 = ?
ORDER BY vc.account_sid IS NOT NULL DESC`;
const sqlSelectCIDRGatewaysForSP =
`SELECT STRAIGHT_JOIN sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid,
vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
vc.register_username, vc.register_password
FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.service_provider_sid IS NOT NULL
AND vc.is_active = 1
AND sg.inbound = 1
AND sg.netmask < 32
ORDER BY sg.netmask DESC`;
const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ? AND is_active = 1'; const sqlAccountByRealm = 'SELECT * from accounts WHERE sip_realm = ? AND is_active = 1';
const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?'; const sqlAccountBySid = 'SELECT * from accounts WHERE account_sid = ?';
@@ -56,15 +74,35 @@ AND sg.voip_carrier_sid = vc.voip_carrier_sid
AND outbound = 1`; AND outbound = 1`;
const sqlSelectCarrierRequiringRegistration = ` const sqlSelectCarrierRequiringRegistration = `
SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid, vc.account_sid, SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid, vc.account_sid,
vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
FROM sip_gateways sg, voip_carriers vc vc.register_username, vc.register_password
WHERE sg.voip_carrier_sid = vc.voip_carrier_sid FROM sip_gateways sg, voip_carriers vc
AND vc.requires_register = 1 WHERE sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.is_active = 1 AND vc.requires_register = 1
AND vc.register_sip_realm = ? AND vc.is_active = 1
AND vc.register_sip_realm = ?
AND vc.register_username = ?`; AND vc.register_username = ?`;
const sqlSelectAuthCarriersForAccountAndSP = `
SELECT * FROM voip_carriers
WHERE trunk_type = 'auth'
AND is_active = 1
AND (
(account_sid = ?)
OR
(service_provider_sid = ? AND account_sid IS NULL)
)`;
const sqlSelectGatewaysByVoipCarrierSids = `
SELECT sg.sip_gateway_sid, sg.voip_carrier_sid, vc.name, vc.service_provider_sid,
vc.account_sid, vc.application_sid, sg.inbound, sg.outbound, sg.is_active, sg.ipv4, sg.netmask, sg.pad_crypto,
vc.register_username, vc.register_password
FROM sip_gateways sg, voip_carriers vc
WHERE sg.voip_carrier_sid IN (?)
AND sg.voip_carrier_sid = vc.voip_carrier_sid
AND vc.is_active = 1`;
const gatewayMatchesSourceAddress = (logger, source_address, gw) => { const gatewayMatchesSourceAddress = (logger, source_address, gw) => {
if (32 === gw.netmask && gw.ipv4 === source_address) return true; if (32 === gw.netmask && gw.ipv4 === source_address) return true;
if (gw.netmask < 32) { if (gw.netmask < 32) {
@@ -80,6 +118,7 @@ const gatewayMatchesSourceAddress = (logger, source_address, gw) => {
module.exports = (srf, logger) => { module.exports = (srf, logger) => {
const {pool} = srf.locals.dbHelpers; const {pool} = srf.locals.dbHelpers;
const {queryEphemeralGateways} = srf.locals.realtimeDbHelpers;
const pp = pool.promise(); const pp = pool.promise();
const getApplicationBySid = async(application_sid) => { const getApplicationBySid = async(application_sid) => {
@@ -107,6 +146,22 @@ module.exports = (srf, logger) => {
} }
}; };
/**
* Retrieves the application for a given DID (phone number) and carrier combination.
* First attempts an exact DID match, then falls back to pattern/wildcard matching
* if no exact match is found. Pattern matching is done in order of pattern length
* (longest/most specific first) and handles both regex and wildcard (*) patterns.
*
* @param {Object} req - The request object containing the called number
* @param {string} voip_carrier_sid - The SID of the VoIP carrier
* @returns {Promise<(string|Object|null)>} Returns either:
* - application_sid (string) if appObject is false
* - full application object if appObject is true
* - null if no matching application is found
* @throws {Error} Database errors or other unexpected errors
*/
// Refactored to handle invalid regular expressions gracefully as part of
// https://github.com/jambonz/sbc-inbound/issues/201
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => { const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
const did = normalizeDID(req.calledNumber) || 'anonymous'; const did = normalizeDID(req.calledNumber) || 'anonymous';
@@ -117,17 +172,117 @@ module.exports = (srf, logger) => {
/* wildcard / regex match */ /* wildcard / regex match */
const [r2] = await pp.query(sqlQueryAllDidsForCarrier, [voip_carrier_sid]); const [r2] = await pp.query(sqlQueryAllDidsForCarrier, [voip_carrier_sid]);
const match = r2 const patterns = r2
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters .filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
.sort((a, b) => b.number.length - a.number.length) // prefer longest match .sort((a, b) => b.number.length - a.number.length);
.find((o) => did.match(new RegExp(o.number.endsWith('*') ? `${o.number.slice(0, -1)}\\d*` : o.number))); for (const pattern of patterns) {
if (match) return match.application_sid; try {
const regexPattern = pattern.number.endsWith('*') ?
`${pattern.number.slice(0, -1)}\\d*` :
pattern.number;
if (did.match(new RegExp(regexPattern))) {
logger.debug({voip_carrier_sid, 'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
return pattern.application_sid;
}
} catch (err) {
logger.warn({err, voip_carrier_sid, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
continue;
}
}
logger.debug({voip_carrier_sid, did}, 'No matching pattern found');
return null; return null;
} catch (err) { } catch (err) {
logger.error({err}, 'getApplicationForDidAndCarrier'); logger.error({err}, 'getApplicationForDidAndCarrier');
} }
}; };
/**
* Searches for a matching application across multiple carriers using pattern/wildcard matching.
* This function is similar to getApplicationForDidAndCarrier but operates on multiple carriers
* simultaneously for better performance. It searches through all patterns from the specified
* carriers, ordered by pattern length (longest/most specific first).
*
* @param {Object} req - The request object containing the called number
* @param {string} voip_carrier_sids - Comma-separated string of carrier SIDs, formatted for SQL IN clause
* (e.g., "'carrier1','carrier2'")
* @returns {Promise<Object|null>} Returns either:
* - The matching phone_numbers record containing application_sid, voip_carrier_sid, and account_sid
* - null if no matching pattern is found
* @throws {Error} Database errors or other unexpected errors
*/
const getApplicationForDidAndCarriers = async(req, voip_carrier_sids) => {
const did = normalizeDID(req.calledNumber) || 'anonymous';
const sql = `SELECT * FROM phone_numbers WHERE voip_carrier_sid IN (${voip_carrier_sids})`;
try {
/* wildcard / regex match */
const [r2] = await pp.query(sql);
const patterns = r2
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
.sort((a, b) => b.number.length - a.number.length);
for (const pattern of patterns) {
try {
const regexPattern = pattern.number.endsWith('*') ?
`${pattern.number.slice(0, -1)}\\d*` :
pattern.number;
if (did.match(new RegExp(regexPattern))) {
logger.debug({'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
return pattern;
}
} catch (err) {
logger.warn({err, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
continue;
}
}
logger.debug('No matching pattern found');
return null;
} catch (err) {
logger.error({err}, 'getApplicationForDidAndCarriers');
}
};
/**
* Queries ephemeral gateways in Redis for the given source IP address.
* Returns an array of active voip_carrier_sid values.
*
* @param {string} source_address - The source IP address to query
* @returns {Promise<Array<string>>} Array of voip_carrier_sid values, or empty array on error
*/
const lookupEphemeralGatewayCarriers = async(source_address) => {
try {
logger.debug({source_address}, 'querying ephemeral gateways');
const carriers = await queryEphemeralGateways(source_address);
if (carriers.length > 0) {
logger.info({source_address, count: carriers.length, carriers},
'found ephemeral gateway carriers');
}
return carriers;
} catch (err) {
logger.error({err, source_address}, 'Error querying ephemeral gateways');
return [];
}
};
/**
* Retrieves full gateway details (with carrier info) for the given voip_carrier_sid array.
* Returns gateway records (joined with carrier data) matching the structure of other gateway queries.
*
* @param {Array<string>} voipCarrierSids - Array of voip_carrier_sid values
* @returns {Promise<Array<Object>>} Array of gateway objects (with carrier fields included)
*/
const lookupGatewaysByCarrierSids = async(voipCarrierSids) => {
if (!voipCarrierSids || voipCarrierSids.length === 0) return [];
try {
const [r] = await pp.query(sqlSelectGatewaysByVoipCarrierSids, [voipCarrierSids]);
logger.debug({count: r.length, voipCarrierSids}, 'retrieved gateway details for ephemeral gateways');
return r;
} catch (err) {
logger.error({err, voipCarrierSids}, 'Error retrieving gateway details by carrier IDs');
return [];
}
};
const wasOriginatedFromCarrier = async(req) => { const wasOriginatedFromCarrier = async(req) => {
const failure = {fromCarrier: false}; const failure = {fromCarrier: false};
const uri = parseUri(req.uri); const uri = parseUri(req.uri);
@@ -159,7 +314,7 @@ module.exports = (srf, logger) => {
/* get all the carriers and gateways for the account owning this sip realm */ /* get all the carriers and gateways for the account owning this sip realm */
const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]); const [gwAcc] = await pp.query(sqlSelectAllCarriersForAccountByRealm, [uri.host]);
const [gwSP] = gwAcc.length ? [[]] : await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host); const [gwSP] = await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host);
const gw = gwAcc const gw = gwAcc
.concat(gwSP) .concat(gwSP)
.sort((a, b) => b.netmask - a.netmask); .sort((a, b) => b.netmask - a.netmask);
@@ -170,13 +325,42 @@ module.exports = (srf, logger) => {
name: gw.name, name: gw.name,
service_provider_sid: gw.service_provider_sid, service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid, account_sid: gw.account_sid,
application_sid: gw.application_sid application_sid: gw.application_sid,
register_username: gw.register_username,
register_password: gw.register_password
}; };
}); });
/* remove duplicates, winnow down to voip_carriers, not gateways */ /* remove duplicates, winnow down to voip_carriers, not gateways */
if (voip_carriers.length > 1) { if (voip_carriers.length > 1) {
voip_carriers = [...new Set(voip_carriers.map(JSON.stringify))].map(JSON.parse); voip_carriers = [...new Set(voip_carriers.map(JSON.stringify))].map(JSON.parse);
} }
/* if no static gateway matches, check ephemeral gateways in Redis */
if (voip_carriers.length === 0 && gateways.length === 0) {
const ephemeralCarrierSids = await lookupEphemeralGatewayCarriers(req.source_address);
if (ephemeralCarrierSids.length > 0) {
const ephemeralGateways = await lookupGatewaysByCarrierSids(ephemeralCarrierSids);
gateways.push(...ephemeralGateways);
voip_carriers = ephemeralGateways.map((gw) => {
return {
voip_carrier_sid: gw.voip_carrier_sid,
name: gw.name,
service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid,
application_sid: gw.application_sid,
register_username: gw.register_username,
register_password: gw.register_password
};
});
/* remove duplicates */
if (voip_carriers.length > 1) {
voip_carriers = [...new Set(voip_carriers.map(JSON.stringify))].map(JSON.parse);
}
logger.info({source_address: req.source_address, count: voip_carriers.length},
'matched call to ephemeral gateway(s) from registration trunk');
}
}
if (voip_carriers.length) { if (voip_carriers.length) {
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */ /* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(','); const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
@@ -184,12 +368,39 @@ module.exports = (srf, logger) => {
`SELECT * FROM phone_numbers WHERE number = '${did}' `SELECT * FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid IN (${vc_sids}) AND voip_carrier_sid IN (${vc_sids})
AND account_sid = '${a[0].account_sid}'`; AND account_sid = '${a[0].account_sid}'`;
logger.debug({voip_carriers, sql, did}, 'looking up DID'); //logger.debug({voip_carriers, sql, did}, 'looking up DID');
const [r] = await pp.query(sql); const [r] = await pp.query(sql);
if (r.length === 0) {
// if no matching phone number is found, find whether there are any applications having phone numbers
// matching the regex pattern or wild card
logger.debug({isDotDecimal},
'Did not find a matching phone number, checking for applications with Regex or wildcard');
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const application = await getApplicationForDidAndCarriers(req, vc_sids);
if (application) {
logger.debug({application}, 'sip_realm looking up DID: found application with Regex or wildcard ');
const gateway = gateways.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
gateway.account_sid = gateway.account_sid ? gateway.account_sid : application.account_sid;
return {
fromCarrier: true,
gateway: gateway,
service_provider_sid: a[0].service_provider_sid,
account_sid: gateway.account_sid,
application_sid: application.application_sid,
account: a[0]
};
}
}
if (r.length > 1) { if (r.length > 1) {
logger.info({r}, const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid}));
'multiple carriers with the same gateway have the same number provisioned for the same account' logger.info({
number: r[0].number,
total: all.length,
matches: all.slice(0, 5)
},
'multiple carriers with the same gateway have the same number provisioned for the same account'
+ ' -- cannot determine which one to use'); + ' -- cannot determine which one to use');
return { return {
fromCarrier: true, fromCarrier: true,
@@ -238,7 +449,7 @@ module.exports = (srf, logger) => {
`SELECT application_sid FROM phone_numbers WHERE number = '${did}' `SELECT application_sid FROM phone_numbers WHERE number = '${did}'
AND voip_carrier_sid = '${matches[0].voip_carrier_sid}' AND voip_carrier_sid = '${matches[0].voip_carrier_sid}'
AND account_sid = '${matches[0].account_sid}'`; AND account_sid = '${matches[0].account_sid}'`;
logger.debug({matches: matches[0], sql, did}, 'looking up DID'); //logger.debug({matches: matches[0], sql, did}, 'looking up DID');
const [r] = await pp.query(sql); const [r] = await pp.query(sql);
return { return {
@@ -264,10 +475,19 @@ module.exports = (srf, logger) => {
} }
/* find all carrier entries that have an inbound gateway matching the source IP */ /* find all carrier entries that have an inbound gateway matching the source IP */
const [gw] = await pp.query(sqlSelectAllGatewaysForSP); /* Query both exact IP matches AND CIDR ranges in parallel to handle the case where
multiple accounts have configured the same carrier with different netmasks.
The phone number lookup will disambiguate which account owns the call. */
const [[gwExact], [gwCidr]] = await Promise.all([
pp.query(sqlSelectExactGatewayForSP, [req.source_address]),
pp.query(sqlSelectCIDRGatewaysForSP)
]);
/* Merge both result sets - exact matches first, then CIDR ranges (already sorted by netmask DESC) */
const gw = [...gwExact, ...gwCidr];
//logger.debug({gw}, `checking gateways for source address ${req.source_address}`); //logger.debug({gw}, `checking gateways for source address ${req.source_address}`);
let matches = gw let matches = gw
.sort((a, b) => b.netmask - a.netmask)
.filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address)) .filter(gatewayMatchesSourceAddress.bind(null, logger, req.source_address))
.map((gw) => { .map((gw) => {
return { return {
@@ -276,22 +496,71 @@ module.exports = (srf, logger) => {
service_provider_sid: gw.service_provider_sid, service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid, account_sid: gw.account_sid,
application_sid: gw.application_sid, application_sid: gw.application_sid,
pad_crypto: gw.pad_crypto pad_crypto: gw.pad_crypto,
register_username: gw.register_username,
register_password: gw.register_password
}; };
}); });
/* remove duplicates, winnow down to voip_carriers, not gateways */ /* remove duplicates, winnow down to voip_carriers, not gateways */
if (matches.length > 1) { if (matches.length > 1) {
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse); matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
} }
/* if no static gateway matches, check ephemeral gateways in Redis */
if (matches.length === 0) {
const ephemeralCarrierSids = await lookupEphemeralGatewayCarriers(req.source_address);
if (ephemeralCarrierSids.length > 0) {
const ephemeralGateways = await lookupGatewaysByCarrierSids(ephemeralCarrierSids);
matches = ephemeralGateways.map((gw) => {
return {
voip_carrier_sid: gw.voip_carrier_sid,
name: gw.name,
service_provider_sid: gw.service_provider_sid,
account_sid: gw.account_sid,
application_sid: gw.application_sid,
pad_crypto: gw.pad_crypto,
register_username: gw.register_username,
register_password: gw.register_password
};
});
/* remove duplicates */
if (matches.length > 1) {
matches = [...new Set(matches.map(JSON.stringify))].map(JSON.parse);
}
logger.info({source_address: req.source_address, count: matches.length},
'matched call to ephemeral gateway(s) from registration trunk');
}
}
//logger.debug({matches}, `matches for source address ${req.source_address}`); //logger.debug({matches}, `matches for source address ${req.source_address}`);
if (matches.length) { if (matches.length) {
/* we have one or more matches. Now check for one with a provisioned phone number matching the DID */ /* we have one or more matches. Now check for one with a provisioned phone number matching the DID */
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(','); const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const sql = `SELECT * FROM phone_numbers WHERE number = '${did}' AND voip_carrier_sid IN (${vc_sids})`; const sql = `SELECT * FROM phone_numbers WHERE number = '${did}' AND voip_carrier_sid IN (${vc_sids})`;
logger.debug({matches, sql, did, vc_sids}, 'looking up DID'); logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
const [r] = await pp.query(sql); const [r] = await pp.query(sql);
if (0 === r.length) { if (0 === r.length) {
// if no matching phone number is found, find whether there are any applications having phone numbers
// matching the regex pattern or wild card
logger.debug({isDotDecimal},
'Did not find a matching phone number, checking for applications with Regex or wildcard');
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
const application = await getApplicationForDidAndCarriers(req, vc_sids);
if (application) {
logger.debug({application}, 'looking up DID: found application with Regex or wildcard');
const gateway = matches.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
const [a] = await pp.query(sqlAccountBySid, [application.account_sid]);
return {
fromCarrier: true,
gateway: gateway,
service_provider_sid: gateway.service_provider_sid,
account_sid: application.account_sid,
application_sid: application.application_sid,
account: a[0],
};
}
/* came from a provisioned carrier, but the dialed number is not provisioned. /* came from a provisioned carrier, but the dialed number is not provisioned.
check if we have an account with default routing of that carrier to an application check if we have an account with default routing of that carrier to an application
*/ */
@@ -337,8 +606,13 @@ module.exports = (srf, logger) => {
} }
} }
else if (r.length > 1) { else if (r.length > 1) {
logger.info({r}, const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid}));
'multiple accounts have added this carrier with default routing -- cannot determine which to use'); logger.info({
number: r[0].number,
total: all.length,
matches: all.slice(0, 5)
},
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
return { return {
fromCarrier: true, fromCarrier: true,
error: 'Multiple accounts are attempting to route the same phone number from the same carrier' error: 'Multiple accounts are attempting to route the same phone number from the same carrier'
@@ -361,11 +635,33 @@ module.exports = (srf, logger) => {
return failure; return failure;
}; };
/**
* Retrieves voip_carriers with trunk_type 'auth' that belong to either:
* 1. The specified account (account_sid matches), OR
* 2. The service provider but with null account_sid (shared across service provider)
*
* @param {string} account_sid - The SID of the account
* @param {string} service_provider_sid - The SID of the service provider
* @returns {Promise<Array>} Array of voip_carrier records matching the criteria
* @throws {Error} Database errors or other unexpected errors
*/
const lookupAuthCarriersForAccountAndSP = async(account_sid, service_provider_sid) => {
try {
const [rows] = await pp.query(sqlSelectAuthCarriersForAccountAndSP, [account_sid, service_provider_sid]);
return rows;
} catch (err) {
logger.error({err, account_sid, service_provider_sid}, 'lookupAuthCarriersForAccountAndSP');
throw err;
}
};
return { return {
wasOriginatedFromCarrier, wasOriginatedFromCarrier,
getApplicationForDidAndCarrier, getApplicationForDidAndCarrier,
getApplicationForDidAndCarriers,
getOutboundGatewayForRefer, getOutboundGatewayForRefer,
getSPForAccount, getSPForAccount,
getApplicationBySid getApplicationBySid,
lookupAuthCarriersForAccountAndSP
}; };
}; };
+51 -7
View File
@@ -28,9 +28,9 @@ module.exports = function(srf, logger) {
lookupAccountBySipRealm, lookupAccountBySipRealm,
lookupAccountBySid, lookupAccountBySid,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
queryCallLimits queryCallLimits,
} = srf.locals.dbHelpers; } = srf.locals.dbHelpers;
const {stats, writeCdrs} = srf.locals; const {stats, writeCdrs, lookupAuthCarriersForAccountAndSP, getApplicationForDidAndCarrier} = srf.locals;
const initLocals = (req, res, next) => { const initLocals = (req, res, next) => {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
@@ -59,13 +59,15 @@ module.exports = function(srf, logger) {
/* write cdr for non-success response here */ /* write cdr for non-success response here */
res.once('end', ({status}) => { res.once('end', ({status}) => {
if (req.locals.cdr && req.locals.cdr.account_sid && status > 200 && 401 !== status) { if (req.locals.cdr && req.locals.cdr.account_sid && status > 200 && 401 !== status) {
const trunk = ['trunk', 'teams'].includes(req.locals.originator) ? req.locals.carrier : req.locals.originator; const trunk = ['trunk', 'teams'].includes(req.locals.originator) ?
req.locals.carrier : req.locals.originator;
writeCdrs({...req.locals.cdr, writeCdrs({...req.locals.cdr,
terminated_at: Date.now(), terminated_at: Date.now(),
termination_reason: status === 487 === status ? 'caller abandoned' : 'failed', termination_reason: status === 487 ? 'caller abandoned' : 'failed',
sip_status: status, sip_status: status,
trunk trunk,
...(req.locals.application_sid && {application_sid: req.locals.application_sid})
}).catch((err) => logger.error({err}, 'Error writing cdr for call failure')); }).catch((err) => logger.error({err}, 'Error writing cdr for call failure'));
} }
}); });
@@ -131,7 +133,11 @@ module.exports = function(srf, logger) {
return res.send(404, 'Number Not Provisioned'); return res.send(404, 'Number Not Provisioned');
} }
logger.info({gateway}, 'identifyAccount: incoming call from gateway'); logger.info({gateway}, 'identifyAccount: incoming call from gateway');
const appSidHeader = req.get('x-application-sid');
if (appSidHeader && appSidHeader == application_sid) {
logger.info({callId}, 'Loop Detected, x-application-sid header on incoming call matches applicationSid');
return res.send(482, 'Loop Detected on x-application-sid');
}
let sid; let sid;
if (siprec) { if (siprec) {
if (!account.siprec_hook_sid) { if (!account.siprec_hook_sid) {
@@ -187,6 +193,10 @@ module.exports = function(srf, logger) {
res.send(404); res.send(404);
return req.srf.endSession(req); return req.srf.endSession(req);
} }
const auth_trunks = await lookupAuthCarriersForAccountAndSP(
account.account_sid,
account.service_provider_sid
);
/* if this is a dedicated SBC (static IP) only take calls for that account's sip realm */ /* if this is a dedicated SBC (static IP) only take calls for that account's sip realm */
if (process.env.SBC_ACCOUNT_SID && account.account_sid !== process.env.SBC_ACCOUNT_SID) { if (process.env.SBC_ACCOUNT_SID && account.account_sid !== process.env.SBC_ACCOUNT_SID) {
@@ -210,6 +220,7 @@ module.exports = function(srf, logger) {
registration_hook_username: account.registration_hook.username, registration_hook_username: account.registration_hook.username,
registration_hook_password: account.registration_hook.password registration_hook_password: account.registration_hook.password
}), }),
...(auth_trunks?.length && {auth_trunks}),
...req.locals ...req.locals
}; };
} }
@@ -361,6 +372,38 @@ module.exports = function(srf, logger) {
} }
}; };
const identifyAuthTrunk = async(req, res, next) => {
try {
if (req.authorization) {
const {grant} = req.authorization;
if (grant && grant.status === 'ok' && grant.auth_trunk) {
// we have successfully authenticated the call for an auth_trunk
const application_sid = await getApplicationForDidAndCarrier(req, grant.auth_trunk.voip_carrier_sid);
req.locals = {
...req.locals,
originator: 'trunk',
carrier: grant.auth_trunk.name,
gateway: grant.auth_trunk,
voip_carrier_sid: grant.auth_trunk.voip_carrier_sid,
application_sid: application_sid || grant.auth_trunk.application_sid,
};
// as call from auth carrier, clean req.authorization that impact on legacy logic for authenticated user
delete req.authorization;
logger.debug({callId: req.locals.callId, auth_trunk: grant.auth_trunk.name},
'identifyAuthTrunk: call authenticated for auth trunk');
}
}
next();
} catch (err) {
stats.increment('sbc.terminations', ['sipStatus:500']);
logger.error(err, `${req.get('Call-ID')} Error challenging auth trunk`);
res.send(500);
req.srf.endSession(req);
}
};
const challengeDeviceCalls = async(req, res, next) => { const challengeDeviceCalls = async(req, res, next) => {
try { try {
/* TODO: check if this is a gateway that we have an ACL for */ /* TODO: check if this is a gateway that we have an ACL for */
@@ -379,6 +422,7 @@ module.exports = function(srf, logger) {
handleSipRec, handleSipRec,
challengeDeviceCalls, challengeDeviceCalls,
identifyAccount, identifyAccount,
identifyAuthTrunk,
checkLimits checkLimits
}; };
}; };
+7
View File
@@ -30,6 +30,13 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) {
dstOpts.flags.push('inject DTMF'); dstOpts.flags.push('inject DTMF');
srcOpts.flags.push('inject DTMF'); srcOpts.flags.push('inject DTMF');
} }
/** By default, and for backwards compatibility, use media handover
* set env var to true to use strict source instead (needed for rtpbleed vulnerability)
*/
const enableStrictSource = !!process.env.RTPENGINE_ENABLE_STRICT_SOURCE;
dstOpts.flags.push(enableStrictSource ? 'strict source' : 'media handover');
srcOpts.flags.push(enableStrictSource ? 'strict source' : 'media handover');
const acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ? const acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ?
process.env.JAMBONES_ACCEPT_AND_TRANSCODE : process.env.JAMBONES_ACCEPT_AND_TRANSCODE :
process.env.JAMBONES_ACCEPT_G729 ? 'g729' : ''; process.env.JAMBONES_ACCEPT_G729 ? 'g729' : '';
+1622 -6625
View File
File diff suppressed because it is too large Load Diff
+14 -13
View File
@@ -1,9 +1,9 @@
{ {
"name": "sbc-inbound", "name": "sbc-inbound",
"version": "0.9.3", "version": "0.9.6",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 18.0.0" "node": ">= 20.0.0"
}, },
"keywords": [ "keywords": [
"sip", "sip",
@@ -26,29 +26,30 @@
"jslint:fix": "npm run jslint --fix" "jslint:fix": "npm run jslint --fix"
}, },
"dependencies": { "dependencies": {
"@jambonz/db-helpers": "^0.9.9", "@aws-sdk/client-auto-scaling": "^3.549.0",
"@aws-sdk/client-sns": "^3.549.0",
"@babel/helpers": "^7.26.10",
"@jambonz/db-helpers": "^0.9.18",
"@jambonz/digest-utils": "^0.0.9",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.8.13", "@jambonz/realtimedb-helpers": "^0.8.18",
"@jambonz/rtpengine-utils": "^0.4.4", "@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.10", "@jambonz/siprec-client-utils": "^0.2.10",
"@jambonz/stats-collector": "^0.1.10", "@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.10", "@jambonz/time-series": "^0.2.10",
"@jambonz/digest-utils": "^0.0.5",
"@aws-sdk/client-sns": "^3.549.0",
"@aws-sdk/client-auto-scaling": "^3.549.0",
"bent": "^7.3.12", "bent": "^7.3.12",
"cidr-matcher": "^2.1.1", "cidr-matcher": "^2.1.1",
"debug": "^4.3.4", "debug": "^4.4.3",
"drachtio-fn-b2b-sugar": "0.2.1", "drachtio-fn-b2b-sugar": "0.2.1",
"drachtio-srf": "^5.0.2", "drachtio-srf": "^5.0.21",
"express": "^4.19.2", "express": "^4.21.2",
"pino": "^8.20.0", "pino": "^10.1.0",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.32.0", "eslint": "^9.17.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^7.2.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"tape": "^5.7.5" "tape": "^5.7.5"
} }
+17 -4
View File
@@ -162,7 +162,7 @@ regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed num
description VARCHAR(1024), description VARCHAR(1024),
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first', priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
PRIMARY KEY (lcr_route_sid) PRIMARY KEY (lcr_route_sid)
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches'; ) COMMENT='An ordered list of digit patterns in an LCR table. The pat';
CREATE TABLE lcr CREATE TABLE lcr
( (
@@ -173,7 +173,7 @@ default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use whe
service_provider_sid CHAR(36), service_provider_sid CHAR(36),
account_sid CHAR(36), account_sid CHAR(36),
PRIMARY KEY (lcr_sid) PRIMARY KEY (lcr_sid)
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.'; ) COMMENT='An LCR (least cost routing) table that is used by a service ';
CREATE TABLE password_settings CREATE TABLE password_settings
( (
@@ -204,6 +204,7 @@ tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to thi
inbound_auth_username VARCHAR(64), inbound_auth_username VARCHAR(64),
inbound_auth_password VARCHAR(64), inbound_auth_password VARCHAR(64),
diversion VARCHAR(32), diversion VARCHAR(32),
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
PRIMARY KEY (predefined_carrier_sid) PRIMARY KEY (predefined_carrier_sid)
); );
@@ -351,6 +352,8 @@ speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL, model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME', reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL, name VARCHAR(64) NOT NULL,
voice_cloning_key MEDIUMTEXT,
use_voice_cloning_key BOOLEAN DEFAULT false,
PRIMARY KEY (google_custom_voice_sid) PRIMARY KEY (google_custom_voice_sid)
); );
@@ -414,6 +417,9 @@ register_from_user VARCHAR(128),
register_from_domain VARCHAR(255), register_from_domain VARCHAR(255),
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false, register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
register_status VARCHAR(4096), register_status VARCHAR(4096),
dtmf_type ENUM('rfc2833','tones','info') NOT NULL DEFAULT 'rfc2833',
outbound_sip_proxy VARCHAR(255),
trunk_type ENUM('static_ip','auth','reg') NOT NULL DEFAULT 'static_ip',
PRIMARY KEY (voip_carrier_sid) PRIMARY KEY (voip_carrier_sid)
) COMMENT='A Carrier or customer PBX that can send or receive calls'; ) COMMENT='A Carrier or customer PBX that can send or receive calls';
@@ -499,7 +505,7 @@ messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT, app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US', speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(256), speech_synthesis_voice VARCHAR(256) DEFAULT 'en-US-Standard-C',
speech_synthesis_label VARCHAR(64), speech_synthesis_label VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US', speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
@@ -512,6 +518,7 @@ fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64), fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64), fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64), fallback_speech_recognizer_label VARCHAR(64),
env_vars TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false, record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid) PRIMARY KEY (application_sid)
@@ -698,6 +705,12 @@ ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_
CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port); CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid);
CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4);
CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask);
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
@@ -739,4 +752,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid); ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1; SET FOREIGN_KEY_CHECKS=0;
+48
View File
@@ -72,6 +72,9 @@ values ('d458bf7a-bcea-47b2-ac96-66dfc9c5c220', '150822233*', '287c1452-620d-419
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid) insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid)
values ('f7ad205d-b92f-4363-8160-f8b5216b40d3', '15083871234', '287c1452-620d-4195-9f19-c9814ef90d78', 'd7cc37cb-d152-49ef-a51b-485f6e917089'); values ('f7ad205d-b92f-4363-8160-f8b5216b40d3', '15083871234', '287c1452-620d-4195-9f19-c9814ef90d78', 'd7cc37cb-d152-49ef-a51b-485f6e917089');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('c17d5a7d-9328-4663-92c0-f65aa8381264', '12125551212', '287c1452-620d-4195-9f19-c9814ef90d78', 'ed649e33-e771-403a-8c99-1780eabbc803', '3b43e39f-4346-4218-8434-a53130e8be49');
-- two accounts that both have the same carrier with default routing (ambiguity test) -- two accounts that both have the same carrier with default routing (ambiguity test)
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm) insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('239d7d49-b3e4-4fdb-9d66-661149f717e8', 'Account B1', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'echo2.sip.jambonz.org'); values ('239d7d49-b3e4-4fdb-9d66-661149f717e8', 'Account B1', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'echo2.sip.jambonz.org');
@@ -92,3 +95,48 @@ insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outb
values ('664a5339-c62c-4075-9e19-f4de70a96597', '731abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false); values ('664a5339-c62c-4075-9e19-f4de70a96597', '731abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound) insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('554a5339-c62c-4075-9e19-f4de70a96597', '987abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false); values ('554a5339-c62c-4075-9e19-f4de70a96597', '987abdc7-0220-4964-bc66-32b5c70cd9ab', '172.38.0.40', true, false);
-- voip carrier belonging to all accounts
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid)
values ('voip100', 'test-voip-carrier', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('sip100', 'voip100', '172.38.0.50', true, false);
insert into voip_carriers (voip_carrier_sid, name, service_provider_sid)
values ('voip101', 'test-voip-carrier-101', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0');
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('sip101', 'voip101', '172.38.0.50', true, false);
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('sip102', 'voip101', '172.38.0.51', true, false);
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('app100', 'app100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '90dda62e-0ea2-47d1-8164-5bd49003476c');
insert into applications (application_sid, name, account_sid, call_hook_sid, call_status_hook_sid)
values ('app101', 'app101', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', '90dda62e-0ea2-47d1-8164-5bd49003476c', '90dda62e-0ea2-47d1-8164-5bd49003476c');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('phone100', '^100', 'voip101', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app100');
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('phone101', '^10012', 'voip100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app101');
-- insert an invalid regex pattern, the below pattern should be ignored during pattern
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('phone102', '\\dkjfhmdf\\', 'voip100', 'ee9d7d49-b3e4-4fdb-9d66-661149f717e8', 'app101');
-- account with a sip realm that is not associated with any voip carriers
insert into accounts (account_sid, name, service_provider_sid, webhook_secret, sip_realm)
values ('acct-100', 'Account 100', '3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'foobar', 'ram.sip.jambonz.org');
-- registration trunk carrier for ephemeral gateway testing
insert into voip_carriers (voip_carrier_sid, name, account_sid, service_provider_sid, trunk_type,
requires_register, register_username, register_sip_realm, register_password, is_active)
values ('4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', 'test-registration-trunk', 'ed649e33-e771-403a-8c99-1780eabbc803',
'3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', 'reg', true, 'testuser',
'sip.carrier.example.com', 'testpass', true);
-- sip_gateway for outbound only (inbound will use ephemeral gateway from Redis)
insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, inbound, outbound)
values ('8b3e5f9a-2c1d-4e7b-9a6c-3d8f1e5a7b2c', '4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', '3.3.3.3', false, true);
-- phone number for ephemeral gateway test
insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, account_sid, application_sid)
values ('7c2d4e6f-8a1b-4c9d-7e5f-2a8b3c6d9e1f', '16175551000', '4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', 'ed649e33-e771-403a-8c99-1780eabbc803',
'3b43e39f-4346-4218-8434-a53130e8be49');
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC with x-application-sid header">
<send retrans="500">
<![CDATA[
INVITE sip:+12125551212@jambonz.org 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:12125551212@jambonz.org>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-carrier-success
Content-Type: application/sdp
Content-Length: [len]
X-Application-Sid: 3b43e39f-4346-4218-8434-a53130e8be49
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100" optional="true">
</recv>
<recv response="482" rtd="true" >
</recv>
</scenario>
@@ -0,0 +1,118 @@
<?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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
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>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true" crlf="true">
</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:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
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="3000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:100678@172.38.0.10:5060 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:100678@172.38.0.10:5060>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Subject: uac-pcap-carrier-success
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>
@@ -0,0 +1,118 @@
<?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:+100678@ram.sip.jambonz.org 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:100678@ram.sip.jambonz.org>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
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>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true" crlf="true">
</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:100678@ram.sip.jambonz.org 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:100678@ram.sip.jambonz.org>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
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="3000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:100678@ram.sip.jambonz.org 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:100678@ram.sip.jambonz.org>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Subject: uac-pcap-carrier-success
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>
@@ -0,0 +1,119 @@
<?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:+16175551000@jambonz.org 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:16175551000@jambonz.org>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:sipp@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-pcap-ephemeral-gateway-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>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true" crlf="true">
</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:16175551000@jambonz.org 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:16175551000@jambonz.org>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Max-Forwards: 70
Subject: uac-pcap-ephemeral-gateway-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="3000"/>
<!-- The 'crlf' option inserts a blank line in the statistics report. -->
<send retrans="500">
<![CDATA[
BYE sip:16175551000@jambonz.org 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:16175551000@jambonz.org>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 BYE
Subject: uac-pcap-ephemeral-gateway-success
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>
+20 -1
View File
@@ -37,9 +37,18 @@ test('incoming call tests', async(t) => {
await sippUac('uac-late-media.xml', '172.38.0.20'); await sippUac('uac-late-media.xml', '172.38.0.20');
t.pass('incoming call with no SDP packet is rejected with a 488'); t.pass('incoming call with no SDP packet is rejected with a 488');
await sippUac('uac-did-applicationsid-loop.xml', '172.38.0.20');
t.pass('incoming call with x-application-sid header is rejected with 482');
await sippUac('uac-pcap-carrier-success.xml', '172.38.0.20'); await sippUac('uac-pcap-carrier-success.xml', '172.38.0.20');
t.pass('incoming call from carrier completed successfully'); t.pass('incoming call from carrier completed successfully');
// Test ephemeral gateway (registration trunk)
const { createEphemeralGateway } = srf.locals.realtimeDbHelpers;
await createEphemeralGateway('172.38.0.60', '4a7d1c8e-5f2b-4d9a-8e3c-6b5a9f1e4c7d', 3600);
await sippUac('uac-pcap-ephemeral-gateway-success.xml', '172.38.0.60');
t.pass('incoming call from ephemeral gateway (registration trunk) completed successfully');
await sippUac('uac-pcap-pbx-success.xml', '172.38.0.21'); await sippUac('uac-pcap-pbx-success.xml', '172.38.0.21');
t.pass('incoming call from account-level carrier completed successfully'); t.pass('incoming call from account-level carrier completed successfully');
@@ -64,6 +73,15 @@ test('incoming call tests', async(t) => {
await sippUac('uac-pcap-carrier-max-call-limit.xml', '172.38.0.20'); await sippUac('uac-pcap-carrier-max-call-limit.xml', '172.38.0.20');
t.pass('rejects incoming call with 503 when max calls per account reached'); t.pass('rejects incoming call with 503 when max calls per account reached');
await sippUac('uac-did-regex-match-vc-all-accts.xml', '172.38.0.50');
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, with sip realm');
await sippUac('uac-did-regex-match-vc-all-accts-nosiprealm.xml', '172.38.0.51');
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, without sip realm');
await sippUac('uac-did-regex-match-vc-all-accts-nosiprealm.xml', '172.38.0.50');
t.pass('incoming call matched by trailing wildcard *, voice gateway belongs to all accounts, without sip realm');
/* switch off this env for remaining tests (JAMBONES_HOSTING is for Saas sts) */ /* switch off this env for remaining tests (JAMBONES_HOSTING is for Saas sts) */
delete process.env.JAMBONES_HOSTING; delete process.env.JAMBONES_HOSTING;
await sippUac('uac-pcap-carrier-fail-ambiguous.xml', '172.38.0.40'); await sippUac('uac-pcap-carrier-fail-ambiguous.xml', '172.38.0.40');
@@ -71,8 +89,9 @@ test('incoming call tests', async(t) => {
await waitFor(12); await waitFor(12);
const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'}); const res = await queryCdrs({account_sid: 'ed649e33-e771-403a-8c99-1780eabbc803'});
console.log(`cdrs res.total: ${res.total}`);
//console.log(`cdrs: ${JSON.stringify(res)}`); //console.log(`cdrs: ${JSON.stringify(res)}`);
t.ok(7 === res.total, 'successfully wrote 8 cdrs for calls'); t.ok(8 === res.total, 'successfully wrote 8 cdrs for calls (including ephemeral gateway)');
srf.disconnect(); srf.disconnect();
t.end(); t.end();