diff --git a/app.js b/app.js index 6694a80..454395b 100644 --- a/app.js +++ b/app.js @@ -44,7 +44,7 @@ const Srf = require('drachtio-srf'); const srf = new Srf(); const StatsCollector = require('@jambonz/stats-collector'); 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 regParser = require('drachtio-mw-registration-parser'); const Registrar = require('@jambonz/mw-registrar'); @@ -65,7 +65,8 @@ const { lookupCarrierBySid, lookupSystemInformation, updateCarrierBySid, - lookupAccountBySid + lookupAccountBySid, + lookupAuthCarriersForAccountAndSP } = require('@jambonz/db-helpers')({ host: JAMBONES_MYSQL_HOST, user: JAMBONES_MYSQL_USER, @@ -125,7 +126,8 @@ srf.locals = { updateSipGatewayBySid, lookupCarrierBySid, lookupSystemInformation, - updateCarrierBySid + updateCarrierBySid, + lookupAuthCarriersForAccountAndSP }, realtimeDbHelpers: { client, @@ -267,7 +269,8 @@ srf.use('register', [ regParser, checkCache, checkAccountLimits, - digestChallenge]); + digestChallenge, + enforceDeviceLimits]); srf.use('options', [ initLocals diff --git a/lib/middleware.js b/lib/middleware.js index a247581..63cc922 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -65,9 +65,8 @@ const checkCache = async(req, res, next) => { const checkAccountLimits = async(req, res, next) => { const {logger} = req.locals; - const {lookupAccountBySipRealm, lookupAccountCapacitiesBySid} = req.srf.locals.dbHelpers; + const {lookupAccountBySipRealm, lookupAuthCarriersForAccountAndSP} = req.srf.locals.dbHelpers; const {realm} = req.locals; - const {registrar, writeAlerts, AlertType} = req.srf.locals; try { const account = await lookupAccountBySipRealm(realm); if (account && !account.is_active) { @@ -77,10 +76,19 @@ const checkAccountLimits = async(req, res, next) => { }}); } 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, + account, account_sid: account.account_sid, + service_provider_sid: account.service_provider_sid, webhook_secret: account.webhook_secret, + ...(auth_trunks?.length && {auth_trunks}), ...(account.registration_hook && { registration_hook_url: account.registration_hook.url, registration_hook_method: account.registration_hook.method, @@ -96,9 +104,39 @@ const checkAccountLimits = async(req, res, next) => { 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 capacities = await lookupAccountCapacitiesBySid(account_sid); 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}`); 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({ alert_type: AlertType.ACCOUNT_DEVICE_LIMIT, account_sid, 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'); } const deviceCount = await registrar.getCountOfUsers(realm); 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({ alert_type: AlertType.ACCOUNT_DEVICE_LIMIT, account_sid, 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'); } - logger.debug(`checkAccountLimits - passed: devices registered ${deviceCount}, limit is ${limit_registrations}`); + logger.debug(`enforceDeviceLimits - passed: devices registered ${deviceCount}, limit is ${limit_registrations}`); next(); } 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 (err.message?.includes('connect ECONNREFUSED')) { return next(); @@ -144,5 +182,6 @@ module.exports = { initLocals, rejectIpv4, checkCache, - checkAccountLimits + checkAccountLimits, + enforceDeviceLimits }; diff --git a/lib/register.js b/lib/register.js index 48b5b72..6f0106b 100644 --- a/lib/register.js +++ b/lib/register.js @@ -7,13 +7,32 @@ function handler({logger}) { return async(req, res) => { 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); 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) { try { const registrar = req.srf.locals.registrar; diff --git a/package-lock.json b/package-lock.json index 6650b17..9c15a1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@babel/helpers": "^7.26.10", - "@jambonz/db-helpers": "^0.9.20", + "@jambonz/db-helpers": "^0.9.21", "@jambonz/digest-utils": "^0.0.9", "@jambonz/mw-registrar": "^0.2.7", "@jambonz/realtimedb-helpers": "^0.8.21", diff --git a/package.json b/package.json index 8ea9952..6bfc9db 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "homepage": "https://github.com/jambonz/sbc-sip-sidecar#readme", "dependencies": { "@babel/helpers": "^7.26.10", - "@jambonz/db-helpers": "^0.9.20", + "@jambonz/db-helpers": "^0.9.21", "@jambonz/digest-utils": "^0.0.9", "@jambonz/mw-registrar": "^0.2.7", "@jambonz/realtimedb-helpers": "^0.8.21",