diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..8c2e19e
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+xtest/*
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..d561f83
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,126 @@
+{
+ "env": {
+ "node": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": false,
+ "modules": false
+ },
+ "ecmaVersion": 2017
+ },
+ "plugins": ["promise"],
+ "rules": {
+ "promise/always-return": "error",
+ "promise/no-return-wrap": "error",
+ "promise/param-names": "error",
+ "promise/catch-or-return": "error",
+ "promise/no-native": "off",
+ "promise/no-nesting": "warn",
+ "promise/no-promise-in-callback": "warn",
+ "promise/no-callback-in-promise": "warn",
+ "promise/no-return-in-finally": "warn",
+
+ // Possible Errors
+ // http://eslint.org/docs/rules/#possible-errors
+ "comma-dangle": [2, "only-multiline"],
+ "no-control-regex": 2,
+ "no-debugger": 2,
+ "no-dupe-args": 2,
+ "no-dupe-keys": 2,
+ "no-duplicate-case": 2,
+ "no-empty-character-class": 2,
+ "no-ex-assign": 2,
+ "no-extra-boolean-cast" : 2,
+ "no-extra-parens": [2, "functions"],
+ "no-extra-semi": 2,
+ "no-func-assign": 2,
+ "no-invalid-regexp": 2,
+ "no-irregular-whitespace": 2,
+ "no-negated-in-lhs": 2,
+ "no-obj-calls": 2,
+ "no-proto": 2,
+ "no-unexpected-multiline": 2,
+ "no-unreachable": 2,
+ "use-isnan": 2,
+ "valid-typeof": 2,
+
+ // Best Practices
+ // http://eslint.org/docs/rules/#best-practices
+ "no-fallthrough": 2,
+ "no-octal": 2,
+ "no-redeclare": 2,
+ "no-self-assign": 2,
+ "no-unused-labels": 2,
+
+ // Strict Mode
+ // http://eslint.org/docs/rules/#strict-mode
+ "strict": [2, "never"],
+
+ // Variables
+ // http://eslint.org/docs/rules/#variables
+ "no-delete-var": 2,
+ "no-undef": 2,
+ "no-unused-vars": [2, {"args": "none"}],
+
+ // Node.js and CommonJS
+ // http://eslint.org/docs/rules/#nodejs-and-commonjs
+ "no-mixed-requires": 2,
+ "no-new-require": 2,
+ "no-path-concat": 2,
+ "no-restricted-modules": [2, "sys", "_linklist"],
+
+ // Stylistic Issues
+ // http://eslint.org/docs/rules/#stylistic-issues
+ "comma-spacing": 2,
+ "eol-last": 2,
+ "indent": [2, 2, {"SwitchCase": 1}],
+ "keyword-spacing": 2,
+ "max-len": [2, 120, 2],
+ "new-parens": 2,
+ "no-mixed-spaces-and-tabs": 2,
+ "no-multiple-empty-lines": [2, {"max": 2}],
+ "no-trailing-spaces": [2, {"skipBlankLines": false }],
+ "quotes": [2, "single", "avoid-escape"],
+ "semi": 2,
+ "space-before-blocks": [2, "always"],
+ "space-before-function-paren": [2, "never"],
+ "space-in-parens": [2, "never"],
+ "space-infix-ops": 2,
+ "space-unary-ops": 2,
+
+ // ECMAScript 6
+ // http://eslint.org/docs/rules/#ecmascript-6
+ "arrow-parens": [2, "always"],
+ "arrow-spacing": [2, {"before": true, "after": true}],
+ "constructor-super": 2,
+ "no-class-assign": 2,
+ "no-confusing-arrow": 2,
+ "no-const-assign": 2,
+ "no-dupe-class-members": 2,
+ "no-new-symbol": 2,
+ "no-this-before-super": 2,
+ "prefer-const": 2
+ },
+ "globals": {
+ "DTRACE_HTTP_CLIENT_REQUEST" : false,
+ "LTTNG_HTTP_CLIENT_REQUEST" : false,
+ "COUNTER_HTTP_CLIENT_REQUEST" : false,
+ "DTRACE_HTTP_CLIENT_RESPONSE" : false,
+ "LTTNG_HTTP_CLIENT_RESPONSE" : false,
+ "COUNTER_HTTP_CLIENT_RESPONSE" : false,
+ "DTRACE_HTTP_SERVER_REQUEST" : false,
+ "LTTNG_HTTP_SERVER_REQUEST" : false,
+ "COUNTER_HTTP_SERVER_REQUEST" : false,
+ "DTRACE_HTTP_SERVER_RESPONSE" : false,
+ "LTTNG_HTTP_SERVER_RESPONSE" : false,
+ "COUNTER_HTTP_SERVER_RESPONSE" : false,
+ "DTRACE_NET_STREAM_END" : false,
+ "LTTNG_NET_STREAM_END" : false,
+ "COUNTER_NET_SERVER_CONNECTION_CLOSE" : false,
+ "DTRACE_NET_SERVER_CONNECTION" : false,
+ "LTTNG_NET_SERVER_CONNECTION" : false,
+ "COUNTER_NET_SERVER_CONNECTION" : false
+ }
+}
diff --git a/.gitignore b/.gitignore
index 92299c1..2ecb6bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,5 @@ node_modules
config.js
.DS_Store
+.vscode
+.eslint*
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..bd8184c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,20 @@
+sudo: required
+
+dist: xenial
+
+language: node_js
+
+node_js:
+ - "lts/*"
+
+services:
+ - docker
+
+before_install:
+ - docker pull drachtio/drachtio-server:latest
+ - docker pull drachtio/sipp:latest
+ - docker pull davehorton/freeswitch-hairpin:latest
+ - docker images
+
+script:
+ - npm test
diff --git a/app.js b/app.js
index c8a4f83..63fae17 100644
--- a/app.js
+++ b/app.js
@@ -1,49 +1,38 @@
-'use strict';
-
-var drachtio = require('drachtio') ;
-var app = drachtio() ;
-var config = app.config = require('./config');
-var fs = require('fs') ;
-var rangeCheck = require('range_check');
-var debug = app.debug = require('debug')('simple-sip-proxy') ;
-
-// Expose app
-exports = module.exports = app;
-
-//connect to drachtio server
-app.connect({
- host: config.drachtioServer.address,
- port: config.drachtioServer.port,
- secret: config.drachtioServer.secret,
-}) ;
-
-app.on('connect', function(err, hostport) {
- if( err ) throw err ;
- console.log('connected to drachtio server at %s', hostport) ;
-})
-.on('error', function(err){
- console.warn(err.message ) ;
-}) ;
-
-app.use(function checkSender(req, res, next) {
- if( !rangeCheck.inRange( req.source_address, app.config.authorizedSources) ) { return res.send(403) ; }
- next() ;
-}) ;
-app.use(function onlyInvites( req, res, next ) {
- if( -1 === ['invite','cancel','prack','ack'].indexOf( req.method.toLowerCase() ) ) { return res.send(405); }
- next() ;
-});
-
-require('./lib/proxy.js')(app);
-
-//handle any errors thrown while executing middleware stack
-app.use(function myErrorHandler(err, req, res, next) {
- debug('caught error generated from middleware: ', err);
- res.send(500, {
- headers: {
- 'X-Error-Info': err.message || 'unknown application error'
- }
- }) ;
-}) ;
-
-
+const Srf = require('drachtio-srf') ;
+const srf = new Srf();
+const config = require('config');
+const pino = require('pino');
+const level = config.has('log.level') ? config.get('log.level') : 'info';
+const logger = srf.locals.logger = pino({level});
+const RouteMgr = require('./lib/routing/route-manager');
+const routeMgr = new RouteMgr({logger, srf});
+const validate = require('./lib/validation-middleware')({logger, routeMgr});
+const inviteHandler = require('./lib/invite')({logger, srf, routeMgr});
+
+process.on('unhandledRejection', (reason, p) => {
+ console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
+});
+
+srf.connect(config.get('drachtio'))
+ .on('connect', (err, hp) => {
+ if (err) return logger.error(err, 'error connecting');
+ logger.info(`connected to drachtio server at ${hp}`);
+ });
+
+if (process.env.NODE_ENV !== 'test') {
+ srf.on('error', (err) => {
+ logger.error(err, 'Error connecting to drachtio server');
+ });
+}
+
+srf.use(validate);
+srf.invite(inviteHandler);
+srf.info((req, res) => res.send(200));
+
+
+// for test purposes only
+if (process.env.NODE_ENV === 'test') {
+ srf.routeMgr = () => routeMgr;
+
+ module.exports = srf;
+}
diff --git a/config/default.json.example b/config/default.json.example
new file mode 100644
index 0000000..91d2b11
--- /dev/null
+++ b/config/default.json.example
@@ -0,0 +1,80 @@
+{
+ "drachtio": {
+ "host": "127.0.0.1",
+ "port": 9022,
+ "secret": "cymru"
+ },
+ "external-trunks": {
+ "carrier-1": {
+ "type": "in",
+ "signaling-address": ["10.10.100.1:5060", "10.10.100.2:5060"],
+ "enabled": false
+ },
+ "carrier-2": {
+ "type": "out",
+ "signaling-address": "10.10.100.5",
+ "groups": "cheap"
+ },
+ "carrier-3": {
+ "type": "both",
+ "signaling-address": "10.10.100.6",
+ "groups": ["cheap", "international"]
+ }
+ },
+ "internal-trunks": {
+ "appserver1": {
+ "signaling-address": "10.10.200.1",
+ "options-ping": true,
+ "enabled": true
+ },
+ "appserver2": {
+ "signaling-address": "10.10.200.2",
+ "options-ping": true,
+ "enabled": false
+ }
+ },
+ "options-ping": {
+ "interval": "6000"
+ },
+ "proxy-request-types": ["INVITE"],
+ "routing": {
+ "outbound": {
+ "default": [
+ {
+ "target": "carrier-2",
+ "ratio": 9
+ },
+ {
+ "target": "carrier-1",
+ "ratio": 1
+ }
+ ]
+ },
+ "international": {
+ "regex": "+?1\\d{10}",
+ "match": ["international"]
+ },
+ "toll-free": {
+ "regex": "+?1\\d{10}",
+ "match": ["carrier-2"],
+ "no-match": ["carrier-3"]
+ },
+ "test": {
+ "regex": "+15083084809",
+ "against": "from",
+ "match": ["carrier-2"]
+ }
+ },
+ "inbound": {
+ "default": [
+ {
+ "target": "appserver1",
+ "ratio": 2
+ },
+ {
+ "target": "appserver2",
+ "ratio": 1
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/config/test-1.json b/config/test-1.json
new file mode 100644
index 0000000..219649b
--- /dev/null
+++ b/config/test-1.json
@@ -0,0 +1,91 @@
+{
+ "drachtio": {
+ "host": "127.0.0.1",
+ "port": 9022,
+ "secret": "cymru"
+ },
+ "trunks": {
+ "outside": {
+ "carrier-1": {
+ "direction": "in",
+ "address": "10.10.100.1:5060",
+ "enabled": false
+ },
+ "carrier-2": {
+ "direction": "out",
+ "address": "10.10.100.5",
+ "groups": "cheap"
+ },
+ "carrier-3": {
+ "direction": "both",
+ "address": ["10.10.100.6","10.10.100.7"],
+ "groups": ["cheap", "international"]
+ },
+ "carrier-4": {
+ "direction": "out",
+ "address": "10.10.100.8",
+ "enabled": false,
+ "groups": "cheap"
+ }
+ },
+ "inside": {
+ "appserver1": {
+ "address": "10.10.200.1",
+ "options-ping": true,
+ "groups": "standby",
+ "enabled": true
+ },
+ "voicemail": {
+ "address": "10.10.200.1",
+ "options-ping": true,
+ "groups": ["media-processing", "storage"],
+ "enabled": true
+ },
+ "appserver2": {
+ "address": ["10.10.200.2", "10.10.200.6"],
+ "options-ping": true,
+ "enabled": false
+ }
+ }
+ },
+ "routes": {
+ "outside": {
+ "international": {
+ "regex": "\\+?1\\d{10}",
+ "match": ["international"]
+ },
+ "toll-free": {
+ "regex": "\\+?1\\d{10}",
+ "match": ["carrier-2"],
+ "no-match": "carrier-3"
+ },
+ "test": {
+ "regex": "^4083084809$",
+ "against": "from",
+ "match": ["carrier-2"]
+ },
+ "default": [
+ {
+ "dest": "carrier-2",
+ "weight": 9
+ },
+ {
+ "dest": "carrier-1",
+ "weight": 1
+ }
+ ]
+ },
+ "inside": {
+ "default": [
+ {
+ "dest": "appserver1",
+ "weight": 2
+ },
+ {
+ "dest": "appserver2",
+ "weight": 1
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/config/test-100.json b/config/test-100.json
new file mode 100644
index 0000000..3fb4fe7
--- /dev/null
+++ b/config/test-100.json
@@ -0,0 +1,67 @@
+{
+ "drachtio": {
+ "host": "127.0.0.1",
+ "port": 9061,
+ "secret": "cymru"
+ },
+ "log": {
+ "level": "info"
+ },
+ "trunks": {
+ "outside": {
+ "carrier-1": {
+ "direction": "in",
+ "address": "172.19.0.100"
+ },
+ "carrier-2": {
+ "direction": "out",
+ "address": "172.19.0.112"
+ }
+ },
+ "inside": {
+ "freeswitch-1": {
+ "address": "172.19.0.21",
+ "options-ping": false
+ },
+ "freeswitch-2": {
+ "address": "172.19.0.22",
+ "options-ping": false
+ },
+ "freeswitch-3": {
+ "address": "172.19.0.23",
+ "options-ping": false
+ },
+ "freeswitch-4": {
+ "address": "172.19.0.24",
+ "options-ping": false
+ },
+ "sipp-online": {
+ "address": "172.19.0.110",
+ "options-ping": true,
+ "options-timeout": 1000,
+ "options-interval": 3000
+ },
+ "sipp-offline": {
+ "address": "172.19.0.111",
+ "options-ping": true,
+ "options-timeout": 1000,
+ "options-interval": 3000
+ },
+ "sipp-noexist": {
+ "address": "172.19.0.121",
+ "options-ping": true,
+ "options-timeout": 1000,
+ "options-interval": 3000
+ }
+ }
+ },
+ "routes": {
+ "inside": {
+ "default": [
+ "172.19.0.21",
+ "172.19.0.22",
+ "172.19.0.23"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/config/test-2.json b/config/test-2.json
new file mode 100644
index 0000000..a880da7
--- /dev/null
+++ b/config/test-2.json
@@ -0,0 +1,79 @@
+{
+ "drachtio": {
+ "host": "127.0.0.1",
+ "port": 9022,
+ "secret": "cymru"
+ },
+ "trunks": {
+ "outside": {
+ "carrier-1": {
+ "direction": "in",
+ "address": "10.10.100.1:5060",
+ "enabled": false
+ },
+ "carrier-2": {
+ "direction": "out",
+ "address": "10.10.100.5",
+ "groups": "cheap"
+ },
+ "carrier-3": {
+ "direction": "both",
+ "address": ["10.10.100.6","10.10.100.7"],
+ "groups": ["cheap", "international"]
+ },
+ "carrier-4": {
+ "direction": "out",
+ "address": "10.10.100.8",
+ "enabled": false,
+ "groups": "cheap"
+ }
+ },
+ "inside": {
+ "appserver1": {
+ "address": "10.10.200.1",
+ "options-ping": true,
+ "groups": "standby",
+ "enabled": true
+ },
+ "voicemail": {
+ "address": "10.10.200.99",
+ "options-ping": true,
+ "groups": ["media-processing", "storage"],
+ "enabled": true
+ },
+ "appserver2": {
+ "address": ["10.10.200.2", "10.10.200.6"],
+ "options-ping": true,
+ "enabled": false
+ }
+ }
+ },
+ "routes": {
+ "outside": {
+ "international": {
+ "regex": "\\+?1\\d{10}",
+ "match": ["international"]
+ },
+ "toll-free": {
+ "regex": "\\+?1\\d{10}",
+ "match": ["carrier-2"],
+ "no-match": "carrier-3"
+ },
+ "test": {
+ "regex": "^4083084809$",
+ "against": "from",
+ "match": ["carrier-2"]
+ },
+ "default": [
+ {
+ "dest": "carrier-2",
+ "weight": 9
+ },
+ {
+ "dest": "carrier-1",
+ "weight": 1
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/invite.js b/lib/invite.js
new file mode 100644
index 0000000..d6cb26a
--- /dev/null
+++ b/lib/invite.js
@@ -0,0 +1,20 @@
+module.exports = function({logger, srf, routeMgr}) {
+ const parentLogger = logger;
+
+ return function(req, res) {
+ const logger = parentLogger.child({'callId': req.get('Call-Id')});
+ const sourceSide = req.locals.source.side;
+ const destSide = sourceSide === 'inside' ? 'outside' : 'inside';
+ const uri = routeMgr.chooseRoute(destSide, req);
+
+ if (!uri) {
+ logger.error(req.locals.source, 'No available routes for this call, returning 480');
+ return res.send(480);
+ }
+ logger.info(req.locals.source, `received call from ${sourceSide}, proxying to uri: ${uri}`);
+
+ srf.proxyRequest(req, uri, {
+ recordRoute: true
+ });
+ };
+};
diff --git a/lib/proxy.js b/lib/proxy.js
deleted file mode 100644
index 9ffa91e..0000000
--- a/lib/proxy.js
+++ /dev/null
@@ -1,168 +0,0 @@
-'use strict';
-
-var fs = require('fs') ;
-var path = require('path') ;
-var _ = require('lodash') ;
-var allPossibleTargets = [] ;
-var dest = [] ;
-var debug ;
-var config ;
-
-function createTargets( config ) {
- allPossibleTargets = _.filter( config.targets, function(t) { return t.enabled === true; }) ;
-}
-function isValidTarget( target ) {
- return _.find( allPossibleTargets, function(t) {return t.address === target.address && t.port === target.port; }) ;
-}
-
-function pingTarget( app, target ) {
- var dest = 'sip:' + target.address + ':' + (target.port || 5060) ;
- if( target.markedForDeath ) {
- console.log('removing target %s due to re-loading config file: ', dest) ;
- return;
- }
-
- //start a timer - if we don't get a response within a second we'll mark it down
- var timeoutHandler = setTimeout( function() {
- //if we get here we timed out
- if( !target.offline ) {
- target.offline = true ;
- console.error('taking target %s offline because it did not respond to ping: ', dest) ;
- }
- }, 1500 ) ;
-
- //send the OPTIONS ping
- console.log('OPTIONS %s', dest) ;
- app.request({
- uri: dest,
- method: 'OPTIONS',
- headers: {
- 'User-Agent': 'phone.com internal proxy'
- }
- }, function( err, req ) {
- if( err ) {
- setTimeout( function() {
- if( isValidTarget( target ) && !target.markedForDeath ) {
- pingTarget( app, target);
- }
- else {
- console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ;
- }
- }, config.pingIntervalWhenOffline || config.optionsPingInterval) ;
- return console.error('Error pinging %s', target) ;
- }
-
- req.on('response', function(res){
-
- console.log('%s: %d %s', dest, res.status, res.reason) ;
- clearTimeout( timeoutHandler ) ;
- if( 200 === res.status ) {
- if( target.offline ) {
- target.offline = false ;
- console.log('bringing target back online: %s', dest) ;
- }
- }
- else if( 503 === res.status ) {
- if( !target.offline) {
- target.offline = true ;
- console.log('taking target offline due to 503 response: ', dest) ;
- }
- }
- setTimeout( function() {
- if( isValidTarget( target ) && !target.markedForDeath ) {
- pingTarget( app, target);
- }
- else {
- console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ;
- }
- }, target.offline === true ? (config.pingIntervalWhenOffline || config.optionsPingInterval) : config.optionsPingInterval) ;
- }) ;
- }) ;
-}
-
-function stopPinging() {
- console.log('stopping OPTIONS pings..') ;
- allPossibleTargets.forEach(function(t) { t.markedForDeath = true; }) ;
-}
-
-function startPinging( app, config ) {
- var pingTargets = _.filter( allPossibleTargets, function(t) {return t.optionsPing === true; }) ;
- console.log('start OPTIONS pings to targets at %d ms interval: ', config.optionsPingInterval, JSON.stringify(pingTargets)) ;
- pingTargets.forEach( function(t) {pingTarget(app,t); }) ;
-}
-
-module.exports = function(app) {
- debug = app.debug ;
- config = app.config ;
-
- createTargets(config) ;
- app.on('connect', function(err) {
- startPinging(app,config);
- });
-
- //min allowed ping interval is 2 seconds
- config.optionsPingInterval = Math.max(config.optionsPingInterval || 5000, 2000) ;
- console.log('options ping interval will be %d milliseconds', config.optionsPingInterval) ;
-
- createTargets(config) ;
- startPinging(app, config) ;
-
- //watch config file for changes - we allow the user to dynamically add or remove targets
- var configPath = path.resolve(__dirname) + '/../config.js' ;
- fs.watchFile(configPath, function () {
- try {
- console.log('config.js was just modified...') ;
-
- delete require.cache[require.resolve(configPath)] ;
- config = require(configPath) ;
- stopPinging() ;
- createTargets( config ) ;
-
- console.log('proxy targets are now: ',
- _.map( allPossibleTargets, function(t) { return _.pick(t, ['address','port','enabled','optionsPing']);}));
-
- startPinging(app, config) ;
-
- } catch( err ) {
- console.error('Error re-reading config.js after modification; check to ensure there are no syntax errors: ', err) ;
- }
- }) ;
-
- app.invite( function(req,res) {
-
- //filter out offline servers
- var onlineServers = _.filter( allPossibleTargets, function(t) { return !t.offline; }) ;
-
- if( 0 === onlineServers.length ) {
- if( config.proxyWhenAllTargetsOffline === true ) {
- onlineServers = allPossibleTargets ;
- console.log('all servers seem down, but attempting to proxy anyways') ;
- }
- else {
- console.error('there are no servers online currently') ;
- return res.send(480) ;
- }
- }
-
- var dest = _.map( onlineServers, function(t) { return t.address + ':' + (t.port || 5060); }) ;
-
- debug('proxy destinations are: ', dest) ;
-
- //finally....here's the magic: proxy the request, attempting destinations in order until we connect or all fail
- req.proxy({
- remainInDialog: false,
- destination: dest
- }, function(err, results) {
- if( err ) console.error('Error proxying: ', err) ;
-
- //this is voluminous, but if you want to know what happened all the detail is here..
- debug('proxy result: ', JSON.stringify(results)) ;
- }) ;
-
- //round-robin the next starting server in the list
- var item = allPossibleTargets.shift() ;
- allPossibleTargets.push( item ) ;
- }) ;
-} ;
-
-
diff --git a/lib/routing/route-manager.js b/lib/routing/route-manager.js
new file mode 100644
index 0000000..0de349e
--- /dev/null
+++ b/lib/routing/route-manager.js
@@ -0,0 +1,145 @@
+const Emitter = require('events');
+const fs = require('fs');
+const clearModule = require('clear-module');
+const assert = require('assert');
+const RouteSet = require('./route-set');
+const nullLogger = require('../utils/null-logger');
+const PingTester = require('../utils/ping-tester');
+const SIDES = ['inside', 'outside'];
+
+class RouteManager extends Emitter {
+ constructor({logger, srf}) {
+ super();
+
+ this.config = require('config');
+ this.logger = logger || nullLogger;
+ this.srf = srf;
+ this.routeSet = {};
+ this.outboundExternalTrunks = [];
+ this.inboundExternalTrunks = [];
+ this.internalTrunks = [];
+ this.pingers = [];
+ this._isPinging = false;
+
+ this._init();
+
+ if (process.env.NODE_ENV !== 'test') {
+ fs.watch(`${__dirname}/../../config`, (eventType, filename) => {
+ if (filename.endsWith('.json')) {
+ this.logger.info(`RouteManager: configuration file change ${filename}: ${eventType}`);
+ clearModule('config');
+ this.config = require('config');
+ this._init();
+ this.emit('changed');
+ }
+ });
+ }
+ }
+
+ get lastTarget() {
+ return this._lastTarget;
+ }
+
+ get lastUri() {
+ return this._lastUri;
+ }
+
+ _init() {
+ this._initSipTrunks();
+ this._initRouteSets();
+ }
+
+ _initSipTrunks() {
+ assert.ok(this.config.has('trunks.outside'), 'trunks.outside configuration is missing');
+ const extTrunks = this.config.get('trunks.outside');
+
+ let arr = Object.keys(extTrunks).filter((k) =>
+ ['out', 'both'].includes(extTrunks[k].direction) && extTrunks[k].enabled !== false);
+ this.outboundExternalTrunks = arr.map((k) => Object.assign(extTrunks[k], {name: k}));
+
+ arr = Object.keys(extTrunks).filter((k) =>
+ ['in', 'both'].includes(extTrunks[k].direction) && extTrunks[k].enabled !== false);
+ this.inboundExternalTrunks = arr.map((k) => Object.assign(extTrunks[k], {name: k}));
+
+ assert.ok(this.config.has('trunks.inside'), 'routes.inside configuration is missing');
+ const intTrunks = this.config.get('trunks.inside');
+
+ arr = Object.keys(intTrunks).filter((k) => intTrunks[k].enabled !== false);
+ this.internalTrunks = arr.map((k) => Object.assign(intTrunks[k], {name: k}));
+
+ assert.ok(this.outboundExternalTrunks.length, 'must have at least one enabled outbound external trunk');
+ assert.ok(this.inboundExternalTrunks.length, 'must have at least one enabled inbound external trunk');
+ assert.ok(this.internalTrunks.length, 'must have at least one internal trunk');
+
+ /* start OPTIONS ping testing internal trunks */
+ this.pingers.forEach((p) => p.stopPinging());
+ this.pingers = [];
+ if (this.srf) {
+ this.internalTrunks.filter((t) => t['options-ping'] === true).forEach((t) => {
+ (typeof t.address === 'string' ? [t.address] : t.address).forEach((uri) => {
+ this.pingers.push(new PingTester({
+ logger: this.logger,
+ srf: this.srf,
+ uri,
+ timeout: t['options-timeout'],
+ interval: t['options-interval']
+ }));
+ });
+ });
+ this.logger.info(`started pinging ${this.pingers.length} servers ${this.pingers}`);
+ }
+ }
+
+ _initRouteSets() {
+ SIDES.forEach((type) => {
+ delete this.routeSet[type];
+ this.routeSet[type] = new RouteSet(type, this.config, this.logger);
+ });
+
+ if (!this.routeSet.outside.hasDefaultRoute) {
+ this.routeSet.outside.setDefaultRoute(
+ this.outboundExternalTrunks.map((t) => Object.assign({}, {dest: t['address'], weight: 1}))
+ );
+ }
+ if (!this.routeSet.inside.hasDefaultRoute) {
+ this.routeSet.inside.setDefaultRoute(
+ Object.keys(this.internalTrunks).map((t) => {
+ return {
+ dest: this.internalTrunks[t]['address'],
+ weight: 1
+ };
+ })
+ );
+ }
+ }
+
+ chooseRoute(type, req) {
+ assert(SIDES.includes(type));
+ const uri = this.routeSet[type].evaluate(req);
+ if (uri) this.logger.info(`selected uri: ${uri}`);
+
+ return uri;
+ }
+
+ isValidSendingAddress(source) {
+ let ip = source;
+ let trunk;
+ const arr = /^(.*):(\d+)$/.exec(source);
+ if (arr) ip = arr[1];
+
+ if (trunk = this.inboundExternalTrunks.find((t) =>
+ (typeof t.address === 'string' && source === t.address) ||
+ (Array.isArray(t.address) && t.address.includes(source)) ||
+ (typeof t.address === 'string' && source.endsWith(':5060') && ip === t.address) ||
+ (Array.isArray(t.address) && source.endsWith(':5060') && t.address.includes(ip))
+ )) {
+ return Object.assign(trunk, {side: 'outside'});
+ }
+
+ if (trunk = this.internalTrunks.find((t) => ip === t['address'])) {
+ return Object.assign(trunk, {side: 'inside'});
+ }
+ }
+}
+
+module.exports = RouteManager;
diff --git a/lib/routing/route-set.js b/lib/routing/route-set.js
new file mode 100644
index 0000000..7b4cffc
--- /dev/null
+++ b/lib/routing/route-set.js
@@ -0,0 +1,53 @@
+const nullLogger = require('../utils/null-logger');
+const Route = require('./route');
+const assert = require('assert');
+
+class RouteSet {
+ constructor(type, config, logger) {
+ this.type = type;
+ this.config = config;
+
+ const opts = config.has(`routes.${type}`) ? config.get(`routes.${type}`) : {};
+
+ const conditionalRoutes = Object.keys(opts).filter((k) => k !== 'default');
+ this.routes = conditionalRoutes.map((key) => new Route(key, opts[key], config, logger));
+ if (opts.default) this.default = new Route('default', opts.default, config, logger);
+ this.logger = logger || nullLogger;
+ }
+
+ get hasDefaultRoute() {
+ return typeof this.default !== 'undefined';
+ }
+
+ setDefaultRoute(def) {
+ assert(!this.hasDefault);
+ this.default = new Route('default', def, this.config, this.logger);
+ }
+
+ toJSON() {
+ return {routes: this.routes, default: this.default};
+ }
+
+ evaluate(req) {
+ const logger = this.logger.child({'Call-ID': req.get('Call-ID')});
+ logger.debug(`RouteSet ${this.type}: evaluating..`);
+
+ // try conditional routes first
+ for (const route of this.routes) {
+ const target = route.evaluate(req);
+ if (target) {
+ logger.debug(`RouteSet ${this.type}: matched ${route.type}`);
+ return target;
+ }
+ }
+
+ if (this.default) {
+ logger.debug(`RouteSet ${this.type}: no matches, returning default route`);
+ return this.default.evaluate(req);
+ }
+
+ logger.info(`RouteSet ${this.type}: no matches and no default`);
+ }
+}
+
+module.exports = RouteSet;
diff --git a/lib/routing/route.js b/lib/routing/route.js
new file mode 100644
index 0000000..55d30c1
--- /dev/null
+++ b/lib/routing/route.js
@@ -0,0 +1,69 @@
+const nullLogger = require('../utils/null-logger');
+const Target = require('./target');
+
+class Route {
+ constructor(name, opts, config, logger) {
+ this.name = name;
+
+ if (Array.isArray(opts)) {
+ this.alwaysTargets = new Target(opts, config, logger);
+ }
+ else if (typeof opts === 'string') {
+ this.alwaysTargets = new Target([opts], config, logger);
+ }
+ else {
+ this.regex = opts.regex;
+ if (opts.match) this.match = new Target(opts.match, config, logger);
+ if (opts['no-match']) this.noMatch = new Target(opts['no-match'], config, logger);
+ this.against = (opts.against || 'to').toLowerCase();
+ }
+ this.logger = logger || nullLogger;
+ }
+
+ toJSON() {
+ return {
+ name: this.name,
+ alwaysTargets: this.alwaysTargets,
+ regex: this.regex,
+ against: this.against,
+ match: this.match,
+ noMatch: this.noMatch
+ };
+ }
+
+ evaluate(req) {
+ const logger = this.logger.child({'Call-ID': req.get('Call-ID')});
+ let inspected;
+
+ if (this.alwaysTargets) {
+ return this.alwaysTargets.evaluate();
+ }
+
+ if (this.against === 'from') {
+ inspected = req.callingNumber;
+ }
+ else {
+ inspected = req.calledNumber;
+ }
+
+ let matched = false;
+ try {
+ matched = inspected && inspected.match(new RegExp(this.regex));
+ } catch (err) {
+ logger.error(`error evaluating regex: ${err}`);
+ return;
+ }
+
+ if (this.match && matched) {
+ logger.debug(`Route ${this.name}: routing based on ${this.against} match ${inspected}: ${this.match}`);
+ return this.match.evaluate();
+ }
+
+ if (this.noMatch && !matched) {
+ logger.debug(`Route: ${this.name} routing based on ${this.against} no-match ${inspected}: ${this.noMatch}`);
+ return this.noMatch.evaluate();
+ }
+ }
+}
+
+module.exports = Route;
diff --git a/lib/routing/target.js b/lib/routing/target.js
new file mode 100644
index 0000000..5b376e3
--- /dev/null
+++ b/lib/routing/target.js
@@ -0,0 +1,144 @@
+const assert = require('assert');
+const nullLogger = require('../utils/null-logger');
+const PingTester = require('../utils/ping-tester');
+
+/**
+ * A Target may be any of the following:
+ *
+ * a single sip address[:port]
+ * '10.10.100.1' or '10.10.100.2:5061'
+ *
+ * an array of sip addresses
+ * ['10.10.100.1', '10.10.100.2:5061']
+ *
+ * a single group name, or array of group names
+ * 'international', or ['cheapest', 'moderate']
+ *
+ * an array of weighted entries (sip addresses or groups)
+ * [{dest: '10.10.100.1', weight: 1}, {dest: '10.10.100.2:5061', weight: 2}]
+ *
+ * or
+ *
+ * [{dest: 'cheapest', weight: 5}, {dest: 'moderate', weight: 1}]
+ */
+class Target {
+ constructor(opts, config, logger) {
+
+ this.entries = (Array.isArray(opts) ? opts : [opts]).map((t) => {
+ assert.ok(typeof t === 'string' || (t.dest && t.weight),
+ `invalid target configuration ${JSON.stringify(t)}: must have dest and weight properties`);
+ return typeof t === 'string' ? {dest: t, weight: 1} : t;
+ });
+
+ // map trunk and group names to ip addresses
+ const disabledTrunks = new Set();
+ const outside = new Map();
+ if (config.get('trunks.outside')) {
+ const obj = config.get('trunks.outside');
+ Object.keys(obj).forEach((k) => {
+ const trunk = obj[k];
+ if (['out', 'both'].includes(trunk.direction)) {
+ const address = Array.isArray(trunk.address) ? trunk.address : [trunk.address];
+ if (trunk.enabled === false) disabledTrunks.add(k);
+ const groups = Array.isArray(trunk.groups) ? trunk.groups :
+ (typeof trunk.groups === 'string' ? [trunk.groups] : []);
+ outside.set(k, (outside.get(k) || []).concat(address));
+
+ if (trunk.enabled !== false) {
+ groups.forEach((g) => {
+ outside.set(g, (outside.get(g) || []).concat(address));
+ });
+ }
+ }
+ });
+ }
+
+ const inside = new Map();
+ if (config.get('trunks.inside')) {
+ const obj = config.get('trunks.inside');
+ Object.keys(obj).forEach((k) => {
+ const trunk = obj[k];
+ if (trunk.enabled === false) disabledTrunks.add(k);
+ const address = Array.isArray(trunk.address) ? trunk.address : [trunk.address];
+ const groups = Array.isArray(trunk.groups) ? trunk.groups :
+ (typeof trunk.groups === 'string' ? [trunk.groups] : []);
+ inside.set(k, (inside.get(k) || []).concat(address));
+
+ if (trunk.enabled !== false) {
+ groups.forEach((g) => {
+ inside.set(g, (inside.get(g) || []).concat(address));
+ });
+ }
+ });
+ }
+
+ // expand entries that reference group or trunk names
+ for (let i = 0; i < this.entries.length; i++) {
+ const e = this.entries[i];
+ if (outside.has(e.dest) || inside.has(e.dest)) {
+ const addresses = outside.get(e.dest) || inside.get(e.dest);
+ const replacement = disabledTrunks.has(e.dest) ?
+ [] :
+ addresses.map((a) => Object.assign({}, {dest: a, weight: e.weight}));
+ this.entries[i] = replacement;
+ for (let j = 0; j < this.entries.length; j++) {
+ if (j !== i) this.entries[j].weight *= replacement.length;
+ }
+ }
+ }
+ this.entries = [].concat(...this.entries); //flatten
+
+ /**
+ * if only 1 target: just select it
+ * if N equally-weighted entries: round robin
+ * otherwise, percentage-based using random number generation
+ */
+ if (this.entries.length > 1) {
+ const total = this.entries.reduce((accumulator, currentValue) => accumulator + currentValue.weight, 0);
+ if (total === this.entries.length) {
+ this.rrSelected = 0;
+ }
+ else {
+ let floor = 0;
+ this.entries.forEach((t) => {
+ const delta = Math.floor(t.weight / total * 100.0);
+ t.floor = floor;
+ t.ceiling = floor + delta;
+ floor = t.ceiling;
+ });
+ this.entries[this.entries.length - 1].ceiling = 100;
+ }
+ }
+ this.logger = logger || nullLogger;
+ }
+
+ toJSON() {
+ return {
+ entries: this.entries,
+ rrSelected: this.rrSelected
+ };
+ }
+
+ evaluate() {
+ /* remove offline servers */
+ const entries = this.entries.filter((e) => !PingTester.isOffline(e.dest));
+
+ if (entries.length === 1) return entries[0].dest;
+
+ if (this.rrSelected >= 0) {
+ const sel = this.rrSelected++;
+ if (this.rrSelected > entries.length - 1) this.rrSelected = 0;
+ return entries[sel].dest;
+ }
+
+ const rand = Math.floor(Math.random() * 100);
+ for (const t of entries) {
+ if (rand >= t.floor && rand < t.ceiling) return t.dest;
+ }
+
+ this.logger.error(`Target#evaluate all targets offline or not responding: ${JSON.stringify(this.entries)}`);
+ }
+
+}
+
+module.exports = Target;
diff --git a/lib/utils/null-logger.js b/lib/utils/null-logger.js
new file mode 100644
index 0000000..af890ce
--- /dev/null
+++ b/lib/utils/null-logger.js
@@ -0,0 +1,5 @@
+const noLogger = {};
+['error', 'info', 'debug'].forEach((m) => noLogger[m] = () => {});
+noLogger.child = function() { return noLogger; };
+
+module.exports = noLogger;
diff --git a/lib/utils/ping-tester.js b/lib/utils/ping-tester.js
new file mode 100644
index 0000000..7191bd4
--- /dev/null
+++ b/lib/utils/ping-tester.js
@@ -0,0 +1,79 @@
+const Emitter = require('events');
+const nullLogger = require('./null-logger');
+const assert = require('assert');
+
+const offlineUris = new Set();
+
+class PingTester extends Emitter {
+ constructor(opts) {
+ super();
+
+ assert(opts.uri);
+ assert(opts.srf);
+ this._logger = opts.logger || nullLogger;
+ this._timeout = opts.timeout || 2000;
+ this._interval = opts.interval || 10000;
+ this._srf = opts.srf;
+ this.online = true;
+
+ this._interval = setInterval(this._ping.bind(this, `sip:${opts.uri}`), this._interval);
+
+ this
+ .on('offline', (uri) => offlineUris.add(uri))
+ .on('online', (uri) => offlineUris.delete(uri));
+
+ this._srf.on('disconnect', () => {
+ this.stopPinging();
+ });
+ }
+
+ static isOffline(uri) {
+ return offlineUris.has(`sip:${uri}`);
+ }
+
+ get logger() {
+ return this._logger;
+ }
+
+ stopPinging() {
+ clearInterval(this._interval);
+
+ }
+
+ _ping(uri) {
+
+ // set a timeout to wait for a response
+ const timeout = setTimeout(() => {
+ if (this.online) {
+ this.online = false;
+ this.emit('offline', uri);
+ }
+ }, this._timeout);
+
+ // send an OPTIONS ping
+ this._srf.request({
+ uri: uri,
+ method: 'OPTIONS'
+ })
+ .then((req) => {
+ return req.on('response', (res) => {
+ clearTimeout(timeout);
+ const status = res.status;
+ if (200 === status && !this.online) {
+ this.online = true;
+ this.emit('online', uri);
+ }
+ else if (200 !== status && this.online) {
+ this.online = false;
+ this.emit('offline', uri);
+ }
+ });
+ })
+ .catch((err) => {
+ this.logger.error(err, `Error sending OPTIONS ping tp ${uri}`);
+ });
+ }
+
+}
+
+module.exports = PingTester;
diff --git a/lib/validation-middleware.js b/lib/validation-middleware.js
new file mode 100644
index 0000000..7eaea52
--- /dev/null
+++ b/lib/validation-middleware.js
@@ -0,0 +1,12 @@
+module.exports = ({logger, routeMgr}) => {
+ return (req, res, next) => {
+ req.locals = {};
+ req.locals.source = routeMgr.isValidSendingAddress(`${req.source_address}:${req.source_port}`);
+ if (!req.locals.source) {
+ logger.info(`rejecting INVITE from invalid source address ${req.source_address}`);
+ return res.send(403);
+ }
+
+ next();
+ };
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..b3925ea
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1207 @@
+{
+ "name": "simple-sip-proxy",
+ "version": "1.1.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "abbrev": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
+ "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=",
+ "dev": true
+ },
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+ "dev": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "async": {
+ "version": "1.5.2",
+ "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "blue-tape": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/blue-tape/-/blue-tape-1.0.0.tgz",
+ "integrity": "sha1-dYHQTAc5XJXEJrLtbR7bRUp2uSs=",
+ "dev": true,
+ "requires": {
+ "tape": ">=2.0.0 <5.0.0"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer-shims": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
+ "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=",
+ "dev": true
+ },
+ "caller-callsite": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
+ "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
+ "requires": {
+ "callsites": "^2.0.0"
+ }
+ },
+ "caller-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
+ "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
+ "requires": {
+ "caller-callsite": "^2.0.0"
+ }
+ },
+ "callsites": {
+ "version": "2.0.0",
+ "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
+ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA="
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "clear-module": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-3.0.0.tgz",
+ "integrity": "sha512-T8RArhIvOMPJ0w2OenM2W4ua6bsJ7BM6V7jbpfr+Ev5nhCGxKZ2cNE2BZylDz+0Pc1BtT7wiGBGR1N0t3MlGCg==",
+ "requires": {
+ "caller-path": "^2.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "dependencies": {
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+ }
+ }
+ },
+ "colors": {
+ "version": "1.0.3",
+ "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
+ "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs="
+ },
+ "commander": {
+ "version": "2.17.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+ "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+ "dev": true,
+ "optional": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "config": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/config/-/config-2.0.1.tgz",
+ "integrity": "sha512-aTaviJnC8ZjQYx8kQf4u6tWqIxWolyQQ3LqXgnCLAsIb78JrUshHG0YuzIarzTaVVe1Pazms3TXImfYra8UsyQ==",
+ "requires": {
+ "json5": "^1.0.1"
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+ "dev": true
+ },
+ "cycle": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
+ "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI="
+ },
+ "debug": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
+ "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
+ "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+ "dev": true
+ },
+ "delegates": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-0.1.0.tgz",
+ "integrity": "sha1-tLV74RoWU1F6BLJ/CUm9wyff45A="
+ },
+ "deprecate": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/deprecate/-/deprecate-1.1.0.tgz",
+ "integrity": "sha512-b5dDNQYdy2vW9WXUD8+RQlfoxvqztLLhDE+T7Gd37I5E8My7nJkKu6FmhdDeRWJ8B+yjZKuwjCta8pgi8kgSqA=="
+ },
+ "drachtio-mw-registration-parser": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/drachtio-mw-registration-parser/-/drachtio-mw-registration-parser-0.0.2.tgz",
+ "integrity": "sha1-Bmlv43Wvt/68AI8oaCJWukEHWAE="
+ },
+ "drachtio-sip": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/drachtio-sip/-/drachtio-sip-0.3.6.tgz",
+ "integrity": "sha512-KqlpGr9RTUB2uR1+7jAFul1+84Jd11GlEUgtB89pushWVEGpO+ejqwACnjyk+OIT/98rctgRoo2QP2xyQVRnzw==",
+ "requires": {
+ "debug": "*",
+ "merge": "^1.2.1",
+ "only": "0.0.2",
+ "sip-status": "~0.1.0"
+ }
+ },
+ "drachtio-srf": {
+ "version": "github:davehorton/drachtio-srf#52788f9f25a77dd5a4b314b171c391606f51d1e7",
+ "from": "github:davehorton/drachtio-srf#v4.4.1-rc2",
+ "requires": {
+ "async": "^1.4.2",
+ "debug": "^3.1.0",
+ "delegates": "^0.1.0",
+ "deprecate": "^1.0.0",
+ "drachtio-mw-registration-parser": "0.0.2",
+ "drachtio-sip": "^0.3.6",
+ "lodash": "^4.17.10",
+ "node-noop": "0.0.1",
+ "only": "0.0.2",
+ "sip-methods": "^0.3.0",
+ "utils-merge": "1.0.0",
+ "uuid": "^3.0.0",
+ "winston": "^2.4.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "duplexer": {
+ "version": "0.1.1",
+ "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
+ "dev": true
+ },
+ "end-of-stream": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+ "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "es-abstract": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz",
+ "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==",
+ "dev": true,
+ "requires": {
+ "es-to-primitive": "^1.1.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.1",
+ "is-callable": "^1.1.3",
+ "is-regex": "^1.0.4"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
+ "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "escodegen": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
+ "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=",
+ "dev": true,
+ "requires": {
+ "esprima": "^2.7.1",
+ "estraverse": "^1.9.1",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.2.0"
+ }
+ },
+ "esprima": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+ "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
+ "dev": true
+ },
+ "estraverse": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+ "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "dev": true
+ },
+ "eyes": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
+ "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A="
+ },
+ "fast-json-parse": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz",
+ "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw=="
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "fast-redact": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-1.3.0.tgz",
+ "integrity": "sha512-ci4qKDR8nDzQCQTPw4hviyDFaSwTgSYiqddRh/EslkUQa0otpzM8IGnuG+LwiUE54t4OjU2T7bYKmjtd7632Wg=="
+ },
+ "fast-safe-stringify": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz",
+ "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg=="
+ },
+ "figures": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "flatstr": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.8.tgz",
+ "integrity": "sha512-YXblbv/vc1zuVVUtnKl1hPqqk7TalZCppnKE7Pr8FI/Rp48vzckS/4SJ4Y9O9RNiI82Vcw/FydmtqdQOg1Dpqw=="
+ },
+ "for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "handlebars": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz",
+ "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==",
+ "dev": true,
+ "requires": {
+ "async": "^2.5.0",
+ "optimist": "^0.6.1",
+ "source-map": "^0.6.1",
+ "uglify-js": "^3.1.4"
+ },
+ "dependencies": {
+ "async": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+ "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.10"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+ "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+ "dev": true
+ },
+ "is-date-object": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+ "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+ "dev": true
+ },
+ "is-finite": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-regex": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.1"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
+ "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+ },
+ "istanbul": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz",
+ "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1.0.x",
+ "async": "1.x",
+ "escodegen": "1.8.x",
+ "esprima": "2.7.x",
+ "glob": "^5.0.15",
+ "handlebars": "^4.0.1",
+ "js-yaml": "3.x",
+ "mkdirp": "0.5.x",
+ "nopt": "3.x",
+ "once": "1.x",
+ "resolve": "1.1.x",
+ "supports-color": "^3.1.0",
+ "which": "^1.1.1",
+ "wordwrap": "^1.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "dev": true,
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "resolve": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+ "dev": true,
+ "requires": {
+ "has-flag": "^1.0.0"
+ }
+ }
+ }
+ },
+ "js-yaml": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+ "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "dependencies": {
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ }
+ }
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+ },
+ "merge": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
+ "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "node-noop": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/node-noop/-/node-noop-0.0.1.tgz",
+ "integrity": "sha1-CFzLsA3AY6eb5EwpDcqLpcXVQVM="
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
+ },
+ "object-inspect": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz",
+ "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
+ "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "only": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
+ "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q="
+ },
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "dev": true,
+ "requires": {
+ "minimist": "~0.0.1",
+ "wordwrap": "~0.0.2"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.10",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+ "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
+ "dev": true
+ },
+ "wordwrap": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+ "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+ "dev": true
+ }
+ }
+ },
+ "optionator": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.4",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "wordwrap": "~1.0.0"
+ }
+ },
+ "parse-ms": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
+ "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "pino": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-5.8.1.tgz",
+ "integrity": "sha512-7bVFzUw3ffIfOM3t7MuQ9KsH+wX5bdGdQhGfccKgleoY7qG4FO3CmVSjywlFmmYGyMOISi1LDGC6JMEH7XkZJg==",
+ "requires": {
+ "fast-json-parse": "^1.0.3",
+ "fast-redact": "^1.2.0",
+ "fast-safe-stringify": "^2.0.6",
+ "flatstr": "^1.0.5",
+ "pino-std-serializers": "^2.3.0",
+ "pump": "^3.0.0",
+ "quick-format-unescaped": "^3.0.0",
+ "sonic-boom": "^0.6.1"
+ }
+ },
+ "pino-std-serializers": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.3.0.tgz",
+ "integrity": "sha512-klfGoOsP6sJH7ON796G4xoUSx2fkpFgKHO4YVVO2zmz31jR+etzc/QzGJILaOIiCD6HTCFgkPx+XN8nk+ruqPw=="
+ },
+ "plur": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz",
+ "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "dev": true
+ },
+ "pretty-ms": {
+ "version": "2.1.0",
+ "resolved": "http://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz",
+ "integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=",
+ "dev": true,
+ "requires": {
+ "is-finite": "^1.0.1",
+ "parse-ms": "^1.0.0",
+ "plur": "^1.0.0"
+ }
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
+ "dev": true
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "quick-format-unescaped": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-3.0.1.tgz",
+ "integrity": "sha512-Tnk4iJQ8x3V8ml3x9sLIf4tSDaVB9OJY/5gOrnxgK63CXKphhn8oYOPI4tqnXPQcZ3tCv7GFjeoYY5h6UAvuzg=="
+ },
+ "re-emitter": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.3.tgz",
+ "integrity": "sha1-+p4xn/3u6zWycpbvDz03TawvUqc=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
+ "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.5"
+ }
+ },
+ "resumer": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz",
+ "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=",
+ "dev": true,
+ "requires": {
+ "through": "~2.3.4"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "sip-message-examples": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/sip-message-examples/-/sip-message-examples-0.0.4.tgz",
+ "integrity": "sha1-5Pi6yfrDjl7xt9gkbYCJNumqNx8=",
+ "dev": true
+ },
+ "sip-methods": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/sip-methods/-/sip-methods-0.3.0.tgz",
+ "integrity": "sha1-6KKiO7EEKPuem1hCv2Q9TrPrnPM="
+ },
+ "sip-status": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/sip-status/-/sip-status-0.1.0.tgz",
+ "integrity": "sha1-fHvT+cvfLQ09g6AGwxyAlVJdH5c="
+ },
+ "sonic-boom": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-0.6.2.tgz",
+ "integrity": "sha512-JVftM6ZJanmU/akt+bfiHUKQq0MtRe0ayXyEXjB1yiZYRH6ettF4gu7Dbei4HbzTmBVNshHpPJLZ9R9lY2FjWA==",
+ "requires": {
+ "flatstr": "^1.0.8"
+ }
+ },
+ "source-map": {
+ "version": "0.2.0",
+ "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+ "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "amdefine": ">=0.0.4"
+ }
+ },
+ "split": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
+ "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
+ "dev": true,
+ "requires": {
+ "through": "2"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
+ },
+ "string.prototype.trim": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz",
+ "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "es-abstract": "^1.5.0",
+ "function-bind": "^1.0.2"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ },
+ "tap-dot": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tap-dot/-/tap-dot-2.0.0.tgz",
+ "integrity": "sha512-7N1yPcRDgdfHCUbG6lZ0hXo53NyXhKIjJNhqKBixl9HVEG4QasG16Nlvr8wRnqr2ZRYVWmbmxwF3NOBbTLtQLQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.1",
+ "tap-out": "^1.3.2",
+ "through2": "^2.0.0"
+ }
+ },
+ "tap-out": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/tap-out/-/tap-out-1.4.2.tgz",
+ "integrity": "sha1-yQfsG/lAURHQiCY+kvVgi4jLs3o=",
+ "dev": true,
+ "requires": {
+ "re-emitter": "^1.0.0",
+ "readable-stream": "^2.0.0",
+ "split": "^1.0.0",
+ "trim": "0.0.1"
+ }
+ },
+ "tap-spec": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tap-spec/-/tap-spec-5.0.0.tgz",
+ "integrity": "sha512-zMDVJiE5I6Y4XGjlueGXJIX2YIkbDN44broZlnypT38Hj/czfOXrszHNNJBF/DXR8n+x6gbfSx68x04kIEHdrw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.0.0",
+ "duplexer": "^0.1.1",
+ "figures": "^1.4.0",
+ "lodash": "^4.17.10",
+ "pretty-ms": "^2.1.0",
+ "repeat-string": "^1.5.2",
+ "tap-out": "^2.1.0",
+ "through2": "^2.0.0"
+ },
+ "dependencies": {
+ "process-nextick-args": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.2.9",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz",
+ "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=",
+ "dev": true,
+ "requires": {
+ "buffer-shims": "~1.0.0",
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~1.0.6",
+ "string_decoder": "~1.0.0",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "split": {
+ "version": "1.0.0",
+ "resolved": "http://registry.npmjs.org/split/-/split-1.0.0.tgz",
+ "integrity": "sha1-xDlc5oOrzSVLwo/h2rtuXCfc/64=",
+ "dev": true,
+ "requires": {
+ "through": "2"
+ }
+ },
+ "string_decoder": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+ "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "tap-out": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tap-out/-/tap-out-2.1.0.tgz",
+ "integrity": "sha512-LJE+TBoVbOWhwdz4+FQk40nmbIuxJLqaGvj3WauQw3NYYU5TdjoV3C0x/yq37YAvVyi+oeBXmWnxWSjJ7IEyUw==",
+ "dev": true,
+ "requires": {
+ "re-emitter": "1.1.3",
+ "readable-stream": "2.2.9",
+ "split": "1.0.0",
+ "trim": "0.0.1"
+ }
+ }
+ }
+ },
+ "tape": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/tape/-/tape-4.9.1.tgz",
+ "integrity": "sha512-6fKIXknLpoe/Jp4rzHKFPpJUHDHDqn8jus99IfPnHIjyz78HYlefTGD3b5EkbQzuLfaEvmfPK3IolLgq2xT3kw==",
+ "dev": true,
+ "requires": {
+ "deep-equal": "~1.0.1",
+ "defined": "~1.0.0",
+ "for-each": "~0.3.3",
+ "function-bind": "~1.1.1",
+ "glob": "~7.1.2",
+ "has": "~1.0.3",
+ "inherits": "~2.0.3",
+ "minimist": "~1.2.0",
+ "object-inspect": "~1.6.0",
+ "resolve": "~1.7.1",
+ "resumer": "~0.0.0",
+ "string.prototype.trim": "~1.1.2",
+ "through": "~2.3.8"
+ }
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
+ },
+ "through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "dev": true,
+ "requires": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "trim": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+ "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "uglify-js": {
+ "version": "3.4.9",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
+ "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "commander": "~2.17.1",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
+ "utils-merge": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
+ "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg="
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "winston": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz",
+ "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==",
+ "requires": {
+ "async": "~1.0.0",
+ "colors": "1.0.x",
+ "cycle": "1.0.x",
+ "eyes": "0.1.x",
+ "isstream": "0.1.x",
+ "stack-trace": "0.0.x"
+ },
+ "dependencies": {
+ "async": {
+ "version": "1.0.0",
+ "resolved": "http://registry.npmjs.org/async/-/async-1.0.0.tgz",
+ "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k="
+ }
+ }
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
index 127c0cf..5f721ef 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,13 @@
{
"name": "simple-sip-proxy",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "Simple load balancing sip proxy",
"main": "app.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "NODE_ENV=test node test/ | ./node_modules/.bin/tap-spec",
+ "coverage": "NODE_ENV=test ./node_modules/.bin/istanbul cover test/ --report html",
+ "debug-test": "NODE_ENV=test node --nolazy --inspect-brk=9229 test/",
+ "jslint": "eslint lib"
},
"repository": {
"type": "git",
@@ -22,9 +25,18 @@
},
"homepage": "https://github.com/davehorton/simple-sip-proxy#readme",
"dependencies": {
- "debug": "*",
- "drachtio": "davehorton/drachtio.git",
- "lodash": "~2.4.1",
- "range_check": "0.0.5"
- }
+ "clear-module": "^3.0.0",
+ "config": "^2.0.1",
+ "drachtio-srf": "davehorton/drachtio-srf#v4.4.1-rc2",
+ "pino": "^5.8.1"
+ },
+ "devDependencies": {
+ "blue-tape": "^1.0.0",
+ "debug": "^4.1.0",
+ "drachtio-sip": "^0.3.6",
+ "istanbul": "^0.4.5",
+ "sip-message-examples": "0.0.4",
+ "tap-dot": "^2.0.0",
+ "tap-spec": "^5.0.0"
+ }
}
diff --git a/test-1.json b/test-1.json
new file mode 100644
index 0000000..e69de29
diff --git a/test/data/invite.js b/test/data/invite.js
new file mode 100644
index 0000000..9d85fa6
--- /dev/null
+++ b/test/data/invite.js
@@ -0,0 +1,19 @@
+const examples = require('sip-message-examples') ;
+const SipMessage = require('drachtio-srf').SipMessage;
+const SipRequest = require('drachtio-srf').SipRequest;
+
+module.exports = function({from, to, source_address, source_port}) {
+ const m = examples('invite');
+ const msg = new SipMessage(m);
+ const req = new SipRequest(msg, {
+ source: 'network',
+ source_address: source_address || '127.0.0.1',
+ source_port: source_port || 5060,
+ protocol: 'udp',
+ stackTime: 'na',
+ stackTxnId: '0xdeadbeef',
+ stackDialogId: '0xdeadbeef'
+ });
+
+ return req;
+};
diff --git a/test/docker-compose-testbed.yaml b/test/docker-compose-testbed.yaml
new file mode 100644
index 0000000..d604f7d
--- /dev/null
+++ b/test/docker-compose-testbed.yaml
@@ -0,0 +1,74 @@
+version: '2'
+
+networks:
+ testbed:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.19.0.0/16
+
+services:
+ drachtio:
+ image: drachtio/drachtio-server:latest
+ command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
+ container_name: drachtio
+ ports:
+ - "9061:9022/tcp"
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.10
+
+ as1:
+ image: davehorton/freeswitch-hairpin:latest
+ container_name: freeswitch-1
+ command: freeswitch --sip-port 5060
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.21
+
+ as2:
+ image: davehorton/freeswitch-hairpin:latest
+ command: freeswitch --sip-port 5060
+ container_name: freeswitch-2
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.22
+
+ as3:
+ image: davehorton/freeswitch-hairpin:latest
+ command: freeswitch --sip-port 5060
+ container_name: freeswitch-3
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.23
+
+ sipp-uas-options-ok:
+ image: drachtio/sipp:latest
+ command: sipp -sf /tmp/uas-options-200.xml
+ volumes:
+ - ./scenarios:/tmp
+ tty: true
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.110
+
+ sipp-uas-options-fail:
+ image: drachtio/sipp:latest
+ command: sipp -sf /tmp/uas-options-503.xml
+ volumes:
+ - ./scenarios:/tmp
+ tty: true
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.111
+
+ sipp-uas:
+ image: drachtio/sipp:latest
+ command: sipp -sf /tmp/uas.xml
+ volumes:
+ - ./scenarios:/tmp
+ tty: true
+ networks:
+ testbed:
+ ipv4_address: 172.19.0.112
+
diff --git a/test/docker-start.js b/test/docker-start.js
new file mode 100644
index 0000000..3c20c58
--- /dev/null
+++ b/test/docker-start.js
@@ -0,0 +1,42 @@
+const test = require('blue-tape');
+const exec = require('child_process').exec ;
+const async = require('async');
+
+test('starting docker network..', (t) => {
+ exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
+ if (-1 != stderr.indexOf('is up-to-date')) return t.end() ;
+ console.log('docker network started, giving extra time for freeswitch to initialize...');
+ testFreeswitches(['freeswitch-1', 'freeswitch-2', 'freeswitch-3'], 40000, (err) => {
+ t.end(err);
+ });
+ });
+});
+
+function testFreeswitches(arr, timeout, callback) {
+ let timeup = false;
+ const timer = setTimeout(() => {
+ timeup = true;
+ }, timeout);
+
+ async.whilst(
+ () => !timeup && arr.length,
+ (callback) => setTimeout(() => async.each(arr, testOneFsw.bind(null, arr), () => callback()), 1000),
+ () => {
+ if (arr.length > 0) {
+ clearTimeout(timer);
+ return callback(new Error('some freeswitches did not initialize'));
+ }
+ callback(null);
+ }
+ );
+}
+
+function testOneFsw(arr, fsw, callback) {
+ exec(`docker exec ${fsw} fs_cli -x "console loglevel debug"`, (err, stdout, stderr) => {
+ if (!err) {
+ const idx = arr.indexOf(fsw);
+ arr.splice(idx, 1);
+ }
+ callback(null);
+ });
+}
diff --git a/test/docker-stop.js b/test/docker-stop.js
new file mode 100644
index 0000000..44f8892
--- /dev/null
+++ b/test/docker-stop.js
@@ -0,0 +1,14 @@
+const test = require('blue-tape');
+const exec = require('child_process').exec ;
+
+test('stopping docker network..', (t) => {
+ t.timeoutAfter(20000);
+ exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml down`, (err, stdout, stderr) => {
+ //console.log(`stdout: ${stdout}`);
+ //if (stderr.length) console.log(`stderr: ${stderr}`);
+ t.pass('docker network stopped');
+ t.end() ;
+ process.exit(0);
+ });
+});
+
diff --git a/test/e2e.js b/test/e2e.js
new file mode 100644
index 0000000..317cdc0
--- /dev/null
+++ b/test/e2e.js
@@ -0,0 +1,64 @@
+const test = require('blue-tape');
+const clearModule = require('clear-module');
+const PingTester = require('../lib/utils/ping-tester');
+
+process.on('unhandledRejection', (reason, p) => {
+ console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
+});
+
+test('end-to-end tests', (t) => {
+ t.timeoutAfter(30000);
+
+ clearModule('config');
+ clearModule('../lib/routing/route-manager');
+ process.env.NODE_APP_INSTANCE = 100;
+
+ let srf, obj;
+
+ Promise.resolve()
+ .then(() => {
+ srf = require('../app');
+ return new Promise((resolve, reject) => {
+ srf.on('connect', (err) => {
+ if (err) return reject(err);
+ resolve();
+ });
+ });
+ })
+ .then(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ t.ok(!PingTester.isOffline('172.19.0.110'), 'server marked online by responding 200 to OPTIONS');
+ t.ok(PingTester.isOffline('172.19.0.111'), 'server marked offline by responding non-200 to OPTIONS');
+ t.ok(PingTester.isOffline('172.19.0.121'), 'server marked offline by not responding to OPTIONS');
+ resolve();
+ }, 4000);
+ });
+ })
+ .then(() => {
+ obj = require('./sipp')('test_testbed', '172.19.0.50');
+ return obj.sippUac('uac-expect-403.xml');
+ })
+ .then(() => {
+ return t.pass('INVITE from unknown carrier / trunk rejected with 403');
+ })
+ .then(() => {
+ obj = require('./sipp')('test_testbed', '172.19.0.100');
+ return obj.sippUac('uac.xml');
+ })
+ .then(() => {
+ return t.pass('INVITE successfully proxied with defaults when no routing specified');
+ })
+ .then(() => {
+ srf.emit('disconnect');
+ srf.disconnect();
+ t.end();
+ return new Promise((resolve) => setTimeout(() => resolve(), 1000));
+ })
+ .catch((err) => {
+ if (srf) srf.disconnect();
+ console.log(`error received: ${err}`);
+ //console.log(obj.output());
+ t.error(err);
+ });
+});
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..46714c7
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,9 @@
+require('./target');
+require('./route');
+require('./route-set');
+require('./route-manager');
+
+// end-to-end tests require docker
+require('./docker-start');
+require('./e2e');
+require('./docker-stop');
diff --git a/test/route-manager.js b/test/route-manager.js
new file mode 100644
index 0000000..489825b
--- /dev/null
+++ b/test/route-manager.js
@@ -0,0 +1,31 @@
+const test = require('blue-tape');
+const makeInviteRequest = require('./data/invite');
+const clearModule = require('clear-module');
+
+test('RouteManager', (t) => {
+ process.env.NODE_APP_INSTANCE = 1;
+
+ clearModule('config');
+
+ const RouteManager = require('../lib/routing/route-manager');
+ const mgr = new RouteManager({});
+ t.equal(mgr.inboundExternalTrunks.length, 1, '1 enabled inbound outside trunks');
+ t.equal(mgr.outboundExternalTrunks.length, 2, '2 enabled outbound outside trunks');
+ t.equal(mgr.internalTrunks.length, 2, '2 enabled inside trunks');
+
+ const req = makeInviteRequest({
+ from: '+16173333456',
+ to: '+16173333456',
+ source_address: '10.10.100.3',
+ source_port: 5060
+ });
+ let uri = mgr.chooseRoute('inside', req);
+ t.equal(uri, '10.10.200.1', 'selects correct target first time');
+ uri = mgr.chooseRoute('inside', req);
+ t.equal(uri, '10.10.200.1', 'selects correct target second time');
+
+ t.ok(mgr.isValidSendingAddress('10.10.100.7'), 'allows valid sending source');
+ t.notOk(mgr.isValidSendingAddress('10.10.100.3'), 'detects invalid sending source');
+
+ t.end();
+});
diff --git a/test/route-set.js b/test/route-set.js
new file mode 100644
index 0000000..969e2bb
--- /dev/null
+++ b/test/route-set.js
@@ -0,0 +1,44 @@
+const test = require('blue-tape');
+const makeInviteRequest = require('./data/invite');
+const clearModule = require('clear-module');
+
+test('RouteSet', (t) => {
+ process.env.NODE_APP_INSTANCE = 1;
+ let config = require('config');
+
+ let RouteSet = require('../lib/routing/route-set');
+ let rsInside = new RouteSet('inside', config);
+ t.ok(rsInside.hasDefaultRoute, 'default route is loaded from config');
+
+ const req = makeInviteRequest({
+ from: '+16173333456',
+ to: '+16173333456',
+ source_address: '10.10.100.3',
+ source_port: 5060
+ });
+
+ let uri = rsInside.evaluate(req);
+ t.equal(uri, '10.10.200.1', 'default route selected');
+
+ let rsOutside = new RouteSet('outside', config);
+ uri = rsOutside.evaluate(req);
+ t.equal(uri, '10.10.100.6', 'selected conditional route');
+
+ clearModule('config');
+ clearModule('../lib/routing/route-set');
+ process.env.NODE_APP_INSTANCE = 2;
+
+ config = require('config');
+ RouteSet = require('../lib/routing/route-set');
+
+ rsInside = new RouteSet('inside', config);
+ t.ok(!rsInside.hasDefaultRoute, 'route.inside is optional');
+ uri = rsInside.evaluate(req);
+ t.notOk(uri, 'empty route set can not return a route');
+
+ rsInside.setDefaultRoute('10.10.200.1');
+ uri = rsInside.evaluate(req);
+ t.equal(uri, '10.10.200.1', 'default route can be set');
+
+ t.end();
+});
diff --git a/test/route.js b/test/route.js
new file mode 100644
index 0000000..50f94c3
--- /dev/null
+++ b/test/route.js
@@ -0,0 +1,42 @@
+const test = require('blue-tape');
+const makeInviteRequest = require('./data/invite');
+const Route = require('../lib/routing/route');
+
+test('Route', (t) => {
+ process.env.NODE_APP_INSTANCE = 1;
+ const config = require('config');
+
+ let route = new Route('default', '10.10.100.1', config);
+ const req = makeInviteRequest({
+ from: '+16173333456',
+ to: '+16173333456',
+ source_address: '10.10.100.3',
+ source_port: 5060
+ });
+
+ t.equal(route.evaluate(req), '10.10.100.1', 'default route with single entry works');
+
+ route = new Route('default', ['10.10.100.1'], config);
+ t.equal(route.evaluate(req), '10.10.100.1', 'default route with single element array works');
+
+ route = new Route('work', {
+ regex: '^5753606$',
+ match: '10.10.100.2'
+ }, config);
+ t.equal(route.evaluate(req), '10.10.100.2', 'regex exact match on calling number works');
+
+ route = new Route('work', {
+ regex: '^4083084809$',
+ match: '10.10.100.2',
+ against: 'from'
+ }, config);
+ t.equal(route.evaluate(req), '10.10.100.2', 'regex exact match on calling number works');
+
+ route = new Route('work', {
+ regex: '^57566',
+ 'no-match': '10.10.100.2'
+ }, config);
+ t.equal(route.evaluate(req), '10.10.100.2', 'regex no-match on calling number works');
+
+ t.end();
+});
diff --git a/test/scenarios/uac-expect-403.xml b/test/scenarios/uac-expect-403.xml
new file mode 100644
index 0000000..8e2e048
--- /dev/null
+++ b/test/scenarios/uac-expect-403.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 INVITE
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ Subject: Performance Test
+ 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
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service] [peer_tag_param]
+ Call-ID: [call_id]
+ CSeq: 1 ACK
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ Subject: Performance Test
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uac.xml b/test/scenarios/uac.xml
new file mode 100644
index 0000000..1c60555
--- /dev/null
+++ b/test/scenarios/uac.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service]
+ Call-ID: [call_id]
+ CSeq: 1 INVITE
+ Contact: sip:sipp@[local_ip]:[local_port]
+ Max-Forwards: 70
+ X-Return-Token: foobar
+ Subject: Performance Test
+ 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
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service] [peer_tag_param]
+ Call-ID: [call_id]
+ CSeq: 1 ACK
+ [routes]
+ Max-Forwards: 70
+ Subject: Performance Test
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+ ;tag=[pid]SIPpTag00[call_number]
+ To: [service] [peer_tag_param]
+ Call-ID: [call_id]
+ CSeq: 2 BYE
+ Max-Forwards: 70
+ [routes]
+ Subject: Performance Test
+ Content-Length: 0
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uas-options-200.xml b/test/scenarios/uas-options-200.xml
new file mode 100644
index 0000000..f04a3e1
--- /dev/null
+++ b/test/scenarios/uas-options-200.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uas-options-503.xml b/test/scenarios/uas-options-503.xml
new file mode 100644
index 0000000..4c36047
--- /dev/null
+++ b/test/scenarios/uas-options-503.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/scenarios/uas.xml b/test/scenarios/uas.xml
new file mode 100644
index 0000000..144ba25
--- /dev/null
+++ b/test/scenarios/uas.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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..51ba1c5
--- /dev/null
+++ b/test/sipp.js
@@ -0,0 +1,67 @@
+const { spawn } = require('child_process');
+const debug = require('debug')('drachtio:test');
+let network, ip;
+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, ipAddress) => {
+ network = networkName ;
+ ip = ipAddress;
+ return obj;
+};
+
+obj.output = () => {
+ return output;
+};
+
+obj.sippUac = (file) => {
+ const cmd = 'docker';
+ const args = [
+ 'run', '-ti', '--rm', '--net', network,
+ '--ip', ip,
+ '-v', `${__dirname}/scenarios:/tmp/scenarios`,
+ 'drachtio/sipp', 'sipp', '-sf', `/tmp/scenarios/${file}`,
+ '-m', '1',
+ '-sleep', '250ms',
+ '-nostdin',
+ '-cid_str', `%u-%p@%s-${idx++}`,
+ 'drachtio'
+ ];
+
+ 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.stdout.on('data', (data) => {
+ //debug(`stdout: ${data}`);
+ addOutput(data.toString());
+ });
+ });
+};
diff --git a/test/target.js b/test/target.js
new file mode 100644
index 0000000..a95331b
--- /dev/null
+++ b/test/target.js
@@ -0,0 +1,62 @@
+const test = require('blue-tape');
+const Target = require('../lib/routing/target');
+
+test('Target', (t) => {
+ process.env.NODE_APP_INSTANCE = 1;
+ const config = require('config');
+
+ let target = new Target('10.10.100.1', config);
+ t.deepEqual(target.entries, [{dest: '10.10.100.1', weight: 1}], 'supplying single IP works');
+ t.equal(target.evaluate(), '10.10.100.1', 'single IP evaluates to itself')
+
+ target = new Target(['10.10.100.1', '10.10.100.2'], config);
+ t.deepEqual(target.entries, [{dest: '10.10.100.1', weight: 1}, {dest: '10.10.100.2', weight: 1}],
+ 'supplying array of ips work');
+ const mySet = new Set();
+ mySet.add(target.evaluate());
+ mySet.add(target.evaluate());
+ t.equal(mySet.size, 2, 'round robin choices when equally-weighted trunks provided');
+
+ target = new Target(['10.10.100.1', {dest: '10.10.100.2', weight: 2}], config);
+ t.deepEqual(target.entries, [
+ {dest: '10.10.100.1', weight: 1, floor: 0, ceiling: 33},
+ {dest: '10.10.100.2', weight: 2, floor: 33, ceiling: 100}],
+ 'supplying explicit weights works');
+
+ // to test weighted average we need to generate lots of "calls" and check the resulting ratios
+ const results = {};
+ for (let i = 0; i < 1000; i++) {
+ const uri = target.evaluate();
+ if (!results[uri]) results[uri] = 1;
+ else results[uri]++;
+ }
+ const score1 = Math.floor(results['10.10.100.1'] / 1000 * 100);
+ const score2 = Math.floor(results['10.10.100.2'] / 1000 * 100);
+ t.ok(score1 > 27 && score1 < 38, 'using weighted averages, appserver1 gets ~33%');
+ t.ok(score2 > 62 && score2 < 73, 'using weighted averages, appserver2 gets ~66%');
+
+ target = new Target('appserver1', config);
+ t.deepEqual(target.entries, [{dest: '10.10.200.1', weight: 1}], 'supplying inside trunk name works');
+
+ target = new Target('carrier-2', config);
+ t.deepEqual(target.entries, [{dest: '10.10.100.5', weight: 1}], 'supplying outside trunk name works');
+
+ target = new Target('carrier-4', config);
+ t.deepEqual(target.entries, [], 'supplying disabled outside trunk name results in empty set');
+
+ target = new Target('cheap', config);
+ t.deepEqual(target.entries, [
+ { dest: '10.10.100.5', weight: 1 },
+ { dest: '10.10.100.6', weight: 1 },
+ { dest: '10.10.100.7', weight: 1 }],
+ 'supplying group name works');
+
+ target = new Target([{dest: 'carrier-3', weight: 1}, {dest:'carrier-2', weight: 2}], config);
+ t.deepEqual(target.entries, [
+ {dest: '10.10.100.6', weight: 1, floor: 0, ceiling: 16},
+ {dest: '10.10.100.7', weight: 1, floor: 16, ceiling: 32},
+ {dest: '10.10.100.5', weight: 4, floor: 32, ceiling: 100}],
+ 'weights are properly recalculated when expanding trunk name destinations');
+
+ t.end();
+});