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 ![Build Status](https://github.com/jambonz/sbc-sip-sidecar/workflows/CI/badge.svg) + +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 + + ]]> + + + + 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 + + ]]> + + + + 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()); + }); + }); +};