Feat/auth trunk register (#141)

* allow REGISTER for auth trunks

* use db-helpers

* bump db-helpers
This commit is contained in:
Sam Machin
2026-07-14 09:17:31 -04:00
committed by GitHub
parent bf6362ec73
commit 3122de4316
5 changed files with 79 additions and 18 deletions
+7 -4
View File
@@ -44,7 +44,7 @@ const Srf = require('drachtio-srf');
const srf = new Srf(); const srf = new Srf();
const StatsCollector = require('@jambonz/stats-collector'); const StatsCollector = require('@jambonz/stats-collector');
const stats = new StatsCollector(logger); const stats = new StatsCollector(logger);
const { initLocals, rejectIpv4, checkCache, checkAccountLimits } = require('./lib/middleware'); const { initLocals, rejectIpv4, checkCache, checkAccountLimits, enforceDeviceLimits } = require('./lib/middleware');
const responseTime = require('drachtio-mw-response-time'); const responseTime = require('drachtio-mw-response-time');
const regParser = require('drachtio-mw-registration-parser'); const regParser = require('drachtio-mw-registration-parser');
const Registrar = require('@jambonz/mw-registrar'); const Registrar = require('@jambonz/mw-registrar');
@@ -65,7 +65,8 @@ const {
lookupCarrierBySid, lookupCarrierBySid,
lookupSystemInformation, lookupSystemInformation,
updateCarrierBySid, updateCarrierBySid,
lookupAccountBySid lookupAccountBySid,
lookupAuthCarriersForAccountAndSP
} = require('@jambonz/db-helpers')({ } = require('@jambonz/db-helpers')({
host: JAMBONES_MYSQL_HOST, host: JAMBONES_MYSQL_HOST,
user: JAMBONES_MYSQL_USER, user: JAMBONES_MYSQL_USER,
@@ -125,7 +126,8 @@ srf.locals = {
updateSipGatewayBySid, updateSipGatewayBySid,
lookupCarrierBySid, lookupCarrierBySid,
lookupSystemInformation, lookupSystemInformation,
updateCarrierBySid updateCarrierBySid,
lookupAuthCarriersForAccountAndSP
}, },
realtimeDbHelpers: { realtimeDbHelpers: {
client, client,
@@ -267,7 +269,8 @@ srf.use('register', [
regParser, regParser,
checkCache, checkCache,
checkAccountLimits, checkAccountLimits,
digestChallenge]); digestChallenge,
enforceDeviceLimits]);
srf.use('options', [ srf.use('options', [
initLocals initLocals
+50 -11
View File
@@ -65,9 +65,8 @@ const checkCache = async(req, res, next) => {
const checkAccountLimits = async(req, res, next) => { const checkAccountLimits = async(req, res, next) => {
const {logger} = req.locals; const {logger} = req.locals;
const {lookupAccountBySipRealm, lookupAccountCapacitiesBySid} = req.srf.locals.dbHelpers; const {lookupAccountBySipRealm, lookupAuthCarriersForAccountAndSP} = req.srf.locals.dbHelpers;
const {realm} = req.locals; const {realm} = req.locals;
const {registrar, writeAlerts, AlertType} = req.srf.locals;
try { try {
const account = await lookupAccountBySipRealm(realm); const account = await lookupAccountBySipRealm(realm);
if (account && !account.is_active) { if (account && !account.is_active) {
@@ -77,10 +76,19 @@ const checkAccountLimits = async(req, res, next) => {
}}); }});
} }
if (account) { if (account) {
/* if the account has auth trunk(s) configured, we validate REGISTER credentials against
those trunk credentials rather than storing a device registration (see digestChallenge) */
const auth_trunks = await lookupAuthCarriersForAccountAndSP(
account.account_sid,
account.service_provider_sid
);
req.locals = { req.locals = {
...req.locals, ...req.locals,
account,
account_sid: account.account_sid, account_sid: account.account_sid,
service_provider_sid: account.service_provider_sid,
webhook_secret: account.webhook_secret, webhook_secret: account.webhook_secret,
...(auth_trunks?.length && {auth_trunks}),
...(account.registration_hook && { ...(account.registration_hook && {
registration_hook_url: account.registration_hook.url, registration_hook_url: account.registration_hook.url,
registration_hook_method: account.registration_hook.method, registration_hook_method: account.registration_hook.method,
@@ -96,9 +104,39 @@ const checkAccountLimits = async(req, res, next) => {
return res.send(403); return res.send(403);
} }
if ('unregister' === req.registration.type || !JAMBONES_HOSTING) return next(); next();
} catch (err) {
logger.error({err, realm}, 'checkAccountLimits: error looking up account');
// if we can not reach the db for some reason, allow the registration to proceed
if (err.message?.includes('connect ECONNREFUSED')) {
return next();
}
res.send(500);
}
};
/* only check limits on the jambonz hosted platform */ /**
* Enforce per-account device registration limits.
*
* Runs after digestChallenge so we know how the request authenticated: REGISTERs that
* validated against an auth trunk are not devices (and are never stored), so they bypass
* the limit entirely. Only client/device registrations are counted and limited.
*/
const enforceDeviceLimits = async(req, res, next) => {
const {logger, realm, account} = req.locals;
const {lookupAccountCapacitiesBySid} = req.srf.locals.dbHelpers;
const {registrar, writeAlerts, AlertType} = req.srf.locals;
/* only check limits on the jambonz hosted platform, and never on unregister */
if ('unregister' === req.registration.type || !JAMBONES_HOSTING) return next();
/* auth trunks are not devices and are never stored, so they don't count against device limits */
if (req.authorization?.grant?.auth_trunk) {
logger.debug('enforceDeviceLimits: authenticated as auth trunk, skipping device limit check');
return next();
}
try {
const {account_sid} = account; const {account_sid} = account;
const capacities = await lookupAccountCapacitiesBySid(account_sid); const capacities = await lookupAccountCapacitiesBySid(account_sid);
const limit_calls = capacities.find((c) => c.category == 'voice_call_session'); const limit_calls = capacities.find((c) => c.category == 'voice_call_session');
@@ -108,30 +146,30 @@ const checkAccountLimits = async(req, res, next) => {
debug(`call capacity: ${limit_calls.quantity}, device capacity: ${limit_registrations}`); debug(`call capacity: ${limit_calls.quantity}, device capacity: ${limit_registrations}`);
if (0 === limit_registrations) { if (0 === limit_registrations) {
logger.info({account_sid}, 'checkAccountLimits: device calling not allowed for this account'); logger.info({account_sid}, 'enforceDeviceLimits: device calling not allowed for this account');
writeAlerts({ writeAlerts({
alert_type: AlertType.ACCOUNT_DEVICE_LIMIT, alert_type: AlertType.ACCOUNT_DEVICE_LIMIT,
account_sid, account_sid,
count: 0 count: 0
}).catch((err) => logger.info({err}, 'checkAccountLimits: error writing alert')); }).catch((err) => logger.info({err}, 'enforceDeviceLimits: error writing alert'));
return res.send(503, 'Max Devices Registered'); return res.send(503, 'Max Devices Registered');
} }
const deviceCount = await registrar.getCountOfUsers(realm); const deviceCount = await registrar.getCountOfUsers(realm);
if (deviceCount > limit_registrations + 1) { if (deviceCount > limit_registrations + 1) {
logger.info({account_sid}, 'checkAccountLimits: registration rejected due to limits'); logger.info({account_sid}, 'enforceDeviceLimits: registration rejected due to limits');
writeAlerts({ writeAlerts({
alert_type: AlertType.ACCOUNT_DEVICE_LIMIT, alert_type: AlertType.ACCOUNT_DEVICE_LIMIT,
account_sid, account_sid,
count: limit_registrations count: limit_registrations
}).catch((err) => logger.info({err}, 'checkAccountLimits: error writing alert')); }).catch((err) => logger.info({err}, 'enforceDeviceLimits: error writing alert'));
return res.send(503, 'Max Devices Registered'); return res.send(503, 'Max Devices Registered');
} }
logger.debug(`checkAccountLimits - passed: devices registered ${deviceCount}, limit is ${limit_registrations}`); logger.debug(`enforceDeviceLimits - passed: devices registered ${deviceCount}, limit is ${limit_registrations}`);
next(); next();
} catch (err) { } catch (err) {
logger.error({err, realm}, 'checkAccountLimits: error checking account limits'); logger.error({err, realm}, 'enforceDeviceLimits: error checking account limits');
// if we can not reach the db for some reason, allow the registration to proceed // if we can not reach the db for some reason, allow the registration to proceed
if (err.message?.includes('connect ECONNREFUSED')) { if (err.message?.includes('connect ECONNREFUSED')) {
return next(); return next();
@@ -144,5 +182,6 @@ module.exports = {
initLocals, initLocals,
rejectIpv4, rejectIpv4,
checkCache, checkCache,
checkAccountLimits checkAccountLimits,
enforceDeviceLimits
}; };
+20 -1
View File
@@ -7,13 +7,32 @@ function handler({logger}) {
return async(req, res) => { return async(req, res) => {
logger.debug(`received ${req.method} from ${req.protocol}/${req.source_address}:${req.source_port}`); logger.debug(`received ${req.method} from ${req.protocol}/${req.source_address}:${req.source_port}`);
if ('register' === req.registration.type && '0' !== req.registration.expires) await register(logger, req, res); /* if credentials were validated against an auth trunk, acknowledge without storing a registration */
if (req.authorization?.grant?.auth_trunk) {
await acknowledgeAuthTrunk(logger, req, res);
}
else if ('register' === req.registration.type && '0' !== req.registration.expires) {
await register(logger, req, res);
}
else await unregister(logger, req, res); else await unregister(logger, req, res);
req.srf.endSession(req); req.srf.endSession(req);
}; };
} }
async function acknowledgeAuthTrunk(logger, req, res) {
const {auth_trunk} = req.authorization.grant;
const expires = req.registration.expires;
logger.debug({voip_carrier_sid: auth_trunk.voip_carrier_sid, name: auth_trunk.name},
'REGISTER validated against auth trunk credentials; responding OK without storing registration');
res.send(200, {
headers: {
'Contact': req.get('Contact'),
'Expires': expires
}
});
}
async function register(logger, req, res) { async function register(logger, req, res) {
try { try {
const registrar = req.srf.locals.registrar; const registrar = req.srf.locals.registrar;
+1 -1
View File
@@ -10,7 +10,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helpers": "^7.26.10", "@babel/helpers": "^7.26.10",
"@jambonz/db-helpers": "^0.9.20", "@jambonz/db-helpers": "^0.9.21",
"@jambonz/digest-utils": "^0.0.9", "@jambonz/digest-utils": "^0.0.9",
"@jambonz/mw-registrar": "^0.2.7", "@jambonz/mw-registrar": "^0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.21", "@jambonz/realtimedb-helpers": "^0.8.21",
+1 -1
View File
@@ -29,7 +29,7 @@
"homepage": "https://github.com/jambonz/sbc-sip-sidecar#readme", "homepage": "https://github.com/jambonz/sbc-sip-sidecar#readme",
"dependencies": { "dependencies": {
"@babel/helpers": "^7.26.10", "@babel/helpers": "^7.26.10",
"@jambonz/db-helpers": "^0.9.20", "@jambonz/db-helpers": "^0.9.21",
"@jambonz/digest-utils": "^0.0.9", "@jambonz/digest-utils": "^0.0.9",
"@jambonz/mw-registrar": "^0.2.7", "@jambonz/mw-registrar": "^0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.21", "@jambonz/realtimedb-helpers": "^0.8.21",