From ca81c04d402b46c1977399a56f1c4f8afc76279e Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 29 Jun 2019 12:18:20 -0700 Subject: [PATCH] MeshCtrl improvements. --- meshctrl.js | 224 ++++++++++++++++++++++++++++++++++++++++++++++++---- meshuser.js | 2 +- 2 files changed, 210 insertions(+), 16 deletions(-) diff --git a/meshctrl.js b/meshctrl.js index aafb539d..dacc44b7 100644 --- a/meshctrl.js +++ b/meshctrl.js @@ -2,23 +2,32 @@ var settings = {}; const args = require('minimist')(process.argv.slice(2)); -const possibleCommands = ['listusers','listgroups','serverinfo','userinfo']; +const possibleCommands = ['listusers', 'listgroups', 'serverinfo', 'userinfo','adduser','removeuser']; //console.log(args); -if (args['_'].length != 1) { - console.log("MeshCtrl is a tool used to perform command line actions on a MeshCentral server."); +if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) { + console.log("MeshCtrl perform command line actions on a MeshCentral server."); console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n"); console.log("Supported actions:"); - console.log(" ServerInfo - Show server information"); - console.log(" UserInfo - Show user information"); - console.log(" ListUsers - List user accounts"); - console.log(" ListGroups - List device groups"); - console.log("\r\nSupported arguments:"); - console.log(" --json - Show result as JSON"); + console.log(" Help [action] - Get help on an action."); + console.log(" ServerInfo - Show server information."); + console.log(" UserInfo - Show user information."); + console.log(" ListUsers - List user accounts."); + console.log(" ListGroups - List device groups."); + console.log(" AddUser - Create a new user account."); + console.log(" RemoveUser - Delete a user account."); + console.log("\r\nSupported login arguments:"); + console.log(" --url [wss://server] - Server url, wss://localhost:443 is default."); + console.log(" --loginuser [username] - Login username, admin is default."); + console.log(" --loginpass [password] - Login password."); + console.log(" --token [number] - 2nd factor authentication token."); + console.log(" --loginkey [hex] - Server login key in hex."); + console.log(" --loginkeyfile [file] - File containing server login key in hex."); + console.log(" --domain [domainid] - Domain id, default is empty."); return; } else { settings.cmd = args['_'][0].toLowerCase(); - if (possibleCommands.indexOf(settings.cmd) == -1) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; } + if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; } //console.log(settings.cmd); var ok = false; @@ -27,30 +36,167 @@ if (args['_'].length != 1) { case 'userinfo': { ok = true; break; } case 'listusers': { ok = true; break; } case 'listgroups': { ok = true; break; } + case 'adduser': { + if (args.user == null) { console.log("New account name missing, use --user [name]"); } + else if (args.pass == null) { console.log("New account password missing, use --pass [password]"); } + else { ok = true; } + break; + } + case 'removeuser': { + if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); } + else { ok = true; } + break; + } + case 'help': { + if (args['_'].length < 2) { + console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.'); + } else { + switch (args['_'][1].toLowerCase()) { + case 'serverinfo': { + console.log("Get information on the MeshCentral server, Example usages:\r\n"); + console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword"); + console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt"); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --json - Show result as JSON."); + break; + } + case 'userinfo': { + console.log("Get account information for the login account, Example usages:\r\n"); + console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword"); + console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt"); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --json - Show result as JSON."); + break; + } + case 'listusers': { + console.log("List the account on the MeshCentral server, Example usages:\r\n"); + console.log(" MeshCtrl ListUsers"); + console.log(" MeshCtrl ListUsers --json"); + console.log(" MeshCtrl ListUsers --nameexists \"bob\""); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --idexists [id] - Return 1 if id exists, 0 if not."); + console.log(" --nameexists [name] - Return id if name exists."); + console.log(" --json - Show result as JSON."); + break; + } + case 'listgroups': { + console.log("List the device groups for this account, Example usages:\r\n"); + console.log(" MeshCtrl ListGroups "); + console.log(" MeshCtrl ListGroups --json"); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --idexists [id] - Return 1 if id exists, 0 if not."); + console.log(" --nameexists [name] - Return id if name exists."); + console.log(" --emailexists [email] - Return id if email exists."); + console.log(" --json - Show result as JSON."); + break; + } + case 'adduser': { + console.log("Add a new user account, Example usages:\r\n"); + console.log(" MeshCtrl AddUser --user newaccountname --pass newpassword"); + console.log("\r\nRequired arguments:\r\n"); + console.log(" --user [name] - New account name."); + console.log(" --pass [password] - New account password."); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --email [email] - New account email address."); + console.log(" --resetpass - Request password reset on next login."); + break; + } + case 'removeuser': { + console.log("Delete a user account, Example usages:\r\n"); + console.log(" MeshCtrl RemoveUser --userid accountid"); + console.log("\r\nRequired arguments:\r\n"); + console.log(" --userid [id] - Account identifier."); + break; + } + default: { + console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.'); + } + } + } + break; + } } - if (ok) serverConnect(); + if (ok) { serverConnect(); } } function serverConnect() { const WebSocket = require('ws'); function onVerifyServer(clientName, certs) { console.log('onVerifyServer', clientName); } - const ws = new WebSocket('wss://localhost/control.ashx', { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }); + var url = 'wss://localhost/control.ashx'; + if (args.url) { + url = args.url; + if (url.length < 5) { console.log("Invalid url."); process.exit(); return; } + if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; } + if (url.endsWith('/') == false) { url += '/'; } + url += 'control.ashx'; + } + + var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer } + + // Password authentication + if (args.loginpass != null) { + var username = 'admin'; + if (args.user != null) { username = args.user; } + var token = ''; + if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); } + options.headers = { 'x-meshauth': Buffer.from(username).toString('base64') + ',' + Buffer.from(args.loginpass).toString('base64') + token } + } + + // Cookie authentication + var ckey = null; + if (args.loginkey != null) { + // User key passed in a argument hex + if (args.loginkey.length != 160) { console.log("Invalid login key."); process.exit(); return; } + ckey = Buffer.from(args.loginkey, 'hex'); + if (ckey != 80) { console.log("Invalid login key."); process.exit(); return; } + } else if (args.loginkeyfile != null) { + // Load key from hex file + var fs = require('fs'); + try { + var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join(''); + ckey = Buffer.from(keydata, 'hex'); + if (ckey.length != 80) { console.log("Invalid login key file."); process.exit(); return; } + } catch (ex) { console.log(ex); process.exit(); return; } + } + + if (ckey != null) { + var domainid = '', username = 'admin'; + if (args.domain != null) { domainid = args.domain; } + if (args.loginuser != null) { username = args.loginuser; } + url += '?auth=' + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey); + } + + const ws = new WebSocket(url, options); //console.log('Connecting...'); ws.on('open', function open() { + //console.log('Connected.'); switch (settings.cmd) { case 'serverinfo': { break; } case 'userinfo': { break; } case 'listusers': { ws.send(JSON.stringify({ action: 'users' })); break; } case 'listgroups': { ws.send(JSON.stringify({ action: 'meshes' })); break; } + case 'adduser': { + var op = { action: 'adduser', username: args.user, pass: args.pass }; + if (args.email) { op.email = args.email; } + if (args.resetpass) { op.resetNextLogin = true; } + ws.send(JSON.stringify(op)); + break; + } + case 'removeuser': { + var op = { action: 'deleteuser', userid: args.userid }; + ws.send(JSON.stringify(op)); + break; + } } }); ws.on('close', function close() { process.exit(); }); ws.on('message', function incoming(rawdata) { + //console.log(rawdata); var data = null; try { data = JSON.parse(rawdata); } catch (ex) { } if (data == null) { console.log('Unable to parse data: ' + rawdata); } @@ -78,10 +224,13 @@ function serverConnect() { break; } case 'users': { // LISTUSERS - console.log('id, name, email\r\n---------------'); if (args.json) { console.log(JSON.stringify(data.users, ' ', 2)); } else { + if (args.idexists) { for (var i in data.users) { const u = data.users[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; } + if (args.nameexists) { for (var i in data.users) { const u = data.users[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; } + + console.log('id, name, email\r\n---------------'); for (var i in data.users) { const u = data.users[i]; var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\""; @@ -93,10 +242,13 @@ function serverConnect() { break; } case 'meshes': { // LISTGROUPS - console.log('id, name\r\n---------------'); if (args.json) { console.log(JSON.stringify(data.meshes, ' ', 2)); } else { + if (args.idexists) { for (var i in data.meshes) { const u = data.meshes[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; } + if (args.nameexists) { for (var i in data.meshes) { const u = data.meshes[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; } + + console.log('id, name\r\n---------------'); for (var i in data.meshes) { const m = data.meshes[i]; var t = "\"" + m._id.split('/')[2] + "\", \"" + m.name + "\""; @@ -106,6 +258,36 @@ function serverConnect() { process.exit(); break; } + case 'close': { + if (data.cause == 'noauth') { + if (data.msg == 'tokenrequired') { + console.log('Authentication token required, use --token [number].'); + } else { + console.log('Invalid login.'); + } + } + process.exit(); + break; + } + case 'event': { + switch (data.event.action) { + case 'accountcreate': { + if ((settings.cmd == 'adduser') && (data.event.account.name == args.user)) { + console.log('Account created, id: ' + data.event.account._id); + process.exit(); + } + break; + } + case 'accountremove': { + if ((settings.cmd == 'removeuser') && (data.event.userid == args.userid)) { + console.log('Account removed'); + process.exit(); + } + break; + } + } + break; + } default: { console.log('Unknown action: ' + data.action); break; @@ -114,4 +296,16 @@ function serverConnect() { //console.log('Data', data); //setTimeout(function timeout() { ws.send(Date.now()); }, 500); }); -} \ No newline at end of file +} + +// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more) +function encodeCookie(o, key) { + var crypto = require('crypto'); + try { + if (key == null) { return null; } + o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time + const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv); + const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]); + return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); + } catch (e) { return null; } +} diff --git a/meshuser.js b/meshuser.js index 9544c574..182e1fb9 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1115,7 +1115,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var event, targets = ['*', 'server-users']; if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } } if (command.email == null) { - event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.user, domain: domain.id }; + event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.username, domain: domain.id }; } else { event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id }; }