diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..07f47b4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+FROM --platform=linux/amd64 node:18.6.0-alpine as base
+
+RUN apk --update --no-cache add --virtual .builds-deps build-base python3
+
+WORKDIR /opt/app/
+
+FROM base as build
+
+COPY package.json package-lock.json ./
+
+RUN npm ci
+
+COPY . .
+
+FROM base
+
+COPY --from=build /opt/app /opt/app/
+
+ARG NODE_ENV
+
+ENV NODE_ENV $NODE_ENV
+
+CMD [ "node", "app.js" ]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c67111d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,104 @@
+# sbc-sip-sidecar 
+
+This application provides a part of the SBC (Session Border Controller) functionality of jambonz. It handles incoming/outgoing REGISTER requests from/to clients/servers (including both sip softphones and WebRTC client applications), incoming OPTIONS. Register Authentication is delegated to customer-side logic via a web callback configured for the account in the jambonz database. Information about active registrations is stored in a redis database.
+
+## registrar database
+
+A redis database is used to hold active registrations. When a register request arrives and is authenticated, the following values are parsed from the request:
+- the address of record, or "aor" (e.g, daveh@drachtio.org),
+- the sip uri, or "contact" that this user is advertising (e.g. sip:daveh@3.44.3.12:5060)
+- the source address and port that sent the REGISTER request to the server
+- the transport protocol that should be used to contact the user (e.g. udp, tcp, wss etc)
+- the sip address of the drachtio server that received the REGISTER request, and
+- the expiration of the registration, in seconds.
+- the application callback that should be invoked when a call is placed from this registered device
+- the application status callback that should invoked for call events on calls placed from this registered device
+
+A hash value is created from these values and stored with an expiry value equal to the number of seconds granted to the registration (note that when a sip client is detected as being behind a firewall, the application will reduce the granted expires value to 30 seconds, in order to force the client to re-register frequently, however the expiry in redis is set to the longer, originally requested expires value).
+
+The hash value is inserted with a key being the aor:
+```
+aor => {contact, source, protocol, sbcAddress, call_hook, call_status_hook}, expiry = registration expires value
+```
+
+## configuration
+
+Configuration is provided via the [npmjs config](https://www.npmjs.com/package/config) package. The following elements make up the configuration for the application:
+##### drachtio server location
+```
+{
+ "drachtio": {
+ "port": 3001,
+ "secret": "cymru"
+ },
+```
+the `drachtio` object specifies the port to listen on for tcp connections from drachtio servers as well as the shared secret that is used to authenticate to the server.
+
+> Note: [outbound connections](https://drachtio.org/docs#outbound-connections) are used for all drachtio applications in jambonz, to allow for easier centralization and clustering of application logic.
+
+##### redis server location
+```
+ "redis": {
+ "port": 6379,
+ "host": "127.0.0.1"
+ },
+```
+the `redis` object specifies the location of the redis database. Any of the options [defined here](https://www.npmjs.com/package/redis#rediscreateclient) may be supplied, but host and port are minimally required.
+
+Note that in a fully-scaled out environment with multiple SBCs there will be one centralized redis database (or cluster) that stores registrations for all SBCs.
+
+##### application log level
+```
+ "logging": {
+ "level": "info"
+ }
+```
+
+## http callback
+Authenticating users is the responsibility of the client by exposing an http callback. A POST request will be sent to the configured callback (i.e. the value in the `accounts.registration_hook` column in the associated sip realm value in the REGISTER request). The body of the POST will be a json payload including the following information:
+```
+{
+ "method": "REGISTER",
+ "expires": 3600,
+ "scheme": "digest",
+ "username": "john",
+ "realm": "jambonz.org",
+ "nonce": "157590482938000",
+ "uri": "sip:172.37.0.10:5060",
+ "response": "be641cf7951ff23ab04c57907d59f37d",
+ "qop": "auth",
+ "nc": "00000001",
+ "cnonce": "6b8b4567",
+ "algorithm": "MD5"
+}
+```
+It is the responsibility of the customer-side logic to retrieve the associated password for the given username and to then authenticate the request by calculating a response hash value (per the algorithm described in [RFC 2617](https://tools.ietf.org/html/rfc2617#section-3.2.2)) and comparing it to the response property in the http body.
+
+For example code showing how to calculate the response hash given the above inputs, [see here](https://github.com/jambonz/customer-auth-server/blob/master/lib/utils.js).
+
+For a simple, full-fledged example server doing the same, [see here](https://github.com/jambonz/customer-auth-server).
+
+The customer server SHOULD return a 200 OK response to the http request in all cases with a json body indicating whether the request was successfully authenticated.
+
+The body MUST include a `status` field with a value of either `ok` or `fail`, indicating whether the request was authenticated or not.
+```
+{"status": "ok"}
+```
+
+Additionally, in the case of failure, the body MAY include a `msg` field with a human-readable description of why the authentication failed.
+```
+{"status": "fail", "msg": "invalid username"}
+```
+
+In the case of success, the body MAY include an `expires` value which specifies the duration of time, in seconds, to grant for this registration. If not provided, the expires value in the REGISTER request is used; if provided, however, the value provided must be less than or equal to the duration requested.
+```
+{"status": "ok", "expires": 300}
+```
+
+Additionally in the case of success, the body SHOULD include `call_hook` and `call_status_hook` properties that reference the application URLs to use when calls are placed from this device. If these values are not provided, outbound calling from the device will not be allowed.
+
+## Running the test suite
+To run the included test suite, you will need to have a mysql server installed on your laptop/server. You will need to set the MYSQL_ROOT_PASSWORD env variable to the mysql root password before running the tests. The test suite creates a database named 'jambones_test' in your mysql server to run the tests against, and removes it when done.
+```
+MYSQL_ROOT_PASSWORD=foobar npm test
+```
diff --git a/app.js b/app.js
index e4f2116..73d2823 100644
--- a/app.js
+++ b/app.js
@@ -1,12 +1,13 @@
const assert = require('assert');
assert.ok(process.env.JAMBONES_MYSQL_HOST &&
- process.env.JAMBONES_MYSQL_USER &&
- process.env.JAMBONES_MYSQL_PASSWORD &&
- process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
+ process.env.JAMBONES_MYSQL_USER &&
+ process.env.JAMBONES_MYSQL_PASSWORD &&
+ process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars');
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.DRACHTIO_HOST, 'missing DRACHTIO_HOST env var');
assert.ok(process.env.DRACHTIO_PORT, 'missing DRACHTIO_PORT env var');
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
+
const opts = Object.assign({
timestamp: () => { return `, "time": "${new Date().toISOString()}"`; }
}, { level: process.env.JAMBONES_LOGLEVEL || 'info' });
@@ -14,10 +15,21 @@ const logger = require('pino')(opts);
const Srf = require('drachtio-srf');
const srf = new Srf();
const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-sip`;
-
+const StatsCollector = require('@jambonz/stats-collector');
+const stats = new StatsCollector(logger);
+const { initLocals, rejectIpv4, checkCache, checkAccountLimits } = require('./lib/middleware');
+const responseTime = require('drachtio-mw-response-time');
+const regParser = require('drachtio-mw-registration-parser');
+const Registrar = require('@jambonz/mw-registrar');
+const Emitter = require('events');
+const debug = require('debug')('jambonz:sbc-registrar');
const {
+ lookupAuthHook,
lookupAllVoipCarriers,
lookupSipGatewaysByCarrier,
+ lookupAccountBySipRealm,
+ lookupAccountCapacitiesBySid,
+ addSbcAddress
} = require('@jambonz/db-helpers')({
host: process.env.JAMBONES_MYSQL_HOST,
user: process.env.JAMBONES_MYSQL_USER,
@@ -25,22 +37,45 @@ const {
database: process.env.JAMBONES_MYSQL_DATABASE,
connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10
}, logger);
+const {
+ writeAlerts,
+ AlertType
+} = require('@jambonz/time-series')(logger, {
+ host: process.env.JAMBONES_TIME_SERIES_HOST,
+ commitSize: 50,
+ commitInterval: 'test' === process.env.NODE_ENV ? 7 : 20
+});
const {
- retrieveSet } = require('@jambonz/realtimedb-helpers')({
+ addToSet,
+ removeFromSet,
+ isMemberOfSet,
+ retrieveSet
+} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST || 'localhost',
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger);
srf.locals = {
...srf.locals,
+ stats,
+ addToSet, removeFromSet, isMemberOfSet, retrieveSet,
+ registrar: new Registrar(logger, {
+ host: process.env.JAMBONES_REDIS_HOST,
+ port: process.env.JAMBONES_REDIS_PORT || 6379
+ }),
dbHelpers: {
+ lookupAuthHook,
lookupAllVoipCarriers,
lookupSipGatewaysByCarrier,
+ lookupAccountBySipRealm,
+ lookupAccountCapacitiesBySid
},
realtimeDbHelpers: {
retrieveSet
- }
+ },
+ writeAlerts,
+ AlertType
};
srf.connect({ host: process.env.DRACHTIO_HOST, port: process.env.DRACHTIO_PORT, secret: process.env.DRACHTIO_SECRET });
@@ -48,6 +83,17 @@ srf.on('connect', (err, hp) => {
const ativateRegBot = async(err, hp) => {
if (err) return logger.error({ err }, 'Error connecting to drachtio server');
logger.info(`connected to drachtio listening on ${hp}`);
+
+ // Add SBC Public IP to Database
+ const hostports = hp.split(',');
+ for (const hp of hostports) {
+ const arr = /^(.*)\/(.*):(\d+)$/.exec(hp);
+ if (arr && 'udp' === arr[1]) {
+ logger.info(`adding sbc public address to database: ${arr[2]}`);
+ addSbcAddress(arr[2]);
+ }
+ }
+
// Only run when I'm the first member in the set Of Actip Sip SBC
const set = await retrieveSet(setName);
const newArray = Array.from(set);
@@ -80,4 +126,77 @@ if (process.env.NODE_ENV === 'test') {
});
}
+const rttMetric = (req, res, time) => {
+ if (res.cached) {
+ stats.histogram('sbc.registration.cached.response_time', time.toFixed(0), [`status:${res.statusCode}`]);
+ }
+ else {
+ stats.histogram('sbc.registration.total.response_time', time.toFixed(0), [`status:${res.statusCode}`]);
+ }
+};
+
+class RegOutcomeReporter extends Emitter {
+ constructor() {
+ super();
+ this
+ .on('regHookOutcome', ({ rtt, status }) => {
+ stats.histogram('app.hook.response_time', rtt, ['hook_type:auth', `status:${status}`]);
+ if (![200, 403].includes(status)) {
+ stats.increment('app.hook.error.count', ['hook_type:auth', `status:${status}`]);
+ }
+ })
+ .on('error', async(err, req) => {
+ logger.error({ err }, 'http webhook failed');
+ const { account_sid } = req.locals;
+ if (account_sid) {
+ let opts = { account_sid };
+ if (err.code === 'ECONNREFUSED') {
+ opts = { ...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook };
+ }
+ else if (err.code === 'ENOTFOUND') {
+ opts = { ...opts, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: err.hook };
+ }
+ else if (err.name === 'StatusError') {
+ opts = { ...opts, alert_type: AlertType.WEBHOOK_STATUS_FAILURE, url: err.hook, status: err.statusCode };
+ }
+
+ if (opts.alert_type) {
+ try {
+ await writeAlerts(opts);
+ } catch (err) {
+ logger.error({ err, opts }, 'Error writing alert');
+ }
+ }
+ }
+ });
+ }
+}
+
+const authenticator = require('@jambonz/http-authenticator')(lookupAuthHook, logger, {
+ emitter: new RegOutcomeReporter()
+});
+
+// middleware
+srf.use('register', [
+ initLocals,
+ responseTime(rttMetric),
+ rejectIpv4(logger),
+ regParser,
+ checkCache(logger),
+ checkAccountLimits(logger),
+ authenticator]);
+
+srf.use('options', [
+ initLocals
+]);
+
+srf.register(require('./lib/register')({logger}));
+srf.options(require('./lib/options')({srf, logger}));
+
+setInterval(async() => {
+ const count = await srf.locals.registrar.getCountOfUsers();
+ debug(`count of registered users: ${count}`);
+ stats.gauge('sbc.users.count', parseInt(count));
+}, 30000);
+
module.exports = { srf, logger };
diff --git a/lib/middleware.js b/lib/middleware.js
new file mode 100644
index 0000000..3670b43
--- /dev/null
+++ b/lib/middleware.js
@@ -0,0 +1,122 @@
+const parseUri = require('drachtio-srf').parseUri;
+const debug = require('debug')('jambonz:sbc-registrar');
+const {NAT_EXPIRES} = require('./utils');
+
+const initLocals = (req, res, next) => {
+ req.locals = req.locals || {};
+ next();
+};
+
+const rejectIpv4 = (logger) => {
+ return (req, res, next) => {
+ const uri = parseUri(req.uri);
+ if (/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(uri.host)) {
+ debug(`rejecting REGISTER from ${req.uri} as it has an ipv4 address and sip realm is required`);
+ res.send(403);
+ return req.srf.endSession(req);
+ }
+ next();
+ };
+};
+
+const checkCache = (logger) => {
+ return async(req, res, next) => {
+ const registration = req.registration;
+ const uri = parseUri(registration.aor);
+ const aor = `${uri.user}@${uri.host}`;
+ req.locals.realm = uri.host;
+
+ if (registration.type === 'unregister') return next();
+
+ const registrar = req.srf.locals.registrar;
+ const result = await registrar.query(aor);
+ if (result) {
+ // if known valid registration coming from same address, no need to hit the reg callback hook
+ if (result.proxy === `sip:${req.source_address}:${req.source_port}`) {
+ debug(`responding to cached register for ${aor}`);
+ res.cached = true;
+ res.send(200, {
+ headers: {
+ 'Contact': req.get('Contact').replace(/expires=\d+/, `expires=${NAT_EXPIRES}`),
+ 'Expires': NAT_EXPIRES
+ }
+ });
+ return req.srf.endSession(req);
+ }
+ }
+ next();
+ };
+};
+
+const checkAccountLimits = (logger) => {
+ return async(req, res, next) => {
+
+ const {lookupAccountBySipRealm, lookupAccountCapacitiesBySid} = req.srf.locals.dbHelpers;
+ const {realm} = req.locals;
+ const {registrar, writeAlerts, AlertType} = req.srf.locals;
+ try {
+ const account = await lookupAccountBySipRealm(realm);
+ if (account) {
+ req.locals = {
+ ...req.locals,
+ account_sid: account.account_sid,
+ webhook_secret: account.webhook_secret
+ };
+ debug(account, `checkAccountLimits: retrieved account for realm: ${realm}`);
+ }
+ else if (process.env.JAMBONES_HOSTING) {
+ debug(`checkAccountLimits: unknown sip realm ${realm}`);
+ logger.info(`checkAccountLimits: rejecting register for unknown sip realm: ${realm}`);
+ return res.send(403);
+ }
+
+ if ('unregister' === req.registration.type || !process.env.JAMBONES_HOSTING) return next();
+
+ /* only check limits on the jambonz hosted platform */
+ const {account_sid} = account;
+ const capacities = await lookupAccountCapacitiesBySid(account_sid);
+ debug(JSON.stringify(capacities));
+ const limit_calls = capacities.find((c) => c.category == 'voice_call_session');
+ let limit_registrations = limit_calls.quantity * account.device_to_call_ratio;
+ const extra = capacities.find((c) => c.category == 'device');
+ if (extra && extra.quantity) limit_registrations += extra.quantity;
+ debug(`call capacity: ${limit_calls.quantity}, device capacity: ${limit_registrations}`);
+
+ if (0 === limit_registrations) {
+ debug('checkAccountLimits: device calling not allowed for this account');
+ logger.info({account_sid}, 'checkAccountLimits: device calling not allowed for this account');
+ writeAlerts({
+ alert_type: AlertType.DEVICE_LIMIT,
+ account_sid,
+ count: 0
+ }).catch((err) => logger.info({err}, 'checkAccountLimits: error writing alert'));
+
+ return res.send(503, 'Max Devices Registered');
+ }
+
+ const deviceCount = await registrar.getCountOfUsers(realm);
+ if (deviceCount >= limit_registrations) {
+ debug(account_sid, `checkAccountLimits: limit ${limit_registrations} count ${deviceCount}`);
+ logger.info({account_sid}, 'checkAccountLimits: registration rejected due to limits');
+ writeAlerts({
+ alert_type: AlertType.DEVICE_LIMIT,
+ account_sid,
+ count: limit_registrations
+ }).catch((err) => logger.info({err}, 'checkAccountLimits: error writing alert'));
+ return res.send(503, 'Max Devices Registered');
+ }
+ debug(`checkAccountLimits - passed: devices registered ${deviceCount}, limit is ${limit_registrations}`);
+ next();
+ } catch (err) {
+ logger.error({err, realm}, 'checkAccountLimits: error checking account limits');
+ res.send(500);
+ }
+ };
+};
+
+module.exports = {
+ initLocals,
+ rejectIpv4,
+ checkCache,
+ checkAccountLimits
+};
diff --git a/lib/options.js b/lib/options.js
new file mode 100644
index 0000000..66566f3
--- /dev/null
+++ b/lib/options.js
@@ -0,0 +1,118 @@
+const debug = require('debug')('jambonz:sbc-options-handler');
+const fsServers = new Map();
+const rtpServers = new Map();
+
+module.exports = ({srf, logger}) => {
+ const {stats, addToSet, removeFromSet, isMemberOfSet, retrieveSet} = srf.locals;
+
+ const setNameFs = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
+ const setNameRtp = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-rtp`;
+
+ /* check for expired servers every so often */
+ setInterval(async() => {
+ const now = Date.now();
+ const expires = process.env.EXPIRES_INTERVAL || 60000;
+ for (const [key, value] of fsServers) {
+ const duration = now - value;
+ if (duration > expires) {
+ fsServers.delete(key);
+ await removeFromSet(setNameFs, key);
+ const members = await retrieveSet(setNameFs);
+ const countOfMembers = members.length;
+ logger.info({members}, `expired member ${key} from ${setNameFs} we now have ${countOfMembers}`);
+ }
+ }
+ for (const [key, value] of rtpServers) {
+ const duration = now - value;
+ if (duration > expires) {
+ rtpServers.delete(key);
+ await removeFromSet(setNameRtp, key);
+ const members = await retrieveSet(setNameRtp);
+ const countOfMembers = members.length;
+ logger.info({members}, `expired member ${key} from ${setNameRtp} we now have ${countOfMembers}`);
+ }
+ }
+ }, process.env.CHECK_EXPIRES_INTERVAL || 20000);
+
+ /* retrieve the initial list of servers, if any, so we can watch them as well */
+ const _init = async() => {
+ try {
+ const now = Date.now();
+ const runningFs = await retrieveSet(setNameFs);
+ const runningRtp = await retrieveSet(setNameRtp);
+
+ if (runningFs.length) {
+ logger.info({runningFs}, 'start watching these FS servers');
+ for (const ip of runningFs) fsServers.set(ip, now);
+ }
+
+ if (runningRtp.length) {
+ logger.info({runningRtp}, 'start watching these RTP servers');
+ for (const ip of runningRtp) rtpServers.set(ip, now);
+ }
+ } catch (err) {
+ logger.error({err}, 'error initializing from redis');
+ }
+ };
+ _init();
+
+ return async(req, res) => {
+
+ /* OPTIONS ping from internal FS or RTP server? */
+ const internal = req.has('X-FS-Status') || req.has('X-RTP-Status');
+ if (!internal) {
+ debug('got external OPTIONS ping');
+ res.send(200);
+ return req.srf.endSession(req);
+ }
+
+ try {
+ let map, status, countOfMembers;
+ const h = ['X-FS-Status', 'X-RTP-Status'].find((h) => req.has(h));
+ if (h) {
+ const isRtpServer = req.has('X-RTP-Status');
+ const key = isRtpServer ? req.source_address : `${req.source_address}:${req.source_port}`;
+ const prefix = isRtpServer ? 'X-RTP' : 'X-FS';
+ map = isRtpServer ? rtpServers : fsServers;
+ const setName = isRtpServer ? setNameRtp : setNameFs;
+ const gaugeName = isRtpServer ? 'rtpservers' : 'featureservers';
+
+ status = req.get(`${prefix}-Status`);
+
+ if (status === 'open') {
+ map.set(key, Date.now());
+ const exists = await isMemberOfSet(setName, key);
+ if (!exists) {
+ await addToSet(setName, key);
+ const members = await retrieveSet(setName);
+ countOfMembers = members.length;
+ logger.info({members}, `added new member ${key} to ${setName} we now have ${countOfMembers}`);
+ debug({members}, `added new member ${key} to ${setName}`);
+ }
+ else {
+ const members = await retrieveSet(setName);
+ countOfMembers = members.length;
+ debug(`checkin from existing member ${key} to ${setName}`);
+ }
+ }
+ else {
+ map.delete(key);
+ await removeFromSet(setName, key);
+ const members = await retrieveSet(setName);
+ countOfMembers = members.length;
+ logger.info({members}, `removed member ${key} from ${setName} we now have ${countOfMembers}`);
+ debug({members}, `removed member ${key} from ${setName}`);
+ }
+ stats.gauge(gaugeName, map.size);
+ }
+ res.send(200, {headers: {
+ 'X-Members': countOfMembers
+ }});
+ } catch (err) {
+ res.send(503);
+ debug(err);
+ logger.error({err}, 'Error handling OPTIONS');
+ }
+ return req.srf.endSession(req);
+ };
+};
diff --git a/lib/register.js b/lib/register.js
new file mode 100644
index 0000000..35912a9
--- /dev/null
+++ b/lib/register.js
@@ -0,0 +1,72 @@
+const {isUacBehindNat, getSipProtocol, NAT_EXPIRES} = require('./utils');
+const parseUri = require('drachtio-srf').parseUri;
+const debug = require('debug')('jambonz:sbc-registrar');
+
+module.exports = handler;
+
+function handler({logger}) {
+ return async(req, res) => {
+ 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);
+ else await unregister(logger, req, res);
+
+ req.srf.endSession(req);
+ };
+}
+
+async function register(logger, req, res) {
+ try {
+ const registrar = req.srf.locals.registrar;
+ const registration = req.registration;
+ const uri = parseUri(registration.aor);
+ const aor = `${uri.user}@${uri.host}`;
+ let expires = req.authorization.grant.expires || registration.expires;
+ const grantedExpires = expires;
+ let contactHdr = req.get('Contact');
+
+ // reduce the registration interval if the device is behind a nat
+ if (isUacBehindNat(req)) {
+ expires = NAT_EXPIRES;
+ contactHdr = contactHdr.replace(/expires=\d+/, `expires=${expires}`);
+ }
+ const opts = {
+ contact: req.getParsedHeader('Contact')[0].uri,
+ sbcAddress: req.server.hostport,
+ protocol: getSipProtocol(req),
+ proxy: `sip:${req.source_address}:${req.source_port}`,
+ callHook: req.authorization.grant.call_hook,
+ callStatusHook: req.authorization.grant.call_status_hook
+ };
+ logger.debug(`adding aor to redis ${aor}`);
+ const result = await registrar.add(aor, opts, grantedExpires);
+ debug(`result ${result} from adding ${JSON.stringify(opts)}`);
+ logger.debug(`successfully added ${aor} to redis, sending 200 OK`);
+
+ res.send(200, {
+ headers: {
+ 'Contact': contactHdr,
+ 'Expires': expires
+ }
+ });
+ } catch (err) {
+ logger.error({err}, 'Error trying to process REGISTER');
+ if (!res.finalResponseSent) res.send(500);
+ }
+}
+
+async function unregister(logger, req, res) {
+ const registrar = req.srf.locals.registrar;
+ const uri = parseUri(req.registration.aor);
+ const aor = `${uri.user}@${uri.host}`;
+ const result = await registrar.remove(aor);
+
+ logger.debug({result}, `successfully unregistered ${req.registration.aor}`);
+
+ res.send(200, {
+ headers: {
+ 'Contact': req.get('Contact'),
+ 'Expires': 0
+ }
+ });
+}
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..b120283
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,21 @@
+function isUacBehindNat(req) {
+
+ // no need for nat handling if wss or tcp being used
+ if (req.protocol !== 'udp') return false;
+
+ // let's keep it simple -- if udp, let's crank down the register interval
+ return true;
+}
+
+function getSipProtocol(req) {
+ if (req.getParsedHeader('Via')[0].protocol.toLowerCase().startsWith('wss')) return 'wss';
+ if (req.getParsedHeader('Via')[0].protocol.toLowerCase().startsWith('ws')) return 'ws';
+ if (req.getParsedHeader('Via')[0].protocol.toLowerCase().startsWith('tcp')) return 'tcp';
+ if (req.getParsedHeader('Via')[0].protocol.toLowerCase().startsWith('udp')) return 'udp';
+}
+
+module.exports = {
+ isUacBehindNat,
+ getSipProtocol,
+ NAT_EXPIRES: 30
+};
diff --git a/package-lock.json b/package-lock.json
index 9421d04..f1eea45 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,8 +10,14 @@
"license": "ISC",
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
+ "@jambonz/http-authenticator": "^0.2.1",
+ "@jambonz/mw-registrar": "^0.2.2",
"@jambonz/realtimedb-helpers": "^0.4.29",
+ "@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.12",
+ "debug": "^4.3.4",
+ "drachtio-mw-registration-parser": "^0.1.0",
+ "drachtio-mw-response-time": "^1.0.2",
"drachtio-srf": "^4.5.17",
"pino": "^6.14.0"
},
@@ -266,6 +272,28 @@
"uuid": "^8.3.2"
}
},
+ "node_modules/@jambonz/http-authenticator": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@jambonz/http-authenticator/-/http-authenticator-0.2.2.tgz",
+ "integrity": "sha512-yl6CajF8c8BOTrXEB/AbTXgqrT6XeymwVZbJWeJG8HZA21UXkKCcM26b8f0P9qqokSvFj0ObjCk22Ks2ytSLNg==",
+ "dependencies": {
+ "bent": "^7.3.12",
+ "debug": "^4.3.1",
+ "drachtio-srf": "^4.4.63",
+ "nonce": "^1.0.4",
+ "qs": "^6.9.4"
+ }
+ },
+ "node_modules/@jambonz/mw-registrar": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@jambonz/mw-registrar/-/mw-registrar-0.2.2.tgz",
+ "integrity": "sha512-CG0MUVRZZ+tBgB5kvjn5IS1R/OdNV5PpAsxCKE0ehe6XVJ1ap4ch9hSZmEh2q7qPnWu8sR451peF33dnoSeaPA==",
+ "dependencies": {
+ "@jambonz/promisify-redis": "^0.0.6",
+ "debug": "^4.3.1",
+ "redis": "^3.1.1"
+ }
+ },
"node_modules/@jambonz/promisify-redis": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@jambonz/promisify-redis/-/promisify-redis-0.0.6.tgz",
@@ -291,6 +319,15 @@
"redis": "^3.1.2"
}
},
+ "node_modules/@jambonz/stats-collector": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@jambonz/stats-collector/-/stats-collector-0.1.6.tgz",
+ "integrity": "sha512-Qk+kpeb2wravpj3OYPC4N3ML1qoAzARNLfKGZtJ05PTAtfWoZMPnyfPAM9EJHIiVfs2Lec3CXDjEpHna0mc9EA==",
+ "dependencies": {
+ "debug": "^4.3.2",
+ "hot-shots": "^8.5.0"
+ }
+ },
"node_modules/@jambonz/time-series": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.12.tgz",
@@ -640,6 +677,15 @@
"url": "https://bevry.me/fund"
}
},
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "optional": true,
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
@@ -887,6 +933,19 @@
"ignored": "bin/ignored"
}
},
+ "node_modules/drachtio-mw-registration-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/drachtio-mw-registration-parser/-/drachtio-mw-registration-parser-0.1.0.tgz",
+ "integrity": "sha512-SNXEbFjOSuvDCArff/fH4xl1424sQTjPVzNE1Lq5LcyV9sVmW9+/CBBwwkFf1QgEtVUvgTa4w/Jx26Amb3hxaA==",
+ "engines": {
+ "node": ">= 6.9.3"
+ }
+ },
+ "node_modules/drachtio-mw-response-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/drachtio-mw-response-time/-/drachtio-mw-response-time-1.0.2.tgz",
+ "integrity": "sha512-d+DtKuqhpkSBBkRfwcgS3x26T3lrdgo86yVWZ4wy+K8RtS1faT3hgLBq9EPEYSDkcw76r4klvdDWPhTrPqGAQw=="
+ },
"node_modules/drachtio-srf": {
"version": "4.5.17",
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.5.17.tgz",
@@ -1350,6 +1409,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "optional": true
+ },
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -1689,6 +1754,17 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz",
"integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg=="
},
+ "node_modules/hot-shots": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-8.5.2.tgz",
+ "integrity": "sha512-1CKCtbYU28KtRriRW+mdOZzKce0WPqU0FOYE4bYs3gD1bFpOrYzQDXfQ09Qz9dJPEltasDOGhFKiYaiuW/j9Dg==",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "optionalDependencies": {
+ "unix-dgram": "2.0.x"
+ }
+ },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -2338,6 +2414,12 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
},
+ "node_modules/nan": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz",
+ "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==",
+ "optional": true
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -2376,6 +2458,14 @@
"resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
"integrity": "sha512-kAUvIRxZyDYFTLqGj+7zqXduG89vtqGmNMt9qDMvYH3H8uNTCOTz5ZN1q2Yg8++fWbzv+ERtYVqaOH42Ag5OpA=="
},
+ "node_modules/nonce": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/nonce/-/nonce-1.0.4.tgz",
+ "integrity": "sha512-FVPu+tMZPP91HDwiq1DNhn9WIhg4/uo6mXR0xXAn0IMOxDmjJOkgbH0tm7qtowvAFZofWZRX+9KWZpNURrgtSA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
@@ -2590,6 +2680,20 @@
"node": ">=6"
}
},
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -3185,6 +3289,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/unix-dgram": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz",
+ "integrity": "sha512-7tpK6x7ls7J7pDrrAU63h93R0dVhRbPwiRRCawR10cl+2e1VOvF3bHlVJc6WI1dl/8qk5He673QU+Ogv7bPNaw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "bindings": "^1.3.0",
+ "nan": "^2.13.2"
+ },
+ "engines": {
+ "node": ">=0.10.48"
+ }
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -3634,6 +3752,28 @@
"uuid": "^8.3.2"
}
},
+ "@jambonz/http-authenticator": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@jambonz/http-authenticator/-/http-authenticator-0.2.2.tgz",
+ "integrity": "sha512-yl6CajF8c8BOTrXEB/AbTXgqrT6XeymwVZbJWeJG8HZA21UXkKCcM26b8f0P9qqokSvFj0ObjCk22Ks2ytSLNg==",
+ "requires": {
+ "bent": "^7.3.12",
+ "debug": "^4.3.1",
+ "drachtio-srf": "^4.4.63",
+ "nonce": "^1.0.4",
+ "qs": "^6.9.4"
+ }
+ },
+ "@jambonz/mw-registrar": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@jambonz/mw-registrar/-/mw-registrar-0.2.2.tgz",
+ "integrity": "sha512-CG0MUVRZZ+tBgB5kvjn5IS1R/OdNV5PpAsxCKE0ehe6XVJ1ap4ch9hSZmEh2q7qPnWu8sR451peF33dnoSeaPA==",
+ "requires": {
+ "@jambonz/promisify-redis": "^0.0.6",
+ "debug": "^4.3.1",
+ "redis": "^3.1.1"
+ }
+ },
"@jambonz/promisify-redis": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@jambonz/promisify-redis/-/promisify-redis-0.0.6.tgz",
@@ -3656,6 +3796,15 @@
"redis": "^3.1.2"
}
},
+ "@jambonz/stats-collector": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@jambonz/stats-collector/-/stats-collector-0.1.6.tgz",
+ "integrity": "sha512-Qk+kpeb2wravpj3OYPC4N3ML1qoAzARNLfKGZtJ05PTAtfWoZMPnyfPAM9EJHIiVfs2Lec3CXDjEpHna0mc9EA==",
+ "requires": {
+ "debug": "^4.3.2",
+ "hot-shots": "^8.5.0"
+ }
+ },
"@jambonz/time-series": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/@jambonz/time-series/-/time-series-0.1.12.tgz",
@@ -3924,6 +4073,15 @@
"resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz",
"integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg=="
},
+ "bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "optional": true,
+ "requires": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
@@ -4121,6 +4279,16 @@
"minimatch": "^3.0.4"
}
},
+ "drachtio-mw-registration-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/drachtio-mw-registration-parser/-/drachtio-mw-registration-parser-0.1.0.tgz",
+ "integrity": "sha512-SNXEbFjOSuvDCArff/fH4xl1424sQTjPVzNE1Lq5LcyV9sVmW9+/CBBwwkFf1QgEtVUvgTa4w/Jx26Amb3hxaA=="
+ },
+ "drachtio-mw-response-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/drachtio-mw-response-time/-/drachtio-mw-response-time-1.0.2.tgz",
+ "integrity": "sha512-d+DtKuqhpkSBBkRfwcgS3x26T3lrdgo86yVWZ4wy+K8RtS1faT3hgLBq9EPEYSDkcw76r4klvdDWPhTrPqGAQw=="
+ },
"drachtio-srf": {
"version": "4.5.17",
"resolved": "https://registry.npmjs.org/drachtio-srf/-/drachtio-srf-4.5.17.tgz",
@@ -4481,6 +4649,12 @@
"flat-cache": "^3.0.4"
}
},
+ "file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "optional": true
+ },
"flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -4735,6 +4909,14 @@
}
}
},
+ "hot-shots": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-8.5.2.tgz",
+ "integrity": "sha512-1CKCtbYU28KtRriRW+mdOZzKce0WPqU0FOYE4bYs3gD1bFpOrYzQDXfQ09Qz9dJPEltasDOGhFKiYaiuW/j9Dg==",
+ "requires": {
+ "unix-dgram": "2.0.x"
+ }
+ },
"https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -5232,6 +5414,12 @@
}
}
},
+ "nan": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz",
+ "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==",
+ "optional": true
+ },
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -5256,6 +5444,11 @@
"resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
"integrity": "sha512-kAUvIRxZyDYFTLqGj+7zqXduG89vtqGmNMt9qDMvYH3H8uNTCOTz5ZN1q2Yg8++fWbzv+ERtYVqaOH42Ag5OpA=="
},
+ "nonce": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/nonce/-/nonce-1.0.4.tgz",
+ "integrity": "sha512-FVPu+tMZPP91HDwiq1DNhn9WIhg4/uo6mXR0xXAn0IMOxDmjJOkgbH0tm7qtowvAFZofWZRX+9KWZpNURrgtSA=="
+ },
"object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
@@ -5420,6 +5613,14 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
+ "qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -5852,6 +6053,16 @@
"which-boxed-primitive": "^1.0.2"
}
},
+ "unix-dgram": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.4.tgz",
+ "integrity": "sha512-7tpK6x7ls7J7pDrrAU63h93R0dVhRbPwiRRCawR10cl+2e1VOvF3bHlVJc6WI1dl/8qk5He673QU+Ogv7bPNaw==",
+ "optional": true,
+ "requires": {
+ "bindings": "^1.3.0",
+ "nan": "^2.13.2"
+ }
+ },
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 62ea5b4..37c57b5 100644
--- a/package.json
+++ b/package.json
@@ -28,8 +28,14 @@
"homepage": "https://github.com/xquanluu/sbc-outbound-handler#readme",
"dependencies": {
"@jambonz/db-helpers": "^0.6.18",
- "@jambonz/realtimedb-helpers": "^0.4.29",
+ "@jambonz/http-authenticator": "^0.2.1",
+ "@jambonz/mw-registrar": "^0.2.2",
+ "@jambonz/stats-collector": "^0.1.6",
"@jambonz/time-series": "^0.1.12",
+ "debug": "^4.3.4",
+ "drachtio-mw-registration-parser": "^0.1.0",
+ "drachtio-mw-response-time": "^1.0.2",
+ "@jambonz/realtimedb-helpers": "^0.4.29",
"drachtio-srf": "^4.5.17",
"pino": "^6.14.0"
},
diff --git a/test/docker-compose-testbed.yaml b/test/docker-compose-testbed.yaml
index 62f9ae4..da19d7e 100644
--- a/test/docker-compose-testbed.yaml
+++ b/test/docker-compose-testbed.yaml
@@ -1,7 +1,7 @@
version: '3'
networks:
- sbc-registrar:
+ sbc-sip-sidecar:
driver: bridge
ipam:
config:
@@ -20,7 +20,7 @@ services:
timeout: 5s
retries: 10
networks:
- sbc-registrar:
+ sbc-sip-sidecar:
ipv4_address: 172.39.0.2
sbc:
image: drachtio/drachtio-server:latest
@@ -28,17 +28,28 @@ services:
ports:
- "9022:9022/tcp"
networks:
- sbc-registrar:
+ sbc-sip-sidecar:
ipv4_address: 172.39.0.10
depends_on:
mysql:
condition: service_healthy
+
+ auth-server:
+ image: jambonz/customer-auth-server:latest
+ command: npm start
+ ports:
+ - "4000:4000/tcp"
+ env_file: docker.env
+ networks:
+ sbc-sip-sidecar:
+ ipv4_address: 172.39.0.12
+
redis:
image: redis:5-alpine
ports:
- "16379:6379/tcp"
networks:
- sbc-registrar:
+ sbc-sip-sidecar:
ipv4_address: 172.39.0.13
sipp-uas-auth-register:
@@ -49,7 +60,7 @@ services:
- ./scenarios:/tmp
tty: true
networks:
- sbc-registrar:
+ sbc-sip-sidecar:
ipv4_address: 172.39.0.14
sipp-uas-auth-register-fail:
@@ -60,5 +71,14 @@ services:
- ./scenarios:/tmp
tty: true
networks:
- sbc-registrar:
+ sbc-sip-sidecar:
ipv4_address: 172.39.0.15
+ uas:
+ image: drachtio/sipp:latest
+ command: sipp -sf /tmp/uas.xml
+ volumes:
+ - ./scenarios:/tmp
+ tty: true
+ networks:
+ sbc-sip-sidecar:
+ ipv4_address: 172.39.0.60
diff --git a/test/index.js b/test/index.js
index f2ccd5b..14ab17b 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,4 +1,6 @@
require('./docker_start');
require('./create-test-db');
require('./regbot-tests');
+require('./sip-register-tests');
+require('./sip-options-tests');
require('./docker_stop');
diff --git a/test/scenarios/bad_password.csv b/test/scenarios/bad_password.csv
new file mode 100644
index 0000000..b13fc59
--- /dev/null
+++ b/test/scenarios/bad_password.csv
@@ -0,0 +1,3 @@
+SEQUENTIAL
+# user, domain, authentication
+jane;jambonz.org;[authentication username=jane password=8765]
\ No newline at end of file
diff --git a/test/scenarios/bad_realm.csv b/test/scenarios/bad_realm.csv
new file mode 100644
index 0000000..0b2d200
--- /dev/null
+++ b/test/scenarios/bad_realm.csv
@@ -0,0 +1,3 @@
+SEQUENTIAL
+# user, domain, authentication
+john;fail.com;[authentication username=john password=1234]
\ No newline at end of file
diff --git a/test/scenarios/good_user.csv b/test/scenarios/good_user.csv
new file mode 100644
index 0000000..dcaf989
--- /dev/null
+++ b/test/scenarios/good_user.csv
@@ -0,0 +1,3 @@
+SEQUENTIAL
+# user, domain, authentication
+john;jambonz.org;[authentication username=john password=1234]
\ No newline at end of file
diff --git a/test/scenarios/good_user2.csv b/test/scenarios/good_user2.csv
new file mode 100644
index 0000000..a5120d8
--- /dev/null
+++ b/test/scenarios/good_user2.csv
@@ -0,0 +1,3 @@
+SEQUENTIAL
+# user, domain, authentication
+jane;jambonz.org;[authentication username=jane password=5678]
\ No newline at end of file
diff --git a/test/scenarios/uac-add-fs-1.xml b/test/scenarios/uac-add-fs-1.xml
new file mode 100644
index 0000000..bd8a7b7
--- /dev/null
+++ b/test/scenarios/uac-add-fs-1.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-FS-Status: open
+ X-FS-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-add-fs-2.xml b/test/scenarios/uac-add-fs-2.xml
new file mode 100644
index 0000000..9d00394
--- /dev/null
+++ b/test/scenarios/uac-add-fs-2.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-FS-Status: open
+ X-FS-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-add-rtp-1.xml b/test/scenarios/uac-add-rtp-1.xml
new file mode 100644
index 0000000..960d20e
--- /dev/null
+++ b/test/scenarios/uac-add-rtp-1.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-RTP-Status: open
+ X-RTP-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-add-rtp-2.xml b/test/scenarios/uac-add-rtp-2.xml
new file mode 100644
index 0000000..a291456
--- /dev/null
+++ b/test/scenarios/uac-add-rtp-2.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-RTP-Status: open
+ X-RTP-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-external-options-ping.xml b/test/scenarios/uac-external-options-ping.xml
new file mode 100644
index 0000000..8979bb8
--- /dev/null
+++ b/test/scenarios/uac-external-options-ping.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-register-auth-failure-expect-403.xml b/test/scenarios/uac-register-auth-failure-expect-403.xml
new file mode 100644
index 0000000..ca12fec
--- /dev/null
+++ b/test/scenarios/uac-register-auth-failure-expect-403.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 7 REGISTER
+Subject: uac-register-auth-failure-expect-403
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 8 REGISTER
+Subject: uac-register-auth-failure-expect-403
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+[field2]
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-register-auth-failure-expect-503.xml b/test/scenarios/uac-register-auth-failure-expect-503.xml
new file mode 100644
index 0000000..efd73c0
--- /dev/null
+++ b/test/scenarios/uac-register-auth-failure-expect-503.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 7 REGISTER
+Subject: uac-register-auth-failure-expect-403
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-register-auth-success.xml b/test/scenarios/uac-register-auth-success.xml
new file mode 100644
index 0000000..70d825b
--- /dev/null
+++ b/test/scenarios/uac-register-auth-success.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+Subject: uac-register-auth-success
+CSeq: 7 REGISTER
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 8 REGISTER
+Subject: uac-register-auth-success
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+[field2]
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-register-auth-success2.xml b/test/scenarios/uac-register-auth-success2.xml
new file mode 100644
index 0000000..e5162fd
--- /dev/null
+++ b/test/scenarios/uac-register-auth-success2.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg2///[call_id]
+Subject: uac-register-auth-success
+CSeq: 7 REGISTER
+Contact:
+Expires: 5
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg2///[call_id]
+CSeq: 8 REGISTER
+Subject: uac-register-auth-success
+Contact:
+Expires: 5
+Content-Length: 0
+User-Agent: SIPp
+[field2]
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-register-unknown-realm.xml b/test/scenarios/uac-register-unknown-realm.xml
new file mode 100644
index 0000000..a22093d
--- /dev/null
+++ b/test/scenarios/uac-register-unknown-realm.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 7 REGISTER
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-reject-ipv4-realm.xml b/test/scenarios/uac-reject-ipv4-realm.xml
new file mode 100644
index 0000000..416c84c
--- /dev/null
+++ b/test/scenarios/uac-reject-ipv4-realm.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 7 REGISTER
+Contact:
+Expires: 3600
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-remove-fs-1.xml b/test/scenarios/uac-remove-fs-1.xml
new file mode 100644
index 0000000..974c6ea
--- /dev/null
+++ b/test/scenarios/uac-remove-fs-1.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-FS-Status: closed
+ X-FS-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-remove-fs-2.xml b/test/scenarios/uac-remove-fs-2.xml
new file mode 100644
index 0000000..d28bf56
--- /dev/null
+++ b/test/scenarios/uac-remove-fs-2.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-FS-Status: closed
+ X-FS-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-remove-rtp-1.xml b/test/scenarios/uac-remove-rtp-1.xml
new file mode 100644
index 0000000..99b5533
--- /dev/null
+++ b/test/scenarios/uac-remove-rtp-1.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-RTP-Status: closed
+ X-RTP-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-remove-rtp-2.xml b/test/scenarios/uac-remove-rtp-2.xml
new file mode 100644
index 0000000..477886a
--- /dev/null
+++ b/test/scenarios/uac-remove-rtp-2.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 OPTIONS
+ Max-Forwards: 70
+ Subject: Performance Test
+ X-RTP-Status: closed
+ X-RTP-Calls: 0
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac-unregister-auth-success.xml b/test/scenarios/uac-unregister-auth-success.xml
new file mode 100644
index 0000000..5eb9c04
--- /dev/null
+++ b/test/scenarios/uac-unregister-auth-success.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 7 REGISTER
+Contact:
+Expires: 0
+Content-Length: 0
+User-Agent: SIPp
+ ]]>
+
+
+
+
+
+
+
+
+
+ ;tag=[call_number]
+To: "sipp"
+Call-ID: reg///[call_id]
+CSeq: 8 REGISTER
+Contact:
+Expires: 0
+Content-Length: 0
+User-Agent: SIPp
+[field2]
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uas-auth-register copy.xml b/test/scenarios/uas-auth-register copy.xml
new file mode 100644
index 0000000..25b65aa
--- /dev/null
+++ b/test/scenarios/uas-auth-register copy.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;expires=60
+ Content-Type: application/sdp
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+ ;expires=60
+ Content-Type: application/sdp
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uas-auth-register2.xml b/test/scenarios/uas-auth-register2.xml
new file mode 100644
index 0000000..6ae84a2
--- /dev/null
+++ b/test/scenarios/uas-auth-register2.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;expires=60
+ Content-Type: application/sdp
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+ ;expires=60
+ Content-Type: application/sdp
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uas.xml b/test/scenarios/uas.xml
new file mode 100644
index 0000000..51035f6
--- /dev/null
+++ b/test/scenarios/uas.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Content-Type: application/sdp
+ Content-Length: [len]
+
+ 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
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/sipp.js b/test/sipp.js
new file mode 100644
index 0000000..215b109
--- /dev/null
+++ b/test/sipp.js
@@ -0,0 +1,94 @@
+const { spawn } = require('child_process');
+const debug = require('debug')('jambonz:ci');
+let network;
+const obj = {};
+let output = '';
+let idx = 1;
+
+function clearOutput() {
+ output = '';
+}
+
+function addOutput(str) {
+ for (let i = 0; i < str.length; i++) {
+ if (str.charCodeAt(i) < 128) output += str.charAt(i);
+ }
+}
+
+module.exports = (networkName) => {
+ network = networkName ;
+ return obj;
+};
+
+obj.output = () => {
+ return output;
+};
+
+obj.sippUac = (file, regObj, bindAddress, injectionFile) => {
+ const cmd = 'docker';
+ let args = null;
+ if(regObj) {
+ args = [
+ 'run', '--rm', '--net', `${network}`,
+ '-v', `${__dirname}/scenarios:/tmp/scenarios`,
+ 'drachtio/sipp', 'sipp', `${regObj.remote_host}`, // remote host is require on auth
+ '-inf', `/tmp/scenarios/${regObj.data_file}`,
+ '-sf', `/tmp/scenarios/${file}`,
+ '-m', '1',
+ '-sleep', '250ms',
+ '-nostdin',
+ '-cid_str', `%u-%p@%s-${idx++}`,
+ 'sbc', '-trace_msg'
+ ];
+ } else if (injectionFile) {
+ args = [
+ 'run', '-t', '--rm', '--net', `${network}`,
+ '-v', `${__dirname}/scenarios:/tmp/scenarios`,
+ 'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
+ '-inf', `/tmp/scenarios/${injectionFile}`,
+ '-m', '1',
+ '-sleep', '250ms',
+ '-nostdin',
+ '-cid_str', `%u-%p@%s-${idx++}`,
+ '172.39.0.10'];
+ }
+ else {
+ args = [
+ 'run', '-t', '--rm', '--net', `${network}`,
+ '-v', `${__dirname}/scenarios:/tmp/scenarios`,
+ 'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
+ '-m', '1',
+ '-sleep', '250ms',
+ '-nostdin',
+ '-cid_str', `%u-%p@%s-${idx++}`,
+ '172.39.0.10'];
+ }
+
+ if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
+
+ clearOutput();
+
+ return new Promise((resolve, reject) => {
+ const child_process = spawn(cmd, args, {stdio: ['inherit', 'pipe', 'pipe']});
+
+ child_process.on('exit', (code, signal) => {
+ if (code === 0) {
+ return resolve();
+ }
+ console.log(`sipp exited with non-zero code ${code} signal ${signal}`);
+ reject(code);
+ });
+ child_process.on('error', (error) => {
+ console.log(`error spawing child process for docker: ${args}`);
+ });
+
+ child_process.stdout.on('data', (data) => {
+ debug(`stdout: ${data}`);
+ addOutput(data.toString());
+ });
+ child_process.stderr.on('data', (data) => {
+ debug(`stderr: ${data}`);
+ addOutput(data.toString());
+ });
+ });
+};