Compare commits

..
Author SHA1 Message Date
Dave Horton 5438f4e147 wip 2025-01-10 19:21:14 -05:00
Dave Horton dd2a4fead5 wip 2025-01-10 19:17:41 -05:00
Dave Horton e706d60c10 wip 2025-01-10 19:15:08 -05:00
Dave Horton 6e892cb48f wip 2025-01-10 19:06:29 -05:00
Dave Horton 53a205db7d wip 2025-01-10 19:05:47 -05:00
23 changed files with 6923 additions and 2909 deletions
+1
View File
@@ -0,0 +1 @@
test/*
+126
View File
@@ -0,0 +1,126 @@
{
"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@v4 uses: actions/checkout@v3
- 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@v3 uses: docker/login-action@v2
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@v6 uses: docker/build-push-action@v4
with: with:
context: . context: .
push: true push: true
+3 -3
View File
@@ -1,10 +1,10 @@
FROM --platform=linux/amd64 node:24-alpine AS base FROM --platform=linux/amd64 node:20.13.0-alpine3.18 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" ]
+7 -27
View File
@@ -64,30 +64,18 @@ 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, process.env.JAMBONES_MYSQL_WRITE_HOST && process.env.JAMBONES_MYSQL_WRITE_USER && }, logger);
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, decrKey} = require('@jambonz/realtimedb-helpers')({}, logger);
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);
@@ -126,13 +114,10 @@ srf.locals = {...srf.locals,
addKey, addKey,
deleteKey, deleteKey,
retrieveKey, retrieveKey,
retrieveHash,
createSet, createSet,
incrKey, incrKey,
decrKey, decrKey,
retrieveSet, retrieveSet
createEphemeralGateway,
queryEphemeralGateways
} }
}; };
const { const {
@@ -140,8 +125,7 @@ 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,
@@ -150,8 +134,7 @@ 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;
@@ -160,8 +143,7 @@ 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');
@@ -249,9 +231,7 @@ 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": ["port latching"], "flags": ["media handover", "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", "port latching"], "flags": ["generate mid", "SDES-no", "media handover", "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", "port latching"], "flags": ["generate mid", "SDES-no", "media handover", "port latching"],
"rtcp-mux": ["accept"] "rtcp-mux": ["accept"]
} }
} }
-137
View File
@@ -1,137 +0,0 @@
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
}
}
];
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = (logger) => {
// listen for SNS lifecycle changes // listen for SNS lifecycle changes
let lifecycleEmitter = new Emitter(); let lifecycleEmitter = new Emitter();
lifecycleEmitter.dryUpCalls = false; lifecycleEmitter.dryUpCalls = false;
if (process.env.AWS_SNS_TOPIC_ARN) { if (process.env.AWS_SNS_TOPIC_ARM) {
(async function() { (async function() {
try { try {
+6 -16
View File
@@ -69,17 +69,7 @@ class SnsNotifier extends Emitter {
subscriptionRequestId: this.subscriptionRequestId subscriptionRequestId: this.subscriptionRequestId
}, 'response from SNS SubscribeURL'); }, 'response from SNS SubscribeURL');
const data = await this.describeInstance(); const data = await this.describeInstance();
this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
const group = data.AutoScalingGroups.find((group) =>
group.Instances && group.Instances.some((instance) => instance.InstanceId === this.instanceId)
);
if (!group) {
this.logger.error('Current instance not found in any Auto Scaling group', data);
} else {
const instance = group.Instances.find((instance) => instance.InstanceId === this.instanceId);
this.lifecycleState = instance.LifecycleState;
}
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp}); this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
break; break;
@@ -147,13 +137,13 @@ class SnsNotifier extends Emitter {
try { try {
const params = { const params = {
Protocol: 'http', Protocol: 'http',
TopicArn: process.env.AWS_SNS_TOPIC_ARN, TopicArn: process.env.AWS_SNS_TOPIC_ARM,
Endpoint: this.snsEndpoint Endpoint: this.snsEndpoint
}; };
const response = await snsClient.send(new SubscribeCommand(params)); const response = await snsClient.send(new SubscribeCommand(params));
this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARN}`); this.logger.info({response}, `response to SNS subscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) { } catch (err) {
this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARN}`); this.logger.error({err}, `Error subscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
} }
} }
@@ -164,9 +154,9 @@ class SnsNotifier extends Emitter {
SubscriptionArn: this.subscriptionArn SubscriptionArn: this.subscriptionArn
}; };
const response = await snsClient.send(new UnsubscribeCommand(params)); const response = await snsClient.send(new UnsubscribeCommand(params));
this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARN}`); this.logger.info({response}, `response to SNS unsubscribe to ${process.env.AWS_SNS_TOPIC_ARM}`);
} catch (err) { } catch (err) {
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARN}`); this.logger.error({err}, `Error unsubscribing to SNS topic arn ${process.env.AWS_SNS_TOPIC_ARM}`);
} }
} }
+12 -69
View File
@@ -19,9 +19,6 @@ 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
@@ -69,7 +66,6 @@ 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;
@@ -184,8 +180,8 @@ class CallSession extends Emitter {
host = obj.host; host = obj.host;
proxy = `sip:${featureServer}`; proxy = `sip:${featureServer}`;
} }
if (obj.user) uri = `sip:${obj.user}@${host}`; if (obj.user) uri = `${scheme}:${obj.user}@${host}`;
else uri = `sip:${host}`; else uri = `${scheme}:${host}`;
this.logger.info(`uri will be: ${uri}, proxy ${proxy}`); this.logger.info(`uri will be: ${uri}, proxy ${proxy}`);
const sendOfferToRtpEngine = async(remoteSdp) => { const sendOfferToRtpEngine = async(remoteSdp) => {
@@ -284,7 +280,6 @@ class CallSession extends Emitter {
proxy, proxy,
headers, headers,
responseHeaders, responseHeaders,
passFailure: false,
proxyRequestHeaders: [ proxyRequestHeaders: [
'all', 'all',
'-Authorization', '-Authorization',
@@ -337,15 +332,6 @@ 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');
@@ -358,49 +344,11 @@ 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') {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
this.logger.error(err, 'unexpected error routing inbound call'); this.logger.error(err, 'unexpected error routing inbound call');
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'));
} }
/* 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);
} }
} }
@@ -495,7 +443,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 {} } catch (err) {}
this.unsubscribeForDTMF(); this.unsubscribeForDTMF();
@@ -506,14 +454,15 @@ class CallSession extends Emitter {
if (process.env.JAMBONES_HOSTING || trackingOn) { if (process.env.JAMBONES_HOSTING || trackingOn) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals; const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
if (!this.req.locals.callCountNudged) { if (!this.req.locals.callCountNudged) {
const {callId} = this.req.locals;
this.req.locals.callCountNudged = true; this.req.locals.callCountNudged = true;
this.logger.debug('decrementing call count at end of call'); this.logger.info('decrementing call count at end of call due to normal termination');
await nudgeCallCounts(this.req, 'complete', { await nudgeCallCounts(this.logger, {
service_provider_sid: this.service_provider_sid, service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid, account_sid: this.account_sid,
application_sid: this.application_sid, application_sid: this.application_sid,
callId: this.req.locals.callId callId
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}) }, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}, -1)
.catch((err) => this.logger.error(err, 'Error decrementing call counts')); .catch((err) => this.logger.error(err, 'Error decrementing call counts'));
} }
} }
@@ -525,9 +474,7 @@ 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 {hasRecording} = await this.retrieveHash(`call:${this.account_sid}:${call_sid}`) ?? {}; const isRecording = this.req.locals.account.record_all_calls || (application && application.record_all_calls);
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')}`;
@@ -909,8 +856,7 @@ Duration=${payload.duration} `
blockMedia: this.blockMedia, blockMedia: this.blockMedia,
unblockMedia: this.unblockMedia, unblockMedia: this.unblockMedia,
unsubscribe: this.unsubscribe, unsubscribe: this.unsubscribe,
headers, headers
isSipRecCall: Array.isArray(this.req.payload) && this.req.payload.length > 1
})); }));
try { try {
this._startRecordingNoAnswerTimer(res); this._startRecordingNoAnswerTimer(res);
@@ -992,13 +938,11 @@ 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) {
@@ -1104,7 +1048,7 @@ Duration=${payload.duration} `
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
'content-length':contentlength, 'refer-to':_referto, 'referred-by': referredby, 'content-length':contentlength, 'refer-to':_referto, 'referred-by': referredby,
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
'X-Refer-To-Leave-Untouched': _leave, contact: _contact, 'X-Refer-To-Leave-Untouched': _leave,
...customHeaders ...customHeaders
} = req.headers; } = req.headers;
@@ -1113,7 +1057,6 @@ Duration=${payload.duration} `
headers: { headers: {
'Refer-To': `<${stringifyUri(uri)}>`, 'Refer-To': `<${stringifyUri(uri)}>`,
'Referred-By': referredby, 'Referred-By': referredby,
'Contact': this.contactHeader,
...customHeaders ...customHeaders
} }
}); });
+35 -331
View File
@@ -7,8 +7,7 @@ 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
@@ -18,8 +17,7 @@ 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
@@ -28,30 +26,14 @@ 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 sqlSelectExactGatewayForSP = const sqlSelectAllGatewaysForSP =
`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 = ?';
@@ -74,35 +56,15 @@ 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
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.requires_register = 1
AND vc.requires_register = 1 AND vc.is_active = 1
AND vc.is_active = 1 AND vc.register_sip_realm = ?
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) {
@@ -118,7 +80,6 @@ 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) => {
@@ -146,22 +107,6 @@ 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';
@@ -172,117 +117,17 @@ 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 patterns = r2 const match = 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); .sort((a, b) => b.number.length - a.number.length) // prefer longest match
for (const pattern of patterns) { .find((o) => did.match(new RegExp(o.number.endsWith('*') ? `${o.number.slice(0, -1)}\\d*` : o.number)));
try { if (match) return match.application_sid;
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);
@@ -314,7 +159,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] = await pp.query(sqlSelectAllCarriersForSPByRealm, uri.host); const [gwSP] = gwAcc.length ? [[]] : 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);
@@ -325,42 +170,13 @@ 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(',');
@@ -368,39 +184,12 @@ 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) {
const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid})); logger.info({r},
logger.info({ 'multiple carriers with the same gateway have the same number provisioned for the same account'
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,
@@ -449,7 +238,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 {
@@ -475,19 +264,10 @@ 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 */
/* Query both exact IP matches AND CIDR ranges in parallel to handle the case where const [gw] = await pp.query(sqlSelectAllGatewaysForSP);
multiple accounts have configured the same carrier with different netmasks. logger.debug({gw}, `checking gateways for source address ${req.source_address}`);
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}`);
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 {
@@ -496,71 +276,22 @@ 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);
} }
logger.debug({matches}, `matches for source address ${req.source_address}`);
/* 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}`);
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
*/ */
@@ -606,13 +337,8 @@ module.exports = (srf, logger) => {
} }
} }
else if (r.length > 1) { else if (r.length > 1) {
const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid})); logger.info({r},
logger.info({ 'multiple accounts have added this carrier with default routing -- cannot determine which to use');
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'
@@ -635,33 +361,11 @@ 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
}; };
}; };
+35 -86
View File
@@ -28,14 +28,13 @@ module.exports = function(srf, logger) {
lookupAccountBySipRealm, lookupAccountBySipRealm,
lookupAccountBySid, lookupAccountBySid,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
queryCallLimits, queryCallLimits
} = srf.locals.dbHelpers; } = srf.locals.dbHelpers;
const {stats, writeCdrs, lookupAuthCarriersForAccountAndSP, getApplicationForDidAndCarrier} = srf.locals; const {stats, writeCdrs} = srf.locals;
const initLocals = (req, res, next) => { const initLocals = (req, res, next) => {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
req.locals = req.locals || {callId}; req.locals = req.locals || {callId};
req.locals.nudge = 0;
/* check if forwarded by a proxy that applied an X-Forwarded-For Header */ /* check if forwarded by a proxy that applied an X-Forwarded-For Header */
if (req.has('X-Forwarded-For') || req.has('X-Subspace-Forwarded-For')) { if (req.has('X-Forwarded-For') || req.has('X-Subspace-Forwarded-For')) {
@@ -59,15 +58,13 @@ 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) ? const trunk = ['trunk', 'teams'].includes(req.locals.originator) ? req.locals.carrier : 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 ? 'caller abandoned' : 'failed', termination_reason: status === 487 === status ? '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'));
} }
}); });
@@ -133,11 +130,7 @@ 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) {
@@ -193,10 +186,6 @@ 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) {
@@ -220,7 +209,6 @@ 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
}; };
} }
@@ -260,6 +248,7 @@ module.exports = function(srf, logger) {
}; };
const checkLimits = async(req, res, next) => { const checkLimits = async(req, res, next) => {
const {callId} = req.locals;
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS || const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
process.env.JAMBONES_TRACK_SP_CALLS || process.env.JAMBONES_TRACK_SP_CALLS ||
process.env.JAMBONES_TRACK_APP_CALLS; process.env.JAMBONES_TRACK_APP_CALLS;
@@ -271,33 +260,29 @@ module.exports = function(srf, logger) {
assert(account_sid); assert(account_sid);
assert(service_provider_sid); assert(service_provider_sid);
/* if quick cancel then we are done */
if (req.canceled) {
logger.info('checkLimits: call was immediately canceled, no need to increment call count as we are done');
return;
}
/* decrement count if INVITE is later rejected */ /* decrement count if INVITE is later rejected */
res.once('end', async({status}) => { res.once('end', async({status}) => {
if (status > 200) { if (status > 200 && !req.locals.callCountNudged) {
nudgeCallCounts(req, 'failure', { req.locals.callCountNudged = true;
logger.debug('decrementing call count due to call rejection');
nudgeCallCounts(logger, {
service_provider_sid, service_provider_sid,
account_sid, account_sid,
application_sid, application_sid,
callId: req.locals.callId callId
}, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}) }, decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}, -1)
.catch((err) => logger.error(err, 'Error decrementing call counts')); .catch((err) => logger.error(err, 'Error decrementing call counts'));
} }
}); });
try { try {
/* increment the call count */ /* increment the call count */
const {callsSP, calls} = await nudgeCallCounts(req, 'init', { const {callsSP, calls} = await nudgeCallCounts(logger, {
service_provider_sid, service_provider_sid,
account_sid, account_sid,
application_sid, application_sid,
callId: req.locals.callId callId
}, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}); }, incrKey, {writeCallCountSP, writeCallCount, writeCallCountApp}, 1);
/* compare to account's limit, though avoid db hit when call count is low */ /* compare to account's limit, though avoid db hit when call count is low */
const minLimit = process.env.MIN_CALL_LIMIT ? const minLimit = process.env.MIN_CALL_LIMIT ?
@@ -306,22 +291,24 @@ module.exports = function(srf, logger) {
logger.debug(`checkLimits: call count is now ${calls}, limit is ${minLimit}`); logger.debug(`checkLimits: call count is now ${calls}, limit is ${minLimit}`);
if (calls <= minLimit) return next(); if (calls <= minLimit) return next();
const accountCapacities = await lookupAccountCapacitiesBySid(account_sid); if (process.env.JAMBONES_HOSTING) {
const accountLimit = accountCapacities.find((c) => c.category == 'voice_call_session'); const accountCapacities = await lookupAccountCapacitiesBySid(account_sid);
if (accountLimit) { const accountLimit = accountCapacities.find((c) => c.category == 'voice_call_session');
/* check account limit */ if (accountLimit) {
const limit_sessions = accountLimit.quantity; /* check account limit */
if (calls > limit_sessions) { const limit_sessions = accountLimit.quantity;
debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`); if (calls > limit_sessions) {
logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded'); debug(`checkLimits: limits exceeded: call count ${calls}, limit ${limit_sessions}`);
writeAlerts({ logger.info({calls, limit_sessions}, 'checkLimits: limits exceeded');
alert_type: AlertType.ACCOUNT_CALL_LIMIT, writeAlerts({
service_provider_sid: account.service_provider_sid, alert_type: AlertType.ACCOUNT_CALL_LIMIT,
account_sid, service_provider_sid: account.service_provider_sid,
count: limit_sessions account_sid,
}).catch((err) => logger.info({err}, 'checkLimits: error writing alert')); count: limit_sessions
res.send(503, 'Maximum Calls In Progress'); }).catch((err) => logger.info({err}, 'checkLimits: error writing alert'));
return req.srf.endSession(req); res.send(503, 'Maximum Calls In Progress');
return req.srf.endSession(req);
}
} }
} }
else if (trackingOn) { else if (trackingOn) {
@@ -342,11 +329,6 @@ module.exports = function(srf, logger) {
}); });
return req.srf.endSession(req); 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) { if (process.env.JAMBONES_TRACK_SP_CALLS && sp_limit > 0 && callsSP > sp_limit) {
logger.info({callsSP, sp_limit}, 'checkLimits: service provider limits exceeded'); logger.info({callsSP, sp_limit}, 'checkLimits: service provider limits exceeded');
writeAlerts({ writeAlerts({
@@ -372,38 +354,6 @@ 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 */
@@ -422,7 +372,6 @@ module.exports = function(srf, logger) {
handleSipRec, handleSipRec,
challengeDeviceCalls, challengeDeviceCalls,
identifyAccount, identifyAccount,
identifyAuthTrunk,
checkLimits checkLimits
}; };
}; };
+30 -103
View File
@@ -30,13 +30,6 @@ 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' : '';
@@ -124,97 +117,12 @@ const createHealthCheckApp = (port, logger) => {
}); });
}; };
/** const nudgeCallCounts = async(logger, sids, nudgeOperator, writers, direction) => {
* nudgeCallCounts - increment or decrement call counts in redis const { service_provider_sid, account_sid, callId, application_sid } = sids;
* const { writeCallCount, writeCallCountSP, writeCallCountApp } = writers;
* 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 nudges = [];
const writes = []; const writes = [];
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:incalls:${account_sid}:${callId}`, new Date().toISOString(), 259200);
}
else {
await deleteKey(`debug:incalls:${account_sid}:${callId}`);
}
}
if (process.env.JAMBONES_TRACK_SP_CALLS) { if (process.env.JAMBONES_TRACK_SP_CALLS) {
const key = makeSPCallCountKey(service_provider_sid); const key = makeSPCallCountKey(service_provider_sid);
nudges.push(nudgeOperator(key)); nudges.push(nudgeOperator(key));
@@ -241,30 +149,49 @@ const nudgeCallCounts = async(req, why, sids, nudgeOperator, writers) => {
try { try {
const [callsSP, calls, callsApp] = await Promise.all(nudges); const [callsSP, calls, callsApp] = await Promise.all(nudges);
logger.debug({ logger.info({
calls, callsSP, callsApp, calls, callsSP, callsApp,
service_provider_sid, account_sid, application_sid}, 'call counts after adjustment'); service_provider_sid, account_sid, application_sid
}, `call counts after ${direction > 0 ? 'increment' : 'decrement'}`);
//if (process.env.JAMBONES_DEBUG_CALL_COUNTS) {
const {srf} = require('..');
const {addKey, deleteKey} = srf.locals.realtimeDbHelpers;
if (direction > 0) {
// save for 3 days
await addKey(`debug:incalls:${account_sid}:${callId}`, new Date().toISOString(), 259200);
}
else {
await deleteKey(`debug:incalls:${account_sid}:${callId}`);
}
//}
if (process.env.JAMBONES_TRACK_SP_CALLS) { if (process.env.JAMBONES_TRACK_SP_CALLS) {
writes.push(writeCallCountSP({service_provider_sid, calls_in_progress: callsSP})); writes.push(writeCallCountSP({ service_provider_sid, calls_in_progress: callsSP }));
} }
if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_HOSTING) { if (process.env.JAMBONES_TRACK_ACCOUNT_CALLS || process.env.JAMBONES_HOSTING) {
writes.push(writeCallCount({service_provider_sid, account_sid, calls_in_progress: calls})); writes.push(writeCallCount({ service_provider_sid, account_sid, calls_in_progress: calls }));
} }
if (process.env.JAMBONES_TRACK_APP_CALLS && application_sid) { if (process.env.JAMBONES_TRACK_APP_CALLS && application_sid) {
writes.push(writeCallCountApp({service_provider_sid, account_sid, application_sid, calls_in_progress: callsApp})); writes.push(writeCallCountApp({
service_provider_sid,
account_sid, application_sid,
calls_in_progress: callsApp
}));
} }
/* write the call counts to the database */ /* write the call counts to the database */
Promise.all(writes).catch((err) => logger.error({err}, 'Error writing call counts')); Promise.all(writes).catch((err) => logger.error({ err }, 'Error writing call counts'));
return {callsSP, calls, callsApp}; return { callsSP, calls, callsApp };
} catch (err) { } catch (err) {
logger.error(err, 'error incrementing call counts'); logger.error(err, 'error incrementing call counts');
} }
return {callsSP: null, calls: null, callsApp: null}; return { callsSP: null, calls: null, callsApp: null };
}; };
const roundTripTime = (startAt) => { const roundTripTime = (startAt) => {
+6640 -1637
View File
File diff suppressed because it is too large Load Diff
+15 -16
View File
@@ -1,9 +1,9 @@
{ {
"name": "sbc-inbound", "name": "sbc-inbound",
"version": "0.9.6", "version": "0.9.2",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 20.0.0" "node": ">= 18.0.0"
}, },
"keywords": [ "keywords": [
"sip", "sip",
@@ -26,30 +26,29 @@
"jslint:fix": "npm run jslint --fix" "jslint:fix": "npm run jslint --fix"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-auto-scaling": "^3.549.0", "@jambonz/db-helpers": "^0.9.7",
"@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.18", "@jambonz/realtimedb-helpers": "^0.8.8",
"@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.9",
"@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.4.3", "debug": "^4.3.4",
"drachtio-fn-b2b-sugar": "0.2.1", "drachtio-fn-b2b-sugar": "0.1.0",
"drachtio-srf": "^5.0.21", "drachtio-srf": "^4.5.38",
"express": "^4.21.2", "express": "^4.19.2",
"pino": "^10.1.0", "pino": "^8.20.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": "^9.17.0", "eslint": "^7.32.0",
"eslint-plugin-promise": "^7.2.1", "eslint-plugin-promise": "^6.1.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"tape": "^5.7.5" "tape": "^5.7.5"
} }
+4 -17
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 pat'; ) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
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 '; ) 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.';
CREATE TABLE password_settings CREATE TABLE password_settings
( (
@@ -204,7 +204,6 @@ 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)
); );
@@ -352,8 +351,6 @@ 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)
); );
@@ -417,9 +414,6 @@ 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';
@@ -505,7 +499,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) DEFAULT 'en-US-Standard-C', speech_synthesis_voice VARCHAR(256),
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',
@@ -518,7 +512,6 @@ 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)
@@ -705,12 +698,6 @@ 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);
@@ -752,4 +739,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=0; SET FOREIGN_KEY_CHECKS=1;
-48
View File
@@ -72,9 +72,6 @@ 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');
@@ -95,48 +92,3 @@ 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');
@@ -1,36 +0,0 @@
<?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>
@@ -1,118 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<scenario name="UAC with media">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip: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>
@@ -1,118 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<scenario name="UAC with media">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:+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>
@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<!-- This program is free software; you can redistribute it and/or -->
<!-- modify it under the terms of the GNU General Public License as -->
<!-- published by the Free Software Foundation; either version 2 of the -->
<!-- License, or (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU General Public License for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU General Public License -->
<!-- along with this program; if not, write to the -->
<!-- Free Software Foundation, Inc., -->
<!-- 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -->
<!-- -->
<!-- Sipp 'uac' scenario with pcap (rtp) play -->
<!-- -->
<scenario name="UAC with media">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:+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>
+1 -20
View File
@@ -37,18 +37,9 @@ 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');
@@ -73,15 +64,6 @@ 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');
@@ -89,9 +71,8 @@ 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(8 === res.total, 'successfully wrote 8 cdrs for calls (including ephemeral gateway)'); t.ok(7 === res.total, 'successfully wrote 8 cdrs for calls');
srf.disconnect(); srf.disconnect();
t.end(); t.end();