From 2bd852cd15ab961537f4bb00caa74ade8ed94b25 Mon Sep 17 00:00:00 2001 From: Hans-Peter Herzog Date: Wed, 26 Oct 2016 09:42:20 +0200 Subject: [PATCH] TT#5036 Create http client to access janus admin api Change-Id: I2d71a012529d9e589737fad9c518265f3ed8d27a --- .editorconfig | 12 ++ .gitignore | 24 ++++ README.md | 100 ++++++++++++++ package.json | 26 ++++ src/admin.js | 230 +++++++++++++++++++++++++++++++++ src/error.js | 20 +++ src/mock/janus-admin-server.js | 225 ++++++++++++++++++++++++++++++++ src/response.js | 43 ++++++ test/admin-spec.js | 168 ++++++++++++++++++++++++ 9 files changed, 848 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 src/admin.js create mode 100644 src/error.js create mode 100644 src/mock/janus-admin-server.js create mode 100644 src/response.js create mode 100644 test/admin-spec.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..759ec5a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*.{js,ts,json,html,css,scss,less,md,gitignore,editorconfig}] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cae7c3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Eclipse +.classpath +.project +.settings/ + +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +log/ +target/ + +# Node.js +node_modules/ +npm-debug.log + +# Bower +bower_components/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9cb83c --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +JanusAdmin +=========== + +JanusAdmin is a node.js http client, that implements the entire admin interface of the Janus WebRTC Gateway. + +Usage +------------- + + var JanusAdmin = require('janus-admin').JanusAdmin; + + var admin = new JanusAdmin({ + url: 'http://janus-admin:7088', + secret: '*****' + }); + +Methods +--------------- + +### List sessions + + admin.listSessions().then((res)=>{ + console.log(res.sessions) + }).catch((err)=>{ + ... + }); + +### List handles + + admin.listHandles(sessionId).then((res)=>{ + console.log(res.handles) + }).catch((err)=>{ + ... + }); + +### Show single handle + + admin.handleInfo(sessionId, handleId).then((res)=>{ + console.log(res.info) + }).catch((err)=>{ + ... + }); + +### Set the log level + + admin.setLogLevel([0...7]).then((res)=>{ + console.log(res.level) + }).catch((err)=>{ + ... + }); + +### Set locking debug + + admin.setLockingDebug([0,1]).then((res)=>{ + console.log(res.debug) + }).catch((err)=>{ + ... + }); + +Methods (token based authentication) +------------------------------------ + +### Add token + + admin.addToken(token).then((res)=>{ + console.log(res.plugins); + }).catch((err)=>{ + ... + }); + +### Allow token + + admin.allowToken(token, plugins).then((res)=>{ + console.log(res.plugins); + }).catch((err)=>{ + ... + }); + +### Disallow token + + admin.disallowToken(token, plugins).then((res)=>{ + console.log(res.plugins); + }).catch((err)=>{ + ... + }); + +### List all tokens + + admin.listTokens().then((res)=>{ + console.log(res.tokens); + }).catch((err)=>{ + ... + }); + +### Remove token + + adminClient.removeToken(token).then((res)=>{ + ... + }).catch((err)=>{ + ... + }); diff --git a/package.json b/package.json new file mode 100644 index 0000000..266df73 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "janus-admin", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "mocha -R spec", + "test-dev": "mocha -R spec -w" + }, + "author": "Hans-Peter Herzog", + "license": "ISC", + "description": "Http client to access the janus admin endpoint", + "devDependencies": { + "body-parser": "^1.15.2", + "express": "^4.14.0", + "mocha": "^3.1.2" + }, + "dependencies": { + "chai": "^3.5.0", + "debug-logger": "^0.4.1", + "lodash": "^4.16.4", + "superagent": "^2.3.0", + "uuid": "^2.0.3", + "validator": "^6.1.0", + "yargs": "^6.3.0" + } +} diff --git a/src/admin.js b/src/admin.js new file mode 100644 index 0000000..2666f9f --- /dev/null +++ b/src/admin.js @@ -0,0 +1,230 @@ +'use strict'; + +var validator = require('validator'); +var _ = require('lodash'); +var createId = require('uuid').v4; +var assert = require('chai').assert; +var userAgent = require('superagent'); +var Response = require('./response').Response; +var ResponseError = require('./error').ResponseError; + +/** + * @class + */ +class Admin { + + constructor(options) { + assert.property(options, 'url', 'Missing option url'); + assert(validator.isURL(options.url), 'Invalid url'); + assert.property(options, 'secret'); + this.url = options.url; + this.secret = options.secret; + this.userAgent = options.userAgent || userAgent; + } + + makeUrl(path) { + return this.url + path; + } + + request(req, options) { + return new Promise((resolve, reject)=>{ + var path = _.get(options, 'path', '/admin'); + req.admin_secret = this.secret; + req.transaction = createId(); + this.userAgent.post(this.makeUrl(path)).type('json').send(req).end((err, res)=>{ + if(_.isObject(err)) { + return reject(err); + } + var response = new Response(req, res.body); + if(response.isSuccess()) { + resolve(response); + } else { + reject(new ResponseError(response)); + } + }); + }); + } + + listSessions() { + return new Promise((resolve, reject)=>{ + this.request({ + janus: 'list_sessions' + }).then((res)=>{ + resolve({ + sessions: _.get(res.getResponse(), 'sessions', []), + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + listHandles(sessionId) { + return new Promise((resolve, reject)=>{ + assert.isString(sessionId); + this.request({ + janus: 'list_handles' + }, { + path: '/admin/' + sessionId + }).then((res)=>{ + resolve({ + handles: _.get(res.getResponse(), 'handles', []), + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + handleInfo(sessionId, handleId) { + return new Promise((resolve, reject)=>{ + assert.isString(sessionId); + assert.isString(handleId); + this.request({ + janus: 'handle_info' + }, { + path: '/admin/' + sessionId + '/' + handleId + }).then((res)=>{ + resolve({ + info: _.get(res.getResponse(), 'info', {}), + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + setLogLevel(level) { + return new Promise((resolve, reject)=>{ + assert.isNumber(level); + assert(_.inRange(level, 0, 8), 'Invalid log level'); + this.request({ + janus: 'set_log_level', + level: level + }).then((res)=>{ + resolve({ + level: res.getResponse().level, + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + setLockingDebug(debug) { + return new Promise((resolve, reject)=>{ + assert.isNumber(debug); + this.request({ + janus: 'set_locking_debug', + debug: debug + }).then((res)=>{ + resolve({ + debug: res.getResponse().debug, + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + addToken(token, plugins) { + return new Promise((resolve, reject)=>{ + assert.isString(token); + var request = { + janus: 'add_token', + token: token + }; + + if(_.isArray(plugins)) { + request.plugins = plugins; + } + + this.request(request).then((res)=>{ + resolve({ + plugins: res.getResponse().data.plugins, + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + allowToken(token, plugins) { + return new Promise((resolve, reject)=>{ + assert.isString(token); + assert.isArray(plugins); + assert(plugins.length > 0, 'Need at least one plugin to allow'); + this.request({ + janus: 'allow_token', + token: token, + plugins: plugins + }).then((res)=>{ + resolve({ + plugins: res.getResponse().data.plugins, + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + disallowToken(token, plugins) { + return new Promise((resolve, reject)=>{ + assert.isString(token); + assert.isArray(plugins); + assert(plugins.length > 0, 'Need at least one plugin to disallow'); + this.request({ + janus: 'disallow_token', + token: token, + plugins: plugins + }).then((res)=>{ + resolve({ + plugins: res.getResponse().data.plugins, + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + listTokens() { + return new Promise((resolve, reject)=>{ + this.request({ + janus: 'list_tokens' + }).then((res)=>{ + resolve({ + tokens: res.getResponse().data.tokens, + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + + removeToken(token) { + return new Promise((resolve, reject)=>{ + this.request({ + janus: 'remove_token', + token: token + }).then((res)=>{ + resolve({ + response: res + }); + }).catch((err)=>{ + reject(err); + }); + }); + } + +} + +module.exports.JanusAdmin = Admin; diff --git a/src/error.js b/src/error.js new file mode 100644 index 0000000..13fd21f --- /dev/null +++ b/src/error.js @@ -0,0 +1,20 @@ +'use strict'; + +var assert = require('chai').assert; + +/** + * @class + */ +class ResponseError extends Error { + + constructor(res) { + super(); + assert.equal(res.isError(), true, 'No error found in response'); + this.name = this.constructor.name; + this.message = res.getErrorMessage(); + this.code = res.getErrorCode(); + this.response = res; + } +} + +module.exports.ResponseError = ResponseError; diff --git a/src/mock/janus-admin-server.js b/src/mock/janus-admin-server.js new file mode 100644 index 0000000..ad8e3d6 --- /dev/null +++ b/src/mock/janus-admin-server.js @@ -0,0 +1,225 @@ +'use strict'; + +process.env.DEBUG = ''; + +var _ = require('lodash'); +var http = require('http'); +var express = require('express'); +var bodyParser = require('body-parser'); +var logger = require('debug-logger')('mock:janus-admin-server'); + +/** + * @class + */ +class JanusAdminServer { + + constructor(options) { + options = options || {}; + this.port = options.port || 9000; + this.app = express(); + this.http = null; + } + + init() { + return new Promise((resolve, reject)=>{ + var requestHandler = (req, res, next)=>{ this.dispatchRequest(req, res, next); } + this.app.use(bodyParser.json()); + this.app.use((req, res, next)=>{ + logger.info('Request', req.originalUrl, req.body); + next(); + }); + this.app.post('/admin', requestHandler); + this.app.post('/admin/:sessionId', requestHandler); + this.app.post('/admin/:sessionId/:handleId', requestHandler); + this.http = http.createServer(this.app); + this.http.listen(this.port, (err)=>{ + if(_.isObject(err)) { + reject(err); + } else { + logger.info('Started'); + resolve(); + } + }); + }); + } + + getUrl() { + return 'http://localhost:' + this.port; + } + + getAdminSecret() { + return 'master'; + } + + dispatchRequest(req, res, next) { + switch(req.body.janus) { + case 'list_sessions': + this.listSessions(req, res, next); + break; + case 'list_handles': + this.listHandles(req, res, next); + break; + case 'handle_info': + this.handleInfo(req, res, next); + break; + case 'set_log_level': + this.setLogLevel(req, res, next); + break; + case 'set_locking_debug': + this.setLockingDebug(req, res, next); + break; + case 'add_token': + this.addToken(req, res, next); + break; + case 'allow_token': + this.allowToken(req, res, next); + break; + case 'disallow_token': + this.disallowToken(req, res, next); + break; + case 'list_tokens': + this.listTokens(req, res, next); + break; + case 'remove_token': + this.removeToken(req, res, next); + break; + } + } + + listSessions(req, res, next) { + res.json({ + janus: 'success', + transaction: req.body.transaction, + session: [] + }); + } + + listHandles(req, res, next) { + res.json({ + janus: 'success', + transaction: req.body.transaction, + session_id: req.params.sessionId, + handles: [] + }); + } + + handleInfo(req, res, next) { + res.json({ + janus: 'success', + transaction: req.body.transaction, + session_id: req.params.sessionId, + handle_id: req.params.handleId, + info: { + + } + }); + } + + setLogLevel(req, res, next) { + res.json({ + janus: 'success', + transaction: req.body.transaction, + level: req.body.level + }); + } + + setLockingDebug(req, res, next) { + res.json({ + janus: 'success', + transaction: req.body.transaction, + debug: req.body.debug + }); + } + + addToken(req, res, next) { + + var plugins = [ + "janus.plugin.audiobridge", + "janus.plugin.voicemail", + "janus.plugin.echotest", + "janus.plugin.recordplay", + "janus.plugin.videoroom", + "janus.plugin.videocall", + "janus.plugin.streaming", + "janus.plugin.sip" + ]; + if(_.isArray(req.body.plugins)) { + plugins = req.body.plugins; + } + + res.json({ + "janus": "success", + "transaction": req.body.transaction, + "data": { + "plugins": plugins + } + }); + } + + allowToken(req, res, next) { + + var plugins = [ + "janus.plugin.audiobridge", + "janus.plugin.voicemail" + ]; + + plugins = req.body.plugins.concat(plugins); + + res.json({ + "janus": "success", + "transaction": req.body.transaction, + "data": { + "plugins": plugins + } + }); + } + + disallowToken(req, res, next) { + + var plugins = [ + "janus.plugin.audiobridge", + "janus.plugin.voicemail" + ]; + + res.json({ + "janus": "success", + "transaction": req.body.transaction, + "data": { + "plugins": plugins + } + }); + } + + listTokens(req, res, next) { + res.json({ + "janus": "success", + "transaction": req.body.transaction, + "data": { + "tokens": [ + { + "token": "abcdef", + "allowed_plugins": [ + "janus.plugin.audiobridge", + "janus.plugin.voicemail", + "janus.plugin.recordplay", + "janus.plugin.videocall", + "janus.plugin.streaming", + "janus.plugin.sip", + "janus.plugin.videoroom", + "janus.plugin.echotest" + ] + } + ] + } + }); + } + + removeToken(req, res, next) { + res.json({ + "janus": "success", + "transaction": req.body.transaction + }); + } +} + +module.exports.JanusAdminServer = JanusAdminServer; diff --git a/src/response.js b/src/response.js new file mode 100644 index 0000000..8a62162 --- /dev/null +++ b/src/response.js @@ -0,0 +1,43 @@ +'use strict'; + +var _ = require('lodash'); +var assert = require('chai').assert; + +/** + * @class + */ +class Response { + + constructor(req, res) { + assert.property(res, 'janus', 'Invalid response'); + this.request = req; + this.response = res; + } + + getRequest() { + return this.request; + } + + getResponse() { + return this.response; + } + + isSuccess() { + return this.response.janus === 'success'; + } + + isError() { + return this.response.janus === 'error' && _.has(this.response, 'error'); + } + + getErrorCode() { + return _.get(this.response, 'error.code', null); + } + + + getErrorMessage() { + return _.get(this.response, 'error.reason', null); + } +} + +module.exports.Response = Response; diff --git a/test/admin-spec.js b/test/admin-spec.js new file mode 100644 index 0000000..4a01249 --- /dev/null +++ b/test/admin-spec.js @@ -0,0 +1,168 @@ +'use strict'; + +var Admin = require('../src/admin').JanusAdmin; +var assert = require('chai').assert; +var _ = require('lodash'); + +var JanusAdminServer = require('../src/mock/janus-admin-server').JanusAdminServer; + +describe('Admin', function() { + + var adminServer; + var adminClient; + + before(function(done){ + adminServer = new JanusAdminServer(); + adminServer.init().then(()=>{ + done(); + }).catch((err)=>{ + done(err); + }); + }); + + beforeEach(function(){ + adminClient = new Admin({ + url: adminServer.getUrl(), + secret: adminServer.getAdminSecret() + }); + }); + + it('should throw an error caused by missing option url', function(done){ + try { + adminClient = new Admin({ + secret: adminServer.getAdminSecret() + }); + } catch(err) { + done(); + } + }); + + it('should throw an error caused by invalid url', function(done){ + try { + adminClient = new Admin({ + url: 'foo', + secret: adminServer.getAdminSecret() + }); + } catch(err) { + done(); + } + }); + + it('should list all sessions', function(done){ + adminClient.listSessions().then((res)=>{ + assert.isArray(res.sessions); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should list all handles', function(done){ + adminClient.listHandles('123456').then((res)=>{ + assert.isArray(res.handles); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should fetch single handle', function(done){ + adminClient.handleInfo('123456', '123456').then((res)=>{ + assert.isObject(res.info); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should set the log level', function(done){ + adminClient.setLogLevel(7).then((res)=>{ + assert.equal(res.level, 7); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should throw an error, caused by wrong log level', function(done){ + adminClient.setLogLevel(8).then(()=>{ + done(new Error()); + }).catch(()=>{ + done(); + }); + }); + + it('should set locking debug', function(done){ + adminClient.setLockingDebug(1).then((res)=>{ + assert.equal(res.debug, 1); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + describe('Token based authentication',function(){ + + var exampleToken = 'abcdef'; + var examplePlugins = [ + 'janus.plugin.videoroom', + 'janus.plugin.echotest' + ]; + + it('should add a new token', function(done){ + adminClient.addToken(exampleToken).then((res)=>{ + assert.isArray(res.plugins); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should add a new token, that has access to a set of given plugins', function(done){ + adminClient.addToken(exampleToken, examplePlugins).then((res)=>{ + assert.isArray(res.plugins); + assert.deepEqual(examplePlugins, res.plugins); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should allow a token to access a set of given plugins', function(done){ + adminClient.allowToken(exampleToken, examplePlugins).then((res)=>{ + assert.isArray(res.plugins); + assert.deepEqual(_.intersection(res.plugins, examplePlugins), examplePlugins); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should disallow a token to access a set of given plugins', function(done){ + adminClient.disallowToken(exampleToken, examplePlugins).then((res)=>{ + assert.isArray(res.plugins); + assert.deepEqual(_.intersection(res.plugins, examplePlugins), []); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should list all tokens', function(done){ + adminClient.listTokens().then((res)=>{ + assert.isArray(res.tokens); + done(); + }).catch((err)=>{ + done(err); + }); + }); + + it('should remove a token', function(done){ + adminClient.removeToken(exampleToken).then((res)=>{ + done(); + }).catch((err)=>{ + done(err); + }); + }); + }); +});