mirror of
https://github.com/jambonz/sbc-sip-sidecar.git
synced 2026-08-19 09:40:25 +00:00
Feat/auth trunk register (#141)
* allow REGISTER for auth trunks * use db-helpers * bump db-helpers
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
Generated
+1
-1
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user