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
}