MeshCtrl improvements.

Ylian Saint-Hilaire committed Jun 29, 2019 at 12:18 UTC 14621ab9f653a6f125ce5329f4838ca44536d6fd
2 files changed +210 -16
meshctrl.js
+209 -15
@@ -2,23 +2,32 @@
2
3 var settings = {};
4 const args = require('minimist')(process.argv.slice(2));
5 -const possibleCommands = ['listusers','listgroups','serverinfo','userinfo'];
5 +const possibleCommands = ['listusers', 'listgroups', 'serverinfo', 'userinfo','adduser','removeuser'];
6 //console.log(args);
7
8 -if (args['_'].length != 1) {
9 - console.log("MeshCtrl is a tool used to perform command line actions on a MeshCentral server.");
8 +if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
9 + console.log("MeshCtrl perform command line actions on a MeshCentral server.");
10 console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n");
11 console.log("Supported actions:");
12 - console.log(" ServerInfo - Show server information");
13 - console.log(" UserInfo - Show user information");
14 - console.log(" ListUsers - List user accounts");
15 - console.log(" ListGroups - List device groups");
16 - console.log("\r\nSupported arguments:");
17 - console.log(" --json - Show result as JSON");
12 + console.log(" Help [action] - Get help on an action.");
13 + console.log(" ServerInfo - Show server information.");
14 + console.log(" UserInfo - Show user information.");
15 + console.log(" ListUsers - List user accounts.");
16 + console.log(" ListGroups - List device groups.");
17 + console.log(" AddUser - Create a new user account.");
18 + console.log(" RemoveUser - Delete a user account.");
19 + console.log("\r\nSupported login arguments:");
20 + console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
21 + console.log(" --loginuser [username] - Login username, admin is default.");
22 + console.log(" --loginpass [password] - Login password.");
23 + console.log(" --token [number] - 2nd factor authentication token.");
24 + console.log(" --loginkey [hex] - Server login key in hex.");
25 + console.log(" --loginkeyfile [file] - File containing server login key in hex.");
26 + console.log(" --domain [domainid] - Domain id, default is empty.");
27 return;
28 } else {
29 settings.cmd = args['_'][0].toLowerCase();
21 - if (possibleCommands.indexOf(settings.cmd) == -1) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
30 + if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
31 //console.log(settings.cmd);
32
33 var ok = false;
@@ -27,30 +36,167 @@ if (args['_'].length != 1) {
36 case 'userinfo': { ok = true; break; }
37 case 'listusers': { ok = true; break; }
38 case 'listgroups': { ok = true; break; }
39 + case 'adduser': {
40 + if (args.user == null) { console.log("New account name missing, use --user [name]"); }
41 + else if (args.pass == null) { console.log("New account password missing, use --pass [password]"); }
42 + else { ok = true; }
43 + break;
44 + }
45 + case 'removeuser': {
46 + if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); }
47 + else { ok = true; }
48 + break;
49 + }
50 + case 'help': {
51 + if (args['_'].length < 2) {
52 + console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
53 + } else {
54 + switch (args['_'][1].toLowerCase()) {
55 + case 'serverinfo': {
56 + console.log("Get information on the MeshCentral server, Example usages:\r\n");
57 + console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword");
58 + console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt");
59 + console.log("\r\nOptional arguments:\r\n");
60 + console.log(" --json - Show result as JSON.");
61 + break;
62 + }
63 + case 'userinfo': {
64 + console.log("Get account information for the login account, Example usages:\r\n");
65 + console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
66 + console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt");
67 + console.log("\r\nOptional arguments:\r\n");
68 + console.log(" --json - Show result as JSON.");
69 + break;
70 + }
71 + case 'listusers': {
72 + console.log("List the account on the MeshCentral server, Example usages:\r\n");
73 + console.log(" MeshCtrl ListUsers");
74 + console.log(" MeshCtrl ListUsers --json");
75 + console.log(" MeshCtrl ListUsers --nameexists \"bob\"");
76 + console.log("\r\nOptional arguments:\r\n");
77 + console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
78 + console.log(" --nameexists [name] - Return id if name exists.");
79 + console.log(" --json - Show result as JSON.");
80 + break;
81 + }
82 + case 'listgroups': {
83 + console.log("List the device groups for this account, Example usages:\r\n");
84 + console.log(" MeshCtrl ListGroups ");
85 + console.log(" MeshCtrl ListGroups --json");
86 + console.log("\r\nOptional arguments:\r\n");
87 + console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
88 + console.log(" --nameexists [name] - Return id if name exists.");
89 + console.log(" --emailexists [email] - Return id if email exists.");
90 + console.log(" --json - Show result as JSON.");
91 + break;
92 + }
93 + case 'adduser': {
94 + console.log("Add a new user account, Example usages:\r\n");
95 + console.log(" MeshCtrl AddUser --user newaccountname --pass newpassword");
96 + console.log("\r\nRequired arguments:\r\n");
97 + console.log(" --user [name] - New account name.");
98 + console.log(" --pass [password] - New account password.");
99 + console.log("\r\nOptional arguments:\r\n");
100 + console.log(" --email [email] - New account email address.");
101 + console.log(" --resetpass - Request password reset on next login.");
102 + break;
103 + }
104 + case 'removeuser': {
105 + console.log("Delete a user account, Example usages:\r\n");
106 + console.log(" MeshCtrl RemoveUser --userid accountid");
107 + console.log("\r\nRequired arguments:\r\n");
108 + console.log(" --userid [id] - Account identifier.");
109 + break;
110 + }
111 + default: {
112 + console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
113 + }
114 + }
115 + }
116 + break;
117 + }
118 }
119
32 - if (ok) serverConnect();
120 + if (ok) { serverConnect(); }
121 }
122
123 function serverConnect() {
124 const WebSocket = require('ws');
125
126 function onVerifyServer(clientName, certs) { console.log('onVerifyServer', clientName); }
39 - const ws = new WebSocket('wss://localhost/control.ashx', { rejectUnauthorized: false, checkServerIdentity: onVerifyServer });
127 + var url = 'wss://localhost/control.ashx';
128 + if (args.url) {
129 + url = args.url;
130 + if (url.length < 5) { console.log("Invalid url."); process.exit(); return; }
131 + if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; }
132 + if (url.endsWith('/') == false) { url += '/'; }
133 + url += 'control.ashx';
134 + }
135 +
136 + var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
137 +
138 + // Password authentication
139 + if (args.loginpass != null) {
140 + var username = 'admin';
141 + if (args.user != null) { username = args.user; }
142 + var token = '';
143 + if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); }
144 + options.headers = { 'x-meshauth': Buffer.from(username).toString('base64') + ',' + Buffer.from(args.loginpass).toString('base64') + token }
145 + }
146 +
147 + // Cookie authentication
148 + var ckey = null;
149 + if (args.loginkey != null) {
150 + // User key passed in a argument hex
151 + if (args.loginkey.length != 160) { console.log("Invalid login key."); process.exit(); return; }
152 + ckey = Buffer.from(args.loginkey, 'hex');
153 + if (ckey != 80) { console.log("Invalid login key."); process.exit(); return; }
154 + } else if (args.loginkeyfile != null) {
155 + // Load key from hex file
156 + var fs = require('fs');
157 + try {
158 + var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join('');
159 + ckey = Buffer.from(keydata, 'hex');
160 + if (ckey.length != 80) { console.log("Invalid login key file."); process.exit(); return; }
161 + } catch (ex) { console.log(ex); process.exit(); return; }
162 + }
163 +
164 + if (ckey != null) {
165 + var domainid = '', username = 'admin';
166 + if (args.domain != null) { domainid = args.domain; }
167 + if (args.loginuser != null) { username = args.loginuser; }
168 + url += '?auth=' + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey);
169 + }
170 +
171 + const ws = new WebSocket(url, options);
172 //console.log('Connecting...');
173
174 ws.on('open', function open() {
175 + //console.log('Connected.');
176 switch (settings.cmd) {
177 case 'serverinfo': { break; }
178 case 'userinfo': { break; }
179 case 'listusers': { ws.send(JSON.stringify({ action: 'users' })); break; }
180 case 'listgroups': { ws.send(JSON.stringify({ action: 'meshes' })); break; }
181 + case 'adduser': {
182 + var op = { action: 'adduser', username: args.user, pass: args.pass };
183 + if (args.email) { op.email = args.email; }
184 + if (args.resetpass) { op.resetNextLogin = true; }
185 + ws.send(JSON.stringify(op));
186 + break;
187 + }
188 + case 'removeuser': {
189 + var op = { action: 'deleteuser', userid: args.userid };
190 + ws.send(JSON.stringify(op));
191 + break;
192 + }
193 }
194 });
195
196 ws.on('close', function close() { process.exit(); });
197
198 ws.on('message', function incoming(rawdata) {
199 + //console.log(rawdata);
200 var data = null;
201 try { data = JSON.parse(rawdata); } catch (ex) { }
202 if (data == null) { console.log('Unable to parse data: ' + rawdata); }
@@ -78,10 +224,13 @@ function serverConnect() {
224 break;
225 }
226 case 'users': { // LISTUSERS
81 - console.log('id, name, email\r\n---------------');
227 if (args.json) {
228 console.log(JSON.stringify(data.users, ' ', 2));
229 } else {
230 + 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; }
231 + 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; }
232 +
233 + console.log('id, name, email\r\n---------------');
234 for (var i in data.users) {
235 const u = data.users[i];
236 var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\"";
@@ -93,10 +242,13 @@ function serverConnect() {
242 break;
243 }
244 case 'meshes': { // LISTGROUPS
96 - console.log('id, name\r\n---------------');
245 if (args.json) {
246 console.log(JSON.stringify(data.meshes, ' ', 2));
247 } else {
248 + 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; }
249 + 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; }
250 +
251 + console.log('id, name\r\n---------------');
252 for (var i in data.meshes) {
253 const m = data.meshes[i];
254 var t = "\"" + m._id.split('/')[2] + "\", \"" + m.name + "\"";
@@ -106,6 +258,36 @@ function serverConnect() {
258 process.exit();
259 break;
260 }
261 + case 'close': {
262 + if (data.cause == 'noauth') {
263 + if (data.msg == 'tokenrequired') {
264 + console.log('Authentication token required, use --token [number].');
265 + } else {
266 + console.log('Invalid login.');
267 + }
268 + }
269 + process.exit();
270 + break;
271 + }
272 + case 'event': {
273 + switch (data.event.action) {
274 + case 'accountcreate': {
275 + if ((settings.cmd == 'adduser') && (data.event.account.name == args.user)) {
276 + console.log('Account created, id: ' + data.event.account._id);
277 + process.exit();
278 + }
279 + break;
280 + }
281 + case 'accountremove': {
282 + if ((settings.cmd == 'removeuser') && (data.event.userid == args.userid)) {
283 + console.log('Account removed');
284 + process.exit();
285 + }
286 + break;
287 + }
288 + }
289 + break;
290 + }
291 default: {
292 console.log('Unknown action: ' + data.action);
293 break;
@@ -114,4 +296,16 @@ function serverConnect() {
296 //console.log('Data', data);
297 //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
298 });
117 -}
\ No newline at end of file
299 +}
300 +
301 +// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
302 +function encodeCookie(o, key) {
303 + var crypto = require('crypto');
304 + try {
305 + if (key == null) { return null; }
306 + o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
307 + const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
308 + const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
309 + return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
310 + } catch (e) { return null; }
311 +}
meshuser.js
+1 -1
@@ -1115,7 +1115,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1115 var event, targets = ['*', 'server-users'];
1116 if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
1117 if (command.email == null) {
1118 - event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.user, domain: domain.id };
1118 + event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.username, domain: domain.id };
1119 } else {
1120 event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id };
1121 }