EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
meshuser.js
Go to the documentation of this file.
1/**
2* @description MeshCentral MeshAgent
3* @author Ylian Saint-Hilaire & Bryan Roe
4* @copyright Intel Corporation 2018-2022
5* @license Apache-2.0
6* @version v0.0.1
7*/
8
9/*jslint node: true */
10/*jshint node: true */
11/*jshint strict:false */
12/*jshint -W097 */
13/*jshint esversion: 6 */
14"use strict";
15
16// Construct a MeshAgent object, called upon connection
17module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, user) {
18 const fs = require('fs');
19 const path = require('path');
20 const common = parent.common;
21 // Cross domain messages, for cross-domain administrators only.
22 const allowedCrossDomainMessages = ['accountcreate', 'accountremove', 'accountchange', 'createusergroup', 'deleteusergroup', 'usergroupchange'];
23
24 // User Consent Flags
25 const USERCONSENT_DesktopNotifyUser = 1;
26 const USERCONSENT_TerminalNotifyUser = 2;
27 const USERCONSENT_FilesNotifyUser = 4;
28 const USERCONSENT_DesktopPromptUser = 8;
29 const USERCONSENT_TerminalPromptUser = 16;
30 const USERCONSENT_FilesPromptUser = 32;
31 const USERCONSENT_ShowConnectionToolbar = 64;
32
33 // Mesh Rights
34 const MESHRIGHT_EDITMESH = 0x00000001; // 1
35 const MESHRIGHT_MANAGEUSERS = 0x00000002; // 2
36 const MESHRIGHT_MANAGECOMPUTERS = 0x00000004; // 4
37 const MESHRIGHT_REMOTECONTROL = 0x00000008; // 8
38 const MESHRIGHT_AGENTCONSOLE = 0x00000010; // 16
39 const MESHRIGHT_SERVERFILES = 0x00000020; // 32
40 const MESHRIGHT_WAKEDEVICE = 0x00000040; // 64
41 const MESHRIGHT_SETNOTES = 0x00000080; // 128
42 const MESHRIGHT_REMOTEVIEWONLY = 0x00000100; // 256
43 const MESHRIGHT_NOTERMINAL = 0x00000200; // 512
44 const MESHRIGHT_NOFILES = 0x00000400; // 1024
45 const MESHRIGHT_NOAMT = 0x00000800; // 2048
46 const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000; // 4096
47 const MESHRIGHT_LIMITEVENTS = 0x00002000; // 8192
48 const MESHRIGHT_CHATNOTIFY = 0x00004000; // 16384
49 const MESHRIGHT_UNINSTALL = 0x00008000; // 32768
50 const MESHRIGHT_NODESKTOP = 0x00010000; // 65536
51 const MESHRIGHT_REMOTECOMMAND = 0x00020000; // 131072
52 const MESHRIGHT_RESETOFF = 0x00040000; // 262144
53 const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
54 const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
55 const MESHRIGHT_RELAY = 0x00200000; // 2097152
56 const MESHRIGHT_ADMIN = 0xFFFFFFFF;
57
58 // Site rights
59 const SITERIGHT_SERVERBACKUP = 0x00000001; // 1
60 const SITERIGHT_MANAGEUSERS = 0x00000002; // 2
61 const SITERIGHT_SERVERRESTORE = 0x00000004; // 4
62 const SITERIGHT_FILEACCESS = 0x00000008; // 8
63 const SITERIGHT_SERVERUPDATE = 0x00000010; // 16
64 const SITERIGHT_LOCKED = 0x00000020; // 32
65 const SITERIGHT_NONEWGROUPS = 0x00000040; // 64
66 const SITERIGHT_NOMESHCMD = 0x00000080; // 128
67 const SITERIGHT_USERGROUPS = 0x00000100; // 256
68 const SITERIGHT_RECORDINGS = 0x00000200; // 512
69 const SITERIGHT_LOCKSETTINGS = 0x00000400; // 1024
70 const SITERIGHT_ALLEVENTS = 0x00000800; // 2048
71 const SITERIGHT_NONEWDEVICES = 0x00001000; // 4096
72 const SITERIGHT_ADMIN = 0xFFFFFFFF;
73
74 // Protocol Numbers
75 const PROTOCOL_TERMINAL = 1;
76 const PROTOCOL_DESKTOP = 2;
77 const PROTOCOL_FILES = 5;
78 const PROTOCOL_AMTWSMAN = 100;
79 const PROTOCOL_AMTREDIR = 101;
80 const PROTOCOL_MESSENGER = 200;
81 const PROTOCOL_WEBRDP = 201;
82 const PROTOCOL_WEBSSH = 202;
83 const PROTOCOL_WEBSFTP = 203;
84 const PROTOCOL_WEBVNC = 204;
85
86 // MeshCentral Satellite
87 const SATELLITE_PRESENT = 1; // This session is a MeshCentral Satellite session
88 const SATELLITE_802_1x = 2; // This session supports 802.1x profile checking and creation
89
90 // Events
91 /*
92 var eventsMessageId = {
93 1: "Account login",
94 2: "Account logout",
95 3: "Changed language from {1} to {2}",
96 4: "Joined desktop multiplex session",
97 5: "Left the desktop multiplex session",
98 6: "Started desktop multiplex session",
99 7: "Finished recording session, {0} second(s)",
100 8: "Closed desktop multiplex session, {0} second(s)"
101 };
102 */
103
104 var obj = {};
105 obj.user = user;
106 obj.domain = domain;
107 obj.ws = ws;
108
109 // Information related to the current page the user is looking at
110 obj.deviceSkip = 0; // How many devices to skip
111 obj.deviceLimit = 0; // How many devices to view
112 obj.visibleDevices = null; // An object of visible nodeid's if the user is in paging mode
113 if (domain.maxdeviceview != null) { obj.deviceLimit = domain.maxdeviceview; }
114
115 // Check if we are a cross-domain administrator
116 if (parent.parent.config.settings.managecrossdomain && (parent.parent.config.settings.managecrossdomain.indexOf(user._id) >= 0)) { obj.crossDomain = true; }
117
118 // Server side Intel AMT stack
119 const WsmanComm = require('./amt/amt-wsman-comm.js');
120 const Wsman = require('./amt/amt-wsman.js');
121 const Amt = require('./amt/amt.js');
122
123 // If this session has an expire time, setup a timer now.
124 if ((req.session != null) && (typeof req.session.expire == 'number')) {
125 var delta = (req.session.expire - Date.now());
126 if (delta <= 0) { req.session = {}; try { ws.close(); } catch (ex) { } return; } // Session is already expired, close now.
127 obj.expireTimer = setTimeout(function () { for (var i in req.session) { delete req.session[i]; } obj.close(); }, delta);
128 }
129
130 // Send data through the websocket
131 obj.send = function (object) { try { ws.send(JSON.stringify(object)); } catch(ex) {} }
132
133 // Clean a IPv6 address that encodes a IPv4 address
134 function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } }
135
136 // Send a PING/PONG message
137 function sendPing() { try { obj.ws.send('{"action":"ping"}'); } catch (ex) { } }
138 function sendPong() { try { obj.ws.send('{"action":"pong"}'); } catch (ex) { } }
139
140 // Setup the agent PING/PONG timers
141 if ((typeof args.browserping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.browserping * 1000); }
142 else if ((typeof args.browserpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.browserpong * 1000); }
143
144 // Disconnect this user
145 obj.close = function (arg) {
146 obj.ws.xclosed = 1; // This is for testing. Will be displayed when running "usersessions" server console command.
147
148 if ((arg == 1) || (arg == null)) { try { obj.ws.close(); parent.parent.debug('user', 'Soft disconnect'); } catch (ex) { console.log(ex); } } // Soft close, close the websocket
149 if (arg == 2) { try { obj.ws._socket._parent.end(); parent.parent.debug('user', 'Hard disconnect'); } catch (ex) { console.log(ex); } } // Hard close, close the TCP socket
150
151 obj.ws.xclosed = 2; // DEBUG
152
153 // Perform timer cleanup
154 if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
155 if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
156
157 obj.ws.xclosed = 3; // DEBUG
158
159 // Clear expire timeout
160 if (obj.expireTimer != null) { clearTimeout(obj.expireTimer); delete obj.expireTimer; }
161
162 obj.ws.xclosed = 4; // DEBUG
163
164 // Perform cleanup
165 parent.parent.RemoveAllEventDispatch(obj.ws);
166 if (obj.serverStatsTimer != null) { clearInterval(obj.serverStatsTimer); delete obj.serverStatsTimer; }
167 if (req.session && req.session.ws && req.session.ws == obj.ws) { delete req.session.ws; }
168 if (parent.wssessions2[ws.sessionId]) { delete parent.wssessions2[ws.sessionId]; }
169
170 obj.ws.xclosed = 5; // DEBUG
171
172 if ((obj.user != null) && (parent.wssessions[obj.user._id])) {
173 obj.ws.xclosed = 6; // DEBUG
174 var i = parent.wssessions[obj.user._id].indexOf(obj.ws);
175 if (i >= 0) {
176 obj.ws.xclosed = 7; // DEBUG
177 parent.wssessions[obj.user._id].splice(i, 1);
178 var user = parent.users[obj.user._id];
179 if (user) {
180 obj.ws.xclosed = 8; // DEBUG
181 if (parent.parent.multiServer == null) {
182 var targets = ['*', 'server-users'];
183 if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } }
184 parent.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: user._id, username: user.name, count: parent.wssessions[obj.user._id].length, nolog: 1, domain: domain.id });
185 } else {
186 parent.recountSessions(ws.sessionId); // Recount sessions
187 }
188 }
189 if (parent.wssessions[obj.user._id].length == 0) { delete parent.wssessions[obj.user._id]; }
190 }
191 }
192
193 obj.ws.xclosed = 9; // DEBUG
194
195 // If we have peer servers, inform them of the disconnected session
196 if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'sessionEnd', sessionid: ws.sessionId }); }
197
198 obj.ws.xclosed = 10; // DEBUG
199
200 // Update user last access time
201 if (obj.user != null) {
202 const timeNow = Math.floor(Date.now() / 1000);
203 if (obj.user.access < (timeNow - 300)) { // Only update user access time if longer than 5 minutes
204 obj.user.access = timeNow;
205 parent.db.SetUser(user);
206
207 // Event the change
208 var message = { etype: 'user', userid: obj.user._id, username: obj.user.name, account: parent.CloneSafeUser(obj.user), action: 'accountchange', domain: domain.id, nolog: 1 };
209 if (parent.db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
210 var targets = ['*', 'server-users', obj.user._id];
211 if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } }
212 parent.parent.DispatchEvent(targets, obj, message);
213 }
214 }
215
216 // Aggressive cleanup
217 delete obj.user;
218 delete obj.domain;
219 delete obj.ws.userid;
220 delete obj.ws.domainid;
221 delete obj.ws.clientIp;
222 delete obj.ws.sessionId;
223 delete obj.ws.HandleEvent;
224 obj.ws.removeAllListeners(['message', 'close', 'error']);
225
226 obj.ws.xclosed = 11; // DEBUG
227 };
228
229 // Convert a mesh path array into a real path on the server side
230 function meshPathToRealPath(meshpath, user) {
231 if (common.validateArray(meshpath, 1) == false) return null;
232 var splitid = meshpath[0].split('/');
233 if (splitid[0] == 'user') {
234 // Check user access
235 if (meshpath[0] != user._id) return null; // Only allow own user folder
236 } else if (splitid[0] == 'mesh') {
237 // Check mesh access
238 if ((parent.GetMeshRights(user, meshpath[0]) & MESHRIGHT_SERVERFILES) == 0) return null; // This user must have mesh rights to "server files"
239 } else return null;
240 var rootfolder = meshpath[0], rootfoldersplit = rootfolder.split('/'), domainx = 'domain';
241 if (rootfoldersplit[1].length > 0) domainx = 'domain-' + rootfoldersplit[1];
242 var path = parent.path.join(parent.filespath, domainx, rootfoldersplit[0] + '-' + rootfoldersplit[2]);
243 for (var i = 1; i < meshpath.length; i++) { if (common.IsFilenameValid(meshpath[i]) == false) { path = null; break; } path += ("/" + meshpath[i]); }
244 return path;
245 }
246
247 // Copy a file using the best technique available
248 function copyFile(src, dest, func, tag) {
249 if (fs.copyFile) {
250 // NodeJS v8.5 and higher
251 fs.copyFile(src, dest, function (err) { func(tag); })
252 } else {
253 // Older NodeJS
254 try {
255 var ss = fs.createReadStream(src), ds = fs.createWriteStream(dest);
256 ss.on('error', function () { func(tag); });
257 ds.on('error', function () { func(tag); });
258 ss.pipe(ds);
259 ds.ss = ss;
260 if (arguments.length == 3 && typeof arguments[2] === 'function') { ds.on('close', arguments[2]); }
261 else if (arguments.length == 4 && typeof arguments[3] === 'function') { ds.on('close', arguments[3]); }
262 ds.on('close', function () { func(tag); });
263 } catch (ex) { }
264 }
265 }
266
267 // Route a command to a target node
268 function routeCommandToNode(command, requiredRights, requiredNonRights, func, options) {
269 if (common.validateString(command.nodeid, 8, 128) == false) { if (func) { func(false); } return false; }
270 var splitnodeid = command.nodeid.split('/');
271 // Check that we are in the same domain and the user has rights over this node.
272 if ((splitnodeid[0] == 'node') && (splitnodeid[1] == domain.id)) {
273 // See if the node is connected
274 var agent = parent.wsagents[command.nodeid];
275 if (agent != null) {
276 // Check if we have permission to send a message to that node
277 parent.GetNodeWithRights(domain, user, agent.dbNodeKey, function (node, rights, visible) {
278 var mesh = parent.meshes[agent.dbMeshKey];
279 if ((node != null) && (mesh != null) && ((rights & MESHRIGHT_REMOTECONTROL) || (rights & MESHRIGHT_REMOTEVIEWONLY))) { // 8 is remote control permission, 256 is desktop read only
280 if ((requiredRights != null) && ((rights & requiredRights) == 0)) { if (func) { func(false); return; } } // Check Required Rights
281 if ((requiredNonRights != null) && (rights != MESHRIGHT_ADMIN) && ((rights & requiredNonRights) != 0)) { if (func) { func(false); return; } } // Check Required None Rights
282
283 command.sessionid = ws.sessionId; // Set the session id, required for responses
284 command.rights = rights; // Add user rights flags to the message
285 if ((options != null) && (options.removeViewOnlyLimitation === true) && (command.rights != 0xFFFFFFFF) && ((command.rights & 0x100) != 0)) { command.rights -= 0x100; } // Since the multiplexor will enforce view-only, remove MESHRIGHT_REMOTEVIEWONLY
286 command.consent = 0;
287 if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags
288 if (typeof mesh.consent == 'number') { command.consent |= mesh.consent; } // Add device group user consent
289 if (typeof node.consent == 'number') { command.consent |= node.consent; } // Add node user consent
290 if (typeof user.consent == 'number') { command.consent |= user.consent; } // Add user consent
291
292 // If desktop is viewonly, add this here.
293 if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { command.desktopviewonly = true; }
294
295 // Check if we need to add consent flags because of a user group link
296 if ((user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
297 // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
298 for (var i in user.links) {
299 var ugrp = parent.userGroups[i];
300 if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
301 command.consent |= ugrp.consent; // Add user group consent flags
302 }
303 }
304 }
305
306 command.username = user.name; // Add user name
307 command.realname = user.realname; // Add real name
308 command.userid = user._id; // Add user id
309 command.remoteaddr = req.clientIp; // User's IP address
310 if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text
311 delete command.nodeid; // Remove the nodeid since it's implied
312 try { agent.send(JSON.stringify(command)); } catch (ex) { }
313 } else { if (func) { func(false); } }
314 });
315 } else {
316 // Check if a peer server is connected to this agent
317 var routing = parent.parent.GetRoutingServerIdNotSelf(command.nodeid, 1); // 1 = MeshAgent routing type
318 if (routing != null) {
319 // Check if we have permission to send a message to that node
320 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
321 if ((requiredRights != null) && ((rights & requiredRights) == 0)) { if (func) { func(false); return; } } // Check Required Rights
322 if ((requiredNonRights != null) && (rights != MESHRIGHT_ADMIN) && ((rights & requiredNonRights) != 0)) { if (func) { func(false); return; } } // Check Required None Rights
323
324 var mesh = parent.meshes[routing.meshid];
325 if ((node != null) && (mesh != null) && ((rights & MESHRIGHT_REMOTECONTROL) || (rights & MESHRIGHT_REMOTEVIEWONLY))) { // 8 is remote control permission
326 command.fromSessionid = ws.sessionId; // Set the session id, required for responses
327 command.rights = rights; // Add user rights flags to the message
328 if ((options != null) && (options.removeViewOnlyLimitation === true) && (command.rights != 0xFFFFFFFF) && ((command.rights & 0x100) != 0)) { command.rights -= 0x100; } // Since the multiplexor will enforce view-only, remove MESHRIGHT_REMOTEVIEWONLY
329 command.consent = 0;
330 if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags
331 if (typeof mesh.consent == 'number') { command.consent |= mesh.consent; } // Add device group user consent
332 if (typeof node.consent == 'number') { command.consent |= node.consent; } // Add node user consent
333 if (typeof user.consent == 'number') { command.consent |= user.consent; } // Add user consent
334
335 // Check if we need to add consent flags because of a user group link
336 if ((user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
337 // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
338 for (var i in user.links) {
339 var ugrp = parent.userGroups[i];
340 if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
341 command.consent |= ugrp.consent; // Add user group consent flags
342 }
343 }
344 }
345
346 command.username = user.name; // Add user name
347 command.realname = user.realname; // Add real name
348 command.userid = user._id; // Add user id
349 command.remoteaddr = req.clientIp; // User's IP address
350 if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text
351 parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid);
352 } else { if (func) { func(false); } }
353 });
354 } else { if (func) { func(false); } return false; }
355 }
356 } else { if (func) { func(false); } return false; }
357 if (func) { func(true); }
358 return true;
359 }
360
361 // Route a command to all targets in a mesh
362 function routeCommandToMesh(meshid, command) {
363 // If we have peer servers, inform them of this command to send to all agents of this device group
364 if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'agentMsgByMeshId', meshid: meshid, command: command }); }
365
366 // See if the node is connected
367 for (var nodeid in parent.wsagents) {
368 var agent = parent.wsagents[nodeid];
369 if (agent.dbMeshKey == meshid) { try { agent.send(JSON.stringify(command)); } catch (ex) { } }
370 }
371 return true;
372 }
373
374 try {
375 // Check if the user is logged in
376 if (user == null) { try { ws.close(); } catch (e) { } return; }
377
378 // Check if we have exceeded the user session limit
379 if ((typeof domain.limits.maxusersessions == 'number') || (typeof domain.limits.maxsingleusersessions == 'number')) {
380 // Count the number of user sessions for this domain
381 var domainUserSessionCount = 0, selfUserSessionCount = 0;
382 for (var i in parent.wssessions2) {
383 if (parent.wssessions2[i].domainid == domain.id) {
384 domainUserSessionCount++; if (parent.wssessions2[i].userid == user._id) { selfUserSessionCount++; }
385 }
386 }
387
388 // Check if we have too many user sessions
389 if (((typeof domain.limits.maxusersessions == 'number') && (domainUserSessionCount >= domain.limits.maxusersessions)) || ((typeof domain.limits.maxsingleusersessions == 'number') && (selfUserSessionCount >= domain.limits.maxsingleusersessions))) {
390 try { ws.send(JSON.stringify({ action: 'stopped', msg: 'Session count exceed' })); } catch (ex) { }
391 try { ws.close(); } catch (e) { }
392 return;
393 }
394 }
395
396 // Associate this websocket session with the web session
397 ws.userid = user._id;
398 ws.domainid = domain.id;
399 ws.clientIp = req.clientIp;
400
401 // Create a new session id for this user.
402 parent.crypto.randomBytes(20, function (err, randombuf) {
403 ws.sessionId = user._id + '/' + randombuf.toString('hex');
404
405 // Add this web socket session to session list
406 parent.wssessions2[ws.sessionId] = ws;
407 if (!parent.wssessions[user._id]) { parent.wssessions[user._id] = [ws]; } else { parent.wssessions[user._id].push(ws); }
408 if (parent.parent.multiServer == null) {
409 var targets = ['*', 'server-users'];
410 if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } }
411 parent.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: user._id, username: user.name, count: parent.wssessions[user._id].length, nolog: 1, domain: domain.id });
412 } else {
413 parent.recountSessions(ws.sessionId); // Recount sessions
414 }
415
416 // If we have peer servers, inform them of the new session
417 if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'sessionStart', sessionid: ws.sessionId }); }
418
419 // Handle events
420 ws.HandleEvent = function (source, event, ids, id) {
421 // If this session is logged in using a loginToken and the token is removed, disconnect.
422 if ((req.session.loginToken != null) && (typeof event == 'object') && (event.action == 'loginTokenChanged') && (event.removed != null) && (event.removed.indexOf(req.session.loginToken) >= 0)) { delete req.session; obj.close(); return; }
423
424 // If this user is not viewing all devices and paging, check if this event is in the current page
425 if (isEventWithinPage(ids) == false) return;
426
427 // Normally, only allow this user to receive messages from it's own domain.
428 // If the user is a cross domain administrator, allow some select messages from different domains.
429 if ((event.domain == null) || (event.domain == domain.id) || ((obj.crossDomain === true) && (allowedCrossDomainMessages.indexOf(event.action) >= 0))) {
430 try {
431 if (event == 'close') { try { delete req.session; } catch (ex) { } obj.close(); return; }
432 else if (event == 'resubscribe') { user.subscriptions = parent.subscribe(user._id, ws); }
433 else if (event == 'updatefiles') { updateUserFiles(user, ws, domain); }
434 else {
435 // If updating guest device shares, if we are updating a user that is not creator of the share, remove the URL.
436 if (((event.action == 'deviceShareUpdate') && (Array.isArray(event.deviceShares))) || ((event.action == 'changenode') && (event.node != null) && ((event.node.rdp != null) || (event.node.ssh != null)))) {
437 event = common.Clone(event);
438 if ((event.action == 'deviceShareUpdate') && (Array.isArray(event.deviceShares))) {
439 for (var i in event.deviceShares) { if (event.deviceShares[i].userid != user._id) { delete event.deviceShares[i].url; } }
440 }
441 if ((event.action == 'changenode') && (event.node != null) && ((event.node.rdp != null) || (event.node.ssh != null))) {
442 // Clean up RDP & SSH credentials
443 if ((event.node.rdp != null) && (typeof event.node.rdp[user._id] == 'number')) { event.node.rdp = event.node.rdp[user._id]; } else { delete event.node.rdp; }
444 if ((event.node.ssh != null) && (typeof event.node.ssh[user._id] == 'number')) { event.node.ssh = event.node.ssh[user._id]; } else { delete event.node.ssh; }
445 }
446 }
447
448 // This is a MeshCentral Satellite message
449 if (event.action == 'satellite') { if ((obj.ws.satelliteFlags & event.satelliteFlags) != 0) { try { ws.send(JSON.stringify(event)); } catch (ex) { } return; } }
450
451 // Because of the device group "Show Self Events Only", we need to do more checks here.
452 if (id.startsWith('mesh/')) {
453 // Check if we have rights to get this message. If we have limited events on this mesh, don't send the event to the user.
454 var meshrights = parent.GetMeshRights(user, id);
455 if ((meshrights === MESHRIGHT_ADMIN) || ((meshrights & MESHRIGHT_LIMITEVENTS) == 0) || (ids.indexOf(user._id) >= 0)) {
456 // We have the device group rights to see this event or we are directly targetted by the event
457 try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { }
458 } else {
459 // Check if no other users are targeted by the event, if not, we can get this event.
460 var userTarget = false;
461 for (var i in ids) { if (ids[i].startsWith('user/')) { userTarget = true; } }
462 if (userTarget == false) { ws.send(JSON.stringify({ action: 'event', event: event })); }
463 }
464 } else if (event.ugrpid != null) {
465 if ((user.siteadmin & SITERIGHT_USERGROUPS) != 0) {
466 // If we have the rights to see users in a group, send the group as is.
467 try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { }
468 } else {
469 // We don't have the rights to see otehr users in the user group, remove the links that are not for ourselves.
470 var links = {};
471 if (event.links) { for (var i in event.links) { if ((i == user._id) || i.startsWith('mesh/') || i.startsWith('node/')) { links[i] = event.links[i]; } } }
472 try { ws.send(JSON.stringify({ action: 'event', event: { ugrpid: event.ugrpid, domain: event.domain, time: event.time, name: event.name, action: event.action, username: event.username, links: links, h: event.h } })); } catch (ex) { }
473 }
474 } else {
475 // This is not a device group event, we can get this event.
476 try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { }
477 }
478 }
479 } catch (ex) { console.log(ex); }
480 }
481 };
482
483 user.subscriptions = parent.subscribe(user._id, ws); // Subscribe to events
484 try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
485
486 // Send current server statistics
487 obj.SendServerStats = function () {
488 // Take a look at server stats
489 var os = require('os');
490 var stats = { action: 'serverstats', totalmem: os.totalmem(), freemem: os.freemem() };
491 try { stats.cpuavg = os.loadavg(); } catch (ex) { }
492 if (parent.parent.platform != 'win32') {
493 try { stats.availablemem = 1024 * Number(/MemAvailable:[ ]+(\d+)/.exec(fs.readFileSync('/proc/meminfo', 'utf8'))[1]); } catch (ex) { }
494 }
495
496 // Count the number of device groups that are not deleted
497 var activeDeviceGroups = 0;
498 for (var i in parent.meshes) { if (parent.meshes[i].deleted == null) { activeDeviceGroups++; } } // This is not ideal for performance, we want to dome something better.
499 var serverStats = {
500 UserAccounts: Object.keys(parent.users).length,
501 DeviceGroups: activeDeviceGroups,
502 AgentSessions: Object.keys(parent.wsagents).length,
503 ConnectedUsers: Object.keys(parent.wssessions).length,
504 UsersSessions: Object.keys(parent.wssessions2).length,
505 RelaySessions: parent.relaySessionCount,
506 RelayCount: Object.keys(parent.wsrelays).length,
507 ConnectedIntelAMT: 0
508 };
509 if (parent.relaySessionErrorCount != 0) { serverStats.RelayErrors = parent.relaySessionErrorCount; }
510 if (parent.parent.mpsserver != null) {
511 serverStats.ConnectedIntelAMTCira = 0;
512 for (var i in parent.parent.mpsserver.ciraConnections) { serverStats.ConnectedIntelAMTCira += parent.parent.mpsserver.ciraConnections[i].length; }
513 }
514 for (var i in parent.parent.connectivityByNode) {
515 const node = parent.parent.connectivityByNode[i];
516 if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { serverStats.ConnectedIntelAMT++; }
517 }
518
519 // Take a look at agent errors
520 var agentstats = parent.getAgentStats();
521 var errorCounters = {}, errorCountersCount = 0;
522 if (agentstats.meshDoesNotExistCount > 0) { errorCountersCount++; errorCounters.UnknownGroup = agentstats.meshDoesNotExistCount; }
523 if (agentstats.invalidPkcsSignatureCount > 0) { errorCountersCount++; errorCounters.InvalidPKCSsignature = agentstats.invalidPkcsSignatureCount; }
524 if (agentstats.invalidRsaSignatureCount > 0) { errorCountersCount++; errorCounters.InvalidRSAsignature = agentstats.invalidRsaSignatureCount; }
525 if (agentstats.invalidJsonCount > 0) { errorCountersCount++; errorCounters.InvalidJSON = agentstats.invalidJsonCount; }
526 if (agentstats.unknownAgentActionCount > 0) { errorCountersCount++; errorCounters.UnknownAction = agentstats.unknownAgentActionCount; }
527 if (agentstats.agentBadWebCertHashCount > 0) { errorCountersCount++; errorCounters.BadWebCertificate = agentstats.agentBadWebCertHashCount; }
528 if ((agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count) > 0) { errorCountersCount++; errorCounters.BadSignature = (agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count); }
529 if (agentstats.agentMaxSessionHoldCount > 0) { errorCountersCount++; errorCounters.MaxSessionsReached = agentstats.agentMaxSessionHoldCount; }
530 if ((agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count) > 0) { errorCountersCount++; errorCounters.UnknownDeviceGroup = (agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count); }
531 if ((agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count) > 0) { errorCountersCount++; errorCounters.InvalidDeviceGroupType = (agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count); }
532 //if (agentstats.duplicateAgentCount > 0) { errorCountersCount++; errorCounters.DuplicateAgent = agentstats.duplicateAgentCount; }
533
534 // Send out the stats
535 stats.values = { ServerState: serverStats }
536 if (errorCountersCount > 0) { stats.values.AgentErrorCounters = errorCounters; }
537 try { ws.send(JSON.stringify(stats)); } catch (ex) { }
538 }
539
540 // When data is received from the web socket
541 ws.on('message', processWebSocketData);
542
543 // If error, do nothing
544 ws.on('error', function (err) { console.log(err); obj.close(0); });
545
546 // If the web socket is closed
547 ws.on('close', function (req) { obj.close(0); });
548
549 // Figure out the MPS port, use the alias if set
550 var mpsport = ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport);
551 var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
552
553 // Build server information object
554 const allFeatures = parent.getDomainUserFeatures(domain, user, req);
555 var serverinfo = { domain: domain.id, name: domain.dns ? domain.dns : parent.certificates.CommonName, mpsname: parent.certificates.AmtMpsName, mpsport: mpsport, mpspass: args.mpspass, port: httpport, emailcheck: ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (args.lanonly != true) && (parent.certificates.CommonName != null) && (parent.certificates.CommonName.indexOf('.') != -1) && (user._id.split('/')[2].startsWith('~') == false)), domainauth: (domain.auth == 'sspi'), serverTime: Date.now(), features: allFeatures.features, features2: allFeatures.features2 };
556 serverinfo.languages = parent.renderLanguages;
557 serverinfo.tlshash = Buffer.from(parent.webCertificateFullHashs[domain.id], 'binary').toString('hex').toUpperCase(); // SHA384 of server HTTPS certificate
558 serverinfo.agentCertHash = parent.agentCertificateHashBase64;
559 if (typeof domain.sessionrecording == 'object') {
560 if (domain.sessionrecording.onlyselectedusers === true) { serverinfo.usersSessionRecording = 1; } // Allow enabling of session recording for users
561 if (domain.sessionrecording.onlyselectedusergroups === true) { serverinfo.userGroupsSessionRecording = 1; } // Allow enabling of session recording for user groups
562 if (domain.sessionrecording.onlyselecteddevicegroups === true) { serverinfo.devGroupSessionRecording = 1; } // Allow enabling of session recording for device groups
563 }
564 if ((parent.parent.config.domains[domain.id].amtacmactivation != null) && (parent.parent.config.domains[domain.id].amtacmactivation.acmmatch != null)) {
565 var matchingDomains = [];
566 for (var i in parent.parent.config.domains[domain.id].amtacmactivation.acmmatch) {
567 var cn = parent.parent.config.domains[domain.id].amtacmactivation.acmmatch[i].cn;
568 if ((cn != '*') && (matchingDomains.indexOf(cn) == -1)) { matchingDomains.push(cn); }
569 }
570 if (matchingDomains.length > 0) { serverinfo.amtAcmFqdn = matchingDomains; }
571 }
572 if (typeof domain.devicemeshrouterlinks == 'object') { serverinfo.devicemeshrouterlinks = domain.devicemeshrouterlinks; }
573 if ((typeof domain.altmessenging == 'object') && (typeof domain.altmessenging.name == 'string') && (typeof domain.altmessenging.url == 'string')) { serverinfo.altmessenging = [{ name: domain.altmessenging.name, url: domain.altmessenging.url, localurl: domain.altmessenging.localurl, type: domain.altmessenging.type }]; }
574 if (Array.isArray(domain.altmessenging)) { serverinfo.altmessenging = []; for (var i in domain.altmessenging) { if ((typeof domain.altmessenging[i] == 'object') && (typeof domain.altmessenging[i].name == 'string') && (typeof domain.altmessenging[i].url == 'string')) { serverinfo.altmessenging.push({ name: domain.altmessenging[i].name, url: domain.altmessenging[i].url, type: domain.altmessenging[i].type }); } } }
575 serverinfo.https = true;
576 serverinfo.redirport = args.redirport;
577 if (parent.parent.webpush != null) { serverinfo.vapidpublickey = parent.parent.webpush.vapidPublicKey; } // Web push public key
578 if (parent.parent.amtProvisioningServer != null) { serverinfo.amtProvServerMeshId = parent.parent.amtProvisioningServer.meshid; } // Device group that allows for bare-metal Intel AMT activation
579 if ((typeof domain.autoremoveinactivedevices == 'number') && (domain.autoremoveinactivedevices > 0)) { serverinfo.autoremoveinactivedevices = domain.autoremoveinactivedevices; } // Default number of days before inactive devices are removed
580 if (domain.passwordrequirements) {
581 if (domain.passwordrequirements.lock2factor == true) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed
582 if (typeof domain.passwordrequirements.maxfidokeys == 'number') { serverinfo.maxfidokeys = domain.passwordrequirements.maxfidokeys; }
583 }
584 if (parent.parent.msgserver != null) { // Setup messaging providers information
585 serverinfo.userMsgProviders = parent.parent.msgserver.providers;
586 if (parent.parent.msgserver.discordUrl != null) { serverinfo.discordUrl = parent.parent.msgserver.discordUrl; }
587 }
588 if ((typeof parent.parent.config.messaging == 'object') && (typeof parent.parent.config.messaging.ntfy == 'object') && (typeof parent.parent.config.messaging.ntfy.userurl == 'string')) { // nfty user url
589 serverinfo.userMsgNftyUrl = parent.parent.config.messaging.ntfy.userurl;
590 }
591
592 // Build the mobile agent URL, this is used to connect mobile devices
593 var agentServerName = parent.getWebServerName(domain, req);
594 if (typeof parent.args.agentaliasdns == 'string') { agentServerName = parent.args.agentaliasdns; }
595 var xdomain = (domain.dns == null) ? domain.id : '';
596 var agentHttpsPort = ((parent.args.aliasport == null) ? parent.args.port : parent.args.aliasport); // Use HTTPS alias port is specified
597 if (parent.args.agentport != null) { agentHttpsPort = parent.args.agentport; } // If an agent only port is enabled, use that.
598 if (parent.args.agentaliasport != null) { agentHttpsPort = parent.args.agentaliasport; } // If an agent alias port is specified, use that.
599 serverinfo.magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '');
600 serverinfo.domainsuffix = xdomain;
601
602 if (domain.guestdevicesharing === false) { serverinfo.guestdevicesharing = false; } else {
603 if (typeof domain.guestdevicesharing == 'object') {
604 if (typeof domain.guestdevicesharing.maxsessiontime == 'number') { serverinfo.guestdevicesharingmaxtime = domain.guestdevicesharing.maxsessiontime; }
605 }
606 }
607 if (typeof domain.userconsentflags == 'number') { serverinfo.consent = domain.userconsentflags; }
608 if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) {serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); }
609 if (typeof domain.logoutonidlesessiontimeout == 'boolean') {
610 serverinfo.logoutonidlesessiontimeout = domain.logoutonidlesessiontimeout;
611 } else {
612 // Default
613 serverinfo.logoutonidlesessiontimeout = true;
614 }
615 if (user.siteadmin === SITERIGHT_ADMIN) {
616 if (parent.parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0 || (user.links && Object.keys(user.links).some(key => parent.parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) { serverinfo.manageAllDeviceGroups = true; }
617 if (obj.crossDomain === true) { serverinfo.crossDomain = []; for (var i in parent.parent.config.domains) { serverinfo.crossDomain.push(i); } }
618 if (typeof parent.webCertificateExpire[domain.id] == 'number') { serverinfo.certExpire = parent.webCertificateExpire[domain.id]; }
619 }
620 if (typeof domain.terminal == 'object') { // Settings used for remote terminal feature
621 if ((typeof domain.terminal.linuxshell == 'string') && (domain.terminal.linuxshell != 'any')) { serverinfo.linuxshell = domain.terminal.linuxshell; }
622 }
623 if (Array.isArray(domain.preconfiguredremoteinput)) { serverinfo.preConfiguredRemoteInput = domain.preconfiguredremoteinput; }
624 if (Array.isArray(domain.preconfiguredscripts)) {
625 const r = [];
626 for (var i in domain.preconfiguredscripts) {
627 const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent
628 const script = domain.preconfiguredscripts[i];
629 if ((typeof script.name == 'string') && (script.name.length <= 32) && (typeof script.type == 'string') && ((typeof script.file == 'string') || (typeof script.cmd == 'string'))) {
630 const s = { name: script.name, type: types.indexOf(script.type.toLowerCase()) };
631 if (s.type > 0) { r.push(s); }
632 }
633 }
634 serverinfo.preConfiguredScripts = r;
635 }
636 if (domain.maxdeviceview != null) { serverinfo.maxdeviceview = domain.maxdeviceview; } // Maximum number of devices a user can view at any given time
637
638 // Send server information
639 try { ws.send(JSON.stringify({ action: 'serverinfo', serverinfo: serverinfo })); } catch (ex) { }
640
641 // Send user information to web socket, this is the first thing we send
642 try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: parent.CloneSafeUser(parent.users[user._id]) })); } catch (ex) { }
643
644 if (user.siteadmin === SITERIGHT_ADMIN) {
645 // Check if tracing is allowed for this domain
646 if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.trace === true))) {
647 // Send server tracing information
648 try { ws.send(JSON.stringify({ action: 'traceinfo', traceSources: parent.parent.debugRemoteSources })); } catch (ex) { }
649 }
650
651 // Send any server warnings if any
652 var serverWarnings = parent.parent.getServerWarnings();
653 if (serverWarnings.length > 0) { try { ws.send(JSON.stringify({ action: 'serverwarnings', warnings: serverWarnings })); } catch (ex) { } }
654 }
655
656 // See how many times bad login attempts where made since the last login
657 const lastLoginTime = parent.users[user._id].pastlogin;
658 if (lastLoginTime != null) {
659 db.GetFailedLoginCount(user._id, user.domain, new Date(lastLoginTime * 1000), function (count) {
660 if (count > 0) { try { ws.send(JSON.stringify({ action: 'msg', type: 'notify', title: "Security Warning", tag: 'ServerNotify', id: Math.random(), value: "There has been " + count + " failed login attempts on this account since the last login.", titleid: 3, msgid: 12, args: [count] })); } catch (ex) { } delete user.pastlogin; }
661 });
662 }
663
664 // If we are site administrator and Google Drive backup is setup, send out the status.
665 if ((user.siteadmin === SITERIGHT_ADMIN) && (domain.id == '') && (typeof parent.parent.config.settings.autobackup == 'object') && (typeof parent.parent.config.settings.autobackup.googledrive == 'object')) {
666 db.Get('GoogleDriveBackup', function (err, docs) {
667 if (err != null) return;
668 if (docs.length == 0) { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 1 })); } catch (ex) { } }
669 else { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: docs[0].state })); } catch (ex) { } }
670 });
671 }
672
673 // We are all set, start receiving data
674 ws._socket.resume();
675 if (parent.parent.pluginHandler != null) parent.parent.pluginHandler.callHook('hook_userLoggedIn', user);
676 });
677 } catch (ex) { console.log(ex); }
678
679 // Process incoming web socket data from the browser
680 function processWebSocketData(msg) {
681 var command, i = 0, mesh = null, meshid = null, nodeid = null, meshlinks = null, change = 0;
682 try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
683 if (common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
684
685 var commandHandler = serverCommands[command.action];
686 if (commandHandler != null) {
687 try { commandHandler(command); return; }
688 catch (e) {
689 console.log('Unhandled error while processing ' + command.action + ' for user ' + user.name + ':\n' + e);
690 parent.parent.logError(e.stack); return; // todo: remove returns when switch is gone
691 }
692 } else { }
693 // console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
694 // pass through to switch statement until refactoring complete
695
696 switch (command.action) {
697 case 'nodes':
698 {
699 // If in paging mode, look to set the skip and limit values
700 if (domain.maxdeviceview != null) {
701 if ((typeof command.skip == 'number') && (command.skip >= 0)) { obj.deviceSkip = command.skip; }
702 if ((typeof command.limit == 'number') && (command.limit > 0)) { obj.deviceLimit = command.limit; }
703 if (obj.deviceLimit > domain.maxdeviceview) { obj.deviceLimit = domain.maxdeviceview; }
704 }
705
706 var links = [], extraids = null, err = null;
707
708 // Resolve the device group name if needed
709 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
710 for (var i in parent.meshes) {
711 var m = parent.meshes[i];
712 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
713 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
714 }
715 }
716 if (command.meshid == null) { err = 'Invalid group id'; }
717 }
718
719 if (err == null) {
720 try {
721 if (command.meshid == null) {
722 // Request a list of all meshes this user as rights to
723 links = parent.GetAllMeshIdWithRights(user);
724
725 // Add any nodes with direct rights or any nodes with user group direct rights
726 extraids = getUserExtraIds();
727 } else {
728 // Request list of all nodes for one specific meshid
729 meshid = command.meshid;
730 if (common.validateString(meshid, 0, 128) == false) { err = 'Invalid group id'; } else {
731 if (meshid.split('/').length == 1) { meshid = 'mesh/' + domain.id + '/' + command.meshid; }
732 if (parent.IsMeshViewable(user, meshid)) { links.push(meshid); } else { err = 'Invalid group id'; }
733 }
734 }
735 } catch (ex) { err = 'Validation exception: ' + ex; }
736 }
737
738 // Handle any errors
739 if (err != null) {
740 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'nodes', responseid: command.responseid, result: err })); } catch (ex) { } }
741 break;
742 }
743
744 // Request a list of all nodes
745 db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, obj.deviceSkip, obj.deviceLimit, function (err, docs) {
746
747 //console.log(err, docs, links, extraids, domain.id, 'node', command.id);
748
749 if (docs == null) { docs = []; }
750 parent.common.unEscapeAllLinksFieldName(docs);
751
752 var r = {}, nodeCount = docs.length;
753 if (domain.maxdeviceview != null) { obj.visibleDevices = {}; }
754 for (i in docs) {
755 // Check device links, if a link points to an unknown user, remove it.
756 parent.cleanDevice(docs[i]); // TODO: This will make the total device count incorrect and will affect device paging.
757
758 // If we are paging, add the device to the page here
759 if (domain.maxdeviceview != null) { obj.visibleDevices[docs[i]._id] = 1; }
760
761 // Remove any connectivity and power state information, that should not be in the database anyway.
762 // TODO: Find why these are sometimes saved in the db.
763 if (docs[i].conn != null) { delete docs[i].conn; }
764 if (docs[i].pwr != null) { delete docs[i].pwr; }
765 if (docs[i].agct != null) { delete docs[i].agct; }
766 if (docs[i].cict != null) { delete docs[i].cict; }
767
768 // Add the connection state
769 var state = parent.parent.GetConnectivityState(docs[i]._id);
770 if (state) {
771 docs[i].conn = state.connectivity;
772 docs[i].pwr = state.powerState;
773 if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[docs[i]._id]; if (agent != null) { docs[i].agct = agent.connectTime; } }
774
775 // Use the connection time of the CIRA/Relay connection
776 if ((state.connectivity & 2) != 0) {
777 var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(docs[i]._id, null, true);
778 if ((ciraConnection != null) && (ciraConnection.tag != null)) { docs[i].cict = ciraConnection.tag.connectTime; }
779 }
780 }
781
782 // Compress the meshid's
783 meshid = docs[i].meshid;
784 if (!r[meshid]) { r[meshid] = []; }
785 delete docs[i].meshid;
786
787 // Remove push messaging token if present
788 if (docs[i].pmt != null) { docs[i].pmt = 1; }
789
790 // Remove SSH credentials if present
791 if (docs[i].ssh != null) {
792 if ((docs[i].ssh[user._id] != null) && (docs[i].ssh[user._id].u)) {
793 if (docs[i].ssh.k && docs[i].ssh[user._id].kp) { docs[i].ssh = 2; } // Username, key and password
794 else if (docs[i].ssh[user._id].k) { docs[i].ssh = 3; } // Username and key. No password.
795 else if (docs[i].ssh[user._id].p) { docs[i].ssh = 1; } // Username and password
796 else { delete docs[i].ssh; }
797 } else {
798 delete docs[i].ssh;
799 }
800 }
801
802 // Remove RDP credentials if present, only set to 1 if our userid has RDP credentials
803 if ((docs[i].rdp != null) && (docs[i].rdp[user._id] != null)) { docs[i].rdp = 1; } else { delete docs[i].rdp; }
804
805 // Remove Intel AMT credential if present
806 if (docs[i].intelamt != null) {
807 if (docs[i].intelamt.pass != null) { docs[i].intelamt.pass = 1; }
808 if (docs[i].intelamt.mpspass != null) { docs[i].intelamt.mpspass = 1; }
809 }
810
811 // If GeoLocation not enabled, remove any node location information
812 if (domain.geolocation != true) {
813 if (docs[i].iploc != null) { delete docs[i].iploc; }
814 if (docs[i].wifiloc != null) { delete docs[i].wifiloc; }
815 if (docs[i].gpsloc != null) { delete docs[i].gpsloc; }
816 if (docs[i].userloc != null) { delete docs[i].userloc; }
817 }
818
819 // Add device sessions
820 const xagent = parent.wsagents[docs[i]._id];
821 if ((xagent != null) && (xagent.sessions != null)) { docs[i].sessions = xagent.sessions; }
822
823 // Add IP-KVM sessions
824 if (parent.parent.ipKvmManager != null) {
825 const xipkvmport = parent.parent.ipKvmManager.managedPorts[docs[i]._id];
826 if ((xipkvmport != null) && (xipkvmport.sessions != null)) { docs[i].sessions = xipkvmport.sessions; }
827 }
828
829 // Patch node links with names, like meshes links with names
830 for (var a in docs[i].links) {
831 if (!docs[i].links[a].name) {
832 if (parent.users[a] && parent.users[a].realname) { docs[i].links[a].name = parent.users[a].realname; }
833 else if (parent.users[a] && parent.users[a].name) { docs[i].links[a].name = parent.users[a].name; }
834 }
835 }
836
837 r[meshid].push(docs[i]);
838 }
839 const response = { action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag };
840 if (domain.maxdeviceview != null) {
841 // If in paging mode, report back the skip and limit values
842 response.skip = obj.deviceSkip;
843 response.limit = obj.deviceLimit;
844
845 // Add total device count
846 // Only set response.totalcount if we need to be in paging mode
847 if (nodeCount < response.limit) {
848 if (obj.deviceSkip > 0) { response.totalcount = obj.deviceSkip + nodeCount; } else { obj.visibleDevices = null; }
849 try { ws.send(JSON.stringify(response)); } catch (ex) { }
850 } else {
851 // Ask the database for the total device count
852 if (db.CountAllTypeNoTypeFieldMeshFiltered) {
853 db.CountAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, function (err, count) {
854 if ((err != null) || (typeof count != 'number') || ((obj.deviceSkip == 0) && (count < obj.deviceLimit))) {
855 obj.visibleDevices = null;
856 } else {
857 response.totalcount = count;
858 }
859 try { ws.send(JSON.stringify(response)); } catch (ex) { }
860 });
861 } else {
862 // The database does not support device counting
863 obj.visibleDevices = null; // We are not in paging mode
864 try { ws.send(JSON.stringify(response)); } catch (ex) { }
865 }
866 }
867 } else {
868 obj.visibleDevices = null; // We are not in paging mode
869 try { ws.send(JSON.stringify(response)); } catch (ex) { }
870 }
871 });
872 break;
873 }
874 case 'fileoperation':
875 {
876 // Check permissions
877 if ((user.siteadmin & 8) != 0) {
878 // Perform a file operation (Create Folder, Delete Folder, Delete File...)
879 if (common.validateString(command.fileop, 3, 16) == false) return;
880 var sendUpdate = true, path = meshPathToRealPath(command.path, user); // This will also check access rights
881 if (path == null) break;
882
883 if ((command.fileop == 'createfolder') && (common.IsFilenameValid(command.newfolder) == true)) {
884 // Create a new folder
885 try { fs.mkdirSync(parent.path.join(path, command.newfolder)); } catch (ex) {
886 try { fs.mkdirSync(path); } catch (ex) { }
887 try { fs.mkdirSync(parent.path.join(path, command.newfolder)); } catch (ex) { }
888 }
889 }
890 else if (command.fileop == 'delete') {
891 // Delete a file
892 if (common.validateArray(command.delfiles, 1) == false) return;
893 for (i in command.delfiles) {
894 if (common.IsFilenameValid(command.delfiles[i]) == true) {
895 var fullpath = parent.path.join(path, command.delfiles[i]);
896 if (command.rec == true) {
897 try { deleteFolderRecursive(fullpath); } catch (ex) { } // TODO, make this an async function
898 } else {
899 try { fs.rmdirSync(fullpath); } catch (ex) { try { fs.unlinkSync(fullpath); } catch (xe) { } }
900 }
901 }
902 }
903
904 // If we deleted something in the mesh root folder and the entire mesh folder is empty, remove it.
905 if (command.path.length == 1) {
906 try {
907 if (command.path[0].startsWith('mesh//')) {
908 path = meshPathToRealPath([command.path[0]], user);
909 fs.readdir(path, function (err, dir) { if ((err == null) && (dir.length == 0)) { fs.rmdir(path, function (err) { }); } });
910 }
911 } catch (ex) { }
912 }
913 }
914 else if ((command.fileop == 'rename') && (common.IsFilenameValid(command.oldname) === true) && (common.IsFilenameValid(command.newname) === true)) {
915 // Rename
916 try { fs.renameSync(parent.path.join(path, command.oldname), parent.path.join(path, command.newname)); } catch (e) { }
917 }
918 else if ((command.fileop == 'copy') || (command.fileop == 'move')) {
919 // Copy or move of one or many files
920 if (common.validateArray(command.names, 1) == false) return;
921 var scpath = meshPathToRealPath(command.scpath, user); // This will also check access rights
922 if (scpath == null) break;
923 // TODO: Check quota if this is a copy
924 for (i in command.names) {
925 if (common.IsFilenameValid(command.names[i]) === true) {
926 var s = parent.path.join(scpath, command.names[i]), d = parent.path.join(path, command.names[i]);
927 sendUpdate = false;
928 try { fs.mkdirSync(path); } catch (ex) { } // try to create folder first incase folder is missing
929 copyFile(s, d, function (op) { if (op != null) { fs.unlink(op, function (err) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); }); } else { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } }, ((command.fileop == 'move') ? s : null));
930 }
931 }
932 } else if (command.fileop == 'get') {
933 // Get a short file and send it back on the web socket
934 if (common.validateString(command.file, 1, 4096) == false) return;
935 const scpath = meshPathToRealPath(command.path, user); // This will also check access rights
936 if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break;
937 const filePath = parent.path.join(scpath, command.file);
938 fs.stat(filePath, function (err, stat) {
939 if ((err != null) || (stat == null) || (stat.size >= 204800)) return;
940 fs.readFile(filePath, function (err, data) {
941 if ((err != null) || (data == null)) return;
942 command.data = data.toString('base64');
943 ws.send(JSON.stringify(command)); // Send the file data back, base64 encoded.
944 });
945 });
946 } else if (command.fileop == 'set') {
947 // Set a short file transfered on the web socket
948 if (common.validateString(command.file, 1, 4096) == false) return;
949 if (typeof command.data != 'string') return;
950 const scpath = meshPathToRealPath(command.path, user); // This will also check access rights
951 if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break;
952 const filePath = parent.path.join(scpath, command.file);
953 var data = null;
954 try { data = Buffer.from(command.data, 'base64'); } catch (ex) { return; }
955 fs.writeFile(filePath, data, function (err) { if (err == null) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } });
956 }
957 if (sendUpdate == true) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } // Fire an event causing this user to update this files
958 }
959 break;
960 }
961 case 'msg':
962 {
963 // Check the nodeid
964 if (common.validateString(command.nodeid, 1, 1024) == false) {
965 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'msg', result: 'Unable to route', tag: command.tag, responseid: command.responseid })); } catch (ex) { } }
966 return;
967 }
968
969 // Rights check
970 var requiredRights = null, requiredNonRights = null, routingOptions = null;
971
972 // Complete the nodeid if needed
973 if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
974
975 // Check if getting / setting clipboard data is allowed
976 if ((command.type == 'getclip') && (domain.clipboardget == false)) { console.log('CG-EXIT'); break; }
977 if ((command.type == 'setclip') && (domain.clipboardset == false)) { console.log('CS-EXIT'); break; }
978
979 // Before routing this command, let's do some security checking.
980 // If this is a tunnel request, we need to make sure the NodeID in the URL matches the NodeID in the command.
981 if (command.type == 'tunnel') {
982 if ((typeof command.value != 'string') || (typeof command.nodeid != 'string')) break;
983 var url = null;
984 try { url = require('url').parse(command.value, true); } catch (ex) { }
985 if (url == null) break; // Bad URL
986 if (url.query && url.query.nodeid && (url.query.nodeid != command.nodeid)) break; // Bad NodeID in URL query string
987
988 // Check rights
989 if (url.query.p == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; }
990 else if ((url.query.p == '4') || (url.query.p == '5')) { requiredNonRights = MESHRIGHT_NOFILES; }
991
992 // If we are using the desktop multiplexor, remove the VIEWONLY limitation. The multiplexor will take care of enforcing that limitation when needed.
993 if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.query.p == '2')) { routingOptions = { removeViewOnlyLimitation: true }; }
994
995 // Add server TLS cert hash
996 var tlsCertHash = null;
997 if ((parent.parent.args.ignoreagenthashcheck == null) || (parent.parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
998 tlsCertHash = parent.webCertificateFullHashs[domain.id];
999 if (tlsCertHash != null) { command.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
1000 }
1001
1002 // Add user consent messages
1003 command.soptions = {};
1004 if (typeof domain.consentmessages == 'object') {
1005 if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
1006 if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
1007 if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; }
1008 if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
1009 if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
1010 if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
1011 if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
1012 if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
1013 if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
1014 if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
1015 if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
1016 if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
1017 if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
1018 if (domain.consentmessages.autoacceptiffilelocked === true) { command.soptions.consentAutoAcceptIfFileLocked = true; }
1019 if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
1020 }
1021 if (typeof domain.notificationmessages == 'object') {
1022 if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
1023 if (typeof domain.notificationmessages.desktop == 'string') { command.soptions.notifyMsgDesktop = domain.notificationmessages.desktop; }
1024 if (typeof domain.notificationmessages.terminal == 'string') { command.soptions.notifyMsgTerminal = domain.notificationmessages.terminal; }
1025 if (typeof domain.notificationmessages.files == 'string') { command.soptions.notifyMsgFiles = domain.notificationmessages.files; }
1026 }
1027
1028 // Add userid
1029 command.userid = user._id;
1030
1031 // Add tunnel pre-message deflate
1032 if (typeof parent.parent.config.settings.agentwscompression == 'boolean') { command.perMessageDeflate = parent.parent.config.settings.agentwscompression; }
1033 }
1034
1035 // If a response is needed, set a callback function
1036 var func = null;
1037 if (command.responseid != null) { func = function (r) { try { ws.send(JSON.stringify({ action: 'msg', result: r ? 'OK' : 'Unable to route', tag: command.tag, responseid: command.responseid })); } catch (ex) { } } }
1038
1039 // Route this command to a target node
1040 routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions);
1041 break;
1042 }
1043 case 'events':
1044 {
1045 // User filtered events
1046 if ((command.userid != null) && ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0)) {
1047 const userSplit = command.userid.split('/');
1048 if ((userSplit.length != 3) || (userSplit[1] != domain.id)) return;
1049
1050 // TODO: Add the meshes command.userid has access to (???)
1051 var filter = [command.userid];
1052
1053 var actionfilter = null;
1054 if (command.filter != null) {
1055 if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter;
1056 }
1057
1058 if ((command.limit == null) || (typeof command.limit != 'number')) {
1059 // Send the list of all events for this session
1060 db.GetUserEvents(filter, domain.id, command.userid, actionfilter, function (err, docs) {
1061 if (err != null) return;
1062 try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { }
1063 });
1064 } else {
1065 // Send the list of most recent events for this session, up to 'limit' count
1066 db.GetUserEventsWithLimit(filter, domain.id, command.userid, command.limit, actionfilter, function (err, docs) {
1067 if (err != null) return;
1068 try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { }
1069 });
1070 }
1071 } else if (command.nodeid != null) { // Device filtered events
1072 // Check that the user has access to this nodeid
1073
1074 const nodeSplit = command.nodeid.split('/');
1075 if (nodeSplit.length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
1076
1077 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
1078 if (node == null) { try { ws.send(JSON.stringify({ action: 'events', events: [], nodeid: command.nodeid, tag: command.tag })); } catch (ex) { } return; }
1079
1080 // Put a limit on the number of returned entries if present
1081 var limit = 10000;
1082 if (common.validateInt(command.limit, 1, 1000000) == true) { limit = command.limit; }
1083
1084 var filter = null;
1085 if (command.filter != null) {
1086 if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) filter = command.filter;
1087 }
1088
1089 if (((rights & MESHRIGHT_LIMITEVENTS) != 0) && (rights != MESHRIGHT_ADMIN)) {
1090 // Send the list of most recent events for this nodeid that only apply to us, up to 'limit' count
1091 db.GetNodeEventsSelfWithLimit(node._id, domain.id, user._id, limit, filter, function (err, docs) {
1092 if (err != null) return;
1093 try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { }
1094 });
1095 } else {
1096 // Send the list of most recent events for this nodeid, up to 'limit' count
1097 db.GetNodeEventsWithLimit(node._id, domain.id, limit, filter, function (err, docs) {
1098 if (err != null) return;
1099 try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { }
1100 });
1101 }
1102 });
1103 } else {
1104 // Create a filter for device groups
1105 if ((obj.user == null) || (obj.user.links == null)) return;
1106
1107 // All events
1108 var exGroupFilter2 = [], filter = [], filter2 = user.subscriptions;
1109
1110 // Add all meshes for groups this user is part of
1111 // TODO (UserGroups)
1112
1113 // Remove MeshID's that we do not have rights to see events for
1114 for (var link in obj.user.links) { if (((obj.user.links[link].rights & MESHRIGHT_LIMITEVENTS) != 0) && ((obj.user.links[link].rights != MESHRIGHT_ADMIN))) { exGroupFilter2.push(link); } }
1115 for (var i in filter2) { if (exGroupFilter2.indexOf(filter2[i]) == -1) { filter.push(filter2[i]); } }
1116
1117 var actionfilter = null;
1118 if (command.filter != null) {
1119 if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter;
1120 }
1121
1122 if ((command.limit == null) || (typeof command.limit != 'number')) {
1123 // Send the list of all events for this session
1124 db.GetEvents(filter, domain.id, actionfilter, function (err, docs) {
1125 if (err != null) return;
1126 try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { }
1127 });
1128 } else {
1129 // Send the list of most recent events for this session, up to 'limit' count
1130 db.GetEventsWithLimit(filter, domain.id, command.limit, actionfilter, function (err, docs) {
1131 if (err != null) return;
1132 try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { }
1133 });
1134 }
1135 }
1136 break;
1137 }
1138 case 'recordings': {
1139 if (((user.siteadmin & SITERIGHT_RECORDINGS) == 0) || (domain.sessionrecording == null)) return; // Check if recordings is enabled and we have rights to do this.
1140 var recordingsPath = null;
1141 if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.parent.recordpath; }
1142 if (recordingsPath == null) return;
1143 fs.readdir(recordingsPath, function (err, files) {
1144 if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 1, tag: command.tag })); } catch (ex) { } return; }
1145 if ((command.limit == null) || (typeof command.limit != 'number')) {
1146 // Send the list of all recordings
1147 db.GetEvents(['recording'], domain.id, null, function (err, docs) {
1148 if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; }
1149 for (var i in docs) {
1150 delete docs[i].action; delete docs[i].etype; delete docs[i].msg; // TODO: We could make a more specific query in the DB and never have these.
1151 if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
1152 }
1153 try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
1154 });
1155 } else {
1156 // Send the list of most recent recordings, up to 'limit' count
1157 db.GetEventsWithLimit(['recording'], domain.id, command.limit, null, function (err, docs) {
1158 if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; }
1159 for (var i in docs) {
1160 delete docs[i].action; delete docs[i].etype; delete docs[i].msg; // TODO: We could make a more specific query in the DB and never have these.
1161 if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
1162 }
1163 try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
1164 });
1165 }
1166 });
1167 break;
1168 }
1169 case 'wssessioncount':
1170 {
1171 // Request a list of all web socket user session count
1172 var wssessions = {};
1173 if ((user.siteadmin & 2) == 0) { try { ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: {}, tag: command.tag })); } catch (ex) { } break; }
1174 if (parent.parent.multiServer == null) {
1175 // No peering, use simple session counting
1176 for (i in parent.wssessions) {
1177 if ((obj.crossDomain === true) || (parent.wssessions[i][0].domainid == domain.id)) {
1178 if ((user.groups == null) || (user.groups.length == 0)) {
1179 // No user groups, count everything
1180 wssessions[i] = parent.wssessions[i].length;
1181 } else {
1182 // Only count if session is for a user in our user groups
1183 var sessionUser = parent.users[parent.wssessions[i][0].userid];
1184 if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
1185 wssessions[i] = parent.wssessions[i].length;
1186 }
1187 }
1188 }
1189 }
1190 } else {
1191 // We have peer servers, use more complex session counting
1192 for (i in parent.sessionsCount) {
1193 if ((obj.crossDomain === true) || (i.split('/')[1] == domain.id)) {
1194 if ((user.groups == null) || (user.groups.length == 0)) {
1195 // No user groups, count everything
1196 wssessions[i] = parent.sessionsCount[i];
1197 } else {
1198 // Only count if session is for a user in our user groups
1199 var sessionUser = parent.users[i];
1200 if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
1201 wssessions[i] = parent.sessionsCount[i];
1202 }
1203 }
1204 }
1205 }
1206 }
1207 try { ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: wssessions, tag: command.tag })); } catch (ex) { } // wssessions is: userid --> count
1208 break;
1209 }
1210 case 'deleteuser':
1211 {
1212 // Delete a user account
1213 var err = null, delusersplit, deluserid, deluser, deluserdomain;
1214 try {
1215 if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
1216 else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
1217 else {
1218 if (command.userid.indexOf('/') < 0) { command.userid = 'user/' + domain.id + '/' + command.userid; }
1219 delusersplit = command.userid.split('/');
1220 deluserid = command.userid;
1221 deluser = parent.users[deluserid];
1222 if (deluser == null) { err = 'User does not exists'; }
1223 else if ((obj.crossDomain !== true) && ((delusersplit.length != 3) || (delusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
1224 else if ((deluser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Permission denied'; } // Need full admin to remote another administrator
1225 else if ((obj.crossDomain !== true) && (user.groups != null) && (user.groups.length > 0) && ((deluser.groups == null) || (findOne(deluser.groups, user.groups) == false))) { err = 'Invalid user group'; } // Can only perform this operation on other users of our group.
1226 }
1227 } catch (ex) { err = 'Validation exception: ' + ex; }
1228
1229 // Get domain
1230 deluserdomain = domain;
1231 if (obj.crossDomain === true) { deluserdomain = parent.parent.config.domains[delusersplit[1]]; }
1232 if (deluserdomain == null) { err = 'Invalid domain'; }
1233
1234 // Handle any errors
1235 if (err != null) {
1236 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: err })); } catch (ex) { } }
1237 break;
1238 }
1239
1240 // Remove all links to this user
1241 if (deluser.links != null) {
1242 for (var i in deluser.links) {
1243 if (i.startsWith('mesh/')) {
1244 // Get the device group
1245 mesh = parent.meshes[i];
1246 if (mesh) {
1247 // Remove user from the mesh
1248 if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
1249
1250 // Notify mesh change
1251 change = 'Removed user ' + deluser.name + ' from device group ' + mesh.name;
1252 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msgid: 72, msgArgs: [deluser.name, mesh.name], msg: change, domain: deluserdomain.id, invite: mesh.invite };
1253 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1254 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [deluser._id, user._id]), obj, event);
1255 }
1256 } else if (i.startsWith('node/')) {
1257 // Get the node and the rights for this node
1258 parent.GetNodeWithRights(deluserdomain, deluser, i, function (node, rights, visible) {
1259 if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
1260
1261 // Remove the link and save the node to the database
1262 delete node.links[deluser._id];
1263 if (Object.keys(node.links).length == 0) { delete node.links; }
1264 db.Set(parent.cleanDevice(node));
1265
1266 // Event the node change
1267 var event;
1268 if (command.rights == 0) {
1269 event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: deluserdomain.id, msgid: 60, msgArgs: [node.name], msg: 'Removed user device rights for ' + node.name, node: parent.CloneSafeNode(node) }
1270 } else {
1271 event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: deluserdomain.id, msgid: 61, msgArgs: [node.name], msg: 'Changed user device rights for ' + node.name, node: parent.CloneSafeNode(node) }
1272 }
1273 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1274 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
1275 });
1276 } else if (i.startsWith('ugrp/')) {
1277 // Get the device group
1278 var ugroup = parent.userGroups[i];
1279 if (ugroup) {
1280 // Remove user from the user group
1281 if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
1282
1283 // Notify user group change
1284 change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
1285 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 62, msgArgs: [deluser.name, ugroup.name], msg: 'Removed user ' + deluser.name + ' from user group ' + ugroup.name, addUserDomain: deluserdomain.id };
1286 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
1287 parent.parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
1288 }
1289 }
1290 }
1291 }
1292
1293 db.Remove('ws' + deluser._id); // Remove user web state
1294 db.Remove('nt' + deluser._id); // Remove notes for this user
1295 db.Remove('ntp' + deluser._id); // Remove personal notes for this user
1296 db.Remove('im' + deluser._id); // Remove image for this user
1297
1298 // Delete any login tokens
1299 parent.parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
1300 if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.parent.db.Remove(docs[i]._id, function () { }); } }
1301 });
1302
1303 // Delete all files on the server for this account
1304 try {
1305 var deluserpath = parent.getServerRootFilePath(deluser);
1306 if (deluserpath != null) { parent.deleteFolderRec(deluserpath); }
1307 } catch (e) { }
1308
1309 db.Remove(deluserid);
1310 delete parent.users[deluserid];
1311
1312 var targets = ['*', 'server-users'];
1313 if (deluser.groups) { for (var i in deluser.groups) { targets.push('server-users:' + i); } }
1314 parent.parent.DispatchEvent(targets, obj, { etype: 'user', userid: deluserid, username: deluser.name, action: 'accountremove', msgid: 63, msg: 'Account removed', domain: deluserdomain.id });
1315 parent.parent.DispatchEvent([deluserid], obj, 'close');
1316
1317 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1318
1319 // Log in the auth log
1320 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted user account ' + deluser.name); }
1321
1322 break;
1323 }
1324 case 'userbroadcast':
1325 {
1326 var err = null;
1327 try {
1328 // Broadcast a message to all currently connected users.
1329 if ((user.siteadmin & 2) == 0) { err = "Permission denied"; }
1330 else if (common.validateString(command.msg, 1, 512) == false) { err = "Message is too long"; } // Notification message is between 1 and 256 characters
1331 } catch (ex) { err = "Validation exception: " + ex; }
1332
1333 // Handle any errors
1334 if (err != null) {
1335 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: err })); } catch (ex) { } }
1336 break;
1337 }
1338
1339 // Create the notification message
1340 var notification = { action: 'msg', type: 'notify', domain: domain.id, value: command.msg, title: user.name, icon: 0, tag: 'broadcast', id: Math.random() };
1341 if ((typeof command.maxtime == 'number') && (command.maxtime > 0)) { notification.maxtime = command.maxtime; }
1342
1343 // Send the notification on all user sessions for this server
1344 for (var i in parent.wssessions2) {
1345 try {
1346 if (parent.wssessions2[i].domainid == domain.id) {
1347 var sessionUser = parent.users[parent.wssessions2[i].userid];
1348 if ((command.userid != null) && (command.userid != sessionUser._id) && (command.userid != sessionUser._id.split('/')[2])) { continue; }
1349 if ((command.target == null) || ((sessionUser.links) != null && (sessionUser.links[command.target] != null))) {
1350 if ((user.groups == null) || (user.groups.length == 0)) {
1351 // We are part of no user groups, send to everyone.
1352 parent.wssessions2[i].send(JSON.stringify(notification));
1353 } else {
1354 // We are part of user groups, only send to sessions of users in our groups.
1355 if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
1356 parent.wssessions2[i].send(JSON.stringify(notification));
1357 }
1358 }
1359 }
1360 }
1361 } catch (ex) { }
1362 }
1363
1364 // TODO: Notify all sessions on other peers.
1365
1366 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1367 break;
1368 }
1369 case 'edituser':
1370 {
1371 // Must be user administrator or edit self.
1372 if (((user.siteadmin & 2) == 0) && (user._id != command.id)) break;
1373
1374 // User the username as userid if needed
1375 if ((typeof command.username == 'string') && (command.userid == null)) { command.userid = command.username; }
1376 if ((typeof command.id == 'string') && (command.userid == null)) { command.userid = command.id; }
1377
1378 // Edit a user account
1379 var err = null, editusersplit, edituserid, edituser, edituserdomain;
1380 try {
1381 if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
1382 else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
1383 else {
1384 if (command.userid.indexOf('/') < 0) { command.userid = 'user/' + domain.id + '/' + command.userid; }
1385 editusersplit = command.userid.split('/');
1386 edituserid = command.userid;
1387 edituser = parent.users[edituserid];
1388 if (edituser == null) { err = 'User does not exists'; }
1389 else if ((obj.crossDomain !== true) && ((editusersplit.length != 3) || (editusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
1390 else if ((edituser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Permission denied'; } // Need full admin to remote another administrator
1391 else if ((obj.crossDomain !== true) && (user.groups != null) && (user.groups.length > 0) && ((edituser.groups == null) || (findOne(edituser.groups, user.groups) == false))) { err = 'Invalid user group'; } // Can only perform this operation on other users of our group.
1392 }
1393 } catch (ex) { err = 'Validation exception: ' + ex; }
1394
1395 // Handle any errors
1396 if (err != null) {
1397 if (command.responseid != null) {
1398 try { ws.send(JSON.stringify({ action: 'edituser', responseid: command.responseid, result: err })); } catch (ex) { }
1399 }
1400 break;
1401 }
1402
1403 // Edit a user account, may involve changing email or administrator permissions
1404 var chguser = parent.users[edituserid];
1405 change = 0;
1406 if (chguser) {
1407 // If the target user is admin and we are not admin, no changes can be made.
1408 if ((chguser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) return;
1409
1410 // Can only perform this operation on other users of our group.
1411 if (user.siteadmin != SITERIGHT_ADMIN) {
1412 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) return;
1413 }
1414
1415 // Fetch and validate the user domain
1416 var edituserdomainid = edituserid.split('/')[1];
1417 if ((obj.crossDomain !== true) && (edituserdomainid != domain.id)) break;
1418 var edituserdomain = parent.parent.config.domains[edituserdomainid];
1419 if (edituserdomain == null) break;
1420
1421 // Validate and change email
1422 if (edituserdomain.usernameisemail !== true) {
1423 if (common.validateString(command.email, 0, 1024) && (chguser.email != command.email)) {
1424 if (command.email == '') { command.emailVerified = false; delete chguser.email; } else { chguser.email = command.email.toLowerCase(); }
1425 change = 1;
1426 }
1427 }
1428
1429 // Validate and change real name
1430 if (common.validateString(command.realname, 0, 256) && (chguser.realname != command.realname)) {
1431 if (command.realname == '') { delete chguser.realname; } else { chguser.realname = command.realname; }
1432 change = 1;
1433 }
1434
1435 // Make changes
1436 if ((command.emailVerified === true || command.emailVerified === false) && (chguser.emailVerified != command.emailVerified)) { chguser.emailVerified = command.emailVerified; change = 1; }
1437 if ((common.validateInt(command.quota, 0) || command.quota == null) && (command.quota != chguser.quota)) { chguser.quota = command.quota; if (chguser.quota == null) { delete chguser.quota; } change = 1; }
1438 if (command.resetNextLogin === true) { chguser.passchange = -1; }
1439 if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { chguser.consent = command.consent; } change = 1; }
1440 if ((command.phone != null) && (typeof command.phone == 'string') && ((command.phone == '') || isPhoneNumber(command.phone))) { if (command.phone == '') { delete chguser.phone; } else { chguser.phone = command.phone; } change = 1; }
1441 if ((command.msghandle != null) && (typeof command.msghandle == 'string')) {
1442 if (command.msghandle.startsWith('callmebot:http')) { const h = parent.parent.msgserver.callmebotUrlToHandle(command.msghandle.substring(10)); if (h) { command.msghandle = h; } else { command.msghandle = ''; } }
1443 if (command.msghandle == '') { delete chguser.msghandle; } else { chguser.msghandle = command.msghandle; }
1444 change = 1;
1445 }
1446 if ((command.flags != null) && (typeof command.flags == 'number')) {
1447 // Flags: 1 = Account Image, 2 = Session Recording
1448 if ((command.flags == 0) && (chguser.flags != null)) { delete chguser.flags; change = 1; } else { if (command.flags !== chguser.flags) { chguser.flags = command.flags; change = 1; } }
1449 }
1450 if ((command.removeRights != null) && (typeof command.removeRights == 'number')) {
1451 if (command.removeRights == 0) {
1452 if (chguser.removeRights != null) { delete chguser.removeRights; change = 1; }
1453 } else {
1454 if (command.removeRights !== chguser.removeRights) { chguser.removeRights = command.removeRights; change = 1; }
1455 }
1456 }
1457
1458 // Site admins can change any server rights, user managers can only change AccountLock, NoMeshCmd and NoNewGroups
1459 if (common.validateInt(command.siteadmin) && (chguser._id !== user._id) && (chguser.siteadmin != command.siteadmin)) { // We can't change our own siteadmin permissions.
1460 var chgusersiteadmin = chguser.siteadmin ? chguser.siteadmin : 0;
1461 if (user.siteadmin === SITERIGHT_ADMIN) { chguser.siteadmin = command.siteadmin; change = 1; }
1462 else if (user.siteadmin & 2) {
1463 var mask = 0xFFFFFF1D; // Mask: 2 (User Mangement) + 32 (Account locked) + 64 (No New Groups) + 128 (No Tools)
1464 if ((user.siteadmin & 256) != 0) { mask -= 256; } // Mask: Manage User Groups
1465 if ((user.siteadmin & 512) != 0) { mask -= 512; } // Mask: Manage Recordings
1466 if (((chgusersiteadmin ^ command.siteadmin) & mask) == 0) { chguser.siteadmin = command.siteadmin; change = 1; }
1467 }
1468 }
1469
1470 // When sending a notification about a group change, we need to send to all the previous and new groups.
1471 var allTargetGroups = chguser.groups;
1472 if ((Array.isArray(command.groups)) && ((user._id != command.id) || (user.siteadmin === SITERIGHT_ADMIN))) {
1473 if (command.groups.length == 0) {
1474 // Remove the user groups
1475 if (chguser.groups != null) { delete chguser.groups; change = 1; }
1476 } else {
1477 // Arrange the user groups
1478 var groups2 = [];
1479 for (var i in command.groups) {
1480 if (typeof command.groups[i] == 'string') {
1481 var gname = command.groups[i].trim().toLowerCase();
1482 if ((gname.length > 0) && (gname.length <= 64) && (groups2.indexOf(gname) == -1)) { groups2.push(gname); }
1483 }
1484 }
1485 groups2.sort();
1486
1487 // Set the user groups (Realms)
1488 if (chguser.groups != groups2) { chguser.groups = groups2; change = 1; }
1489
1490 // Add any missing groups in the target list
1491 if (allTargetGroups == null) { allTargetGroups = []; }
1492 for (var i in groups2) { if (allTargetGroups.indexOf(i) == -1) { allTargetGroups.push(i); } }
1493 }
1494 }
1495
1496 if (change == 1) {
1497 // Update the user
1498 db.SetUser(chguser);
1499 parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
1500
1501 var targets = ['*', 'server-users', user._id, chguser._id];
1502 if (allTargetGroups) { for (var i in allTargetGroups) { targets.push('server-users:' + i); } }
1503 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 66, msgArgs: [chguser.name], msg: 'Account changed: ' + chguser.name, domain: edituserdomain.id };
1504 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1505 parent.parent.DispatchEvent(targets, obj, event);
1506 }
1507 if ((chguser.siteadmin) && (chguser.siteadmin !== SITERIGHT_ADMIN) && (chguser.siteadmin & 32)) {
1508 // If the user is locked out of this account, disconnect now
1509 parent.parent.DispatchEvent([chguser._id], obj, 'close'); // Disconnect all this user's sessions
1510 }
1511 }
1512
1513 // OK Response
1514 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'edituser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1515 break;
1516 }
1517 case 'usergroups':
1518 {
1519 // Return only groups in the same administrative domain
1520 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) {
1521 // We are not user group administrator, return a list with limited data for our domain.
1522 var groups = {}, groupCount = 0;
1523 for (var i in parent.userGroups) { if (parent.userGroups[i].domain == domain.id) { groupCount++; groups[i] = { name: parent.userGroups[i].name }; } }
1524 try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: groupCount ? groups : null, tag: command.tag })); } catch (ex) { }
1525 } else {
1526 // We are user group administrator, return a full user group list for our domain.
1527 var groups = {}, groupCount = 0;
1528 for (var i in parent.userGroups) { if ((obj.crossDomain == true) || (parent.userGroups[i].domain == domain.id)) { groupCount++; groups[i] = parent.userGroups[i]; } }
1529 try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: groupCount ? groups : null, tag: command.tag })); } catch (ex) { }
1530 }
1531 break;
1532 }
1533 case 'createusergroup':
1534 {
1535 var ugrpdomain, err = null;
1536 try {
1537 // Check if we are in a mode that does not allow manual user group creation
1538 if (
1539 (typeof domain.authstrategies == 'object') &&
1540 (typeof domain.authstrategies['oidc'] == 'object') &&
1541 (typeof domain.authstrategies['oidc'].groups == 'object') &&
1542 ((domain.authstrategies['oidc'].groups.sync == true) || ((typeof domain.authstrategies['oidc'].groups.sync == 'object') && (domain.authstrategies['oidc'].groups.sync.enabled == true)))
1543 ) {
1544 err = "Not allowed in OIDC mode with user group sync.";
1545 }
1546
1547 // Check if we have new group restriction
1548 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = "Permission denied"; }
1549
1550 // Create user group validation
1551 else if (common.validateString(command.name, 1, 64) == false) { err = "Invalid group name"; } // User group name is between 1 and 64 characters
1552 else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = "Invalid group description"; } // User group description is between 0 and 1024 characters
1553
1554 // If we are cloning from an existing user group, check that.
1555 if (command.clone) {
1556 if (common.validateString(command.clone, 1, 256) == false) { err = "Invalid clone groupid"; }
1557 else {
1558 var clonesplit = command.clone.split('/');
1559 if ((clonesplit.length != 3) || (clonesplit[0] != 'ugrp') || ((command.domain == null) && (clonesplit[1] != domain.id))) { err = "Invalid clone groupid"; }
1560 else if (parent.userGroups[command.clone] == null) { err = "Invalid clone groupid"; }
1561 }
1562
1563 if (err == null) {
1564 // Get new user group domain
1565 ugrpdomain = parent.parent.config.domains[clonesplit[1]];
1566 if (ugrpdomain == null) { err = "Invalid domain"; }
1567 }
1568 } else {
1569 // Get new user group domain
1570 ugrpdomain = domain;
1571 if ((obj.crossDomain === true) && (command.domain != null)) { ugrpdomain = parent.parent.config.domains[command.domain]; }
1572 if (ugrpdomain == null) { err = "Invalid domain"; }
1573 }
1574
1575 // In some situations, we need a verified email address to create a device group.
1576 if ((err == null) && (domain.mailserver != null) && (ugrpdomain.auth != 'sspi') && (ugrpdomain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != SITERIGHT_ADMIN)) { err = "Email verification required"; } // User must verify it's email first.
1577 } catch (ex) { err = "Validation exception: " + ex; }
1578
1579 // Handle any errors
1580 if (err != null) {
1581 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
1582 break;
1583 }
1584
1585 // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
1586 parent.crypto.randomBytes(48, function (err, buf) {
1587 // Create new device group identifier
1588 var ugrpid = 'ugrp/' + ugrpdomain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1589
1590 // Create the new device group
1591 var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: ugrpdomain.id, links: {} };
1592
1593 // Clone the existing group if required
1594 var pendingDispatchEvents = [];
1595 if (command.clone != null) {
1596 var cgroup = parent.userGroups[command.clone];
1597 if (cgroup.links) {
1598 for (var i in cgroup.links) {
1599 if (i.startsWith('user/')) {
1600 var xuser = parent.users[i];
1601 if ((xuser != null) && (xuser.links != null)) {
1602 ugrp.links[i] = { rights: cgroup.links[i].rights };
1603 xuser.links[ugrpid] = { rights: cgroup.links[i].rights };
1604 db.SetUser(xuser);
1605 parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
1606
1607 // Notify user change
1608 var targets = ['*', 'server-users', user._id, xuser._id];
1609 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(xuser), action: 'accountchange', msgid: 67, msgArgs: [xuser.name], msg: 'User group membership changed: ' + xuser.name, domain: ugrpdomain.id };
1610 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1611 //parent.parent.DispatchEvent(targets, obj, event);
1612 pendingDispatchEvents.push([targets, obj, event]);
1613 }
1614 } else if (i.startsWith('mesh/')) {
1615 var xmesh = parent.meshes[i];
1616 if (xmesh && xmesh.links) {
1617 ugrp.links[i] = { rights: cgroup.links[i].rights };
1618 xmesh.links[ugrpid] = { rights: cgroup.links[i].rights };
1619 db.Set(xmesh);
1620
1621 // Notify mesh change
1622 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: xmesh._id, name: xmesh.name, mtype: xmesh.mtype, desc: xmesh.desc, action: 'meshchange', links: xmesh.links, msgid: 68, msgArgs: [ugrp.name, xmesh.name], msg: 'Added user group ' + ugrp.name + ' to device group ' + xmesh.name, domain: ugrpdomain.id, invite: xmesh.invite };
1623 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1624 //parent.parent.DispatchEvent(['*', xmesh._id, user._id], obj, event);
1625 pendingDispatchEvents.push([parent.CreateMeshDispatchTargets(xmesh, [user._id]), obj, event]);
1626 }
1627 }
1628 }
1629 }
1630 }
1631
1632 // Save the new group
1633 db.Set(ugrp);
1634 if (db.changeStream == false) { parent.userGroups[ugrpid] = ugrp; }
1635
1636 // Event the user group creation
1637 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrpid, name: ugrp.name, desc: ugrp.desc, action: 'createusergroup', links: ugrp.links, msgid: 69, msgArgv: [ugrp.name], msg: 'User group created: ' + ugrp.name, ugrpdomain: domain.id };
1638 parent.parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
1639
1640 // Event any pending events, these must be sent out after the group creation event is dispatched.
1641 for (var i in pendingDispatchEvents) { var ev = pendingDispatchEvents[i]; parent.parent.DispatchEvent(ev[0], ev[1], ev[2]); }
1642
1643 // Log in the auth log
1644 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created user group ' + ugrp.name); }
1645
1646 try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: 'ok', ugrpid: ugrpid, links: ugrp.links })); } catch (ex) { }
1647 });
1648 break;
1649 }
1650 case 'deleteusergroup':
1651 {
1652 var err = null;
1653
1654 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = "Permission denied"; }
1655
1656 // Change the name or description of a user group
1657 else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = "Invalid group id"; } // Check the user group id
1658 else {
1659 var ugroupidsplit = command.ugrpid.split('/');
1660 if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = "Invalid domain id"; }
1661 }
1662
1663 // Get the domain
1664 var delGroupDomain;
1665 if (ugroupidsplit != null) {
1666 delGroupDomain = parent.parent.config.domains[ugroupidsplit[1]];
1667 if (delGroupDomain == null) { err = "Invalid domain id"; }
1668 }
1669
1670 // Handle any errors
1671 if (err != null) {
1672 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
1673 break;
1674 }
1675
1676 db.Get(command.ugrpid, function (err, groups) {
1677 if ((err != null) || (groups.length != 1)) {
1678 try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { }
1679 return;
1680 }
1681 var group = groups[0];
1682
1683 // If this user group is an externally managed user group, it can't be deleted unless there are no users in it.
1684 if (group.membershipType != null) {
1685 var userCount = 0;
1686 if (group.links != null) { for (var i in group.links) { if (i.startsWith('user/')) { userCount++; } } }
1687 if (userCount > 0) return;
1688 }
1689
1690 // Unlink any user and meshes that have a link to this group
1691 if (group.links) {
1692 for (var i in group.links) {
1693 if (i.startsWith('user/')) {
1694 var xuser = parent.users[i];
1695 if ((xuser != null) && (xuser.links != null)) {
1696 delete xuser.links[group._id];
1697 db.SetUser(xuser);
1698 parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
1699
1700 // Notify user change
1701 var targets = ['*', 'server-users', user._id, xuser._id];
1702 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(xuser), action: 'accountchange', msgid: 67, msgArgs: [xuser.name], msg: 'User group membership changed: ' + xuser.name, delGroupDomain: domain.id };
1703 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1704 parent.parent.DispatchEvent(targets, obj, event);
1705 }
1706 } else if (i.startsWith('mesh/')) {
1707 var xmesh = parent.meshes[i];
1708 if (xmesh && xmesh.links) {
1709 delete xmesh.links[group._id];
1710 db.Set(xmesh);
1711
1712 // Notify mesh change
1713 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: xmesh._id, name: xmesh.name, mtype: xmesh.mtype, desc: xmesh.desc, action: 'meshchange', links: xmesh.links, msgid: 70, msgArgs: [group.name, xmesh.name], msg: 'Removed user group ' + group.name + ' from device group ' + xmesh.name, domain: delGroupDomain.id };
1714 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1715 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(xmesh, [user._id]), obj, event);
1716 }
1717 }
1718 }
1719 }
1720
1721 // Remove the user group from the database
1722 db.Remove(group._id);
1723 if (db.changeStream == false) { delete parent.userGroups[group._id]; }
1724
1725 // Event the user group being removed
1726 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, domain: delGroupDomain.id };
1727 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1728 parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
1729
1730 // Log in the auth log
1731 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted user group ' + group.name); }
1732
1733 try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: 'ok', ugrpid: group._id })); } catch (ex) { }
1734 });
1735 break;
1736 }
1737 case 'editusergroup':
1738 {
1739 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
1740
1741 // Change the name or description of a user group
1742 if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id
1743 var ugroupidsplit = command.ugrpid.split('/');
1744 if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break;
1745
1746 // Get the user group
1747 change = '';
1748 var group = parent.userGroups[command.ugrpid];
1749 if (group != null) {
1750 // If this user group is an externally managed user group, the name of the user group can't be edited
1751 if ((group.membershipType == null) && (common.validateString(command.name, 1, 64) == true) && (command.name != group.name)) { change = 'User group name changed from "' + group.name + '" to "' + command.name + '"'; group.name = command.name; }
1752 if ((common.validateString(command.desc, 0, 1024) == true) && (command.desc != group.desc)) { if (change != '') change += ' and description changed'; else change += 'User group "' + group.name + '" description changed'; group.desc = command.desc; }
1753 if ((typeof command.consent == 'number') && (command.consent != group.consent)) { if (change != '') change += ' and consent changed'; else change += 'User group "' + group.name + '" consent changed'; group.consent = command.consent; }
1754
1755 if ((command.flags != null) && (typeof command.flags == 'number')) {
1756 // Flags: 2 = Session Recording
1757 if ((command.flags == 0) && (group.flags != null)) { delete group.flags; } else { if (command.flags !== group.flags) { group.flags = command.flags; } }
1758 if (change == '') { change = 'User group features changed.'; }
1759 }
1760
1761 if (change != '') {
1762 db.Set(group);
1763 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, consent: ((group.consent == null) ? 0 : group.consent), action: 'usergroupchange', links: group.links, flags: group.flags, msg: change, domain: domain.id };
1764 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1765 parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
1766 }
1767 }
1768 break;
1769 }
1770 case 'changemeshnotify':
1771 {
1772 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
1773
1774 // 2 = WebPage device connections
1775 // 4 = WebPage device disconnections
1776 // 8 = WebPage device desktop and serial events
1777 // 16 = Email device connections
1778 // 32 = Email device disconnections
1779 // 64 = Email device help request
1780 // 128 = Messaging device connections
1781 // 256 = Messaging device disconnections
1782 // 512 = Messaging device help request
1783
1784 var err = null;
1785 try {
1786 // Change the current user's notification flags for a meshid
1787 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
1788 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
1789 if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
1790 if (parent.IsMeshViewable(user, command.meshid) == false) err = 'Access denied';
1791 } catch (ex) { err = 'Validation exception: ' + ex; }
1792
1793 // Handle any errors
1794 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changemeshnotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
1795
1796 // Change the device group notification
1797 if (user.links == null) { user.links = {}; }
1798 if (user.links[command.meshid]) {
1799 // The user has direct rights for this device group
1800 if (command.notify == 0) {
1801 delete user.links[command.meshid].notify;
1802 } else {
1803 user.links[command.meshid].notify = command.notify;
1804 }
1805 }
1806
1807 // Change user notification if needed, this is needed then a user has device rights thru a user group
1808 if ((command.notify == 0) && (user.notify != null) && (user.notify[command.meshid] != null)) { delete user.notify[command.meshid]; }
1809 if ((command.notify != 0) && (user.links[command.meshid] == null)) { if (user.notify == null) { user.notify = {} } user.notify[command.meshid] = command.notify; }
1810
1811 // Save the user
1812 parent.db.SetUser(user);
1813
1814 // Notify change
1815 var targets = ['*', 'server-users', user._id];
1816 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1817 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 73, msg: 'Device group notification changed', domain: domain.id };
1818 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1819 parent.parent.DispatchEvent(targets, obj, event);
1820
1821 break;
1822 }
1823 case 'changeusernotify':
1824 {
1825 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
1826
1827 // 2 = WebPage device connections
1828 // 4 = WebPage device disconnections
1829 // 8 = WebPage device desktop and serial events
1830 // 16 = Email device connections
1831 // 32 = Email device disconnections
1832 // 64 = Email device help request
1833 // 128 = Messaging device connections
1834 // 256 = Messaging device disconnections
1835 // 512 = Messaging device help request
1836
1837 var err = null;
1838 try {
1839 // Change the current user's notification flags for a meshid
1840 if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid device identifier'; } // Check the meshid
1841 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
1842 if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
1843 //if (parent.IsMeshViewable(user, command.nodeid) == false) err = 'Access denied';
1844 } catch (ex) { err = 'Validation exception: ' + ex; }
1845
1846 // Handle any errors
1847 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeusernotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
1848
1849 // Check if nothing has changed
1850 if ((user.notify == null) && (command.notify == 0)) return;
1851 if ((user.notify != null) && (user.notify[command.nodeid] == command.notify)) return;
1852
1853 // Change the notification
1854 if (user.notify == null) { user.notify = {}; }
1855 if (command.notify == 0) { delete user.notify[command.nodeid]; } else { user.notify[command.nodeid] = command.notify; }
1856 if (Object.keys(user.notify).length == 0) { delete user.notify; }
1857
1858 // Save the user
1859 parent.db.SetUser(user);
1860
1861 // Notify change
1862 var targets = ['*', 'server-users', user._id];
1863 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1864 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 130, msg: 'User notifications changed', domain: domain.id };
1865 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1866 parent.parent.DispatchEvent(targets, obj, event);
1867
1868 break;
1869 }
1870 case 'changepassword':
1871 {
1872 // Do not allow this command when logged in using a login token
1873 if (req.session.loginToken != null) break;
1874
1875 // If this account is settings locked, return here.
1876 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
1877
1878 // Do not allow change password if sspi or ldap
1879 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
1880
1881 // Change our own password
1882 if (common.validateString(command.oldpass, 1, 256) == false) break;
1883 if (common.validateString(command.newpass, 1, 256) == false) break;
1884 if ((command.hint != null) && (common.validateString(command.hint, 0, 256) == false)) break;
1885 if (common.checkPasswordRequirements(command.newpass, domain.passwordrequirements) == false) break; // Password does not meet requirements
1886
1887 // Start by checking the old password
1888 parent.checkUserPassword(domain, user, command.oldpass, function (result) {
1889 if (result == true) {
1890 parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
1891 if (result == 1) {
1892 // Send user notification of error
1893 displayNotificationMessage("Error, unable to change to previously used password.", "Account Settings", 'ServerNotify', 4, 17);
1894 } else if (result == 2) {
1895 // Send user notification of error
1896 displayNotificationMessage("Error, unable to change to commonly used password.", "Account Settings", 'ServerNotify', 4, 18);
1897 } else {
1898 // Update the password
1899 require('./pass').hash(command.newpass, function (err, salt, hash, tag) {
1900 if (err) {
1901 // Send user notification of error
1902 displayNotificationMessage("Error, password not changed.", "Account Settings", 'ServerNotify', 4, 19);
1903 } else {
1904 const nowSeconds = Math.floor(Date.now() / 1000);
1905
1906 // Change the password
1907 if (domain.passwordrequirements != null) {
1908 // Save password hint if this feature is enabled
1909 if ((domain.passwordrequirements.hint === true) && (command.hint != null)) { var hint = command.hint; if (hint.length > 250) { hint = hint.substring(0, 250); } user.passhint = hint; } else { delete user.passhint; }
1910
1911 // Save previous password if this feature is enabled
1912 if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1913 if (user.oldpasswords == null) { user.oldpasswords = []; }
1914 user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1915 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1916 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1917 }
1918 }
1919 user.salt = salt;
1920 user.hash = hash;
1921 user.passchange = nowSeconds;
1922 delete user.passtype;
1923 db.SetUser(user);
1924
1925 var targets = ['*', 'server-users'];
1926 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1927 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 74, msgArgs: [user.name], msg: 'Account password changed: ' + user.name, domain: domain.id };
1928 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1929 parent.parent.DispatchEvent(targets, obj, event);
1930
1931 // Send user notification of password change
1932 displayNotificationMessage("Password changed.", "Account Settings", 'ServerNotify', 4, 20);
1933
1934 // Log in the auth log
1935 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed this password'); }
1936 }
1937 }, 0);
1938 }
1939 });
1940 } else {
1941 // Send user notification of error
1942 displayNotificationMessage("Current password not correct.", "Account Settings", 'ServerNotify', 4, 21);
1943 }
1944 });
1945 break;
1946 }
1947 case 'changeuserpass':
1948 {
1949 // Change a user's password
1950 if ((user.siteadmin & 2) == 0) break;
1951 if (common.validateString(command.userid, 1, 256) == false) break;
1952 if (common.validateString(command.pass, 0, 256) == false) break;
1953 if ((command.hint != null) && (common.validateString(command.hint, 0, 256) == false)) break;
1954 if (typeof command.removeMultiFactor != 'boolean') break;
1955 if ((command.pass != '') && (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false)) break; // Password does not meet requirements
1956
1957 var chguser = parent.users[command.userid];
1958 if (chguser) {
1959 // If we are not full administrator, we can't change anything on a different full administrator
1960 if ((user.siteadmin != SITERIGHT_ADMIN) & (chguser.siteadmin === SITERIGHT_ADMIN)) break;
1961
1962 // Can only perform this operation on other users of our group.
1963 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
1964
1965 // Compute the password hash & save it
1966 require('./pass').hash(command.pass, function (err, salt, hash, tag) {
1967 if (!err) {
1968 if (command.pass != '') { chguser.salt = salt; chguser.hash = hash; }
1969 if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true) && (command.hint != null)) {
1970 var hint = command.hint;
1971 if (hint.length > 250) { hint = hint.substring(0, 250); }
1972 chguser.passhint = hint;
1973 }
1974 if (command.resetNextLogin === true) { chguser.passchange = -1; } else { chguser.passchange = Math.floor(Date.now() / 1000); }
1975 delete chguser.passtype; // Remove the password type if one was present.
1976 if (command.removeMultiFactor === true) {
1977 delete chguser.otpkeys; // One time backup codes
1978 delete chguser.otpsecret; // OTP Google Authenticator
1979 delete chguser.otphkeys; // FIDO keys
1980 delete chguser.otpekey; // Email 2FA
1981 delete chguser.phone; // SMS 2FA
1982 delete chguser.otpdev; // Push notification 2FA
1983 delete chguser.otpduo; // Duo 2FA
1984 }
1985 db.SetUser(chguser);
1986
1987 var targets = ['*', 'server-users', user._id, chguser._id];
1988 if (chguser.groups) { for (var i in chguser.groups) { targets.push('server-users:' + i); } }
1989 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 75, msg: 'Changed account credentials', domain: domain.id };
1990 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1991 parent.parent.DispatchEvent(targets, obj, event);
1992
1993 // Log in the auth log
1994 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed account password of user ' + chguser.name); }
1995 } else {
1996 // Report that the password change failed
1997 // TODO
1998 }
1999 }, 0);
2000 }
2001 break;
2002 }
2003 case 'notifyuser':
2004 {
2005 // Send a notification message to a user
2006 if ((user.siteadmin & 2) == 0) break;
2007 if (common.validateString(command.userid, 1, 2048) == false) break;
2008 if (common.validateString(command.msg, 1, 4096) == false) break;
2009
2010 // Can only perform this operation on other users of our group.
2011 var chguser = parent.users[command.userid];
2012 if (chguser == null) break; // This user does not exists
2013 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
2014
2015 // Create the notification message
2016 var notification = { action: 'msg', type: 'notify', id: Math.random(), value: command.msg, title: user.name, icon: 8, userid: user._id, username: user.name };
2017 if (typeof command.url == 'string') { notification.url = command.url; }
2018 if ((typeof command.maxtime == 'number') && (command.maxtime > 0)) { notification.maxtime = command.maxtime; }
2019 if (command.msgid == 11) { notification.value = "Chat Request, Click here to accept."; notification.msgid = 11; } // Chat request
2020
2021 // Get the list of sessions for this user
2022 var sessions = parent.wssessions[command.userid];
2023 if (sessions != null) { for (i in sessions) { try { sessions[i].send(JSON.stringify(notification)); } catch (ex) { } } }
2024
2025 if (parent.parent.multiServer != null) {
2026 // TODO: Add multi-server support
2027 }
2028
2029 // If the user is not connected, use web push if available.
2030 if ((parent.wssessions[chguser._id] == null) && (parent.sessionsCount[chguser._id] == null)) {
2031 // Perform web push notification
2032 var payload = { body: command.msg, icon: 8 }; // Icon 8 is the user icon.
2033 if (command.url) { payload.url = command.url; }
2034 if (domain.title != null) { payload.title = domain.title; } else { payload.title = "MeshCentral"; }
2035 payload.title += ' - ' + user.name;
2036 parent.performWebPush(domain, chguser, payload, { TTL: 60 }); // For now, 1 minute TTL
2037 }
2038
2039 break;
2040 }
2041 case 'meshmessenger':
2042 {
2043 // Setup a user-to-user session
2044 if (common.validateString(command.userid, 1, 2048)) {
2045 // Send a notification message to a user
2046 if ((user.siteadmin & 2) == 0) break;
2047
2048 // Can only perform this operation on other users of our group.
2049 var chguser = parent.users[command.userid];
2050 if (chguser == null) break; // This user does not exists
2051 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
2052
2053 // Create the notification message
2054 var notification = {
2055 'action': 'msg', 'type': 'notify', id: Math.random(), 'value': "Chat Request, Click here to accept.", 'title': user.name, 'userid': user._id, 'username': user.name, 'tag': 'meshmessenger/' + encodeURIComponent(command.userid) + '/' + encodeURIComponent(user._id), msgid: 11
2056 };
2057
2058 // Get the list of sessions for this user
2059 var sessions = parent.wssessions[command.userid];
2060 if (sessions != null) { for (i in sessions) { try { sessions[i].send(JSON.stringify(notification)); } catch (ex) { } } }
2061
2062 if (parent.parent.multiServer != null) {
2063 // TODO: Add multi-server support
2064 }
2065
2066 // If the user is not connected, use web push if available.
2067 if ((parent.wssessions[chguser._id] == null) && (parent.sessionsCount[chguser._id] == null)) {
2068 // Create the server url
2069 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
2070 var xdomain = (domain.dns == null) ? domain.id : '';
2071 if (xdomain != '') xdomain += "/";
2072 var url = "https://" + parent.getWebServerName(domain, req) + ":" + httpsPort + "/" + xdomain + "messenger?id=meshmessenger/" + encodeURIComponent(command.userid) + "/" + encodeURIComponent(user._id);
2073
2074 // Perform web push notification
2075 var payload = { body: "Chat Request, Click here to accept.", icon: 8, url: url }; // Icon 8 is the user icon.
2076 if (domain.title != null) { payload.title = domain.title; } else { payload.title = "MeshCentral"; }
2077 payload.title += ' - ' + user.name;
2078 parent.performWebPush(domain, chguser, payload, { TTL: 60 }); // For now, 1 minute TTL
2079 }
2080 return;
2081 }
2082
2083 // User-to-device chat is not support in LAN-only mode yet. We need the agent to replace the IP address of the server??
2084 if (args.lanonly == true) { return; }
2085
2086 // Setup a user-to-node session
2087 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
2088 // Check if this user has rights to do this
2089 if ((rights & MESHRIGHT_CHATNOTIFY) == 0) return;
2090
2091 // Create the server url
2092 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
2093 var xdomain = (domain.dns == null) ? domain.id : '';
2094 if (xdomain != '') xdomain += "/";
2095 var url = "https://" + parent.getWebServerName(domain, req) + ":" + httpsPort + "/" + xdomain + "messenger?id=meshmessenger/" + encodeURIComponent(command.nodeid) + "/" + encodeURIComponent(user._id);
2096
2097 // Open a web page on the remote device
2098 routeCommandToNode({ 'action': 'openUrl', 'nodeid': command.nodeid, 'userid': user._id, 'username': user.name, 'url': url });
2099 });
2100 break;
2101 }
2102 case 'createmesh':
2103 {
2104 var err = null;
2105 try {
2106 // Support for old web pages that sent the meshtype as a string.
2107 if (typeof command.meshtype == 'string') { command.meshtype = parseInt(command.meshtype); }
2108
2109 // Check if we have new group restriction
2110 if ((user.siteadmin != SITERIGHT_ADMIN) && ((user.siteadmin & 64) != 0)) { err = 'Permission denied'; }
2111
2112 // In some situations, we need a verified email address to create a device group.
2113 else if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Email verification required'; } // User must verify it's email first.
2114
2115 // Create mesh
2116 else if (common.validateString(command.meshname, 1, 128) == false) { err = 'Invalid group name'; } // Meshname is between 1 and 128 characters
2117 else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = 'Invalid group description'; } // Mesh description is between 0 and 1024 characters
2118 else if ((command.meshtype < 1) || (command.meshtype > 4)) { err = 'Invalid group type'; } // Device group types are 1 = AMT, 2 = Agent, 3 = Local
2119 else if (((command.meshtype == 3) || (command.meshtype == 4)) && (parent.args.wanonly == true) && (typeof command.relayid != 'string')) { err = 'Invalid group type'; } // Local device group type wihtout relay is not allowed in WAN mode
2120 else if (((command.meshtype == 3) || (command.meshtype == 4)) && (parent.args.lanonly == true) && (typeof command.relayid == 'string')) { err = 'Invalid group type'; } // Local device group type with relay is not allowed in WAN mode
2121 else if ((domain.ipkvm == null) && (command.meshtype == 4)) { err = 'Invalid group type'; } // IP KVM device group type is not allowed unless enabled
2122 if ((err == null) && (command.meshtype == 4)) {
2123 if ((command.kvmmodel < 1) || (command.kvmmodel > 2)) { err = 'Invalid KVM model'; }
2124 else if (common.validateString(command.kvmhost, 1, 128) == false) { err = 'Invalid KVM hostname'; }
2125 else if (common.validateString(command.kvmuser, 1, 128) == false) { err = 'Invalid KVM username'; }
2126 else if (common.validateString(command.kvmpass, 1, 128) == false) { err = 'Invalid KVM password'; }
2127 }
2128 } catch (ex) { err = 'Validation exception: ' + ex; }
2129
2130 // Handle any errors
2131 if (err != null) {
2132 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2133 break;
2134 }
2135
2136 // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
2137 parent.crypto.randomBytes(48, function (err, buf) {
2138 // Create new device group identifier
2139 meshid = 'mesh/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
2140
2141 // Create the new device group
2142 var links = {};
2143 links[user._id] = { name: user.name, rights: 4294967295 };
2144 mesh = { type: 'mesh', _id: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, domain: domain.id, links: links, creation: Date.now(), creatorid: user._id, creatorname: user.name };
2145
2146 // Add flags and consent if present
2147 if (typeof command.flags == 'number') { mesh.flags = command.flags; }
2148 if (typeof command.consent == 'number') { mesh.consent = command.consent; }
2149
2150 // Add KVM information if needed
2151 if (command.meshtype == 4) { mesh.kvm = { model: command.kvmmodel, host: command.kvmhost, user: command.kvmuser, pass: command.kvmpass }; }
2152
2153 // If this is device group that requires a relay device, store that now
2154 if ((parent.args.lanonly != true) && ((command.meshtype == 3) || (command.meshtype == 4)) && (typeof command.relayid == 'string')) {
2155 // Check the relay id
2156 var relayIdSplit = command.relayid.split('/');
2157 if ((relayIdSplit[0] == 'node') && (relayIdSplit[1] == domain.id)) { mesh.relayid = command.relayid; }
2158 }
2159
2160 // Save the new device group
2161 db.Set(mesh);
2162 parent.meshes[meshid] = mesh;
2163 parent.parent.AddEventDispatch([meshid], ws);
2164
2165 // Change the user to make him administration of the new device group
2166 if (user.links == null) user.links = {};
2167 user.links[meshid] = { rights: 4294967295 };
2168 user.subscriptions = parent.subscribe(user._id, ws);
2169 db.SetUser(user);
2170
2171 // Event the user change
2172 var targets = ['*', 'server-users', user._id];
2173 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2174 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
2175 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2176 parent.parent.DispatchEvent(targets, obj, event);
2177
2178 // Event the device group creation
2179 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: meshid, mtype: command.meshtype, mesh: parent.CloneSafeMesh(mesh), action: 'createmesh', msgid: 76, msgArgs: [command.meshname], msg: 'Device group created: ' + command.meshname, domain: domain.id };
2180 parent.parent.DispatchEvent(['*', 'server-createmesh', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
2181
2182 // Log in the auth log
2183 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created device group ' + mesh.name); }
2184
2185 try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: 'ok', meshid: meshid, links: links })); } catch (ex) { }
2186
2187 // If needed, event that a device is now a device group relay
2188 if (mesh.relayid != null) {
2189 // Get the node and the rights for this node
2190 parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
2191 if (node == null) return;
2192 var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Is a relay for ' + mesh.name + '.', msgid: 153, msgArgs: [mesh.name], node: parent.CloneSafeNode(node) };
2193 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
2194 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2195 });
2196 }
2197 });
2198 break;
2199 }
2200 case 'deletemesh':
2201 {
2202 // Delete a mesh and all computers within it
2203 var err = null;
2204
2205 // Resolve the device group name if needed
2206 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
2207 for (var i in parent.meshes) {
2208 var m = parent.meshes[i];
2209 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
2210 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2211 }
2212 }
2213 }
2214
2215 // Validate input
2216 try {
2217 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
2218 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2219 } catch (ex) { err = 'Validation exception: ' + ex; }
2220
2221 // Handle any errors
2222 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
2223
2224 // Get the device group reference we are going to delete
2225 var mesh = parent.meshes[command.meshid];
2226 if (mesh == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { } } return; }
2227
2228 // Check if this user has rights to do this
2229 var err = null;
2230 if (parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN) { err = 'Access denied'; }
2231 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group'; } // Invalid domain, operation only valid for current domain
2232
2233 // Handle any errors
2234 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } return; }
2235
2236 // Fire the removal event first, because after this, the event will not route
2237 var event = { etype: 'mesh', userid: user._id, username: user.name, mtype: mesh.mtype, meshid: command.meshid, name: command.meshname, action: 'deletemesh', msgid: 77, msgArgs: [command.meshname], msg: 'Device group deleted: ' + command.meshname, domain: domain.id };
2238 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, ['server-deletemesh']), obj, event); // Even if DB change stream is active, this event need to be acted on.
2239
2240 // Remove all user links to this mesh
2241 for (var j in mesh.links) {
2242 if (j.startsWith('user/')) {
2243 var xuser = parent.users[j];
2244 if (xuser && xuser.links) {
2245 delete xuser.links[mesh._id];
2246 db.SetUser(xuser);
2247 parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
2248
2249 // Notify user change
2250 var targets = ['*', 'server-users', user._id, xuser._id];
2251 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(xuser), action: 'accountchange', msgid: 78, msgArgs: [xuser.name], msg: 'Device group membership changed: ' + xuser.name, domain: domain.id };
2252 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2253 parent.parent.DispatchEvent(targets, obj, event);
2254 }
2255 } else if (j.startsWith('ugrp/')) {
2256 var xgroup = parent.userGroups[j];
2257 if (xgroup && xgroup.links) {
2258 delete xgroup.links[mesh._id];
2259 db.Set(xgroup);
2260
2261 // Notify user group change
2262 var targets = ['*', 'server-ugroups', user._id, xgroup._id];
2263 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: xgroup._id, name: xgroup.name, desc: xgroup.desc, action: 'usergroupchange', links: xgroup.links, msgid: 79, msgArgs: [xgroup.name], msg: 'User group changed: ' + xgroup.name, domain: domain.id };
2264 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2265 parent.parent.DispatchEvent(targets, obj, event);
2266 }
2267 }
2268 }
2269
2270 // Delete any invitation codes
2271 delete mesh.invite;
2272
2273 // Delete all files on the server for this mesh
2274 try {
2275 var meshpath = parent.getServerRootFilePath(mesh);
2276 if (meshpath != null) { parent.deleteFolderRec(meshpath); }
2277 } catch (e) { }
2278
2279 parent.parent.RemoveEventDispatchId(command.meshid); // Remove all subscriptions to this mesh
2280
2281 // Notify the devices that they have changed relay roles
2282 if (mesh.relayid != null) {
2283 // Get the node and the rights for this node
2284 parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
2285 if (node == null) return;
2286 var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'No longer a relay for ' + mesh.name + '.', msgid: 152, msgArgs: [mesh.name], node: parent.CloneSafeNode(node) };
2287 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
2288 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2289 });
2290 }
2291
2292 // Mark the mesh as deleted
2293 mesh.deleted = new Date(); // Mark the time this mesh was deleted, we can expire it at some point.
2294 db.Set(mesh); // We don't really delete meshes because if a device connects to is again, we will un-delete it.
2295
2296 // Delete all devices attached to this mesh in the database
2297 db.RemoveMeshDocuments(command.meshid);
2298 // TODO: We are possibly deleting devices that users will have links to. We need to clean up the broken links from on occasion.
2299
2300 // Log in the auth log
2301 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted device group ' + mesh.name); }
2302
2303 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2304 break;
2305 }
2306 case 'editmesh':
2307 {
2308 // Change the name or description of a device group (mesh)
2309 var err = null;
2310
2311 // Resolve the device group name if needed
2312 if ((typeof command.meshidname == 'string') && (command.meshid == null)) {
2313 for (var i in parent.meshes) {
2314 var m = parent.meshes[i];
2315 if ((m.mtype == 2) && (m.name == command.meshidname) && parent.IsMeshViewable(user, m)) {
2316 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2317 }
2318 }
2319 }
2320
2321 // Validate input
2322 try {
2323 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
2324 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2325 if (err == null) {
2326 mesh = parent.meshes[command.meshid];
2327 if (mesh == null) { err = 'Invalid group identifier '; }
2328 }
2329 } catch (ex) { err = 'Validation exception: ' + ex; }
2330
2331 // Handle any errors
2332 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
2333
2334 change = '';
2335
2336 // Check if this user has rights to do this
2337 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return;
2338 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
2339
2340 var changesids = [];
2341 if ((common.validateString(command.meshname, 1, 128) == true) && (command.meshname != mesh.name)) { change = 'Device group name changed from "' + mesh.name + '" to "' + command.meshname + '"'; changesids.push(1); mesh.name = command.meshname; }
2342 if ((common.validateString(command.desc, 0, 1024) == true) && (command.desc != mesh.desc)) { if (change != '') change += ' and description changed'; else change += 'Device group "' + mesh.name + '" description changed'; changesids.push(2); mesh.desc = command.desc; }
2343 if ((common.validateInt(command.flags) == true) && (command.flags != mesh.flags)) { if (change != '') change += ' and flags changed'; else change += 'Device group "' + mesh.name + '" flags changed'; changesids.push(3); mesh.flags = command.flags; }
2344 if ((common.validateInt(command.consent) == true) && (command.consent != mesh.consent)) { if (change != '') change += ' and consent changed'; else change += 'Device group "' + mesh.name + '" consent changed'; changesids.push(4); mesh.consent = command.consent; }
2345 if ((common.validateInt(command.expireDevs, 0, 2000) == true) && (command.expireDevs != mesh.expireDevs)) { if (change != '') change += ' and auto-remove changed'; else change += 'Device group "' + mesh.name + '" auto-remove changed'; changesids.push(5); if (command.expireDevs == 0) { delete mesh.expireDevs; } else { mesh.expireDevs = command.expireDevs; } }
2346
2347 var oldRelayNodeId = null, newRelayNodeId = null;
2348 if ((typeof command.relayid == 'string') && ((mesh.mtype == 3) || (mesh.mtype == 4)) && (mesh.relayid != null) && (command.relayid != mesh.relayid)) {
2349 var relayIdSplit = command.relayid.split('/');
2350 if ((relayIdSplit.length == 3) && (relayIdSplit[0] = 'node') && (relayIdSplit[1] == domain.id)) {
2351 if (change != '') { change += ' and device relay changed'; } else { change = 'Device relay changed'; }
2352 changesids.push(7);
2353 oldRelayNodeId = mesh.relayid;
2354 newRelayNodeId = mesh.relayid = command.relayid;
2355 }
2356 }
2357
2358 // See if we need to change device group invitation codes
2359 if (mesh.mtype == 2) {
2360 if (command.invite === '*') {
2361 // Clear invite codes
2362 if (mesh.invite != null) { delete mesh.invite; }
2363 if (change != '') { change += ' and invite code changed'; } else { change += 'Device group "' + mesh.name + '" invite code changed'; }
2364 changesids.push(6);
2365 } else if ((typeof command.invite == 'object') && (Array.isArray(command.invite.codes)) && (typeof command.invite.flags == 'number')) {
2366 // Set invite codes
2367 if ((mesh.invite == null) || (mesh.invite.codes != command.invite.codes) || (mesh.invite.flags != command.invite.flags)) {
2368 // Check if an invite code is not already in use.
2369 var dup = null;
2370 for (var i in command.invite.codes) {
2371 for (var j in parent.meshes) {
2372 if ((j != command.meshid) && (parent.meshes[j].deleted == null) && (parent.meshes[j].domain == domain.id) && (parent.meshes[j].invite != null) && (parent.meshes[j].invite.codes.indexOf(command.invite.codes[i]) >= 0)) { dup = command.invite.codes[i]; break; }
2373 }
2374 }
2375 if (dup != null) {
2376 // A duplicate was found, don't allow this change.
2377 displayNotificationMessage("Error, invite code \"" + dup + "\" already in use.", "Invite Codes", null, 6, 22, [dup]);
2378 return;
2379 }
2380 mesh.invite = { codes: command.invite.codes, flags: command.invite.flags };
2381 if (typeof command.invite.ag == 'number') { mesh.invite.ag = command.invite.ag; }
2382 if (change != '') { change += ' and invite code changed'; } else { change += 'Device group "' + mesh.name + '" invite code changed'; }
2383 changesids.push(6);
2384 }
2385 }
2386 }
2387
2388 if (change != '') {
2389 db.Set(mesh);
2390 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, flags: mesh.flags, consent: mesh.consent, action: 'meshchange', links: mesh.links, msgid: 142, msgArgs: [mesh.name, changesids], msg: change, domain: domain.id, invite: mesh.invite, expireDevs: command.expireDevs, relayid: mesh.relayid };
2391 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
2392 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, 'server-editmesh']), obj, event);
2393 }
2394
2395 // Notify the devices that they have changed relay roles
2396 if (oldRelayNodeId != null) {
2397 // Get the node and the rights for this node
2398 parent.GetNodeWithRights(domain, user, oldRelayNodeId, function (node, rights, visible) {
2399 if (node == null) return;
2400 var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'No longer a relay for ' + mesh.name + '.', msgid: 152, msgArgs: [mesh.name], node: parent.CloneSafeNode(node) };
2401 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
2402 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2403 });
2404 }
2405 if (newRelayNodeId != null) {
2406 // Get the node and the rights for this node
2407 parent.GetNodeWithRights(domain, user, newRelayNodeId, function (node, rights, visible) {
2408 if (node == null) return;
2409 var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Is a relay for ' + mesh.name + '.', msgid: 153, msgArgs: [mesh.name], node: parent.CloneSafeNode(node) };
2410 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
2411 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2412 });
2413 } else if ((mesh.relayid != null) && (changesids.indexOf(1) >= 0)) {
2414 // Notify of node name change, get the node and the rights for this node, we just want to trigger a device update.
2415 parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
2416 if (node == null) return;
2417 var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, node: parent.CloneSafeNode(node), nolog: 1 };
2418 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
2419 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2420 });
2421 }
2422
2423 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2424 break;
2425 }
2426 case 'removemeshuser':
2427 {
2428 var xdomain, err = null;
2429
2430 // Resolve the device group name if needed
2431 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
2432 for (var i in parent.meshes) {
2433 var m = parent.meshes[i];
2434 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
2435 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2436 }
2437 }
2438 }
2439
2440 try {
2441 if (common.validateString(command.userid, 1, 1024) == false) { err = "Invalid userid"; } // Check userid
2442 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid
2443 if (command.userid.indexOf('/') == -1) { command.userid = 'user/' + domain.id + '/' + command.userid; }
2444 if (command.userid == obj.user._id) { err = "Can't remove self"; } // Can't add of modify self
2445 if ((command.userid.split('/').length != 3) || ((obj.crossDomain !== true) && (command.userid.split('/')[1] != domain.id))) { err = "Invalid userid"; } // Invalid domain, operation only valid for current domain
2446 else {
2447 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2448 mesh = parent.meshes[command.meshid];
2449 var meshIdSplit = command.meshid.split('/');
2450 if (mesh == null) { err = "Unknown device group"; }
2451 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGEUSERS) == 0) { err = "Permission denied"; }
2452 else if (meshIdSplit.length != 3) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2453 else {
2454 xdomain = domain;
2455 if (obj.crossDomain !== true) { xdomain = parent.parent.config.domains[meshIdSplit[1]]; }
2456 if (xdomain == null) { err = "Invalid domain"; }
2457 }
2458 }
2459 } catch (ex) { err = "Validation exception: " + ex; }
2460
2461 // Handle any errors
2462 if (err != null) {
2463 console.log(err);
2464 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: err })); } catch (ex) { } }
2465 break;
2466 }
2467
2468 // Check if the user exists - Just in case we need to delete a mesh right for a non-existant user, we do it this way. Technically, it's not possible, but just in case.
2469 var deluserid = command.userid, deluser = null;
2470 if (deluserid.startsWith('user/')) { deluser = parent.users[deluserid]; }
2471 else if (deluserid.startsWith('ugrp/')) { deluser = parent.userGroups[deluserid]; }
2472
2473 // Search for a user name in that windows domain is the username starts with *\
2474 if ((deluser == null) && (deluserid.startsWith('user/' + xdomain.id + '/*\\')) == true) {
2475 var search = deluserid.split('/')[2].substring(1);
2476 for (var i in parent.users) { if (i.endsWith(search) && (parent.users[i].domain == xdomain.id)) { deluser = parent.users[i]; command.userid = deluserid = deluser._id; break; } }
2477 }
2478
2479 if (deluser != null) {
2480 // Remove mesh from user
2481 if (deluser.links != null && deluser.links[command.meshid] != null) {
2482 var delmeshrights = deluser.links[command.meshid].rights;
2483 if ((delmeshrights == MESHRIGHT_ADMIN) && (parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN)) return; // A non-admin can't kick out an admin
2484 delete deluser.links[command.meshid];
2485 if (deluserid.startsWith('user/')) { db.SetUser(deluser); }
2486 else if (deluserid.startsWith('ugrp/')) { db.Set(deluser); }
2487 parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe');
2488
2489 if (deluserid.startsWith('user/')) {
2490 // Notify user change
2491 var targets = ['*', 'server-users', user._id, deluser._id];
2492 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(deluser), action: 'accountchange', msgid: 78, msgArgs: [deluser.name], msg: 'Device group membership changed: ' + deluser.name, domain: xdomain.id };
2493 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2494 parent.parent.DispatchEvent(targets, obj, event);
2495 } else if (deluserid.startsWith('ugrp/')) {
2496 // Notify user group change
2497 var targets = ['*', 'server-ugroups', user._id, deluser._id];
2498 var event = { etype: 'ugrp', username: user.name, ugrpid: deluser._id, name: deluser.name, desc: deluser.desc, action: 'usergroupchange', links: deluser.links, msgid: 79, msgArgs: [deluser.name], msg: 'User group changed: ' + deluser.name, domain: xdomain.id };
2499 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2500 parent.parent.DispatchEvent(targets, obj, event);
2501 }
2502 }
2503 }
2504
2505 // Remove user from the mesh
2506 if (mesh.links[command.userid] != null) {
2507 delete mesh.links[command.userid];
2508 db.Set(mesh);
2509
2510 // Notify mesh change
2511 var event;
2512 if (deluser != null) {
2513 event = { etype: 'mesh', username: user.name, userid: deluser.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msgid: 83, msgArgs: [deluser.name, mesh.name], msg: 'Removed user ' + deluser.name + ' from device group ' + mesh.name, domain: xdomain.id, invite: mesh.invite };
2514 } else {
2515 event = { etype: 'mesh', username: user.name, userid: (deluserid.split('/')[2]), meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msgid: 83, msgArgs: [(deluserid.split('/')[2]), mesh.name], msg: 'Removed user ' + (deluserid.split('/')[2]) + ' from device group ' + mesh.name, domain: xdomain.id, invite: mesh.invite };
2516 }
2517 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, command.userid]), obj, event);
2518 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2519 } else {
2520 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'User not in group' })); } catch (ex) { } }
2521 }
2522 break;
2523 }
2524 case 'meshamtpolicy':
2525 {
2526 // Change a mesh Intel AMT policy
2527 if (common.validateString(command.meshid, 8, 134) == false) break; // Check the meshid
2528 if (common.validateObject(command.amtpolicy) == false) break; // Check the amtpolicy
2529 if (common.validateInt(command.amtpolicy.type, 0, 4) == false) break; // Check the amtpolicy.type
2530 if (command.amtpolicy.type === 2) {
2531 if ((command.amtpolicy.password != null) && (common.validateString(command.amtpolicy.password, 0, 32) == false)) break; // Check the amtpolicy.password
2532 if ((command.amtpolicy.badpass != null) && common.validateInt(command.amtpolicy.badpass, 0, 1) == false) break; // Check the amtpolicy.badpass
2533 if (common.validateInt(command.amtpolicy.cirasetup, 0, 2) == false) break; // Check the amtpolicy.cirasetup
2534 } else if (command.amtpolicy.type === 3) {
2535 if ((command.amtpolicy.password != null) && (common.validateString(command.amtpolicy.password, 0, 32) == false)) break; // Check the amtpolicy.password
2536 if ((command.amtpolicy.badpass != null) && common.validateInt(command.amtpolicy.badpass, 0, 1) == false) break; // Check the amtpolicy.badpass
2537 if ((command.amtpolicy.ccm != null) && common.validateInt(command.amtpolicy.ccm, 0, 2) == false) break; // Check the amtpolicy.ccm
2538 if (common.validateInt(command.amtpolicy.cirasetup, 0, 2) == false) break; // Check the amtpolicy.cirasetup
2539 }
2540
2541 mesh = parent.meshes[command.meshid];
2542 if (mesh) {
2543 // Check if this user has rights to do this
2544 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return;
2545 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
2546
2547 // TODO: Check if this is a change from the existing policy
2548
2549 // Perform the Intel AMT policy change
2550 var amtpolicy = { type: command.amtpolicy.type };
2551 if ((command.amtpolicy.type === 2) || (command.amtpolicy.type === 3)) {
2552 amtpolicy = { type: command.amtpolicy.type, badpass: command.amtpolicy.badpass, cirasetup: command.amtpolicy.cirasetup };
2553 if (command.amtpolicy.type === 3) { amtpolicy.ccm = command.amtpolicy.ccm; }
2554 if ((command.amtpolicy.password == null) && (mesh.amt != null) && (typeof mesh.amt.password == 'string')) { amtpolicy.password = mesh.amt.password; } // Keep the last password
2555 if ((typeof command.amtpolicy.password == 'string') && (command.amtpolicy.password.length >= 8)) { amtpolicy.password = command.amtpolicy.password; } // Set a new password
2556 }
2557 mesh.amt = amtpolicy;
2558 db.Set(mesh);
2559 var amtpolicy2 = Object.assign({}, amtpolicy); // Shallow clone
2560 if (amtpolicy2.password != null) { amtpolicy2.password = 1; }
2561 var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: mesh._id, amt: amtpolicy2, action: 'meshchange', links: mesh.links, msgid: 141, msg: "Intel(r) AMT policy change", domain: domain.id, invite: mesh.invite };
2562 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
2563 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id]), obj, event);
2564
2565 // If we have peer servers, inform them of the new Intel AMT policy for this device group
2566 if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'newIntelAmtPolicy', meshid: command.meshid, amtpolicy: amtpolicy }); }
2567
2568 // See if any agents for the affected device group is connected, if so, update the Intel AMT policy
2569 for (var nodeid in parent.wsagents) {
2570 const agent = parent.wsagents[nodeid];
2571 if (agent.dbMeshKey == command.meshid) { agent.sendUpdatedIntelAmtPolicy(amtpolicy); }
2572 }
2573 }
2574 break;
2575 }
2576 case 'addlocaldevice':
2577 {
2578 var err = null;
2579 // Perform input validation
2580 try {
2581 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid
2582 if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name
2583 if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname
2584 if (typeof command.type != 'number') { err = "Invalid type"; } // Type must be a number
2585 if ((command.type != 4) && (command.type != 6) && (command.type != 29)) { err = "Invalid type"; } // Check device type
2586 else {
2587 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2588 mesh = parent.meshes[command.meshid];
2589 if (mesh == null) { err = "Unknown device group"; }
2590 if (mesh.mtype != 3) { err = "Local device agentless mesh only allowed" } // This operation is only allowed for mesh type 3, local device agentless mesh.
2591 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
2592 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2593 }
2594 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
2595 // Handle any errors
2596 if (err != null) {
2597 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2598 break;
2599 }
2600
2601 // Create a new nodeid
2602 parent.crypto.randomBytes(48, function (err, buf) {
2603 // Create the new node
2604 nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
2605 var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 3, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, agent: { id: command.type, caps: 0 } };
2606 db.Set(device);
2607
2608 // Event the new node
2609 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(device), msgid: 84, msgArgs: [command.devicename, mesh.name], msg: 'Added device ' + command.devicename + ' to device group ' + mesh.name, domain: domain.id });
2610 // Send response if required
2611 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addlocaldevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2612 });
2613 break;
2614 }
2615 case 'addamtdevice':
2616 {
2617 if (args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added
2618 var err = null;
2619 // Perform input validation
2620 try {
2621 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid
2622 if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name
2623 if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname
2624 if (common.validateString(command.amtusername, 0, 16) == false) { err = "Invalid amtusername"; } // Check username
2625 if (common.validateString(command.amtpassword, 0, 16) == false) { err = "Invalid amtpassword"; } // Check password
2626 if (command.amttls == '0') { command.amttls = 0; } else if (command.amttls == '1') { command.amttls = 1; } // Check TLS flag
2627 if ((command.amttls != 1) && (command.amttls != 0)) { err = "Invalid amttls"; }
2628 else {
2629 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2630 // Get the mesh
2631 mesh = parent.meshes[command.meshid];
2632 if (mesh == null) { err = "Unknown device group"; }
2633 if (mesh.mtype != 1) { err = "Intel AMT agentless mesh only allowed"; } // This operation is only allowed for mesh type 1, Intel AMT agentless mesh.
2634 // Check if this user has rights to do this
2635 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
2636 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2637 }
2638 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
2639
2640 // Handle any errors
2641 if (err != null) {
2642 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2643 break;
2644 }
2645
2646 // If we are in WAN-only mode, hostname is not used
2647 if ((args.wanonly == true) && (command.hostname)) { delete command.hostname; }
2648
2649 // Create a new nodeid
2650 parent.crypto.randomBytes(48, function (err, buf) {
2651 // Create the new node
2652 nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
2653 var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 1, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, intelamt: { user: command.amtusername, pass: command.amtpassword, tls: command.amttls } };
2654
2655 // Add optional feilds
2656 if (common.validateInt(command.state, 0, 3)) { device.intelamt.state = command.state; }
2657 if (common.validateString(command.ver, 1, 16)) { device.intelamt.ver = command.ver; }
2658 if (common.validateString(command.hash, 1, 256)) { device.intelamt.hash = command.hash; }
2659 if (common.validateString(command.realm, 1, 256)) { device.intelamt.realm = command.realm; }
2660
2661 // Save the device to the database
2662 db.Set(device);
2663
2664 // Event the new node
2665 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(device), msgid: 84, msgArgs: [command.devicename, mesh.name], msg: 'Added device ' + command.devicename + ' to device group ' + mesh.name, domain: domain.id });
2666 // Send response if required
2667 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addamtdevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2668 });
2669
2670 break;
2671 }
2672 case 'scanamtdevice':
2673 {
2674 if (args.wanonly == true) return; // This is a WAN-only server, this type of scanning is not allowed.
2675 if (common.validateString(command.range, 1, 256) == false) break; // Check range string
2676
2677 // Ask the RMCP scanning to scan a range of IP addresses
2678 if (parent.parent.amtScanner) {
2679 if (parent.parent.amtScanner.performRangeScan(user._id, command.range) == false) {
2680 parent.parent.DispatchEvent(['*', user._id], obj, { action: 'scanamtdevice', range: command.range, results: null, nolog: 1 });
2681 }
2682 }
2683 break;
2684 }
2685 case 'changeDeviceMesh':
2686 {
2687 var err = null;
2688
2689 // Resolve the device group name if needed
2690 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
2691 for (var i in parent.meshes) {
2692 var m = parent.meshes[i];
2693 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
2694 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2695 }
2696 }
2697 }
2698
2699 // Perform input validation
2700 try {
2701 if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
2702 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid
2703 else {
2704 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2705 mesh = parent.meshes[command.meshid];
2706 if (mesh == null) { err = "Unknown device group"; }
2707 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
2708 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2709 }
2710 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
2711
2712 // Handle any errors
2713 if (err != null) {
2714 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2715 break;
2716 }
2717
2718 // This is to change device guest sharing to the new device group
2719 var changeDeviceShareMeshIdNodeCount = command.nodeids.length;
2720 var changeDeviceShareMeshIdNodeList = [];
2721
2722 // For each nodeid, change the group
2723 for (var i = 0; i < command.nodeids.length; i++) {
2724 var xnodeid = command.nodeids[i];
2725 if (xnodeid.indexOf('/') == -1) { xnodeid = 'node/' + domain.id + '/' + xnodeid; }
2726
2727 // Get the node and the rights for this node
2728 parent.GetNodeWithRights(domain, user, xnodeid, function (node, rights, visible) {
2729 // Check if we found this device
2730 if (node == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Device not found' })); } catch (ex) { } } changeDeviceShareMeshIdNodeCount--; return; }
2731
2732 // Check if already in the right mesh
2733 if (node.meshid == command.meshid) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Device already in correct group' })); } catch (ex) { } } changeDeviceShareMeshIdNodeCount--; return; }
2734
2735 // Make sure both source and target mesh are the same type
2736 try { if (parent.meshes[node.meshid].mtype != parent.meshes[command.meshid].mtype) { changeDeviceShareMeshIdNodeCount--; return; } } catch (e) {
2737 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Device groups are of different types' })); } catch (ex) { } }
2738 changeDeviceShareMeshIdNodeCount--;
2739 return;
2740 };
2741
2742 // Make sure that we have rights on both source and destination mesh
2743 const targetMeshRights = parent.GetMeshRights(user, command.meshid);
2744 if (((rights & MESHRIGHT_EDITMESH) == 0) || ((targetMeshRights & MESHRIGHT_EDITMESH) == 0)) {
2745 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Permission denied' })); } catch (ex) { } }
2746 changeDeviceShareMeshIdNodeCount--;
2747 return;
2748 }
2749
2750 // Perform the switch, start by saving the node with the new meshid.
2751 changeDeviceShareMeshIdNodeList.push(node._id);
2752 changeDeviceShareMeshIdNodeCount--;
2753 if (changeDeviceShareMeshIdNodeCount == 0) { changeDeviceShareMeshId(changeDeviceShareMeshIdNodeList, command.meshid); }
2754 const oldMeshId = node.meshid;
2755 node.meshid = command.meshid;
2756 db.Set(parent.cleanDevice(node));
2757
2758 // If the device is connected on this server, switch it now.
2759 var agentSession = parent.wsagents[node._id];
2760 if (agentSession != null) {
2761 agentSession.dbMeshKey = command.meshid; // Switch the agent mesh
2762 agentSession.meshid = command.meshid.split('/')[2]; // Switch the agent mesh
2763 agentSession.sendUpdatedIntelAmtPolicy(); // Send the new Intel AMT policy
2764 }
2765
2766 // If any MQTT sessions are connected on this server, switch it now.
2767 if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.changeDeviceMesh(node._id, command.meshid); }
2768
2769 // If any CIRA sessions are connected on this server, switch it now.
2770 if (parent.parent.mpsserver != null) { parent.parent.mpsserver.changeDeviceMesh(node._id, command.meshid); }
2771
2772 // Add the connection state
2773 const state = parent.parent.GetConnectivityState(node._id);
2774 if (state) {
2775 node.conn = state.connectivity;
2776 node.pwr = state.powerState;
2777 if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[node._id]; if (agent != null) { node.agct = agent.connectTime; } }
2778
2779 // Uuse the connection time of the CIRA/Relay connection
2780 if ((state.connectivity & 2) != 0) {
2781 var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(node._id, null, true);
2782 if ((ciraConnection != null) && (ciraConnection.tag != null)) { node.cict = ciraConnection.tag.connectTime; }
2783 }
2784 }
2785
2786 // Update lastconnect meshid for this node
2787 db.Get('lc' + node._id, function (err, xnodes) {
2788 if ((xnodes != null) && (xnodes.length == 1) && (xnodes[0].meshid != command.meshid)) { xnodes[0].meshid = command.meshid; db.Set(xnodes[0]); }
2789 });
2790
2791 // Event the node change
2792 var newMesh = parent.meshes[command.meshid];
2793 var event = { etype: 'node', userid: user._id, username: user.name, action: 'nodemeshchange', nodeid: node._id, node: node, oldMeshId: oldMeshId, newMeshId: command.meshid, msgid: 85, msgArgs: [node.name, newMesh.name], msg: 'Moved device ' + node.name + ' to group ' + newMesh.name, domain: domain.id };
2794 // Even if change stream is enabled on this server, we still make the nodemeshchange actionable. This is because the DB can't send out a change event that will match this.
2795 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [oldMeshId, node._id]), obj, event);
2796
2797 // Send response if required
2798 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2799 });
2800 }
2801 break;
2802 }
2803 case 'removedevices':
2804 {
2805 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
2806 for (i in command.nodeids) {
2807 var nodeid = command.nodeids[i], err = null;
2808
2809 // Argument validation
2810 if (common.validateString(nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check nodeid
2811 else {
2812 if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
2813 if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
2814 }
2815 if (err != null) {
2816 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: err })); } catch (ex) { } }
2817 continue;
2818 }
2819 // Get the node and the rights for this node
2820 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
2821 // Check we have the rights to delete this device
2822 if ((rights & MESHRIGHT_UNINSTALL) == 0) {
2823 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
2824 return;
2825 }
2826
2827 // Delete this node including network interface information, events and timeline
2828 db.Remove(node._id); // Remove node with that id
2829 db.Remove('if' + node._id); // Remove interface information
2830 db.Remove('nt' + node._id); // Remove notes
2831 db.Remove('lc' + node._id); // Remove last connect time
2832 db.Remove('si' + node._id); // Remove system information
2833 db.Remove('al' + node._id); // Remove error log last time
2834 if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
2835 db.RemoveAllNodeEvents(node._id); // Remove all events for this node
2836 db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
2837 if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
2838 db.Get('ra' + node._id, function (err, nodes) {
2839 if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
2840 db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
2841 });
2842
2843 // Remove any user node links
2844 if (node.links != null) {
2845 for (var i in node.links) {
2846 if (i.startsWith('user/')) {
2847 var cuser = parent.users[i];
2848 if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
2849 // Remove the user link & save the user
2850 delete cuser.links[node._id];
2851 if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
2852 db.SetUser(cuser);
2853
2854 // Notify user change
2855 var targets = ['*', 'server-users', cuser._id];
2856 var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: domain.id, account: parent.CloneSafeUser(cuser) };
2857 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2858 parent.parent.DispatchEvent(targets, obj, event);
2859 }
2860 } else if (i.startsWith('ugrp/')) {
2861 var cusergroup = parent.userGroups[i];
2862 if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
2863 // Remove the user link & save the user
2864 delete cusergroup.links[node._id];
2865 if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
2866 db.Set(cusergroup);
2867
2868 // Notify user change
2869 var targets = ['*', 'server-users', cusergroup._id];
2870 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: cusergroup._id, name: cusergroup.name, desc: cusergroup.desc, action: 'usergroupchange', links: cusergroup.links, msgid: 163, msgArgs: [node.name, cusergroup.name], msg: 'Removed device ' + node.name + ' from user group ' + cusergroup.name };
2871 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
2872 parent.parent.DispatchEvent(targets, obj, event);
2873 }
2874 }
2875 }
2876 }
2877
2878 // Event node deletion
2879 var event = { etype: 'node', userid: user._id, username: user.name, action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, parent.meshes[node.meshid].name], msg: 'Removed device ' + node.name + ' from device group ' + parent.meshes[node.meshid].name, domain: domain.id };
2880 // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
2881 //if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come.
2882 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
2883
2884 // Disconnect all connections if needed
2885 var state = parent.parent.GetConnectivityState(nodeid);
2886 if ((state != null) && (state.connectivity != null)) {
2887 if ((state.connectivity & 1) != 0) { parent.wsagents[nodeid].close(); } // Disconnect mesh agent
2888 if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(nodeid); } // Disconnect CIRA/Relay/LMS connections
2889 }
2890
2891 // Send response if required
2892 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2893 });
2894 }
2895
2896 break;
2897 }
2898 case 'wakedevices':
2899 {
2900 // TODO: We can optimize this a lot.
2901 // - We should get a full list of all MAC's to wake first.
2902 // - We should try to only have one agent per subnet (using Gateway MAC) send a wake-on-lan.
2903 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
2904
2905 // Event wakeup, this will cause Intel AMT wake operations on this and other servers.
2906 parent.parent.DispatchEvent('*', obj, { action: 'wakedevices', userid: user._id, username: user.name, nodeids: command.nodeids, domain: domain.id, nolog: 1 });
2907
2908 // Perform wake-on-lan
2909 for (i in command.nodeids) {
2910 var nodeid = command.nodeids[i];
2911
2912 // Argument validation
2913 if (common.validateString(nodeid, 8, 128) == false) { // Check the nodeid
2914 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
2915 continue;
2916 }
2917 else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
2918 else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { // Invalid domain, operation only valid for current domain
2919 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid domain' })); } catch (ex) { } }
2920 continue;
2921 }
2922
2923 // Get the node and the rights for this node
2924 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
2925 // Check we have the rights to wake this device
2926 if ((node == null) || (visible == false) || (rights & MESHRIGHT_WAKEDEVICE) == 0) {
2927 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
2928 return;
2929 }
2930
2931 // If this device is connected on MQTT, send a wake action.
2932 if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.publish(node._id, 'powerAction', 'wake'); }
2933
2934 // If this is a IP-KVM or Power Distribution Unit (PDU), dispatch an action event
2935 if (node.mtype == 4) {
2936 // Send out an event to perform turn off command on the port
2937 const targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['devport-operation', 'server-users', user._id]);
2938 const event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'turnon', domain: domain.id, portid: node.portid, porttype: node.porttype, portnum: node.portnum, meshid: node.meshid, mtype: node.mtype, msgid: 132, msg: "Turn on." };
2939 parent.parent.DispatchEvent(targets, obj, event);
2940 return;
2941 }
2942
2943 // Get the device interface information
2944 db.Get('if' + node._id, function (err, nodeifs) {
2945 if ((nodeifs != null) && (nodeifs.length == 1)) {
2946 var macs = [], nodeif = nodeifs[0];
2947 if (nodeif.netif) {
2948 for (var j in nodeif.netif) { if (nodeif.netif[j].mac && (nodeif.netif[j].mac != '00:00:00:00:00:00') && (macs.indexOf(nodeif.netif[j].mac) == -1)) { macs.push(nodeif.netif[j].mac); } }
2949 } else if (nodeif.netif2) {
2950 for (var j in nodeif.netif2) { for (var k in nodeif.netif2[j]) { if (nodeif.netif2[j][k].mac && (nodeif.netif2[j][k].mac != '00:00:00:00:00:00') && (macs.indexOf(nodeif.netif2[j][k].mac) == -1)) { macs.push(nodeif.netif2[j][k].mac); } } }
2951 }
2952 if (macs.length == 0) {
2953 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'No known MAC addresses for this device' })); } catch (ex) { } }
2954 return;
2955 }
2956
2957 // Have the server send a wake-on-lan packet (Will not work in WAN-only)
2958 if (parent.parent.meshScanner != null) { parent.parent.meshScanner.wakeOnLan(macs, node.host); }
2959
2960 // Get the list of device groups this user as wake permissions on
2961 var targets = [], targetDeviceGroups = parent.GetAllMeshWithRights(user, MESHRIGHT_WAKEDEVICE);
2962 for (j in targetDeviceGroups) { targets.push(targetDeviceGroups[j]._id); }
2963 for (j in user.links) { if ((j.startsWith('node/')) && (typeof user.links[j].rights == 'number') && ((user.links[j].rights & MESHRIGHT_WAKEDEVICE) != 0)) { targets.push(j); } }
2964
2965 // Go thru all the connected agents and send wake-on-lan on all the ones in the target mesh list
2966 var wakeCount = 0;
2967 for (j in parent.wsagents) {
2968 var agent = parent.wsagents[j];
2969 if ((agent.authenticated == 2) && ((targets.indexOf(agent.dbMeshKey) >= 0) || (targets.indexOf(agent.dbNodeKey) >= 0))) {
2970 //console.log('Asking agent ' + agent.dbNodeKey + ' to wake ' + macs.join(','));
2971 try { agent.send(JSON.stringify({ action: 'wakeonlan', macs: macs })); wakeCount++; } catch (ex) { }
2972 }
2973 }
2974 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Used ' + wakeCount + ' device(s) to send wake packets' })); } catch (ex) { } }
2975 } else {
2976 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'No network information for this device' })); } catch (ex) { } }
2977 }
2978 });
2979 });
2980
2981 if (command.nodeids.length > 1) {
2982 // If we are waking multiple devices, confirm we got the command.
2983 try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
2984 }
2985 }
2986 break;
2987 }
2988 case 'webrelay':
2989 {
2990 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
2991 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
2992 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
2993 else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present
2994 else {
2995 if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
2996 var snode = command.nodeid.split('/');
2997 if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; }
2998 }
2999 // Handle any errors
3000 if (err != null) {
3001 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: err })); } catch (ex) { } }
3002 break;
3003 }
3004 // Get the device rights
3005 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3006 // If node not found or we don't have remote control, reject.
3007 if (node == null) {
3008 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
3009 return;
3010 }
3011 var relayid = null;
3012 var addr = null;
3013 if (node.mtype == 3) { // Setup device relay if needed
3014 var mesh = parent.meshes[node.meshid];
3015 if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
3016 }
3017 var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
3018 var webRelayPort = ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0));
3019 if (webRelayPort == 0) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'WebRelay Disabled' })); return; } catch (ex) { } }
3020 const authRelayCookie = parent.parent.encodeCookie({ ruserid: user._id, x: req.session.x }, parent.parent.loginCookieEncryptionKey);
3021 var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + command.nodeid + '&p=' + command.port + '&appid=' + command.appid + '&c=' + authRelayCookie;
3022 if (addr != null) { url += '&addr=' + addr; }
3023 if (relayid != null) { url += '&relayid=' + relayid }
3024 command.url = url;
3025 if (command.responseid != null) { command.result = 'OK'; }
3026 try { ws.send(JSON.stringify(command)); } catch (ex) { }
3027 });
3028 break;
3029 }
3030 case 'runcommands':
3031 {
3032 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3033 if (typeof command.presetcmd != 'number') {
3034 if (typeof command.type != 'number') break; // Check command type
3035 if (typeof command.runAsUser != 'number') { command.runAsUser = 0; } // Check runAsUser
3036 }
3037
3038 const processRunCommand = function (command) {
3039 for (i in command.nodeids) {
3040 var nodeid = command.nodeids[i], err = null;
3041
3042 // Argument validation
3043 if (common.validateString(nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check nodeid
3044 else {
3045 if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
3046 if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
3047 }
3048 if (err != null) {
3049 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: err })); } catch (ex) { } }
3050 continue;
3051 }
3052
3053 // Get the node and the rights for this node
3054 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3055 // Check if this node was found
3056 if (node == null) {
3057 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
3058 return;
3059 }
3060
3061 if (command.type == 4) {
3062 // This is an agent console command
3063
3064 // Check we have the rights to run commands on this device, MESHRIGHT_REMOTECONTROL & MESHRIGHT_AGENTCONSOLE are needed
3065 if ((rights & 24) != 24) {
3066 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
3067 return;
3068 }
3069
3070 var theCommand = { action: 'msg', type: 'console', value: command.cmds, rights: rights, sessionid: ws.sessionId };
3071 if (parent.parent.multiServer != null) { // peering setup
3072 parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand});
3073 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3074 } else {
3075 // Send the commands to the agent
3076 var agent = parent.wsagents[node._id];
3077 if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
3078 try { agent.send(JSON.stringify(theCommand)); } catch (ex) { }
3079 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3080 } else {
3081 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
3082 }
3083 }
3084 } else {
3085 // This is a standard (bash/shell/powershell) command.
3086
3087 // Check we have the rights to run commands on this device
3088 if ((rights & MESHRIGHT_REMOTECOMMAND) == 0) {
3089 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
3090 return;
3091 }
3092
3093 if (typeof command.reply != 'boolean') command.reply = false;
3094 if (typeof command.responseid != 'string') command.responseid = null;
3095 var msgid = 24; // "Running commands"
3096 if (command.type == 1) { msgid = 99; } // "Running commands as user"
3097 if (command.type == 2) { msgid = 100; } // "Running commands as user if possible"
3098 // Check if this agent is correct for this command type
3099 // command.type 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux/BSD/macOS
3100 var commandsOk = false;
3101 if ((node.agent.id > 0) && (node.agent.id < 5) || (node.agent.id > 41 && node.agent.id < 44)) {
3102 // Windows Agent
3103 if ((command.type == 1) || (command.type == 2)) { commandsOk = true; }
3104 else if (command.type === 0) { command.type = 1; commandsOk = true; } // Set the default type of this agent
3105 } else {
3106 // Non-Windows Agent
3107 if (command.type == 3) { commandsOk = true; }
3108 else if (command.type === 0) { command.type = 3; commandsOk = true; } // Set the default type of this agent
3109 }
3110 if (commandsOk == true) {
3111 var theCommand = { action: 'runcommands', type: command.type, cmds: command.cmds, runAsUser: command.runAsUser, reply: command.reply, responseid: command.responseid };
3112 var agent = parent.wsagents[node._id];
3113 if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
3114 // Send the commands to the agent
3115 try { agent.send(JSON.stringify(theCommand)); } catch (ex) { }
3116 if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3117 // Send out an event that these commands where run on this device
3118 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
3119 var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'runcommands', msg: 'Running commands', msgid: msgid, cmds: command.cmds, cmdType: command.type, runAsUser: command.runAsUser, domain: domain.id };
3120 parent.parent.DispatchEvent(targets, obj, event);
3121 } else if (parent.parent.multiServer != null) { // peering setup
3122 // Send the commands to the agent
3123 parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand});
3124 if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3125 // Send out an event that these commands where run on this device
3126 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
3127 var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'runcommands', msg: 'Running commands', msgid: msgid, cmds: command.cmds, cmdType: command.type, runAsUser: command.runAsUser, domain: domain.id };
3128 parent.parent.multiServer.DispatchEvent(targets, obj, event);
3129 } else {
3130 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
3131 }
3132 } else {
3133 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid command type' })); } catch (ex) { } }
3134 }
3135 }
3136 });
3137 }
3138 }
3139
3140 if (typeof command.presetcmd == 'number') {
3141 // If a pre-set command is used, load the command
3142 if (Array.isArray(domain.preconfiguredscripts) == false) return;
3143 const script = domain.preconfiguredscripts[command.presetcmd];
3144 if (script == null) return;
3145 delete command.presetcmd;
3146
3147 // Decode script type
3148 const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent
3149 if (typeof script.type == 'string') { const stype = types.indexOf(script.type.toLowerCase()); if (stype > 0) { command.type = stype; } }
3150 if (command.type == null) return;
3151
3152 // Decode script runas
3153 if (command.type != 4) {
3154 const runAsModes = ['agent', 'userfirst', 'user']; // 0 = AsAgent, 1 = UserFirst, 2 = UserOnly
3155 if (typeof script.runas == 'string') { const srunas = runAsModes.indexOf(script.runas.toLowerCase()); if (srunas >= 0) { command.runAsUser = srunas; } }
3156 }
3157
3158 if (typeof script.file == 'string') {
3159 // The pre-defined script commands are in a file, load it
3160 const scriptPath = parent.common.joinPath(parent.parent.datapath, script.file);
3161 fs.readFile(scriptPath, function (err, data) {
3162 // If loaded correctly, run loaded commands
3163 if ((err != null) || (data == null) || (data.length == 0) || (data.length > 65535)) return;
3164 command.cmds = data.toString();
3165 processRunCommand(command);
3166 });
3167 } else if (typeof script.cmd == 'string') {
3168 // The pre-defined script commands are right in the config.json, use that
3169 command.cmds = script.cmd;
3170 processRunCommand(command);
3171 }
3172 } else if (typeof command.cmdpath == 'string') {
3173 // If a server command path is used, load the script from the path
3174 var file = parent.getServerFilePath(user, domain, command.cmdpath);
3175 if (file != null) {
3176 fs.readFile(file.fullpath, function (err, data) {
3177 // If loaded correctly, run loaded commands
3178 if ((err != null) || (data == null) || (data.length == 0) || (data.length > 65535)) return;
3179 command.cmds = data.toString();
3180 delete command.cmdpath;
3181 processRunCommand(command);
3182 });
3183 }
3184 } else if (typeof command.cmds == 'string') {
3185 // Run provided commands
3186 if (command.cmds.length > 65535) return;
3187 processRunCommand(command);
3188 }
3189 break;
3190 }
3191 case 'uninstallagent':
3192 {
3193 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3194 for (i in command.nodeids) {
3195 // Get the node and the rights for this node
3196 parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
3197 // Check we have the rights to delete this device
3198 if ((rights & MESHRIGHT_UNINSTALL) == 0) return;
3199
3200 // Send uninstall command to connected agent
3201 const agent = parent.wsagents[node._id];
3202 if (agent != null) {
3203 //console.log('Asking agent ' + agent.dbNodeKey + ' to uninstall.');
3204 try { agent.send(JSON.stringify({ action: 'uninstallagent' })); } catch (ex) { }
3205 }
3206 });
3207 }
3208 break;
3209 }
3210 case 'poweraction':
3211 {
3212 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3213 if (common.validateInt(command.actiontype, 2, 401) == false) break; // Check actiontype
3214 for (i in command.nodeids) {
3215 var nodeid = command.nodeids[i];
3216
3217 // Argument validation
3218 if (common.validateString(nodeid, 8, 128) == false) { continue; } // Check the nodeid
3219 else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
3220 else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { continue; } // Invalid domain, operation only valid for current domain
3221
3222 // Get the node and the rights for this node
3223 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3224 if ((command.actiontype >= 400) && ((rights & MESHRIGHT_REMOTECONTROL) != 0)) {
3225 // Flash and vibrate
3226 if ((command.actiontype == 400) && common.validateInt(command.time, 1, 30000)) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: node._id, value: 'flash ' + command.time }, MESHRIGHT_ADMIN, 0); }
3227 if ((command.actiontype == 401) && common.validateInt(command.time, 1, 30000)) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: node._id, value: 'vibrate ' + command.time }, MESHRIGHT_ADMIN, 0); }
3228 } else {
3229 // Check we have the rights to perform this operation
3230 if ((command.actiontype == 302) && ((rights & MESHRIGHT_WAKEDEVICE) == 0)) return; // This is a Intel AMT power on operation, check if we have WAKE rights
3231 if ((command.actiontype != 302) && ((rights & MESHRIGHT_RESETOFF) == 0)) return; // For all other operations, check that we have RESET/OFF rights
3232
3233 // If this device is connected on MQTT, send a power action.
3234 if ((parent.parent.mqttbroker != null) && (command.actiontype >= 0) && (command.actiontype <= 4)) { parent.parent.mqttbroker.publish(node._id, 'powerAction', ['', '', 'poweroff', 'reset', 'sleep'][command.actiontype]); }
3235
3236 // If this is a IP-KVM or Power Distribution Unit (PDU), dispatch an action event
3237 if (node.mtype == 4) {
3238 // Send out an event to perform turn off command on the port
3239 const targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['devport-operation', 'server-users', user._id]);
3240 const event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'turnoff', domain: domain.id, portid: node.portid, porttype: node.porttype, portnum: node.portnum, meshid: node.meshid, mtype: node.mtype, msgid: 133, msg: "Turn off." };
3241 parent.parent.DispatchEvent(targets, obj, event);
3242 return;
3243 }
3244
3245 if ((command.actiontype >= 300) && (command.actiontype < 400)) {
3246 if ((command.actiontype != 302) && (command.actiontype != 308) && (command.actiontype < 310) && (command.actiontype > 316)) return; // Invalid action type.
3247 // Intel AMT power command, actiontype: 2 = Power on, 8 = Power down, 10 = reset, 11 = Power on to BIOS, 12 = Reset to BIOS, 13 = Power on to BIOS with SOL, 14 = Reset to BIOS with SOL, 15 = Power on to PXE, 16 = Reset to PXE
3248 parent.parent.DispatchEvent('*', obj, { action: 'amtpoweraction', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, actiontype: command.actiontype - 300 });
3249 } else {
3250 if ((command.actiontype < 2) && (command.actiontype > 4)) return; // Invalid action type.
3251 // Mesh Agent power command, get this device and send the power command
3252 const agent = parent.wsagents[node._id];
3253 if (agent != null) {
3254 try { agent.send(JSON.stringify({ action: 'poweraction', actiontype: command.actiontype, userid: user._id, username: user.name, remoteaddr: req.clientIp })); } catch (ex) { }
3255 }
3256 }
3257 }
3258 });
3259
3260 // Confirm we may be doing something (TODO)
3261 if (command.responseid != null) {
3262 try { ws.send(JSON.stringify({ action: 'poweraction', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
3263 } else {
3264 try { ws.send(JSON.stringify({ action: 'poweraction' })); } catch (ex) { }
3265 }
3266 }
3267 break;
3268 }
3269 case 'toast':
3270 {
3271 var err = null;
3272
3273 // Perform input validation
3274 try {
3275 if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
3276 else if (common.validateString(command.msg, 1, 4096) == false) { err = "Invalid message"; } // Check message
3277 else {
3278 var nodeids = [];
3279 for (i in command.nodeids) { if (command.nodeids[i].indexOf('/') == -1) { nodeids.push('node/' + domain.id + '/' + command.nodeids[i]); } else { nodeids.push(command.nodeids[i]); } }
3280 command.nodeids = nodeids;
3281 }
3282 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
3283
3284 // Handle any errors
3285 if (err != null) {
3286 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: err })); } catch (ex) { } }
3287 break;
3288 }
3289
3290 // Check the title, if needed, use a default one
3291 if (common.validateString(command.title, 1, 512) == false) { delete command.title } // Check title
3292 if ((command.title == null) && (typeof domain.notificationmessages == 'object') && (typeof domain.notificationmessages.title == 'string')) { command.title = domain.notificationmessages.title; }
3293 if ((command.title == null) && (typeof domain.title == 'string')) { command.title = domain.title; }
3294 if (command.title == null) { command.title = "MeshCentral"; }
3295
3296 for (i in command.nodeids) {
3297 // Get the node and the rights for this node
3298 parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
3299 // Check we have the rights to notify this device
3300 if ((rights & MESHRIGHT_CHATNOTIFY) == 0) {
3301 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } }
3302 return;
3303 }
3304
3305 // Get this device and send toast command
3306 const agent = parent.wsagents[node._id];
3307 if (agent != null) {
3308 try { agent.send(JSON.stringify({ action: 'toast', title: command.title, msg: command.msg, sessionid: ws.sessionId, username: user.name, userid: user._id })); } catch (ex) { }
3309 }
3310 });
3311 }
3312
3313 // Send response if required
3314 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
3315 break;
3316 }
3317 case 'changedevice':
3318 {
3319 var err = null;
3320
3321 // Argument validation
3322 try {
3323 if (common.validateString(command.nodeid, 1, 1024) == false) { err = "Invalid nodeid"; } // Check nodeid
3324 else {
3325 if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
3326 if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = "Invalid nodeid"; } // Invalid domain, operation only valid for current domain
3327 else if ((command.userloc) && (command.userloc.length != 2) && (command.userloc.length != 0)) { err = "Invalid user location"; }
3328 }
3329 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
3330
3331 // Handle any errors
3332 if (err != null) {
3333 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: err })); } catch (ex) { } }
3334 break;
3335 }
3336
3337 // Get the node and the rights for this node
3338 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3339 if ((rights & MESHRIGHT_MANAGECOMPUTERS) == 0) {
3340 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } }
3341 return;
3342 }
3343 node = common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials
3344 var mesh = parent.meshes[node.meshid], amtchange = 0;
3345
3346 // Ready the node change event
3347 var changes = [], event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id };
3348 change = 0;
3349 event.msg = ': ';
3350
3351 // If we are in WAN-only mode, host is not used
3352 if ((args.wanonly == true) && (command.host) && (node.mtype != 3) && (node.mtype != 4)) { delete command.host; }
3353
3354 // Look for a change
3355 if ((typeof command.icon == 'number') && (command.icon != node.icon)) { change = 1; node.icon = command.icon; changes.push('icon'); }
3356 if ((typeof command.name == 'string') && (command.name != node.name)) { change = 1; node.name = command.name; changes.push('name'); }
3357 if ((typeof command.host == 'string') && (command.host != node.host)) { change = 1; node.host = command.host; changes.push('host'); }
3358 if (typeof command.consent == 'number') {
3359 var oldConsent = node.consent;
3360 if (command.consent != node.consent) { node.consent = command.consent; }
3361 if (command.consent == 0) { delete node.consent; }
3362 if (oldConsent != node.consent) { change = 1; changes.push('consent'); }
3363 }
3364
3365 if ((typeof command.rdpport == 'number') && (command.rdpport > 0) && (command.rdpport < 65536)) {
3366 if ((command.rdpport == 3389) && (node.rdpport != null)) {
3367 delete node.rdpport; change = 1; changes.push('rdpport'); // Delete the RDP port
3368 } else {
3369 node.rdpport = command.rdpport; change = 1; changes.push('rdpport'); // Set the RDP port
3370 }
3371 }
3372
3373 if ((typeof command.rfbport == 'number') && (command.rfbport > 0) && (command.rfbport < 65536)) {
3374 if ((command.rfbport == 5900) && (node.rfbport != null)) {
3375 delete node.rfbport; change = 1; changes.push('rfbport'); // Delete the RFB port
3376 } else {
3377 node.rfbport = command.rfbport; change = 1; changes.push('rfbport'); // Set the RFB port
3378 }
3379 }
3380
3381 if ((typeof command.sshport == 'number') && (command.sshport > 0) && (command.sshport < 65536)) {
3382 if ((command.sshport == 22) && (node.sshport != null)) {
3383 delete node.sshport; change = 1; changes.push('sshport'); // Delete the SSH port
3384 } else {
3385 node.sshport = command.sshport; change = 1; changes.push('sshport'); // Set the SSH port
3386 }
3387 }
3388
3389 if ((typeof command.httpport == 'number') && (command.httpport > 0) && (command.httpport < 65536)) {
3390 if ((command.httpport == 80) && (node.httpport != null)) {
3391 delete node.httpport; change = 1; changes.push('httpport'); // Delete the HTTP port
3392 } else {
3393 node.httpport = command.httpport; change = 1; changes.push('httpport'); // Set the HTTP port
3394 }
3395 }
3396
3397 if ((typeof command.httpsport == 'number') && (command.httpsport > 0) && (command.httpsport < 65536)) {
3398 if ((command.httpsport == 443) && (node.httpsport != null)) {
3399 delete node.httpsport; change = 1; changes.push('httpsport'); // Delete the HTTPS port
3400 } else {
3401 node.httpsport = command.httpsport; change = 1; changes.push('httpsport'); // Set the HTTPS port
3402 }
3403 }
3404
3405 if ((typeof command.ssh == 'number') && (command.ssh == 0)) {
3406 if ((node.ssh != null) && (node.ssh[user._id] != null)) { delete node.ssh[user._id]; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
3407 }
3408
3409 if ((typeof command.rdp == 'number') && (command.rdp == 0)) {
3410 if ((node.rdp != null) && (node.rdp[user._id] != null)) { delete node.rdp[user._id]; change = 1; changes.push('rdp'); } // Delete the RDP cendentials
3411 }
3412
3413 // Clean up any legacy RDP and SSH credentials
3414 if (node.rdp != null) { delete node.rdp.d; delete node.rdp.u; delete node.rdp.p; }
3415 if (node.ssh != null) { delete node.ssh.u; delete node.ssh.p; delete node.ssh.k; delete node.ssh.kp; }
3416
3417 if (domain.geolocation && command.userloc && ((node.userloc == null) || (command.userloc[0] != node.userloc[0]) || (command.userloc[1] != node.userloc[1]))) {
3418 change = 1;
3419 if ((command.userloc.length == 0) && (node.userloc)) {
3420 delete node.userloc;
3421 changes.push('location removed');
3422 } else {
3423 command.userloc.push((Math.floor((new Date()) / 1000)));
3424 node.userloc = command.userloc.join(',');
3425 changes.push('location');
3426 }
3427 }
3428 if (command.desc != null && (command.desc != node.desc)) { change = 1; node.desc = command.desc; changes.push('description'); }
3429 if (command.intelamt != null) {
3430 if ((parent.parent.amtManager == null) || (node.intelamt.pass == null) || (node.intelamt.pass == '') || ((node.intelamt.warn != null) && (((node.intelamt.warn) & 9) != 0))) { // Only allow changes to Intel AMT credentials if AMT manager is not running, or manager warned of unknown/trying credentials.
3431 if ((command.intelamt.user != null) && (command.intelamt.pass != null) && ((command.intelamt.user != node.intelamt.user) || (command.intelamt.pass != node.intelamt.pass))) {
3432 change = 1;
3433 node.intelamt.user = command.intelamt.user;
3434 node.intelamt.pass = command.intelamt.pass;
3435 node.intelamt.warn |= 8; // Change warning to "Trying". Bit flags: 1 = Unknown credentials, 2 = Realm Mismatch, 4 = TLS Cert Mismatch, 8 = Trying credentials
3436 changes.push('Intel AMT credentials');
3437 amtchange = 1;
3438 }
3439 }
3440 // Only allow the user to set Intel AMT TLS state if AMT Manager is not active. AMT manager will auto-detect TLS state.
3441 if ((parent.parent.amtManager != null) && (command.intelamt.tls != null) && (command.intelamt.tls != node.intelamt.tls)) { change = 1; node.intelamt.tls = command.intelamt.tls; changes.push('Intel AMT TLS'); }
3442 }
3443 if (command.tags) { // Node grouping tag, this is a array of strings that can't be empty and can't contain a comma
3444 var ok = true, group2 = [];
3445 if (common.validateString(command.tags, 0, 4096) == true) { command.tags = command.tags.split(','); }
3446 for (var i in command.tags) { var tname = command.tags[i].trim(); if ((tname.length > 0) && (tname.length < 64) && (group2.indexOf(tname) == -1)) { group2.push(tname); } }
3447 group2.sort();
3448 if (node.tags != group2) { node.tags = group2; change = 1; }
3449 } else if ((command.tags === '') && node.tags) { delete node.tags; change = 1; }
3450
3451 if (change == 1) {
3452 // Save the node
3453 db.Set(parent.cleanDevice(node));
3454
3455 // Event the node change. Only do this if the database will not do it.
3456 event.msg = 'Changed device ' + node.name + ' from group ' + mesh.name + ': ' + changes.join(', ');
3457 event.node = parent.CloneSafeNode(node);
3458 event.msgid = 140;
3459 event.msgArgs = [ node.name, mesh.name, changes.join(', ') ];
3460 if (amtchange == 1) { event.amtchange = 1; } // This will give a hint to the AMT Manager to reconnect using new AMT credentials
3461 if (command.rdpport == 3389) { event.node.rdpport = 3389; }
3462 if (command.rfbport == 5900) { event.node.rfbport = 5900; }
3463 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
3464 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
3465 }
3466
3467 // Send response if required
3468 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
3469 });
3470 break;
3471 }
3472 case 'uploadagentcore':
3473 {
3474 if (common.validateString(command.type, 1, 40) == false) break; // Check path
3475 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3476
3477 // Go thru all node identifiers and run the operation
3478 for (var i in command.nodeids) {
3479 var nodeid = command.nodeids[i];
3480 if (typeof nodeid != 'string') return;
3481
3482 // Get the node and the rights for this node
3483 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3484 if ((node == null) || (((rights & MESHRIGHT_AGENTCONSOLE) == 0) && (user.siteadmin != SITERIGHT_ADMIN))) return;
3485
3486 // TODO: If we have peer servers, inform...
3487 //if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'uploadagentcore', sessionid: ws.sessionId }); }
3488
3489 if (command.type == 'default') {
3490 // Send the default core to the agent
3491 parent.parent.updateMeshCore(function () { parent.sendMeshAgentCore(user, domain, node._id, 'default'); });
3492 } else if (command.type == 'clear') {
3493 // Clear the mesh agent core on the mesh agent
3494 parent.sendMeshAgentCore(user, domain, node._id, 'clear');
3495 } else if (command.type == 'recovery') {
3496 // Send the recovery core to the agent
3497 parent.sendMeshAgentCore(user, domain, node._id, 'recovery');
3498 } else if (command.type == 'tiny') {
3499 // Send the tiny core to the agent
3500 parent.sendMeshAgentCore(user, domain, node._id, 'tiny');
3501 } else if ((command.type == 'custom') && (common.validateString(command.path, 1, 2048) == true)) {
3502 // Send a mesh agent core to the mesh agent
3503 var file = parent.getServerFilePath(user, domain, command.path);
3504 if (file != null) {
3505 fs.readFile(file.fullpath, 'utf8', function (err, data) {
3506 if (err != null) {
3507 data = common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
3508 parent.sendMeshAgentCore(user, domain, node._id, 'custom', data);
3509 }
3510 });
3511 }
3512 }
3513 });
3514 }
3515 break;
3516 }
3517 case 'inviteAgent':
3518 {
3519 var err = null, mesh = null;
3520
3521 // Resolve the device group name if needed
3522 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
3523 for (var i in parent.meshes) {
3524 var m = parent.meshes[i];
3525 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
3526 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
3527 }
3528 }
3529 }
3530
3531 try {
3532 if ((domain.mailserver == null) || (args.lanonly == true)) { err = 'Unsupported feature'; } // This operation requires the email server
3533 else if ((parent.parent.certificates.CommonName == null) || (parent.parent.certificates.CommonName.indexOf('.') == -1)) { err = 'Unsupported feature'; } // Server name must be configured
3534 else if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check meshid
3535 else {
3536 if (command.meshid.split('/').length == 1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
3537 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group identifier'; } // Invalid domain, operation only valid for current domain
3538 else if (common.validateString(command.email, 4, 1024) == false) { err = 'Invalid email'; } // Check email
3539 else if (command.email.split('@').length != 2) { err = 'Invalid email'; } // Check email
3540 else {
3541 mesh = parent.meshes[command.meshid];
3542 if (mesh == null) { err = 'Unknown device group'; } // Check if the group exists
3543 else if (mesh.mtype != 2) { err = 'Invalid group type'; } // Check if this is the correct group type
3544 else if (parent.IsMeshViewable(user, mesh) == false) { err = 'Not allowed'; } // Check if this user has rights to do this
3545 }
3546 }
3547 } catch (ex) { err = 'Validation exception: ' + ex; }
3548
3549 // Handle any errors
3550 if (err != null) {
3551 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: err })); } catch (ex) { } }
3552 break;
3553 }
3554
3555 // Perform email invitation
3556 domain.mailserver.sendAgentInviteMail(domain, (user.realname ? user.realname : user.name), command.email.toLowerCase(), command.meshid, command.name, command.os, command.msg, command.flags, command.expire, parent.getLanguageCodes(req), req.query.key);
3557
3558 // Send a response if needed
3559 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
3560 break;
3561 }
3562 case 'setDeviceEvent':
3563 {
3564 // Argument validation
3565 if (common.validateString(command.msg, 1, 4096) == false) break; // Check event
3566
3567 // Get the node and the rights for this node
3568 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3569 if (rights == 0) return;
3570
3571 // Add an event for this device
3572 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
3573 var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'manual', msg: decodeURIComponent(command.msg), domain: domain.id };
3574 parent.parent.DispatchEvent(targets, obj, event);
3575 });
3576 break;
3577 }
3578 case 'setNotes':
3579 {
3580 // Argument validation
3581 if (common.validateString(command.id, 1, 1024) == false) break; // Check id
3582 var splitid = command.id.split('/');
3583 if ((splitid.length != 3) || (splitid[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
3584 var idtype = splitid[0];
3585 if ((idtype != 'puser') && (idtype != 'user') && (idtype != 'mesh') && (idtype != 'node')) return;
3586
3587 if (idtype == 'node') {
3588 // Get the node and the rights for this node
3589 parent.GetNodeWithRights(domain, user, command.id, function (node, rights, visible) {
3590 if ((rights & MESHRIGHT_SETNOTES) != 0) {
3591 // Set the id's notes
3592 if (common.validateString(command.notes, 1) == false) {
3593 db.Remove('nt' + node._id); // Delete the note for this node
3594 } else {
3595 db.Set({ _id: 'nt' + node._id, type: 'note', value: command.notes }); // Set the note for this node
3596 }
3597 }
3598 });
3599 } else if (idtype == 'mesh') {
3600 // Get the mesh for this device
3601 mesh = parent.meshes[command.id];
3602 if (mesh) {
3603 // Check if this user has rights to do this
3604 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return; // Must have rights to edit the mesh
3605
3606 // Set the id's notes
3607 if (common.validateString(command.notes, 1) == false) {
3608 db.Remove('nt' + command.id); // Delete the note for this node
3609 } else {
3610 db.Set({ _id: 'nt' + command.id, type: 'note', value: command.notes }); // Set the note for this mesh
3611 }
3612 }
3613 } else if ((idtype == 'user') && ((user.siteadmin & 2) != 0)) {
3614 // Set the id's notes
3615 if (common.validateString(command.notes, 1) == false) {
3616 db.Remove('nt' + command.id); // Delete the note for this node
3617 } else {
3618 // Can only perform this operation on other users of our group.
3619 var chguser = parent.users[command.id];
3620 if (chguser == null) break; // This user does not exists
3621 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
3622 db.Set({ _id: 'nt' + command.id, type: 'note', value: command.notes }); // Set the note for this user
3623 }
3624 } else if (idtype == 'puser') {
3625 // Set the user's personal note, starts with 'ntp' + userid.
3626 if (common.validateString(command.notes, 1) == false) {
3627 db.Remove('ntp' + user._id); // Delete the note for this node
3628 } else {
3629 db.Set({ _id: 'ntp' + user._id, type: 'note', value: command.notes }); // Set the note for this user
3630 }
3631 }
3632
3633 break;
3634 }
3635 case 'otpemail':
3636 {
3637 // Do not allow this command if 2FA's are locked
3638 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3639
3640 // Do not allow this command when logged in using a login token
3641 if (req.session.loginToken != null) break;
3642
3643 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3644
3645 // Check input
3646 if (typeof command.enabled != 'boolean') return;
3647
3648 // See if we really need to change the state
3649 if ((command.enabled === true) && (user.otpekey != null)) return;
3650 if ((command.enabled === false) && (user.otpekey == null)) return;
3651
3652 // Change the email 2FA of this user
3653 if (command.enabled === true) { user.otpekey = {}; } else { delete user.otpekey; }
3654 parent.db.SetUser(user);
3655 ws.send(JSON.stringify({ action: 'otpemail', success: true, enabled: command.enabled })); // Report success
3656
3657 // Notify change
3658 var targets = ['*', 'server-users', user._id];
3659 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3660 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: command.enabled ? 88 : 89, msg: command.enabled ? "Enabled email two-factor authentication." : "Disabled email two-factor authentication.", domain: domain.id };
3661 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3662 parent.parent.DispatchEvent(targets, obj, event);
3663 break;
3664 }
3665 case 'otpduo':
3666 {
3667 // Do not allow this command if 2FA's are locked
3668 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3669
3670 // Do not allow if Duo is not supported
3671 if ((typeof domain.duo2factor != 'object') || (typeof domain.duo2factor.integrationkey != 'string') || (typeof domain.duo2factor.secretkey != 'string') || (typeof domain.duo2factor.apihostname != 'string')) return;
3672
3673 // Do not allow if Duo is disabled
3674 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) return;
3675
3676 // Do not allow this command when logged in using a login token
3677 if (req.session.loginToken != null) break;
3678
3679 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3680
3681 // Check input
3682 if ((typeof command.enabled != 'boolean') || (command.enabled != false)) return;
3683
3684 // See if we really need to change the state
3685 if ((command.enabled === false) && (user.otpduo == null)) return;
3686
3687 // Change the duo 2FA of this user
3688 delete user.otpduo;
3689 parent.db.SetUser(user);
3690 ws.send(JSON.stringify({ action: 'otpduo', success: true, enabled: command.enabled })); // Report success
3691
3692 // Notify change
3693 var targets = ['*', 'server-users', user._id];
3694 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3695 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: command.enabled ? 160 : 161, msg: command.enabled ? "Enabled duo two-factor authentication." : "Disabled duo two-factor authentication.", domain: domain.id };
3696 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3697 parent.parent.DispatchEvent(targets, obj, event);
3698 break;
3699 }
3700 case 'otpauth-request':
3701 {
3702 // Do not allow this command if 2FA's are locked
3703 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 1 })); return; }
3704
3705 // Do not allow this command when logged in using a login token
3706 if (req.session.loginToken != null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 3 })); return; }
3707
3708 // Check of OTP 2FA is allowed
3709 if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 4 })); return; }
3710
3711 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 5 })); return; } // If this account is settings locked, return here.
3712
3713 // Check if 2-step login is supported
3714 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3715 if (twoStepLoginSupported) {
3716 // Request a one time password to be setup
3717 var otplib = null;
3718 try { otplib = require('otplib'); } catch (ex) { }
3719 if (otplib == null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 6 })); return; }
3720 const secret = otplib.authenticator.generateSecret(); // TODO: Check the random source of this value.
3721
3722 var domainName = parent.certificates.CommonName;
3723 if (domain.dns != null) {
3724 domainName = domain.dns;
3725 } else if (domain.dns == null && domain.id != '') {
3726 domainName += "/" + domain.id;
3727 }
3728 ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, domainName, secret) }));
3729 }
3730 break;
3731 }
3732 case 'otpauth-setup':
3733 {
3734 // Do not allow this command if 2FA's are locked
3735 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3736
3737 // Do not allow this command when logged in using a login token
3738 if (req.session.loginToken != null) break;
3739
3740 // Check of OTP 2FA is allowed
3741 if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) break;
3742
3743 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3744
3745 // Check if 2-step login is supported
3746 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3747 if (twoStepLoginSupported) {
3748 // Perform the one time password setup
3749 var otplib = null;
3750 try { otplib = require('otplib'); } catch (ex) { }
3751 if (otplib == null) { break; }
3752 otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
3753 if (otplib.authenticator.check(command.token, command.secret) === true) {
3754 // Token is valid, activate 2-step login on this account.
3755 user.otpsecret = command.secret;
3756 parent.db.SetUser(user);
3757 ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
3758
3759 // Notify change
3760 var targets = ['*', 'server-users', user._id];
3761 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3762 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 90, msg: 'Added authentication application', domain: domain.id };
3763 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3764 parent.parent.DispatchEvent(targets, obj, event);
3765 } else {
3766 ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
3767 }
3768 }
3769 break;
3770 }
3771 case 'otpauth-clear':
3772 {
3773 // Do not allow this command if 2FA's are locked
3774 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3775
3776 // Do not allow this command when logged in using a login token
3777 if (req.session.loginToken != null) break;
3778
3779 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3780
3781 // Check if 2-step login is supported
3782 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3783 if (twoStepLoginSupported) {
3784 // Clear the one time password secret
3785 if (user.otpsecret) {
3786 delete user.otpsecret;
3787 parent.db.SetUser(user);
3788 ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // Report success
3789
3790 // Notify change
3791 var targets = ['*', 'server-users', user._id];
3792 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3793 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 91, msg: 'Removed authentication application', domain: domain.id };
3794 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3795 parent.parent.DispatchEvent(targets, obj, event);
3796 } else {
3797 ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
3798 }
3799 }
3800 break;
3801 }
3802 case 'otpauth-getpasswords':
3803 {
3804 // Do not allow this command if 2FA's are locked
3805 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3806
3807 // Do not allow this command if backup codes are not allowed
3808 if ((domain.passwordrequirements) && (domain.passwordrequirements.backupcode2factor == false)) return;
3809
3810 // Do not allow this command when logged in using a login token
3811 if (req.session.loginToken != null) break;
3812
3813 // Check if 2-step login is supported
3814 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3815 if (twoStepLoginSupported == false) break;
3816
3817 var actionTaken = false, actionText = null, actionId = 0;
3818 if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 1024) == 0)) { // Don't allow generation of tokens if the account is settings locked
3819 // Perform a sub-action
3820 if (command.subaction == 1) { // Generate a new set of tokens
3821 var randomNumbers = [], v;
3822 for (var i = 0; i < 10; i++) { do { v = getRandomEightDigitInteger(); } while (randomNumbers.indexOf(v) >= 0); randomNumbers.push(v); }
3823 user.otpkeys = { keys: [] };
3824 for (var i = 0; i < 10; i++) { user.otpkeys.keys[i] = { p: randomNumbers[i], u: true } }
3825 actionTaken = true;
3826 actionId = 92;
3827 actionText = "New 2FA backup codes generated";
3828 } else if (command.subaction == 2) { // Clear all tokens
3829 actionTaken = (user.otpkeys != null);
3830 delete user.otpkeys;
3831 if (actionTaken) {
3832 actionId = 93;
3833 actionText = "2FA backup codes cleared";
3834 }
3835 }
3836
3837 // Save the changed user
3838 if (actionTaken) { parent.db.SetUser(user); }
3839 }
3840
3841 // Return one time passwords for this user
3842 if (count2factoraAuths() > 0) {
3843 ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys ? user.otpkeys.keys : null }));
3844 }
3845
3846 // Notify change
3847 if (actionText != null) {
3848 var targets = ['*', 'server-users', user._id];
3849 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3850 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: actionId, msg: actionText, domain: domain.id };
3851 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3852 parent.parent.DispatchEvent(targets, obj, event);
3853 }
3854 break;
3855 }
3856 case 'otp-hkey-get':
3857 {
3858 // Do not allow this command if 2FA's are locked
3859 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3860
3861 // Do not allow this command when logged in using a login token
3862 if (req.session.loginToken != null) break;
3863
3864 // Check if 2-step login is supported
3865 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3866 if (twoStepLoginSupported == false) break;
3867
3868 // Send back the list of keys we have, just send the list of names and index
3869 var hkeys = [];
3870 if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { hkeys.push({ i: user.otphkeys[i].keyIndex, name: user.otphkeys[i].name, type: user.otphkeys[i].type }); } }
3871
3872 ws.send(JSON.stringify({ action: 'otp-hkey-get', keys: hkeys }));
3873 break;
3874 }
3875 case 'otp-hkey-remove':
3876 {
3877 // Do not allow this command if 2FA's are locked
3878 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3879
3880 // Do not allow this command when logged in using a login token
3881 if (req.session.loginToken != null) break;
3882
3883 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3884
3885 // Check if 2-step login is supported
3886 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3887 if (twoStepLoginSupported == false || command.index == null) break;
3888
3889 // Remove a key
3890 var foundAtIndex = -1;
3891 if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyIndex == command.index) { foundAtIndex = i; } } }
3892 if (foundAtIndex != -1) {
3893 user.otphkeys.splice(foundAtIndex, 1);
3894 parent.db.SetUser(user);
3895 }
3896
3897 // Notify change
3898 var targets = ['*', 'server-users', user._id];
3899 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3900 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 94, msg: 'Removed security key', domain: domain.id };
3901 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3902 parent.parent.DispatchEvent(targets, obj, event);
3903 break;
3904 }
3905 case 'otp-hkey-yubikey-add':
3906 {
3907 // Do not allow this command if 2FA's are locked or max keys reached
3908 if (domain.passwordrequirements) {
3909 if (domain.passwordrequirements.lock2factor == true) return;
3910 if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return;
3911 }
3912
3913 // Do not allow this command when logged in using a login token
3914 if (req.session.loginToken != null) break;
3915
3916 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3917
3918 // Yubico API id and signature key can be requested from https://upgrade.yubico.com/getapikey/
3919 var yub = null;
3920 try { yub = require('yub'); } catch (ex) { }
3921
3922 // Check if 2-step login is supported
3923 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3924 if ((yub == null) || (twoStepLoginSupported == false) || (typeof command.otp != 'string')) {
3925 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
3926 break;
3927 }
3928
3929 // Check if Yubikey support is present or OTP no exactly 44 in length
3930 if ((typeof domain.yubikey != 'object') || (typeof domain.yubikey.id != 'string') || (typeof domain.yubikey.secret != 'string') || (command.otp.length != 44)) {
3931 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
3932 break;
3933 }
3934
3935 // TODO: Check if command.otp is modhex encoded, reject if not.
3936
3937 // Query the YubiKey server to validate the OTP
3938 yub.init(domain.yubikey.id, domain.yubikey.secret);
3939 yub.verify(command.otp, function (err, results) {
3940 if ((results != null) && (results.status == 'OK')) {
3941 var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
3942 var keyId = command.otp.substring(0, 12);
3943 if (user.otphkeys == null) { user.otphkeys = []; }
3944
3945 // Check if this key was already registered, if so, remove it.
3946 var foundAtIndex = -1;
3947 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyid == keyId) { foundAtIndex = i; } }
3948 if (foundAtIndex != -1) { user.otphkeys.splice(foundAtIndex, 1); }
3949
3950 // Add the new key and notify
3951 user.otphkeys.push({ name: command.name, type: 2, keyid: keyId, keyIndex: keyIndex });
3952 parent.db.SetUser(user);
3953 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: true, name: command.name, index: keyIndex }));
3954
3955 // Notify change TODO: Should be done on all sessions/servers for this user.
3956 var targets = ['*', 'server-users', user._id];
3957 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3958 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 95, msg: 'Added security key', domain: domain.id };
3959 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3960 parent.parent.DispatchEvent(targets, obj, event);
3961 } else {
3962 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
3963 }
3964 });
3965
3966 break;
3967 }
3968 case 'otpdev-clear':
3969 {
3970 // Do not allow this command if 2FA's are locked
3971 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3972
3973 // Do not allow this command when logged in using a login token
3974 if (req.session.loginToken != null) break;
3975
3976 // Remove the authentication push notification device
3977 if (user.otpdev != null) {
3978 // Change the user
3979 user.otpdev = obj.dbNodeKey;
3980 parent.db.SetUser(user);
3981
3982 // Notify change
3983 var targets = ['*', 'server-users', user._id];
3984 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3985 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 114, msg: "Removed push notification authentication device", domain: domain.id };
3986 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
3987 parent.parent.DispatchEvent(targets, obj, event);
3988 }
3989 break;
3990 }
3991 case 'otpdev-set':
3992 {
3993 // Do not allow this command if 2FA's are locked
3994 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3995
3996 // Do not allow this command when logged in using a login token
3997 if (req.session.loginToken != null) break;
3998
3999 // Attempt to add a authentication push notification device
4000 // This will only send a push notification to the device, the device needs to confirm for the auth device to be added.
4001 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
4002 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4003 // Only allow use of devices with full rights
4004 if ((node == null) || (visible == false) || (rights != 0xFFFFFFFF) || (node.agent == null) || (node.agent.id != 14) || (node.pmt == null)) return;
4005
4006 // Encode the cookie
4007 const code = Buffer.from(user.name).toString('base64');
4008 const authCookie = parent.parent.encodeCookie({ a: 'addAuth', c: code, u: user._id, n: node._id });
4009
4010 // Send out a push message to the device
4011 var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
4012 var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
4013 parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
4014 if (err == null) {
4015 parent.parent.debug('email', 'Successfully auth addition send push message to device ' + node.name);
4016 } else {
4017 parent.parent.debug('email', 'Failed auth addition push message to device ' + node.name + ', error: ' + errdesc);
4018 }
4019 });
4020 });
4021 break;
4022 }
4023 case 'webauthn-startregister':
4024 {
4025 // Do not allow this command if 2FA's are locked or max keys reached
4026 if (domain.passwordrequirements) {
4027 if (domain.passwordrequirements.lock2factor == true) return;
4028 if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return;
4029 }
4030
4031 // Do not allow this command when logged in using a login token
4032 if (req.session.loginToken != null) break;
4033
4034 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
4035
4036 // Check if 2-step login is supported
4037 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
4038 if ((twoStepLoginSupported == false) || (command.name == null)) break;
4039
4040 // Send the registration request
4041 var registrationOptions = parent.webauthn.generateRegistrationChallenge("Anonymous Service", { id: Buffer.from(user._id, 'binary').toString('base64'), name: user._id, displayName: user._id.split('/')[2] });
4042 //console.log('registrationOptions', registrationOptions);
4043 registrationOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.
4044 obj.webAuthnReqistrationRequest = { action: 'webauthn-startregister', keyname: command.name, request: registrationOptions };
4045 ws.send(JSON.stringify(obj.webAuthnReqistrationRequest));
4046 break;
4047 }
4048 case 'webauthn-endregister':
4049 {
4050 // Do not allow this command if 2FA's are locked or max keys reached
4051 if (domain.passwordrequirements) {
4052 if (domain.passwordrequirements.lock2factor == true) return;
4053 if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return;
4054 }
4055
4056 // Do not allow this command when logged in using a login token
4057 if (req.session.loginToken != null) break;
4058
4059 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
4060 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
4061 if ((twoStepLoginSupported == false) || (obj.webAuthnReqistrationRequest == null)) return;
4062
4063 // Figure out the origin
4064 var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
4065 var origin = "https://" + (domain.dns ? domain.dns : parent.certificates.CommonName);
4066 if (httpport != 443) { origin += ':' + httpport; }
4067
4068 // Use internal WebAuthn module to check the response
4069 var regResult = null;
4070 try { regResult = parent.webauthn.verifyAuthenticatorAttestationResponse(command.response.response); } catch (ex) { regResult = { verified: false, error: ex }; }
4071 if (regResult.verified === true) {
4072 // Since we are registering a WebAuthn/FIDO2 key, remove all U2F keys (Type 1).
4073 var otphkeys2 = [];
4074 if (user.otphkeys && Array.isArray(user.otphkeys)) { for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type != 1) { otphkeys2.push(user.otphkeys[i]); } } }
4075 user.otphkeys = otphkeys2;
4076
4077 // Add the new WebAuthn/FIDO2 keys
4078 var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
4079 if (user.otphkeys == null) { user.otphkeys = []; }
4080 user.otphkeys.push({ name: obj.webAuthnReqistrationRequest.keyname, type: 3, publicKey: regResult.authrInfo.publicKey, counter: regResult.authrInfo.counter, keyIndex: keyIndex, keyId: regResult.authrInfo.keyId });
4081 parent.db.SetUser(user);
4082 ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: true, name: command.name, index: keyIndex }));
4083
4084 // Notify change
4085 var targets = ['*', 'server-users', user._id];
4086 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
4087 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 95, msg: 'Added security key', domain: domain.id };
4088 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
4089 parent.parent.DispatchEvent(targets, obj, event);
4090 } else {
4091 //console.log('webauthn-endregister-error', regResult.error);
4092 ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: false, error: regResult.error, name: command.name, index: keyIndex }));
4093 }
4094
4095 delete obj.hardwareKeyRegistrationRequest;
4096 break;
4097 }
4098 case 'userWebState': {
4099 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
4100 if (common.validateString(command.state, 1, 30000) == false) break; // Check state size, no more than 30k
4101 command.state = parent.filterUserWebState(command.state); // Filter the state to remove anything bad
4102 if ((command.state == null) || (typeof command.state !== 'string')) break; // If state did not validate correctly, quit here.
4103 command.domain = domain.id;
4104 db.Set({ _id: 'ws' + user._id, state: command.state });
4105 parent.parent.DispatchEvent([user._id], obj, { action: 'userWebState', nolog: 1, domain: domain.id, state: command.state });
4106 break;
4107 }
4108 case 'getNotes':
4109 {
4110 // Argument validation
4111 if (common.validateString(command.id, 1, 1024) == false) break; // Check id
4112 var splitid = command.id.split('/');
4113 if ((splitid.length != 3) || (splitid[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
4114 var idtype = splitid[0];
4115 if ((idtype != 'puser') && (idtype != 'user') && (idtype != 'mesh') && (idtype != 'node')) return;
4116
4117 if (idtype == 'node') {
4118 // Get the node and the rights for this node
4119 parent.GetNodeWithRights(domain, user, command.id, function (node, rights, visible) {
4120 if (visible == false) return;
4121
4122 // Get the notes about this node
4123 db.Get('nt' + command.id, function (err, notes) {
4124 try {
4125 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4126 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4127 } catch (ex) { }
4128 });
4129 });
4130 } else if (idtype == 'mesh') {
4131 // Get the mesh for this device
4132 mesh = parent.meshes[command.id];
4133 if (mesh) {
4134 // Check if this user has rights to do this
4135 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return; // Must have rights to edit the mesh
4136
4137 // Get the notes about this node
4138 db.Get('nt' + command.id, function (err, notes) {
4139 try {
4140 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4141 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4142 } catch (ex) { }
4143 });
4144 }
4145 } else if ((idtype == 'user') && ((user.siteadmin & 2) != 0)) {
4146 // Get the notes about this node
4147 db.Get('nt' + command.id, function (err, notes) {
4148 try {
4149 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4150 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4151 } catch (ex) { }
4152 });
4153 } else if (idtype == 'puser') {
4154 // Get personal note, starts with 'ntp' + userid
4155 db.Get('ntp' + user._id, function (err, notes) {
4156 try {
4157 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4158 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4159 } catch (ex) { }
4160 });
4161 }
4162
4163 break;
4164 }
4165 case 'createInviteLink': {
4166 var err = null;
4167
4168 // Resolve the device group name if needed
4169 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
4170 for (var i in parent.meshes) {
4171 var m = parent.meshes[i];
4172 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
4173 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
4174 }
4175 }
4176 }
4177
4178 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group id'; } // Check the meshid (Max length of a meshid is 134 bytes).
4179 else if (common.validateInt(command.expire, 0, 99999) == false) { err = 'Invalid expire time'; } // Check the expire time in hours
4180 else if (common.validateInt(command.flags, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
4181 else {
4182 if (command.meshid.split('/').length == 1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
4183 var smesh = command.meshid.split('/');
4184 if ((smesh.length != 3) || (smesh[0] != 'mesh') || (smesh[1] != domain.id)) { err = 'Invalid group id'; }
4185 mesh = parent.meshes[command.meshid];
4186 if ((mesh == null) || (parent.IsMeshViewable(user, mesh) == false)) { err = 'Invalid group id'; }
4187 }
4188 var serverName = parent.getWebServerName(domain, req);
4189
4190 // Handle any errors
4191 if (err != null) {
4192 console.log(err, command.meshid);
4193 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createInviteLink', responseid: command.responseid, result: err })); } catch (ex) { } }
4194 break;
4195 }
4196
4197 const cookie = { a: 4, mid: command.meshid, f: command.flags, expire: command.expire * 60 };
4198 if ((typeof command.agents == 'number') && (command.agents != 0)) { cookie.ag = command.agents; }
4199 const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
4200 if (inviteCookie == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createInviteLink', responseid: command.responseid, result: 'Unable to generate invitation cookie' })); } catch (ex) { } } break; }
4201
4202 // Create the server url
4203 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
4204 var xdomain = (domain.dns == null) ? domain.id : '';
4205 if (xdomain != '') xdomain += '/';
4206 var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'agentinvite?c=' + inviteCookie;
4207 if (serverName.split('.') == 1) { url = '/' + xdomain + 'agentinvite?c=' + inviteCookie; }
4208
4209 ws.send(JSON.stringify({ action: 'createInviteLink', meshid: command.meshid, url: url, expire: command.expire, cookie: inviteCookie, responseid: command.responseid, tag: command.tag }));
4210 break;
4211 }
4212 case 'deviceMeshShares': {
4213 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4214 var err = null;
4215
4216 // Argument validation
4217 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid device group id'; } // Check the meshid
4218 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
4219 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4220 else {
4221 // Check if we have rights on this device group
4222 mesh = parent.meshes[command.meshid];
4223 if (mesh == null) { err = 'Invalid device group id'; } // Check the meshid
4224 else if (parent.GetMeshRights(user, mesh) == 0) { err = 'Access denied'; }
4225 }
4226
4227 // Handle any errors
4228 if (err != null) {
4229 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: err })); } catch (ex) { } }
4230 break;
4231 }
4232
4233 // Get all device shares
4234 parent.db.GetAllTypeNoTypeField('deviceshare', domain.id, function (err, docs) {
4235 if (err != null) return;
4236 var now = Date.now(), okDocs = [];
4237 for (var i = 0; i < docs.length; i++) {
4238 const doc = docs[i];
4239 if ((doc.expireTime != null) && (doc.expireTime < now)) {
4240 // This share is expired.
4241 parent.db.Remove(doc._id, function () { });
4242
4243 // Send device share update
4244 var targets = parent.CreateNodeDispatchTargets(doc.xmeshid, doc.nodeid, ['server-users', user._id]);
4245 parent.parent.DispatchEvent(targets, obj, { etype: 'node', meshid: doc.xmeshid, nodeid: doc.nodeid, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
4246 } else {
4247 if (doc.xmeshid == null) {
4248 // This is an old share with missing meshid, fix it here.
4249 const f = function fixShareMeshId(err, nodes) {
4250 if (err != null) return;
4251 if (nodes.length == 1) {
4252 // Add the meshid to the device share
4253 fixShareMeshId.xdoc.xmeshid = nodes[0].meshid;
4254 fixShareMeshId.xdoc.type = 'deviceshare';
4255 delete fixShareMeshId.xdoc.meshid;
4256 parent.db.Set(fixShareMeshId.xdoc);
4257 } else {
4258 // This node no longer exists, remove the device share.
4259 parent.db.Remove(fixShareMeshId.xdoc._id);
4260 }
4261 }
4262 f.xdoc = doc;
4263 db.Get(doc.nodeid, f);
4264 } else if (doc.xmeshid == command.meshid) {
4265 // This share is ok, remove extra data we don't need to send.
4266 delete doc._id; delete doc.domain; delete doc.type; delete doc.xmeshid;
4267 if (doc.userid != user._id) { delete doc.url; } // If this is not the user who created this link, don't give the link.
4268 okDocs.push(doc);
4269 }
4270 }
4271 }
4272 try { ws.send(JSON.stringify({ action: 'deviceMeshShares', meshid: command.meshid, deviceShares: okDocs })); } catch (ex) { }
4273 });
4274 break;
4275 }
4276 case 'deviceShares': {
4277 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4278 var err = null;
4279
4280 // Argument validation
4281 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
4282 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4283 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4284
4285 // Handle any errors
4286 if (err != null) {
4287 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: err })); } catch (ex) { } }
4288 break;
4289 }
4290
4291 // Get the device rights
4292 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4293 // If node not found or we don't have remote control, reject.
4294 if (node == null) {
4295 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
4296 return;
4297 }
4298
4299 // If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
4300 if (rights != MESHRIGHT_ADMIN) {
4301 // If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
4302 if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
4303 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4304 return;
4305 }
4306 }
4307
4308 parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
4309 if (err != null) return;
4310 var now = Date.now(), removed = false, okDocs = [];
4311 for (var i = 0; i < docs.length; i++) {
4312 const doc = docs[i];
4313 if ((doc.expireTime != null) && (doc.expireTime < now)) {
4314 // This share is expired.
4315 parent.db.Remove(doc._id, function () { }); removed = true;
4316 } else {
4317 // This share is ok, remove extra data we don't need to send.
4318 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.xmeshid;
4319 if (doc.userid != user._id) { delete doc.url; } // If this is not the user who created this link, don't give the link.
4320 okDocs.push(doc);
4321 }
4322 }
4323 try { ws.send(JSON.stringify({ action: 'deviceShares', nodeid: command.nodeid, deviceShares: okDocs })); } catch (ex) { }
4324
4325 // If we removed any shares, send device share update
4326 if (removed == true) {
4327 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4328 parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
4329 }
4330 });
4331 });
4332
4333 break;
4334 }
4335 case 'removeDeviceShare': {
4336 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4337 var err = null;
4338
4339 // Argument validation
4340 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
4341 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4342 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4343 if (common.validateString(command.publicid, 1, 128) == false) { err = 'Invalid public id'; } // Check the public identifier
4344
4345 // Handle any errors
4346 if (err != null) {
4347 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: err })); } catch (ex) { } }
4348 break;
4349 }
4350
4351 // Get the device rights
4352 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4353 // If node not found or we don't have remote control, reject.
4354 if (node == null) {
4355 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
4356 return;
4357 }
4358
4359 // If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
4360 if (rights != MESHRIGHT_ADMIN) {
4361 // If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
4362 if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
4363 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4364 return;
4365 }
4366 }
4367
4368 parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
4369 if (err != null) return;
4370
4371 // Remove device sharing
4372 var now = Date.now(), removedExact = null, removed = false, okDocs = [];
4373 for (var i = 0; i < docs.length; i++) {
4374 const doc = docs[i];
4375 if (doc.publicid == command.publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
4376 else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
4377 // This share is ok, remove extra data we don't need to send.
4378 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
4379 okDocs.push(doc);
4380 }
4381 }
4382
4383 // Confirm removal if requested
4384 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, nodeid: command.nodeid, publicid: command.publicid, removed: removedExact })); } catch (ex) { } }
4385
4386 // Event device share removal
4387 if (removedExact != null) {
4388 // Send out an event that we removed a device share
4389 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', 'server-shareremove', user._id]);
4390 var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: [removedExact.guestName], domain: domain.id, publicid: command.publicid };
4391 parent.parent.DispatchEvent(targets, obj, event);
4392
4393 // If this is an agent self-sharing link, notify the agent
4394 if (command.publicid.startsWith('AS:node/')) { routeCommandToNode({ action: 'msg', type: 'guestShare', nodeid: command.publicid.substring(3), flags: 0, url: null, viewOnly: false }); }
4395 }
4396
4397 // If we removed any shares, send device share update
4398 if (removed == true) {
4399 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4400 parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
4401 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
4402 } else {
4403 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'Invalid device share identifier.' })); } catch (ex) { } }
4404 }
4405 });
4406 });
4407 break;
4408 }
4409 case 'createDeviceShareLink': {
4410 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4411 var err = null;
4412
4413 // Argument validation
4414 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
4415 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4416 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4417 if (common.validateString(command.guestname, 1, 128) == false) { err = 'Invalid guest name'; } // Check the guest name
4418 else if ((command.expire != null) && (typeof command.expire != 'number')) { err = 'Invalid expire time'; } // Check the expire time in minutes
4419 else if ((command.start != null) && (typeof command.start != 'number')) { err = 'Invalid start time'; } // Check the start time in UTC seconds
4420 else if ((command.end != null) && (typeof command.end != 'number')) { err = 'Invalid end time'; } // Check the end time in UTC seconds
4421 else if (common.validateInt(command.consent, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
4422 else if (common.validateInt(command.p, 1, 31) == false) { err = 'Invalid protocol'; } // Check the protocol, 1 = Terminal, 2 = Desktop, 4 = Files, 8 = HTTP, 16 = HTTPS
4423 else if ((command.recurring != null) && (common.validateInt(command.recurring, 1, 2) == false)) { err = 'Invalid recurring value'; } // Check the recurring value, 1 = Daily, 2 = Weekly
4424 else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present
4425 else if ((command.recurring != null) && ((command.end != null) || (command.start == null) || (command.expire == null))) { err = 'Invalid recurring command'; }
4426 else if ((command.expire == null) && ((command.start == null) || (command.end == null) || (command.start > command.end))) { err = 'No time specified'; } // Check that a time range is present
4427 else {
4428 if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4429 var snode = command.nodeid.split('/');
4430 if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; }
4431 }
4432
4433 // Handle any errors
4434 if (err != null) {
4435 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createDeviceShareLink', responseid: command.responseid, result: err })); } catch (ex) { } }
4436 break;
4437 }
4438
4439 // Correct maximum session length if needed
4440 if ((typeof domain.guestdevicesharing == 'object') && (typeof domain.guestdevicesharing.maxsessiontime == 'number') && (domain.guestdevicesharing.maxsessiontime > 0)) {
4441 const maxtime = domain.guestdevicesharing.maxsessiontime;
4442 if ((command.expire != null) && (command.expire > maxtime)) { command.expire = maxtime; }
4443 if ((command.start != null) && (command.end != null)) { if ((command.end - command.start) > (maxtime * 60)) { command.end = (command.start + (maxtime * 60)); } }
4444 }
4445
4446 // Get the device rights
4447 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4448 // If node not found or we don't have remote control, reject.
4449 if (node == null) {
4450 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
4451 return;
4452 }
4453
4454 // If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
4455 if (rights != MESHRIGHT_ADMIN) {
4456 // If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
4457 if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
4458 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4459 return;
4460 }
4461 }
4462
4463 // If we are limited to no terminal, don't allow terminal sharing
4464 if (((command.p & 1) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOTERMINAL) != 0)) {
4465 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4466 return;
4467 }
4468
4469 // If we are limited to no desktop, don't allow desktop sharing
4470 if (((command.p & 2) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NODESKTOP) != 0)) {
4471 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4472 return;
4473 }
4474
4475 // If we are limited to no files, don't allow file sharing
4476 if (((command.p & 4) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOFILES) != 0)) {
4477 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4478 return;
4479 }
4480
4481 // If we have view only remote desktop rights, force view-only on the guest share.
4482 if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_REMOTEVIEWONLY) != 0)) { command.viewOnly = true; }
4483
4484 // Create cookie
4485 var publicid = getRandomPassword(), startTime = null, expireTime = null, duration = null;
4486 if (command.recurring) {
4487 // Recurring share
4488 startTime = command.start * 1000;
4489 duration = command.expire;
4490 } else if (command.expire != null) {
4491 if (command.expire !== 0) {
4492 // Now until expire in hours
4493 startTime = Date.now();
4494 expireTime = Date.now() + (60000 * command.expire);
4495 } else {
4496 delete command.expire;
4497 }
4498 } else {
4499 // Time range in seconds
4500 startTime = command.start * 1000;
4501 expireTime = command.end * 1000;
4502 }
4503
4504 //var cookie = { a: 5, p: command.p, uid: user._id, gn: command.guestname, nid: node._id, cf: command.consent, pid: publicid }; // Old style sharing cookie
4505 var cookie = { a: 6, pid: publicid }; // New style sharing cookie
4506 if ((startTime != null) && (expireTime != null)) { command.start = startTime; command.expire = cookie.e = expireTime; }
4507 else if ((startTime != null) && (duration != null)) { command.start = startTime; }
4508 const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
4509 if (inviteCookie == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createDeviceShareLink', responseid: command.responseid, result: 'Unable to generate shareing cookie' })); } catch (ex) { } } return; }
4510
4511 // Create the server url
4512 var serverName = parent.getWebServerName(domain, req);
4513 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
4514 var xdomain = (domain.dns == null) ? domain.id : '';
4515 if (xdomain != '') xdomain += '/';
4516 var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
4517 if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
4518 command.url = url;
4519 command.publicid = publicid;
4520 if (command.responseid != null) { command.result = 'OK'; }
4521 try { ws.send(JSON.stringify(command)); } catch (ex) { }
4522
4523 // Create a device sharing database entry
4524 var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', xmeshid: node.meshid, nodeid: node._id, p: command.p, domain: node.domain, publicid: publicid, userid: user._id, guestName: command.guestname, consent: command.consent, port: command.port, url: url };
4525 if ((startTime != null) && (expireTime != null)) { shareEntry.startTime = startTime; shareEntry.expireTime = expireTime; }
4526 else if ((startTime != null) && (duration != null)) { shareEntry.startTime = startTime; shareEntry.duration = duration; }
4527 if (command.recurring) { shareEntry.recurring = command.recurring; }
4528 if (command.viewOnly === true) { shareEntry.viewOnly = true; }
4529 parent.db.Set(shareEntry);
4530
4531 // Send out an event that we added a device share
4532 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4533 var event;
4534 if (command.recurring == 1) {
4535 event = { etype: 'node', userid: user._id, username: user.name, meshid: node.meshid, nodeid: node._id, action: 'addedDeviceShare', msg: 'Added device share ' + command.guestname + ' recurring daily.', msgid: 138, msgArgs: [command.guestname], domain: domain.id };
4536 } else if (command.recurring == 2) {
4537 event = { etype: 'node', userid: user._id, username: user.name, meshid: node.meshid, nodeid: node._id, action: 'addedDeviceShare', msg: 'Added device share ' + command.guestname + ' recurring weekly.', msgid: 139, msgArgs: [command.guestname], domain: domain.id };
4538 } else if ((startTime != null) && (expireTime != null)) {
4539 event = { etype: 'node', userid: user._id, username: user.name, meshid: node.meshid, nodeid: node._id, action: 'addedDeviceShare', msg: 'Added device share: ' + command.guestname + '.', msgid: 101, msgArgs: [command.guestname, 'DATETIME:' + startTime, 'DATETIME:' + expireTime], domain: domain.id };
4540 } else {
4541 event = { etype: 'node', userid: user._id, username: user.name, meshid: node.meshid, nodeid: node._id, action: 'addedDeviceShare', msg: 'Added device share ' + command.guestname + ' with unlimited time.', msgid: 131, msgArgs: [command.guestname], domain: domain.id };
4542 }
4543 parent.parent.DispatchEvent(targets, obj, event);
4544
4545 // Send device share update
4546 parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
4547 if (err != null) return;
4548
4549 // Check device sharing
4550 var now = Date.now();
4551 for (var i = 0; i < docs.length; i++) {
4552 const doc = docs[i];
4553 if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
4554 // This share is ok, remove extra data we don't need to send.
4555 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.xmeshid;
4556 }
4557 }
4558
4559 // Send device share update
4560 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4561 parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
4562 });
4563 });
4564 break;
4565 }
4566 case 'traceinfo': {
4567 // Only accept if the tracing tab is allowed for this domain
4568 if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.trace !== true))) break;
4569
4570 if ((user.siteadmin === SITERIGHT_ADMIN) && (typeof command.traceSources == 'object')) {
4571 parent.parent.debugRemoteSources = command.traceSources;
4572 parent.parent.DispatchEvent(['*'], obj, { action: 'traceinfo', userid: user._id, username: user.name, traceSources: command.traceSources, nolog: 1, domain: domain.id });
4573 }
4574 break;
4575 }
4576 case 'sendmqttmsg': {
4577 if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }; // MQTT not available
4578 if (common.validateArray(command.nodeids, 1) == false) { err = 'Invalid nodeids'; }; // Check nodeid's
4579 if (common.validateString(command.topic, 1, 64) == false) { err = 'Invalid topic'; } // Check the topic
4580 if (common.validateString(command.msg, 1, 4096) == false) { err = 'Invalid msg'; } // Check the message
4581
4582 // Handle any errors
4583 if (err != null) {
4584 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'sendmqttmsg', responseid: command.responseid, result: err })); } catch (ex) { } }
4585 break;
4586 }
4587
4588 // Send the MQTT message
4589 for (i in command.nodeids) {
4590 // Get the node and the rights for this node
4591 parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
4592 // If this device is connected on MQTT, send a wake action.
4593 if (rights != 0) {
4594 parent.parent.mqttbroker.publish(node._id, command.topic, command.msg);
4595 }
4596 });
4597 }
4598
4599 break;
4600 }
4601 case 'getmqttlogin': {
4602 var err = null;
4603 if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }
4604 if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid
4605
4606 // Handle any errors
4607 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
4608
4609 // Get the node and the rights for this node
4610 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4611 // Check if this user has rights to do this
4612 if (rights == MESHRIGHT_ADMIN) {
4613 var token = parent.parent.mqttbroker.generateLogin(node.meshid, node._id);
4614 var r = { action: 'getmqttlogin', responseid: command.responseid, nodeid: node._id, user: token.user, pass: token.pass };
4615 const serverName = parent.getWebServerName(domain, req);
4616
4617 // Add MPS URL
4618 if (parent.parent.mpsserver != null) {
4619 r.mpsCertHashSha384 = parent.parent.certificateOperations.getCertHash(parent.parent.mpsserver.certificates.mps.cert);
4620 r.mpsCertHashSha1 = parent.parent.certificateOperations.getCertHashSha1(parent.parent.mpsserver.certificates.mps.cert);
4621 r.mpsUrl = 'mqtts://' + serverName + ':' + ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport) + '/';
4622 }
4623
4624 // Add WS URL
4625 var xdomain = (domain.dns == null) ? domain.id : '';
4626 if (xdomain != '') xdomain += '/';
4627 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
4628 r.wsUrl = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'mqtt.ashx';
4629 r.wsTrustedCert = parent.isTrustedCert(domain);
4630
4631 try { ws.send(JSON.stringify(r)); } catch (ex) { }
4632 } else {
4633 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Unable to perform this operation' })); } catch (ex) { } }
4634 }
4635 });
4636 break;
4637 }
4638 case 'amt': {
4639 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
4640 if (common.validateInt(command.mode, 0, 3) == false) break; // Check connection mode
4641 // Validate if communication mode is possible
4642 if (command.mode == null || command.mode == 0) {
4643 break; //unsupported
4644 } else if (command.mode == 1) {
4645 var state = parent.parent.GetConnectivityState(command.nodeid);
4646 if ((state == null) || (state.connectivity & 4) == 0) break;
4647 } else if (command.mode == 2) {
4648 if (parent.parent.mpsserver.ciraConnections[command.nodeid] == null) break;
4649 }
4650 /*
4651 else if (command.mode == 3) {
4652 if (parent.parent.apfserver.apfConnections[command.nodeid] == null) break;
4653 }
4654 */
4655
4656 // Get the node and the rights for this node
4657 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4658 if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
4659 handleAmtCommand(command, node);
4660 });
4661 break;
4662 }
4663 case 'distributeCore': {
4664 // This is only available when plugins are enabled since it could cause stress on the server
4665 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4666 for (var i in command.nodes) {
4667 parent.sendMeshAgentCore(user, domain, command.nodes[i]._id, 'default');
4668 }
4669 break;
4670 }
4671 case 'plugins': {
4672 // Since plugin actions generally require a server restart, use the Full admin permission
4673 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4674 parent.db.getPlugins(function(err, docs) {
4675 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4676 });
4677 break;
4678 }
4679 case 'pluginLatestCheck': {
4680 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4681 parent.parent.pluginHandler.getPluginLatest()
4682 .then(function(latest) {
4683 try { ws.send(JSON.stringify({ action: 'pluginVersionsAvailable', list: latest })); } catch (ex) { }
4684 });
4685 break;
4686 }
4687 case 'addplugin': {
4688 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4689 try {
4690 parent.parent.pluginHandler.getPluginConfig(command.url)
4691 .then(parent.parent.pluginHandler.addPlugin)
4692 .then(function(docs){
4693 var targets = ['*', 'server-users'];
4694 parent.parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs });
4695 })
4696 .catch(function(err) {
4697 if (typeof err == 'object') err = err.message;
4698 try { ws.send(JSON.stringify({ action: 'pluginError', msg: err })); } catch (er) { }
4699 });
4700
4701 } catch(ex) { console.log('Cannot add plugin: ' + e); }
4702 break;
4703 }
4704 case 'installplugin': {
4705 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4706 parent.parent.pluginHandler.installPlugin(command.id, command.version_only, null, function(){
4707 parent.db.getPlugins(function(err, docs) {
4708 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4709 });
4710 var targets = ['*', 'server-users'];
4711 parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
4712 });
4713 break;
4714 }
4715 case 'disableplugin': {
4716 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4717 parent.parent.pluginHandler.disablePlugin(command.id, function(){
4718 parent.db.getPlugins(function(err, docs) {
4719 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4720 var targets = ['*', 'server-users'];
4721 parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
4722 });
4723 });
4724 break;
4725 }
4726 case 'removeplugin': {
4727 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4728 parent.parent.pluginHandler.removePlugin(command.id, function(){
4729 parent.db.getPlugins(function(err, docs) {
4730 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4731 });
4732 });
4733 break;
4734 }
4735 case 'getpluginversions': {
4736 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4737 parent.parent.pluginHandler.getPluginVersions(command.id)
4738 .then(function (versionInfo) {
4739 try { ws.send(JSON.stringify({ action: 'downgradePluginVersions', info: versionInfo, error: null })); } catch (ex) { }
4740 })
4741 .catch(function (e) {
4742 try { ws.send(JSON.stringify({ action: 'pluginError', msg: e })); } catch (ex) { }
4743 });
4744
4745 break;
4746 }
4747 case 'plugin': {
4748 if (parent.parent.pluginHandler == null) break; // If the plugin's are not supported, reject this command.
4749 command.userid = user._id;
4750 if (command.routeToNode === true) {
4751 routeCommandToNode(command);
4752 } else {
4753 try {
4754 parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
4755 } catch (ex) { console.log('Error loading plugin handler (' + ex + ')'); }
4756 }
4757 break;
4758 }
4759 case 'uicustomevent': {
4760 if ((command.src != null) && (Array.isArray(command.src.selectedDevices))) {
4761 // Contains a list of nodeid's, check that we have permissions for them.
4762 parent.GetNodesWithRights(domain, user, command.src.selectedDevices, function (nodes) {
4763 var nodeids = [];
4764 for (var i in nodes) { nodeids.push(i); }
4765 if (nodeids.length == 0) return;
4766
4767 // Event the custom UI action
4768 var message = { etype: 'user', userid: user._id, username: user.name, action: 'uicustomevent', domain: domain.id, uisection: command.section, element: command.element };
4769 if (nodeids.length == 1) { message.nodeid = nodeids[0]; }
4770 if (command.selectedDevices != null) { message.selectedDevices = command.selectedDevices; }
4771 if (command.src != null) { message.src = command.src; }
4772 if (command.values != null) { message.values = command.values; }
4773 if (typeof command.logmsg == 'string') { message.msg = command.logmsg; } else { message.nolog = 1; }
4774 parent.parent.DispatchEvent(['*', user._id], obj, message);
4775 });
4776 } else {
4777 // Event the custom UI action
4778 var message = { etype: 'user', userid: user._id, username: user.name, action: 'uicustomevent', domain: domain.id, uisection: command.section, element: command.element };
4779 if (command.selectedDevices != null) { message.selectedDevices = command.selectedDevices; }
4780 if (command.src != null) { message.src = command.src; }
4781 if (command.values != null) { message.values = command.values; }
4782 if (typeof command.logmsg == 'string') { message.msg = command.logmsg; } else { message.nolog = 1; }
4783 parent.parent.DispatchEvent(['*', user._id], obj, message);
4784 }
4785
4786 if (parent.parent.pluginHandler != null) // If the plugin's are not supported, reject this command.
4787 {
4788 command.userid = user._id;
4789 try {
4790 for( var pluginName in parent.parent.pluginHandler.plugins)
4791 if( typeof parent.parent.pluginHandler.plugins[pluginName].uiCustomEvent === 'function' )
4792 parent.parent.pluginHandler.plugins[pluginName].uiCustomEvent(command, obj);
4793 } catch (ex) { console.log('Error loading plugin handler (' + ex + ')'); }
4794 }
4795 break;
4796 }
4797 case 'serverBackup': {
4798 // Do not allow this command when logged in using a login token
4799 if (req.session.loginToken != null) break;
4800
4801 if ((user.siteadmin != SITERIGHT_ADMIN) || (typeof parent.parent.config.settings.autobackup.googledrive != 'object')) return;
4802 if (command.service == 'googleDrive') {
4803 if (command.state == 0) {
4804 parent.db.Remove('GoogleDriveBackup', function () { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 1 })); } catch (ex) { } });
4805 } else if (command.state == 1) {
4806 const {google} = require('googleapis');
4807 obj.oAuth2Client = new google.auth.OAuth2(command.clientid, command.clientsecret, "urn:ietf:wg:oauth:2.0:oob");
4808 obj.oAuth2Client.xxclientid = command.clientid;
4809 obj.oAuth2Client.xxclientsecret = command.clientsecret;
4810 const authUrl = obj.oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/drive.file'] });
4811 try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 2, url: authUrl })); } catch (ex) { }
4812 } else if ((command.state == 2) && (obj.oAuth2Client != null)) {
4813 obj.oAuth2Client.getToken(command.code, function (err, token) {
4814 if (err != null) { console.log('GoogleDrive (getToken) error: ', err); return; }
4815 parent.db.Set({ _id: 'GoogleDriveBackup', state: 3, clientid: obj.oAuth2Client.xxclientid, clientsecret: obj.oAuth2Client.xxclientsecret, token: token });
4816 try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 3 })); } catch (ex) { }
4817 });
4818 }
4819 }
4820 break;
4821 }
4822 case 'twoFactorCookie': {
4823 try {
4824 // Do not allow this command when logged in using a login token
4825 if (req.session.loginToken != null) break;
4826
4827 // Do not allows this command is 2FA cookie duration is set to zero
4828 if (domain.twofactorcookiedurationdays === 0) break;
4829
4830 // Generate a two-factor cookie
4831 var maxCookieAge = domain.twofactorcookiedurationdays;
4832 if ((typeof maxCookieAge != 'number') || (maxCookieAge < 1)) { maxCookieAge = 30; }
4833 const twoFactorCookie = parent.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, parent.parent.loginCookieEncryptionKey);
4834 try { ws.send(JSON.stringify({ action: 'twoFactorCookie', cookie: twoFactorCookie })); } catch (ex) { }
4835 } catch (ex) { console.log(ex); }
4836 break;
4837 }
4838 case 'amtsetupbin': {
4839 if ((command.oldmebxpass != 'admin') && (common.validateString(command.oldmebxpass, 8, 16) == false)) break; // Check password
4840 if (common.validateString(command.newmebxpass, 8, 16) == false) break; // Check password
4841 if ((command.baremetal) && (parent.parent.amtProvisioningServer != null)) {
4842 // Create bare metal setup.bin
4843 var bin = parent.parent.certificateOperations.GetBareMetalSetupBinFile(domain.amtacmactivation, command.oldmebxpass, command.newmebxpass, domain, user);
4844 try { ws.send(JSON.stringify({ action: 'amtsetupbin', file: Buffer.from(bin, 'binary').toString('base64') })); } catch (ex) { }
4845 } else {
4846 // Create standard setup.bin
4847 var bin = parent.parent.certificateOperations.GetSetupBinFile(domain.amtacmactivation, command.oldmebxpass, command.newmebxpass, domain, user);
4848 try { ws.send(JSON.stringify({ action: 'amtsetupbin', file: Buffer.from(bin, 'binary').toString('base64') })); } catch (ex) { }
4849 }
4850 break;
4851 }
4852 case 'meshToolInfo': {
4853 if (typeof command.name != 'string') break;
4854 var info = parent.parent.meshToolsBinaries[command.name];
4855 var responseCmd = { action: 'meshToolInfo', name: command.name, hash: info.hash, size: info.size, url: info.url };
4856 if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
4857 try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
4858 break;
4859 }
4860 case 'pushmessage': {
4861 // Check if this user has rights on this nodeid
4862 if (parent.parent.firebase == null) return;
4863 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
4864 if (common.validateString(command.title, 1, 1024) == false) break; // Check title
4865 if (common.validateString(command.msg, 1, 1024) == false) break; // Check message
4866 db.Get(command.nodeid, function (err, nodes) { // TODO: Make a NodeRights(user) method that also does not do a db call if agent is connected (???)
4867 if ((err == null) && (nodes.length == 1)) {
4868 const node = nodes[0];
4869 if (((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_CHATNOTIFY) != 0) && (typeof node.pmt == 'string')) {
4870 // Send out a push message to the device
4871 var payload = { notification: { title: command.title, body: command.msg } };
4872 var options = { priority: "Normal", timeToLive: 5 * 60 }; // TTL: 5 minutes
4873 parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
4874 if (err == null) {
4875 parent.parent.debug('email', 'Successfully send push message to device ' + node.name + ', title: ' + command.title + ', msg: ' + command.msg);
4876 } else {
4877 parent.parent.debug('email', 'Failed to send push message to device ' + node.name + ', title: ' + command.title + ', msg: ' + command.msg + ', error: ' + errdesc);
4878 }
4879 });
4880 }
4881 }
4882 });
4883 break;
4884 }
4885 case 'pushconsole': {
4886 // Check if this user has rights on this nodeid
4887 if (parent.parent.firebase == null) return;
4888 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
4889 if (common.validateString(command.console, 1, 3000) == false) break; // Check console command
4890 db.Get(command.nodeid, function (err, nodes) { // TODO: Make a NodeRights(user) method that also does not do a db call if agent is connected (???)
4891 if ((err == null) && (nodes.length == 1)) {
4892 const node = nodes[0];
4893 if ((parent.GetNodeRights(user, node.meshid, node._id) == MESHRIGHT_ADMIN) && (typeof node.pmt == 'string')) {
4894 // Send out a push message to the device
4895 var payload = { data: { con: command.console, s: ws.sessionId } };
4896 var options = { priority: "Normal", timeToLive: 60 }; // TTL: 1 minutes, priority 'Normal' or 'High'
4897 parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
4898 if (err != null) {
4899 try { ws.send(JSON.stringify({ action: 'msg', type: 'console', nodeid: node._id, value: 'Failed: ' + errdesc })); } catch (ex) { }
4900 parent.parent.debug('email', 'Failed to send push console message to device ' + node.name + ', command: ' + command.console + ', error: ' + errdesc);
4901 }
4902 });
4903 }
4904 }
4905 });
4906 break;
4907 }
4908 case 'webpush': {
4909 // Check if web push is enabled
4910 if (parent.parent.webpush == null) break;
4911
4912 // Adds a web push session to the user. Start by sanitizing the input.
4913 if ((typeof command.sub != 'object') && (typeof command.sub.keys != 'object') && (typeof command.sub.endpoint != 'string')) break;
4914 if (common.validateString(command.sub.endpoint, 1, 1024) == false) break; // Check endpoint
4915 if (common.validateString(command.sub.keys.auth, 1, 64) == false) break; // Check key auth
4916 if (common.validateString(command.sub.keys.p256dh, 1, 256) == false) break; // Check key dh
4917 var newWebPush = { endpoint: command.sub.endpoint, keys: { auth: command.sub.keys.auth, p256dh: command.sub.keys.p256dh } }
4918
4919 // See if we need to add this session
4920 var changed = false;
4921 if (user.webpush == null) {
4922 changed = true;
4923 user.webpush = [newWebPush];
4924 } else {
4925 var found = false;
4926 for (var i in user.webpush) {
4927 if ((user.webpush[i].endpoint == newWebPush.endpoint) && (user.webpush[i].keys.auth == newWebPush.keys.auth) && (user.webpush[i].keys.p256dh == newWebPush.keys.p256dh)) { found = true; }
4928 }
4929 if (found == true) break;
4930 changed = true;
4931 user.webpush.push(newWebPush);
4932 while (user.webpush.length > 5) { user.webpush.shift(); }
4933 }
4934
4935 // If we added the session, update the user
4936 if (changed == true) {
4937 // Update the database
4938 parent.db.SetUser(user);
4939
4940 // Event the change
4941 var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
4942 if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
4943 var targets = ['*', 'server-users', user._id];
4944 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
4945 parent.parent.DispatchEvent(targets, obj, message);
4946 }
4947
4948 break;
4949 }
4950 case 'previousLogins': {
4951 // TODO: Make a better database call to get filtered data.
4952 if (command.userid == null) {
4953 var splitUser = user._id.split('/');
4954 // Get previous logins for self
4955 if (db.GetUserLoginEvents) {
4956 // New way
4957 db.GetUserLoginEvents(domain.id, user._id, function (err, docs) {
4958 if (err != null) return;
4959 var e = [];
4960 for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName }); }
4961 try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { }
4962 });
4963 } else {
4964 // Old way
4965 db.GetUserEvents([user._id], domain.id, user._id, null, function (err, docs) {
4966 if (err != null) return;
4967 var e = [];
4968 for (var i in docs) {
4969 if ((docs[i].msgArgs) && (docs[i].userid == user._id) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) {
4970 e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName });
4971 }
4972 }
4973 try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { }
4974 });
4975 }
4976 } else {
4977 // Get previous logins for specific userid
4978 if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) {
4979 var splitUser = command.userid.split('/');
4980 if ((obj.crossDomain === true) || (splitUser[1] === domain.id)) {
4981 if (db.GetUserLoginEvents) {
4982 // New way
4983 db.GetUserLoginEvents(splitUser[1], command.userid, function (err, docs) {
4984 if (err != null) return;
4985 var e = [];
4986 for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); }
4987 try { ws.send(JSON.stringify({ action: 'previousLogins', userid: command.userid, events: e })); } catch (ex) { }
4988 });
4989 } else {
4990 // Old way
4991 db.GetUserEvents([command.userid], domain.id, user._id, null, function (err, docs) {
4992 if (err != null) return;
4993 var e = [];
4994 for (var i in docs) { if ((docs[i].msgArgs) && (docs[i].userid == command.userid) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); } }
4995 try { ws.send(JSON.stringify({ action: 'previousLogins', userid: command.userid, events: e })); } catch (ex) { }
4996 });
4997 }
4998 }
4999 }
5000 }
5001 break;
5002 }
5003 case 'oneclickrecovery': { // Intel(R) AMT One Click Recovery (OCR)
5004 if (common.validateStrArray(command.nodeids, 1) == false) break; // Check nodeids
5005 if (common.validateString(command.path, 1, 2048) == false) break; // Check file path
5006 if (command.type != 'diskimage') break; // Make sure type is correct
5007
5008 var file = parent.getServerFilePath(user, domain, command.path);
5009 if (file == null) return;
5010
5011 // For each nodeid, change the group
5012 for (var i = 0; i < command.nodeids.length; i++) {
5013 var xnodeid = command.nodeids[i];
5014 if (xnodeid.indexOf('/') == -1) { xnodeid = 'node/' + domain.id + '/' + xnodeid; }
5015
5016 // Get the node and the rights for this node
5017 parent.GetNodeWithRights(domain, user, xnodeid, function (node, rights, visible) {
5018 // Check if we found this device and if we have full rights
5019 if ((node == null) || (rights != 0xFFFFFFFF)) return;
5020
5021 // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
5022 parent.parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.fullpath });
5023 });
5024 }
5025 break;
5026 }
5027 case 'loginTokens': { // Respond with the list of currently valid login tokens
5028 if (req.session.loginToken != null) break; // Do not allow this command when logged in using a login token
5029 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.logintokens == false)) break; // Login tokens are not supported on this server
5030
5031 // If remove is an array or strings, we are going to be removing these and returning the results.
5032 if (common.validateStrArray(command.remove, 1) == false) { delete command.remove; }
5033
5034 parent.db.GetAllTypeNodeFiltered(['logintoken-' + user._id], domain.id, 'logintoken', null, function (err, docs) {
5035 if (err != null) return;
5036 var now = Date.now(), removed = [], okDocs = [];
5037 for (var i = 0; i < docs.length; i++) {
5038 const doc = docs[i];
5039 if (((doc.expire != 0) && (doc.expire < now)) || (doc.tokenUser == null) || ((command.remove != null) && (command.remove.indexOf(doc.tokenUser) >= 0))) {
5040 // This share is expired.
5041 parent.db.Remove(doc._id, function () { }); removed.push(doc.tokenUser);
5042 } else {
5043 // This share is ok, remove extra data we don't need to send.
5044 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.userid; delete doc.salt; delete doc.hash;
5045 okDocs.push(doc);
5046 }
5047 }
5048 try { ws.send(JSON.stringify({ action: 'loginTokens', loginTokens: okDocs })); } catch (ex) { }
5049
5050 // If any login tokens where removed, event the change.
5051 if (removed.length > 0) {
5052 // Dispatch the new event
5053 var targets = ['*', 'server-users', user._id];
5054 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
5055 var event = { etype: 'user', userid: user._id, username: user.name, action: 'loginTokenChanged', domain: domain.id, loginTokens: okDocs, removed: removed, nolog: 1 };
5056 parent.parent.DispatchEvent(targets, obj, event);
5057 }
5058 });
5059 break;
5060 }
5061 case 'createLoginToken': { // Create a new login token
5062 var err = null;
5063
5064 if (req.session.loginToken != null) { err = "Access denied"; } // Do not allow this command when logged in using a login token
5065 else if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.logintokens === false)) { err = "Not supported"; } // Login tokens are not supported on this server
5066 else if ((typeof domain.passwordrequirements == 'object') && Array.isArray(domain.passwordrequirements.logintokens) && ((domain.passwordrequirements.logintokens.indexOf(user._id) < 0) && (user.links && Object.keys(user.links).some(key => domain.passwordrequirements.logintokens.indexOf(key) < 0)))) { err = "Not supported"; } // Login tokens are not supported by this user
5067 else if (common.validateString(command.name, 1, 100) == false) { err = "Invalid name"; } // Check name
5068 else if ((typeof command.expire != 'number') || (command.expire < 0)) { err = "Invalid expire value"; } // Check expire
5069
5070 // Handle any errors
5071 if (err != null) {
5072 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createLoginToken', responseid: command.responseid, result: err })); } catch (ex) { } }
5073 break;
5074 }
5075
5076 // Generate a token username. Don't have any + or / in the username or password
5077 var tokenUser = '~t:' + Buffer.from(parent.parent.crypto.randomBytes(12), 'binary').toString('base64');
5078 while ((tokenUser.indexOf('+') >= 0) || (tokenUser.indexOf('/') >= 0)) { tokenUser = '~t:' + Buffer.from(parent.parent.crypto.randomBytes(12), 'binary').toString('base64'); };
5079 var tokenPass = Buffer.from(parent.parent.crypto.randomBytes(15), 'binary').toString('base64');
5080 while ((tokenPass.indexOf('+') >= 0) || (tokenPass.indexOf('/') >= 0)) { tokenPass = Buffer.from(parent.parent.crypto.randomBytes(15), 'binary').toString('base64'); };
5081
5082 // Create a user, generate a salt and hash the password
5083 require('./pass').hash(tokenPass, function (err, salt, hash, tag) {
5084 if (err) throw err;
5085
5086 // Compute expire time
5087 const created = Date.now();
5088 var expire = 0;
5089 if (command.expire > 0) { expire = created + (command.expire * 60000); }
5090
5091 // Generate the token password
5092 const dbentry = { _id: 'logintoken-' + tokenUser, type: 'logintoken', nodeid: 'logintoken-' + user._id, userid: user._id, name: command.name, tokenUser: tokenUser, salt: salt, hash: hash, domain: domain.id, created: created, expire: expire };
5093 parent.db.Set(dbentry);
5094
5095 // Send the token information back
5096 try { ws.send(JSON.stringify({ action: 'createLoginToken', name: command.name, tokenUser: tokenUser, tokenPass: tokenPass, created: created, expire: expire })); } catch (ex) { }
5097
5098 // Dispatch the new event
5099 var targets = ['*', 'server-users', user._id];
5100 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
5101 var event = { etype: 'user', userid: user._id, username: user.name, action: 'loginTokenAdded', msgid: 115, msg: "Added login token", domain: domain.id, newToken: { name: command.name, tokenUser: tokenUser, created: created, expire: expire } };
5102 parent.parent.DispatchEvent(targets, obj, event);
5103 });
5104 break;
5105 }
5106 case 'getDeviceDetails': {
5107 if ((common.validateStrArray(command.nodeids, 1) == false) && (command.nodeids != null)) break; // Check nodeids
5108 if (common.validateString(command.type, 3, 4) == false) break; // Check type
5109
5110 const links = parent.GetAllMeshIdWithRights(user);
5111 const extraids = getUserExtraIds();
5112 db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, obj.deviceSkip, obj.deviceLimit, function (err, docs) {
5113 if (docs == null) return;
5114 const ids = [];
5115 if (command.nodeids != null) {
5116 // Create a list of node ids and query them for last device connection time
5117 for (var i in command.nodeids) { ids.push('lc' + command.nodeids[i]); }
5118 } else {
5119 // Create a list of node ids for this user and query them for last device connection time
5120 for (var i in docs) { ids.push('lc' + docs[i]._id); }
5121 }
5122 db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) {
5123 const lastConnects = {};
5124 if (docs != null) { for (var i in docs) { lastConnects[docs[i]._id] = docs[i]; } }
5125
5126 getDeviceDetailedInfo(command.nodeids, command.type, function (results, type) {
5127 for (var i = 0; i < results.length; i++) {
5128 // Remove any device system and network information is we do not have details rights to this device
5129 if ((parent.GetNodeRights(user, results[i].node.meshid, results[i].node._id) & MESHRIGHT_DEVICEDETAILS) == 0) {
5130 delete results[i].sys; delete results[i].net;
5131 }
5132
5133 // Merge any last connection information
5134 const lc = lastConnects['lc' + results[i].node._id];
5135 if (lc != null) { delete lc._id; delete lc.type; delete lc.meshid; delete lc.domain; results[i].lastConnect = lc; }
5136
5137 // Remove any connectivity and power state information, that should not be in the database anyway.
5138 // TODO: Find why these are sometimes saved in the db.
5139 if (results[i].node.conn != null) { delete results[i].node.conn; }
5140 if (results[i].node.pwr != null) { delete results[i].node.pwr; }
5141 if (results[i].node.agct != null) { delete results[i].node.agct; }
5142 if (results[i].node.cict != null) { delete results[i].node.cict; }
5143
5144 // Add the connection state
5145 var state = parent.parent.GetConnectivityState(results[i].node._id);
5146 if (state) {
5147 results[i].node.conn = state.connectivity;
5148 results[i].node.pwr = state.powerState;
5149 if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[results[i].node._id]; if (agent != null) { results[i].node.agct = agent.connectTime; } }
5150
5151 // Use the connection time of the CIRA/Relay connection
5152 if ((state.connectivity & 2) != 0) {
5153 var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(results[i].node._id, null, true);
5154 if ((ciraConnection != null) && (ciraConnection.tag != null)) { results[i].node.cict = ciraConnection.tag.connectTime; }
5155 }
5156 }
5157
5158 }
5159
5160 var output = null;
5161 if (type == 'csv') {
5162 try {
5163 // Create the CSV file
5164 output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,bitlocker,avdetails,tags,lastbootuptime,cpu,osbuild,biosDate,biosVendor,biosVersion,biosSerial,biosMode,boardName,boardVendor,boardVersion,boardSerial,chassisSerial,chassisAssetTag,chassisManufacturer,productUuid,tpmversion,tpmmanufacturer,tpmmanufacturerversion,tpmisactivated,tpmisenabled,tpmisowned,totalMemory,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n';
5165 for (var i = 0; i < results.length; i++) {
5166 const nodeinfo = results[i];
5167
5168 // Node information
5169 if (nodeinfo.node != null) {
5170 const n = nodeinfo.node;
5171 output += csvClean(n._id) + ',' + csvClean(n.name) + ',' + csvClean(n.rname ? n.rname : '') + ',' + csvClean(n.host ? n.host : '') + ',' + (n.icon ? n.icon : 1) + ',' + (n.ip ? n.ip : '') + ',' + (n.osdesc ? csvClean(n.osdesc) : '') + ',' + csvClean(parent.meshes[n.meshid].name);
5172 if (typeof n.wsc == 'object') {
5173 output += ',' + csvClean(n.wsc.antiVirus ? n.wsc.antiVirus : '') + ',' + csvClean(n.wsc.autoUpdate ? n.wsc.autoUpdate : '') + ',' + csvClean(n.wsc.firewall ? n.wsc.firewall : '')
5174 } else { output += ',,,'; }
5175 if (typeof n.volumes == 'object') {
5176 var bitlockerdetails = '', firstbitlocker = true;
5177 for (var a in n.volumes) { if (typeof n.volumes[a].protectionStatus !== 'undefined') { if (firstbitlocker) { firstbitlocker = false; } else { bitlockerdetails += '|'; } bitlockerdetails += a + '/' + n.volumes[a].volumeStatus; } }
5178 output += ',' + csvClean(bitlockerdetails);
5179 } else {
5180 output += ',';
5181 }
5182 if (typeof n.av == 'object') {
5183 var avdetails = '', firstav = true;
5184 for (var a in n.av) { if (typeof n.av[a].product == 'string') { if (firstav) { firstav = false; } else { avdetails += '|'; } avdetails += (n.av[a].product + '/' + ((n.av[a].enabled) ? 'enabled' : 'disabled') + '/' + ((n.av[a].updated) ? 'updated' : 'notupdated')); } }
5185 output += ',' + csvClean(avdetails);
5186 } else {
5187 output += ',';
5188 }
5189 if (typeof n.tags == 'object') {
5190 var tagsdetails = '', firsttags = true;
5191 for (var a in n.tags) { if (firsttags) { firsttags = false; } else { tagsdetails += '|'; } tagsdetails += n.tags[a]; }
5192 output += ',' + csvClean(tagsdetails);
5193 } else {
5194 output += ',';
5195 }
5196 if (typeof n.lastbootuptime == 'number') { output += ',' + n.lastbootuptime; } else { output += ','; }
5197 } else {
5198 output += ',,,,,,,,,,,,,,,,,,,,';
5199 }
5200
5201 // System infomation
5202 if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.windows)) {
5203 // Windows
5204 output += ',';
5205 if (nodeinfo.sys.hardware.windows.cpu && (nodeinfo.sys.hardware.windows.cpu.length > 0) && (typeof nodeinfo.sys.hardware.windows.cpu[0].Name == 'string')) { output += csvClean(nodeinfo.sys.hardware.windows.cpu[0].Name); }
5206 output += ',';
5207 if (nodeinfo.sys.hardware.windows.osinfo && (nodeinfo.sys.hardware.windows.osinfo.BuildNumber)) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.BuildNumber); }
5208 output += ',';
5209 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_date)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_date); }
5210 output += ',';
5211 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_vendor); }
5212 output += ',';
5213 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_version); }
5214 output += ',';
5215 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_serial)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_serial); }
5216 output += ',';
5217 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_mode)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_mode); }
5218 output += ',';
5219 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_name); }
5220 output += ',';
5221 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_vendor); }
5222 output += ',';
5223 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_version); }
5224 output += ',';
5225 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_serial)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_serial); }
5226 output += ',';
5227 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.chassis_serial)) { output += csvClean(nodeinfo.sys.hardware.identifiers.chassis_serial); }
5228 output += ',';
5229 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.chassis_assettag)) { output += csvClean(nodeinfo.sys.hardware.identifiers.chassis_assettag); }
5230 output += ',';
5231 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.chassis_manufacturer)) { output += csvClean(nodeinfo.sys.hardware.identifiers.chassis_manufacturer); }
5232 output += ',';
5233 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.identifiers.product_uuid); }
5234 output += ',';
5235 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.SpecVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.SpecVersion); }
5236 output += ',';
5237 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerId) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerId); }
5238 output += ',';
5239 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerVersion); }
5240 output += ',';
5241 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsActivated) { output += csvClean(nodeinfo.sys.hardware.tpm.IsActivated ? 'true' : 'false'); }
5242 output += ',';
5243 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsEnabled) { output += csvClean(nodeinfo.sys.hardware.tpm.IsEnabled ? 'true' : 'false'); }
5244 output += ',';
5245 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsOwned) { output += csvClean(nodeinfo.sys.hardware.tpm.IsOwned ? 'true' : 'false'); }
5246 output += ',';
5247 if (nodeinfo.sys.hardware.windows.memory) {
5248 var totalMemory = 0;
5249 for (var j in nodeinfo.sys.hardware.windows.memory) {
5250 if (nodeinfo.sys.hardware.windows.memory[j].Capacity) {
5251 if (typeof nodeinfo.sys.hardware.windows.memory[j].Capacity == 'number') { totalMemory += nodeinfo.sys.hardware.windows.memory[j].Capacity; }
5252 if (typeof nodeinfo.sys.hardware.windows.memory[j].Capacity == 'string') { totalMemory += parseInt(nodeinfo.sys.hardware.windows.memory[j].Capacity); }
5253 }
5254 }
5255 output += csvClean('' + totalMemory);
5256 }
5257 } else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.mobile)) {
5258 // Mobile
5259 output += ',';
5260 output += ',';
5261 output += ',';
5262 output += ',';
5263 output += ',';
5264 if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.bootloader)) { output += csvClean(nodeinfo.sys.hardware.mobile.bootloader); }
5265 output += ',';
5266 output += ',';
5267 output += ',';
5268 if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.model)) { output += csvClean(nodeinfo.sys.hardware.mobile.model); }
5269 output += ',';
5270 if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.brand)) { output += csvClean(nodeinfo.sys.hardware.mobile.brand); }
5271 output += ',';
5272 output += ',';
5273 output += ',';
5274 output += ',';
5275 output += ',';
5276 output += ',';
5277 if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.id)) { output += csvClean(nodeinfo.sys.hardware.mobile.id); }
5278 output += ',';
5279 output += ',';
5280 output += ',';
5281 output += ',';
5282 output += ',';
5283 output += ',';
5284 output += ',';
5285 } else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.linux)) {
5286 // Linux
5287 output += ',';
5288 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.cpu_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.cpu_name); }
5289 output += ',,';
5290 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_date)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_date); }
5291 output += ',';
5292 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_vendor); }
5293 output += ',';
5294 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_version)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_version); }
5295 output += ',';
5296 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.product_serial)) { output += csvClean(nodeinfo.sys.hardware.linux.product_serial); }
5297 else if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_serial)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_serial); }
5298 output += ',';
5299 if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_mode)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_mode); }
5300 output += ',';
5301 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_name)) { output += csvClean(nodeinfo.sys.hardware.linux.board_name); }
5302 output += ',';
5303 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.board_vendor); }
5304 output += ',';
5305 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_version)) { output += csvClean(nodeinfo.sys.hardware.linux.board_version); }
5306 output += ',';
5307 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_serial)) { output += csvClean(nodeinfo.sys.hardware.linux.board_serial); }
5308 output += ',';
5309 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.chassis_serial)) { output += csvClean(nodeinfo.sys.hardware.linux.chassis_serial); }
5310 output += ',';
5311 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.chassis_assettag)) { output += csvClean(nodeinfo.sys.hardware.linux.chassis_assettag); }
5312 output += ',';
5313 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.chassis_manufacturer)) { output += csvClean(nodeinfo.sys.hardware.linux.chassis_manufacturer); }
5314 output += ',';
5315 if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.linux.product_uuid); }
5316 output += ',';
5317 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.SpecVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.SpecVersion); }
5318 output += ',';
5319 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerId) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerId); }
5320 output += ',';
5321 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.ManufacturerVersion) { output += csvClean(nodeinfo.sys.hardware.tpm.ManufacturerVersion); }
5322 output += ',';
5323 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsActivated) { output += csvClean(nodeinfo.sys.hardware.tpm.IsActivated ? 'true' : 'false'); }
5324 output += ',';
5325 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsEnabled) { output += csvClean(nodeinfo.sys.hardware.tpm.IsEnabled ? 'true' : 'false'); }
5326 output += ',';
5327 if (nodeinfo.sys.hardware.tpm && nodeinfo.sys.hardware.tpm.IsOwned) { output += csvClean(nodeinfo.sys.hardware.tpm.IsOwned ? 'true' : 'false'); }
5328 output += ',';
5329 if (nodeinfo.sys.hardware.linux.memory) {
5330 if (nodeinfo.sys.hardware.linux.memory.Memory_Device) {
5331 var totalMemory = 0;
5332 for (var j in nodeinfo.sys.hardware.linux.memory.Memory_Device) {
5333 if (nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size) {
5334 if (typeof nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size == 'number') { totalMemory += nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size; }
5335 if (typeof nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size == 'string') { totalMemory += parseInt(nodeinfo.sys.hardware.linux.memory.Memory_Device[j].Size); }
5336 }
5337 }
5338 output += csvClean('' + (totalMemory * Math.pow(1024, 3)));
5339 }
5340 }
5341 } else {
5342 output += ',,,,,,,,,,,,,,,,,,,,,,';
5343 }
5344
5345 // Agent information
5346 if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.agentvers)) {
5347 output += ',';
5348 if (nodeinfo.sys.hardware.agentvers.openssl) { output += csvClean(nodeinfo.sys.hardware.agentvers.openssl); }
5349 output += ',';
5350 if (nodeinfo.sys.hardware.agentvers.commitDate) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitDate); }
5351 output += ',';
5352 if (nodeinfo.sys.hardware.agentvers.commitHash) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitHash); }
5353 output += ',';
5354 if (nodeinfo.sys.hardware.agentvers.compileTime) { output += csvClean(nodeinfo.sys.hardware.agentvers.compileTime); }
5355 } else {
5356 output += ',,,,';
5357 }
5358
5359 // Network interfaces
5360 if ((nodeinfo.net) && (nodeinfo.net.netif2)) {
5361 output += ',';
5362 output += Object.keys(nodeinfo.net.netif2).length; // Interface count
5363 var macs = [], addresses = [];
5364 for (var j in nodeinfo.net.netif2) {
5365 if (Array.isArray(nodeinfo.net.netif2[j])) {
5366 for (var k = 0; k < nodeinfo.net.netif2[j].length; k++) {
5367 if (typeof nodeinfo.net.netif2[j][k].mac == 'string') { macs.push(nodeinfo.net.netif2[j][k].mac); }
5368 if (typeof nodeinfo.net.netif2[j][k].address == 'string') { addresses.push(nodeinfo.net.netif2[j][k].address); }
5369 }
5370 }
5371 }
5372 output += ',';
5373 output += csvClean(macs.join(' ')); // MACS
5374 output += ',';
5375 output += csvClean(addresses.join(' ')); // Addresses
5376 } else {
5377 output += ',,,';
5378 }
5379
5380 // Last connection information
5381 if (nodeinfo.lastConnect) {
5382 output += ',';
5383 if (nodeinfo.lastConnect.time) {
5384 // Last connection time
5385 if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
5386 output += csvClean(new Date(nodeinfo.lastConnect.time).toLocaleString(command.l, { timeZone: command.tz }))
5387 } else {
5388 output += nodeinfo.lastConnect.time;
5389 }
5390 }
5391 output += ',';
5392 if (typeof nodeinfo.lastConnect.addr == 'string') { output += csvClean(nodeinfo.lastConnect.addr); } // Last connection address and port
5393 } else {
5394 output += ',,';
5395 }
5396
5397 output += '\r\n';
5398 }
5399 } catch (ex) { console.log(ex); }
5400 } else {
5401 // Create the JSON file
5402
5403 // Add the device group name to each device
5404 for (var i = 0; i < results.length; i++) {
5405 const nodeinfo = results[i];
5406 if (nodeinfo.node) {
5407 const mesh = parent.meshes[nodeinfo.node.meshid];
5408 if (mesh) { results[i].node.groupname = mesh.name; }
5409 }
5410 }
5411
5412 output = JSON.stringify(results);
5413 }
5414 try { ws.send(JSON.stringify({ action: 'getDeviceDetails', data: output, type: type })); } catch (ex) { }
5415 });
5416 });
5417 });
5418 break;
5419 }
5420 case 'endDesktopMultiplex': {
5421 var err = null, xuser = null;
5422 try {
5423 if (command.xuserid.indexOf('/') < 0) { command.xuserid = 'user/' + domain.id + '/' + command.xuserid; }
5424 if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid device identifier'; } // Check the meshid
5425 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
5426 const xusersplit = command.xuserid.split('/');
5427 xuser = parent.users[command.xuserid];
5428 if (xuser == null) { err = 'User does not exists'; }
5429 else if ((obj.crossDomain !== true) && ((xusersplit.length != 3) || (xusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
5430 } catch (ex) { err = 'Validation exception: ' + ex; }
5431
5432 // Handle any errors
5433 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeusernotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
5434
5435 // Get the node and the rights for this node
5436 parent.GetNodeWithRights(domain, xuser, command.nodeid, function (node, rights, visible) {
5437 if ((rights != 0xFFFFFFFF) && (xuser._id != command.xuserid)) return;
5438 const desktopRelay = parent.desktoprelays[command.nodeid];
5439 if ((desktopRelay == null) || (desktopRelay === 1)) return; // If the desktopRelay is equal to 1, the relay is being constructed.
5440 var viewersToClose = []; // Create a list of viewers to close. We don't want to close directly because it will change "desktopRelay.viewers" and we will not enumerate correctly.
5441 for (var i = 0; i < desktopRelay.viewers.length; i++) {
5442 const viewer = desktopRelay.viewers[i];
5443 if ((viewer.user._id == command.xuserid) && (viewer.guestName == command.guestname)) { viewersToClose.push(viewer); } // Only close viewers that match the userid and guestname if present.
5444 }
5445 for (var i = 0; i < viewersToClose.length; i++) { viewersToClose[i].close(); } // Close any viewers we need closed.
5446
5447 // Log the desktop session disconnection
5448 var targets = ['*', user._id, command.xuserid];
5449 const splitxuser = command.xuserid.split('/');
5450 var xusername = splitxuser[2];
5451 if (command.guestname != null) { xusername += '/' + command.guestname; }
5452 const event = { etype: 'user', userid: user._id, username: user.name, nodeid: command.nodeid, xuserid: command.xuserid, action: 'endsession', msgid: 134, msgArgs: [xusername], msg: 'Forcibly disconnected desktop session of user ' + xusername, domain: domain.id };
5453 if (command.guestname != null) { event.guestname = command.guestname; }
5454 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
5455 parent.parent.DispatchEvent(targets, obj, event);
5456 });
5457
5458 break;
5459 }
5460 case 'satellite': {
5461 // Command indicates this is a MeshCentral Satellite session and what featues it supports
5462 if ((command.setFlags != null) && (typeof command.setFlags == 'number')) { obj.ws.satelliteFlags = command.setFlags; }
5463 if ((command.reqid != null) && (typeof command.satelliteFlags == 'number')) {
5464 const event = { action: 'satelliteResponse', subaction: command.subaction, reqid: command.reqid, response: command.response, satelliteFlags: command.satelliteFlags, nolog: 1 }
5465 if (typeof command.nodeid == 'string') { event.nodeid = command.nodeid; }
5466 parent.parent.DispatchEvent(['*'], obj, event);
5467 }
5468 break;
5469 }
5470 case 'importamtdevices': {
5471 if ((command.amtdevices == null) || (command.meshid == null) || (typeof command.meshid != 'string') || (command.meshid.startsWith('mesh/' + domain.id + '/') == false)) return;
5472 const mesh = parent.meshes[command.meshid];
5473 if ((mesh == null) || (mesh.mtype != 1) || (parent.GetMeshRights(user, command.meshid) & MESHRIGHT_EDITMESH) == 0) return null; // This user must have mesh rights to edit the device group
5474 var amtDevices = [];
5475
5476 // Decode a JSON file from the Intel SCS migration tool
5477 if ((typeof command.amtdevices == 'object') && (typeof command.amtdevices.ApplicationData == 'object') && (command.amtdevices.ApplicationData.Application == 'Intel vPro(R) Manageability Migration Tool') && (typeof command.amtdevices['ManagedSystems'] == 'object') && (Array.isArray(command.amtdevices['ManagedSystems']['ManagedSystemsList']))) {
5478 for (var i in command.amtdevices['ManagedSystems']['ManagedSystemsList']) {
5479 const importDev = command.amtdevices['ManagedSystems']['ManagedSystemsList'][i];
5480 var host = null;
5481 if ((typeof importDev.Fqdn == 'string') && (importDev.Fqdn != '')) { host = importDev.Fqdn; }
5482 if ((host == null) && (typeof importDev.IPv4 == 'string') && (importDev.IPv4 != '')) { host = importDev.IPv4; }
5483 if (host != null) {
5484 // Create a new Intel AMT device
5485 const nodeid = 'node/' + domain.id + '/' + parent.crypto.randomBytes(48).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
5486 const device = { type: 'node', _id: nodeid, meshid: mesh._id, mtype: 1, icon: 1, name: host, host: host, domain: domain.id, intelamt: { user: 'admin', state: 2 } };
5487
5488 // Add optional fields
5489 if (typeof importDev.AmtVersion == 'string') { device.intelamt.ver = importDev.AmtVersion; }
5490 if (typeof importDev.ConfiguredPassword == 'string') { device.intelamt.pass = importDev.ConfiguredPassword; }
5491 if (typeof importDev.Uuid == 'string') { device.intelamt.uuid = importDev.Uuid; }
5492 if (importDev.ConnectionType == 'TLS') { device.intelamt.tls = 1; }
5493
5494 // Check if we are already adding a device with the same hostname, if so, skip it.
5495 var skip = false;
5496 for (var i in amtDevices) { if (amtDevices[i].host.toLowerCase() == device.host.toLowerCase()) { skip = true; } }
5497 if (skip == false) { amtDevices.push(device); }
5498 }
5499 }
5500 }
5501
5502 // Decode a JSON file from MeshCommander
5503 if ((typeof command.amtdevices == 'object') && (typeof command.amtdevices.webappversion == 'string') && (Array.isArray(command.amtdevices.computers))) {
5504 for (var i in command.amtdevices.computers) {
5505 const importDev = command.amtdevices.computers[i];
5506 if ((typeof importDev.host == 'string') && (importDev.host != '') && (importDev.host != '127.0.0.1') && (importDev.host.toLowerCase() != 'localhost')) {
5507 // Create a new Intel AMT device
5508 const nodeid = 'node/' + domain.id + '/' + parent.crypto.randomBytes(48).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
5509 const device = { type: 'node', _id: nodeid, meshid: mesh._id, mtype: 1, icon: 1, host: importDev.host, domain: domain.id, intelamt: { user: 'admin', state: 2 } };
5510 if (typeof importDev.name == 'string') { device.name = importDev.name; } else { device.name = importDev.host; }
5511
5512 // Add optional fields
5513 if (typeof importDev.user == 'string') { device.intelamt.user = importDev.user; }
5514 if (typeof importDev.pass == 'string') { device.intelamt.pass = importDev.pass; }
5515 if ((importDev.tls === true) || (importDev.tls === 1)) { device.intelamt.tls = 1; }
5516 if (typeof importDev.digestrealm == 'string') { device.intelamt.realm = importDev.digestrealm; }
5517 if (typeof importDev.ver == 'string') { device.intelamt.ver = importDev.ver; }
5518 if (typeof importDev.uuid == 'string') { device.intelamt.uuid = importDev.uuid; }
5519 if (typeof importDev.pstate == 'number') { device.intelamt.state = importDev.pstate; }
5520 if (typeof importDev.tlscerthash == 'string') { device.intelamt.hash = importDev.tlscerthash; }
5521 if (typeof importDev.icon == 'number') { device.icon = importDev.icon; }
5522 if (typeof importDev.desc == 'string') { device.desc = importDev.desc; }
5523
5524 // Check if we are already adding a device with the same hostname, if so, skip it.
5525 var skip = false;
5526 for (var i in amtDevices) { if (amtDevices[i].host.toLowerCase() == device.host.toLowerCase()) { skip = true; } }
5527 if (skip == false) { amtDevices.push(device); }
5528 }
5529 }
5530 }
5531
5532 // Decode a JSON file in simple format
5533 if (Array.isArray(command.amtdevices)) {
5534 for (var i in command.amtdevices) {
5535 const importDev = command.amtdevices[i];
5536 if ((typeof importDev.fqdn == 'string') && (importDev.fqdn != '') && (importDev.fqdn != '127.0.0.1') && (importDev.fqdn.toLowerCase() != 'localhost')) {
5537 // Create a new Intel AMT device
5538 const nodeid = 'node/' + domain.id + '/' + parent.crypto.randomBytes(48).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
5539 const device = { type: 'node', _id: nodeid, meshid: mesh._id, mtype: 1, icon: 1, host: importDev.fqdn, domain: domain.id, intelamt: { user: 'admin', state: 2 } };
5540 if (typeof importDev.name == 'string') { device.name = importDev.name; } else { device.name = importDev.fqdn; }
5541
5542 // Add optional fields
5543 if (typeof importDev.username == 'string') { device.intelamt.user = importDev.username; }
5544 if (typeof importDev.password == 'string') { device.intelamt.pass = importDev.password; }
5545 if ((importDev.tls === true) || (importDev.tls === 1)) { device.intelamt.tls = 1; }
5546 if (typeof importDev.version == 'string') { device.intelamt.ver = importDev.version; }
5547 if (typeof importDev.digestrealm == 'string') { device.intelamt.realm = importDev.digestrealm; }
5548 if (typeof importDev.uuid == 'string') { device.intelamt.uuid = importDev.uuid; }
5549 if (typeof importDev.pstate == 'number') { device.intelamt.state = importDev.pstate; }
5550 if (typeof importDev.tlscerthash == 'string') { device.intelamt.hash = importDev.tlscerthash; }
5551 if (typeof importDev.icon == 'number') { device.icon = importDev.icon; }
5552 if (typeof importDev.desc == 'string') { device.desc = importDev.desc; }
5553
5554 // Check if we are already adding a device with the same hostname, if so, skip it.
5555 var skip = false;
5556 for (var i in amtDevices) { if (amtDevices[i].host.toLowerCase() == device.host.toLowerCase()) { skip = true; } }
5557 if (skip == false) { amtDevices.push(device); }
5558 }
5559 }
5560 }
5561
5562 // Add all the correctly parsed devices to the database and event them
5563 // TODO: We may want to remove any devices with duplicate hostnames
5564 if (amtDevices.length == 0) return;
5565 for (var i in amtDevices) {
5566 // Save the device to the database
5567 db.Set(amtDevices[i]);
5568 // Event the new node
5569 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [nodeid]), obj, { etype: 'node', userid: user._id, username: user.name, action: 'addnode', node: parent.CloneSafeNode(amtDevices[i]), msgid: 84, msgArgs: [amtDevices[i].name, mesh.name], msg: 'Added device ' + amtDevices[i].name + ' to device group ' + mesh.name, domain: domain.id });
5570 }
5571 break;
5572 }
5573 default: {
5574 // Unknown user action
5575 console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
5576 break;
5577 }
5578 }
5579 }
5580
5581 const serverCommands = {
5582 'adddeviceuser': serverCommandAddDeviceUser,
5583 'addmeshuser': serverCommandAddMeshUser,
5584 'adduser': serverCommandAddUser,
5585 'adduserbatch': serverCommandAddUserBatch,
5586 'addusertousergroup': serverCommandAddUserToUserGroup,
5587 'agentdisconnect': serverCommandAgentDisconnect,
5588 'authcookie': serverCommandAuthCookie,
5589 'changeemail': serverCommandChangeEmail,
5590 'changelang': serverCommandChangeLang,
5591 'close': serverCommandClose,
5592 'confirmPhone': serverCommandConfirmPhone,
5593 'confirmMessaging': serverCommandConfirmMessaging,
5594 'emailuser': serverCommandEmailUser,
5595 'files': serverCommandFiles,
5596 'getClip': serverCommandGetClip,
5597 'getcookie': serverCommandGetCookie,
5598 'getnetworkinfo': serverCommandGetNetworkInfo,
5599 'getsysinfo': serverCommandGetSysInfo,
5600 'intersession': serverCommandInterSession,
5601 'interuser': serverCommandInterUser,
5602 'lastconnect': serverCommandLastConnect,
5603 'lastconnects': serverCommandLastConnects,
5604 'logincookie': serverCommandLoginCookie,
5605 'meshes': serverCommandMeshes,
5606 'ping': serverCommandPing,
5607 'pong': serverCommandPong,
5608 'powertimeline': serverCommandPowerTimeline,
5609 'print': serverCommandPrint,
5610 'removePhone': serverCommandRemovePhone,
5611 'removeMessaging': serverCommandRemoveMessaging,
5612 'removeuserfromusergroup': serverCommandRemoveUserFromUserGroup,
5613 'report': serverCommandReport,
5614 'serverclearerrorlog': serverCommandServerClearErrorLog,
5615 'serverconsole': serverCommandServerConsole,
5616 'servererrors': serverCommandServerErrors,
5617 'serverconfig': serverCommandServerConfig,
5618 'serverstats': serverCommandServerStats,
5619 'servertimelinestats': serverCommandServerTimelineStats,
5620 'serverupdate': serverCommandServerUpdate,
5621 'serverversion': serverCommandServerVersion,
5622 'setClip': serverCommandSetClip,
5623 'smsuser': serverCommandSmsUser,
5624 'msguser': serverCommandMsgUser,
5625 'trafficdelta': serverCommandTrafficDelta,
5626 'trafficstats': serverCommandTrafficStats,
5627 'updateAgents': serverCommandUpdateAgents,
5628 'updateUserImage': serverCommandUpdateUserImage,
5629 'urlargs': serverCommandUrlArgs,
5630 'users': serverCommandUsers,
5631 'verifyemail': serverCommandVerifyEmail,
5632 'verifyPhone': serverCommandVerifyPhone,
5633 'verifyMessaging': serverCommandVerifyMessaging
5634 };
5635
5636 const serverUserCommands = {
5637 '2falock': [serverUserCommand2faLock, "Shows and changes the 2FA lock state"],
5638 'acceleratorsstats': [serverUserCommandAcceleratorsStats, "Show data on work being offloaded to other CPU's"],
5639 'agentissues': [serverUserCommandAgentIssues, ""],
5640 'agentstats': [serverUserCommandAgentStats, ""],
5641 'amtacm': [serverUserCommandAmtAcm, ""],
5642 'amtmanager': [serverUserCommandAmtManager, ""],
5643 'amtpasswords': [serverUserCommandAmtPasswords, ""],
5644 'amtstats': [serverUserCommandAmtStats, ""],
5645 'args': [serverUserCommandArgs, ""],
5646 'autobackup': [serverUserCommandAutoBackup, ""],
5647 'backupconfig': [serverUserCommandBackupConfig, ""],
5648 'badlogins': [serverUserCommandBadLogins, "Displays or resets the invalid login rate limiting table."],
5649 'bad2fa': [serverUserCommandBad2fa, "Displays or resets the invalid 2FA rate limiting table."],
5650 'certexpire': [serverUserCommandCertExpire, ""],
5651 'certhashes': [serverUserCommandCertHashes, ""],
5652 'closeusersessions': [serverUserCommandCloseUserSessions, "Disconnects all sessions for a specified user."],
5653 'cores': [serverUserCommandCores, ""],
5654 'dbcounters': [serverUserCommandDbCounters, ""],
5655 'dbstats': [serverUserCommandDbStats, ""],
5656 'dispatchtable': [serverUserCommandDispatchTable, ""],
5657 'dropallcira': [serverUserCommandDropAllCira, ""],
5658 'dupagents': [serverUserCommandDupAgents, ""],
5659 'email': [serverUserCommandEmail, ""],
5660 'emailnotifications': [serverUserCommandEmailNotifications, ""],
5661 'msgnotifications': [serverUserCommandMessageNotifications, ""],
5662 'firebase': [serverUserCommandFirebase, ""],
5663 'heapdump': [serverUserCommandHeapDump, ""],
5664 'heapdump2': [serverUserCommandHeapDump2, ""],
5665 'help': [serverUserCommandHelp, ""],
5666 'info': [serverUserCommandInfo, "Returns the most immidiatly useful information about this server, including MeshCentral and NodeJS versions. This is often information required to file a bug. Optionally use info h for human readable form."],
5667 'le': [serverUserCommandLe, ""],
5668 'lecheck': [serverUserCommandLeCheck, ""],
5669 'leevents': [serverUserCommandLeEvents, ""],
5670 'maintenance': [serverUserCommandMaintenance, ""],
5671 'migrationagents': [serverUserCommandMigrationAgents, ""],
5672 'mps': [serverUserCommandMps, ""],
5673 'mpsstats': [serverUserCommandMpsStats, ""],
5674 'nodeconfig': [serverUserCommandNodeConfig, ""],
5675 'print': [serverUserCommandPrint, ""],
5676 'relays': [serverUserCommandRelays, ""],
5677 'removeinactivedevices': [serverUserCommandRemoveInactiveDevices, ""],
5678 'resetserver': [serverUserCommandResetServer, "Causes the server to reset, this is sometimes useful is the config.json file was changed."],
5679 'serverupdate': [serverUserCommandServerUpdate, "Updates server to latest version. Optional version argument to install specific version. Example: serverupdate 0.8.49"],
5680 'setmaxtasks': [serverUserCommandSetMaxTasks, ""],
5681 'showpaths': [serverUserCommandShowPaths, ""],
5682 'sms': [serverUserCommandSMS, "Send a SMS message to a specified phone number"],
5683 'msg': [serverUserCommandMsg, "Send a user message to a user handle"],
5684 'swarmstats': [serverUserCommandSwarmStats, ""],
5685 'tasklimiter': [serverUserCommandTaskLimiter, "Returns the internal status of the tasklimiter. This is a system used to smooth out work done by the server. It's used by, for example, agent updates so that not all agents are updated at the same time."],
5686 'trafficdelta': [serverUserCommandTrafficDelta, ""],
5687 'trafficstats': [serverUserCommandTrafficStats, ""],
5688 'updatecheck': [serverUserCommandUpdateCheck, ""],
5689 'usersessions': [serverUserCommandUserSessions, "Returns a list of active sessions grouped by user."],
5690 'versions': [serverUserCommandVersions, "Returns all internal versions for NodeJS running this server."],
5691 'watchdog': [serverUserCommandWatchdog, ""],
5692 'webpush': [serverUserCommandWebPush, ""],
5693 'webstats': [serverUserCommandWebStats, ""]
5694 };
5695
5696 function serverCommandAddDeviceUser(command) {
5697 if (typeof command.userid == 'string') { command.userids = [command.userid]; }
5698 var err = null;
5699 try {
5700 if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid
5701 else if (common.validateInt(command.rights) == false) { err = 'Invalid rights'; } // Device rights must be an integer
5702 else if ((command.rights & 7) != 0) { err = 'Invalid rights'; } // EDITMESH, MANAGEUSERS or MANAGECOMPUTERS rights can't be assigned to a user to device link
5703 else if ((common.validateStrArray(command.usernames, 1, 128) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 128 characters
5704 else {
5705 if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
5706 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
5707 }
5708 } catch (ex) { err = 'Validation exception: ' + ex; }
5709
5710 // Handle any errors
5711 if (err != null) {
5712 if (command.responseid != null) { obj.send({ action: 'adddeviceuser', responseid: command.responseid, result: err }); }
5713 return;
5714 }
5715
5716 // Convert user names to userid's
5717 if (command.userids == null) {
5718 command.userids = [];
5719 for (var i in command.usernames) {
5720 if (command.usernames[i] != null) {
5721 if (parent.users['user/' + domain.id + '/' + command.usernames[i].toLowerCase()] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i].toLowerCase()); }
5722 else if (parent.users['user/' + domain.id + '/' + command.usernames[i]] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i]); }
5723 }
5724 }
5725 }
5726
5727 // Get the node and the rights for this node
5728 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
5729 // Check if already in the right mesh
5730 if ((node == null) || (node.meshid == command.meshid)) return;
5731 var dispatchTargets = ['*', node.meshid, node._id];
5732
5733 // Check that we have rights to manage users on this device
5734 if ((rights & MESHRIGHT_MANAGEUSERS) == 0) return;
5735
5736 // Add the new link to the users
5737 var nodeChanged = false;
5738 for (var i in command.userids) {
5739 var newuserid = command.userids[i];
5740
5741 // Add a user
5742 var newuser = null;
5743 if (newuserid.startsWith('ugrp/')) { newuser = parent.userGroups[newuserid]; }
5744 if (newuserid.startsWith('user/')) {
5745 newuser = parent.users[newuserid];
5746
5747 // Search for a user name in that windows domain is the username starts with *\
5748 if ((newuser == null) && (newuserid.startsWith('user/' + domain.id + '/*\\')) == true) {
5749 var search = newuserid.split('/')[2].substring(1);
5750 for (var i in parent.users) { if (i.endsWith(search) && (parent.users[i].domain == domain.id)) { newuser = parent.users[i]; command.userids[i] = newuserid = newuser._id; break; } }
5751 }
5752 }
5753
5754 // Check the the user and device are in the same domain
5755 if (command.nodeid.split('/')[1] != newuserid.split('/')[1]) return; // Domain mismatch
5756
5757 if (newuser != null) {
5758 // Add this user to the dispatch target list
5759 dispatchTargets.push(newuser._id);
5760
5761 if (command.remove === true) {
5762 // Remove link to this user
5763 if (newuser.links != null) {
5764 delete newuser.links[command.nodeid];
5765 if (Object.keys(newuser.links).length == 0) { delete newuser.links; }
5766 }
5767
5768 // Remove link to this device
5769 if (node.links != null) {
5770 delete node.links[newuserid];
5771 nodeChanged = true;
5772 if (Object.keys(node.links).length == 0) { delete node.links; }
5773 }
5774 } else {
5775 // Add the new link to this user
5776 if (newuser.links == null) { newuser.links = {}; }
5777 newuser.links[command.nodeid] = { rights: command.rights };
5778
5779 // Add the new link to the device
5780 if (node.links == null) { node.links = {}; }
5781 node.links[newuserid] = { rights: command.rights };
5782 nodeChanged = true;
5783 }
5784
5785 // Save the user to the database
5786 if (newuserid.startsWith('user/')) {
5787 db.SetUser(newuser);
5788 parent.parent.DispatchEvent([newuser], obj, 'resubscribe');
5789
5790 // Notify user change
5791 var targets = ['*', 'server-users', newuserid];
5792 var event;
5793 if (command.rights == 0) {
5794 event = { etype: 'user', userid: user._id, username: user.name, action: 'accountchange', msgid: 81, msgArgs: [newuser.name], msg: 'Removed user device rights for ' + newuser.name, domain: domain.id, account: parent.CloneSafeUser(newuser), nodeListChange: newuserid };
5795 } else {
5796 event = { etype: 'user', userid: user._id, username: user.name, action: 'accountchange', msgid: 82, msgArgs: [newuser.name], msg: 'Changed user device rights for ' + newuser.name, domain: domain.id, account: parent.CloneSafeUser(newuser), nodeListChange: newuserid };
5797 }
5798 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
5799 parent.parent.DispatchEvent(targets, obj, event);
5800 } else if (newuserid.startsWith('ugrp/')) {
5801 db.Set(newuser);
5802
5803 // Notify user group change
5804 var targets = ['*', 'server-ugroups', newuser._id];
5805 var event = { etype: 'ugrp', username: user.name, ugrpid: newuser._id, name: newuser.name, action: 'usergroupchange', links: newuser.links, msgid: 79, msgArgs: [newuser.name], msg: 'User group changed: ' + newuser.name, domain: domain.id };
5806 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
5807 parent.parent.DispatchEvent(targets, obj, event);
5808 }
5809 parent.InvalidateNodeCache(newuser, node.meshid, node._id)
5810 }
5811 }
5812
5813 // Save the device
5814 if (nodeChanged == true) {
5815 // Save the node to the database
5816 db.Set(parent.cleanDevice(node));
5817
5818 // Event the node change
5819 var event;
5820 if (command.rights == 0) {
5821 event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msgid: 81, msgArgs: [node.name], msg: 'Removed user device rights for ' + node.name, node: parent.CloneSafeNode(node) };
5822 } else {
5823 event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msgid: 82, msgArgs: [node.name], msg: 'Changed user device rights for ' + node.name, node: parent.CloneSafeNode(node) };
5824 }
5825 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
5826 parent.parent.DispatchEvent(dispatchTargets, obj, event);
5827 }
5828 if (command.responseid != null) { obj.send({ action: 'adddeviceuser', responseid: command.responseid, result: 'ok' }); }
5829 });
5830 }
5831
5832 function serverCommandAddMeshUser(command) {
5833 var err = null, mesh, meshIdSplit;
5834 if (typeof command.userid == 'string') { command.userids = [command.userid]; }
5835
5836 // Resolve the device group name if needed
5837 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
5838 for (var i in parent.meshes) {
5839 var m = parent.meshes[i];
5840 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
5841 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
5842 }
5843 }
5844 }
5845
5846 var selfMeshRights = 0;
5847 try {
5848 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid groupid'; } // Check the meshid
5849 else if (common.validateInt(command.meshadmin) == false) { err = 'Invalid group rights'; } // Mesh rights must be an integer
5850 else if ((common.validateStrArray(command.usernames, 1, 128) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 128 characters
5851 else {
5852 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
5853 mesh = parent.meshes[command.meshid];
5854 meshIdSplit = command.meshid.split('/');
5855 if (mesh == null) { err = 'Unknown group'; }
5856 else if (((selfMeshRights = parent.GetMeshRights(user, mesh)) & MESHRIGHT_MANAGEUSERS) == 0) { err = 'Permission denied'; }
5857 else if ((meshIdSplit.length != 3) || (meshIdSplit[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
5858 }
5859 } catch (ex) { err = 'Validation exception: ' + ex; }
5860
5861 // Handle any errors
5862 if (err != null) {
5863 if (command.responseid != null) { obj.send({ action: 'addmeshuser', responseid: command.responseid, result: err }); }
5864 return;
5865 }
5866
5867 // Convert user names to userid's
5868 if (command.userids == null) {
5869 command.userids = [];
5870 for (var i in command.usernames) {
5871 if (parent.users['user/' + domain.id + '/' + command.usernames[i].toLowerCase()] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i].toLowerCase()); }
5872 else if (parent.users['user/' + domain.id + '/' + command.usernames[i]] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i]); }
5873 }
5874 }
5875 var unknownUsers = [], successCount = 0, failCount = 0, msgs = [];
5876 for (var i in command.userids) {
5877 // Check if the user exists
5878 var newuserid = command.userids[i], newuser = null;
5879 if (newuserid.startsWith('user/')) { newuser = parent.users[newuserid]; }
5880 else if (newuserid.startsWith('ugrp/')) { newuser = parent.userGroups[newuserid]; }
5881
5882 // Search for a user name in that windows domain is the username starts with *\
5883 if ((newuser == null) && (newuserid.startsWith('user/' + domain.id + '/*\\')) == true) {
5884 var search = newuserid.split('/')[2].substring(1);
5885 for (var i in parent.users) { if (i.endsWith(search) && (parent.users[i].domain == domain.id)) { newuser = parent.users[i]; command.userids[i] = newuserid = parent.users[i]._id; break; } }
5886 }
5887
5888 // Make sure this user is in the same domain as the device group
5889 if (meshIdSplit[1] != newuserid.split('/')[1]) { msgs.push("Mismatch domains"); continue; }
5890
5891 if (newuser != null) {
5892 // Can't add or modify self
5893 if (newuserid == obj.user._id) { msgs.push("Can't change self"); continue; }
5894
5895 var targetMeshRights = 0;
5896 if ((newuser.links != null) && (newuser.links[command.meshid] != null) && (newuser.links[command.meshid].rights != null)) { targetMeshRights = newuser.links[command.meshid].rights; }
5897 if ((targetMeshRights === MESHRIGHT_ADMIN) && (selfMeshRights != MESHRIGHT_ADMIN)) { msgs.push("Can't change rights of device group administrator"); continue; } // A non-admin can't kick out an admin
5898
5899 if (command.remove === true) {
5900 // Remove mesh from user or user group
5901 delete newuser.links[command.meshid];
5902 } else {
5903 // Adjust rights since we can't add more rights that we have outself for MESHRIGHT_MANAGEUSERS
5904 if ((selfMeshRights != MESHRIGHT_ADMIN) && (command.meshadmin == MESHRIGHT_ADMIN)) { msgs.push("Can't set device group administrator, if not administrator"); continue; }
5905 if (((selfMeshRights & 2) == 0) && ((command.meshadmin & 2) != 0) && ((targetMeshRights & 2) == 0)) { command.meshadmin -= 2; }
5906
5907 // Add mesh to user or user group
5908 if (newuser.links == null) { newuser.links = {}; }
5909 if (newuser.links[command.meshid]) { newuser.links[command.meshid].rights = command.meshadmin; } else { newuser.links[command.meshid] = { rights: command.meshadmin }; }
5910 }
5911 if (newuserid.startsWith('user/')) { db.SetUser(newuser); }
5912 else if (newuserid.startsWith('ugrp/')) { db.Set(newuser); }
5913 parent.parent.DispatchEvent([newuser._id], obj, 'resubscribe');
5914
5915 if (newuserid.startsWith('user/')) {
5916 // Notify user change
5917 var targets = ['*', 'server-users', user._id, newuser._id];
5918 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(newuser), action: 'accountchange', msgid: 78, msgArgs: [newuser.name], msg: 'Device group membership changed: ' + newuser.name, domain: domain.id };
5919 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
5920 parent.parent.DispatchEvent(targets, obj, event);
5921 } else if (newuserid.startsWith('ugrp/')) {
5922 // Notify user group change
5923 var targets = ['*', 'server-ugroups', user._id, newuser._id];
5924 var event = { etype: 'ugrp', username: user.name, ugrpid: newuser._id, name: newuser.name, desc: newuser.desc, action: 'usergroupchange', links: newuser.links, msgid: 79, msgArgs: [newuser.name], msg: 'User group changed: ' + newuser.name, domain: domain.id };
5925 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
5926 parent.parent.DispatchEvent(targets, obj, event);
5927 }
5928
5929 var event;
5930 if (command.remove === true) {
5931 // Remove userid from the mesh
5932 delete mesh.links[newuserid];
5933 db.Set(mesh);
5934 event = { etype: 'mesh', username: newuser.name, userid: user._id, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Removed user ' + newuser.name + ' from device group ' + mesh.name, domain: domain.id, invite: mesh.invite };
5935 } else {
5936 // Add userid to the mesh
5937 mesh.links[newuserid] = { name: newuser.name, rights: command.meshadmin };
5938 db.Set(mesh);
5939 event = { etype: 'mesh', username: newuser.name, userid: user._id, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Added user ' + newuser.name + ' to device group ' + mesh.name, domain: domain.id, invite: mesh.invite };
5940 }
5941
5942 // Notify mesh change
5943 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
5944 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, newuserid]), obj, event);
5945 if (command.remove === true) { msgs.push("Removed user " + newuserid.split('/')[2]); } else { msgs.push("Added user " + newuserid.split('/')[2]); }
5946 successCount++;
5947 parent.InvalidateNodeCache(newuser, mesh)
5948 } else {
5949 msgs.push("Unknown user " + newuserid.split('/')[2]);
5950 unknownUsers.push(newuserid.split('/')[2]);
5951 failCount++;
5952 }
5953 }
5954
5955 if ((successCount == 0) && (failCount == 0)) { msgs.push("Nothing done"); }
5956
5957 if (unknownUsers.length > 0) {
5958 // Send error back, user not found.
5959 displayNotificationMessage('User' + ((unknownUsers.length > 1) ? 's' : '') + ' ' + EscapeHtml(unknownUsers.join(', ')) + ' not found.', "Device Group", 'ServerNotify', 5, (unknownUsers.length > 1) ? 16 : 15, [EscapeHtml(unknownUsers.join(', '))]);
5960 }
5961
5962 if (command.responseid != null) { obj.send({ action: 'addmeshuser', responseid: command.responseid, result: msgs.join(', '), success: successCount, failed: failCount }); }
5963 }
5964
5965 function serverCommandAddUser(command) {
5966 // If the email is the username, set this here.
5967 if (domain.usernameisemail) { if (command.email) { command.username = command.email; } else { command.email = command.username; } }
5968
5969 // Randomize the password if needed
5970 if (command.randomPassword === true) { command.pass = getRandomPassword(); }
5971
5972 // Add a new user account
5973 var err = null, errid = 0, args = null, newusername, newuserid, newuserdomain;
5974 try {
5975 if ((user.siteadmin & MESHRIGHT_MANAGEUSERS) == 0) { err = "Permission denied"; errid = 1; }
5976 else if (common.validateUsername(command.username, 1, 256) == false) { err = "Invalid username"; errid = 2; } // Username is between 1 and 64 characters, no spaces
5977 else if ((command.username[0] == '~') || (command.username.indexOf('/') >= 0)) { err = "Invalid username"; errid = 2; } // Usernames cant' start with ~ and can't have '/'
5978 else if (common.validateString(command.pass, 1, 256) == false) { err = "Invalid password"; errid = 3; } // Password is between 1 and 256 characters
5979 else if ((command.randomPassword !== true) && (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false)) { err = "Invalid password"; errid = 3; } // Password does not meet requirements
5980 else if ((command.email != null) && (common.validateEmail(command.email, 1, 1024) == false)) { err = "Invalid email"; errid = 4; } // Check if this is a valid email address
5981 else if ((obj.crossDomain === true) && (command.domain != null) && ((typeof command.domain != 'string') || (parent.parent.config.domains[command.domain] == null))) { err = "Invalid domain"; errid = 5; } // Check if this is a valid domain
5982 else if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains) && !common.validateEmailDomain(command.email, domain.newaccountemaildomains)) { err = "Email domain is not allowed. Only (" + domain.newaccountemaildomains.join(', ') + ") are allowed."; errid=30; args = [common.getEmailDomain(command.email), domain.newaccountemaildomains.join(', ')]; }
5983 else {
5984 newuserdomain = domain;
5985 if ((obj.crossDomain === true) && (command.domain != null)) { newuserdomain = parent.parent.config.domains[command.domain]; }
5986 newusername = command.username;
5987 newuserid = 'user/' + newuserdomain.id + '/' + command.username.toLowerCase();
5988 if (command.siteadmin != null) {
5989 if ((typeof command.siteadmin != 'number') || (Number.isInteger(command.siteadmin) == false)) { err = "Invalid site permissions"; errid = 6; } // Check permissions
5990 else if ((user.siteadmin != SITERIGHT_ADMIN) && ((command.siteadmin & (SITERIGHT_ADMIN - 224)) != 0)) { err = "Invalid site permissions"; errid = 6; }
5991 }
5992 if (parent.users[newuserid]) { err = "User already exists"; errid = 7; } // Account already exists
5993 else if ((newuserdomain.auth == 'sspi') || (newuserdomain.auth == 'ldap')) { err = "Unable to add user in this mode"; errid = 8; }
5994 }
5995 } catch (ex) { err = "Validation exception"; errid = 9; }
5996
5997 // Handle any errors
5998 if (err != null) {
5999 if (command.responseid != null) {
6000 obj.send({ action: 'adduser', responseid: command.responseid, result: err, msgid: errid });
6001 } else {
6002 // Send error back, user not found.
6003 displayNotificationMessage(err, "New Account", 'ServerNotify', 1, errid, args);
6004 }
6005 return;
6006 }
6007
6008 // Check if we exceed the maximum number of user accounts
6009 db.isMaxType(newuserdomain.limits.maxuseraccounts, 'user', newuserdomain.id, function (maxExceed) {
6010 if (maxExceed) {
6011 // Account count exceed, do notification
6012 if (command.responseid != null) {
6013 // Respond privately if requested
6014 obj.send({ action: 'adduser', responseid: command.responseid, result: 'maxUsersExceed' });
6015 } else {
6016 // Create the notification message
6017 var notification = { action: 'msg', type: 'notify', id: Math.random(), value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: newuserdomain.id, titleid: 2, msgid: 10 };
6018
6019 // Get the list of sessions for this user
6020 var sessions = parent.wssessions[user._id];
6021 if (sessions != null) { for (var i in sessions) { try { if (sessions[i].domainid == newuserdomain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } }
6022 // TODO: Notify all sessions on other peers.
6023 }
6024 } else {
6025 // Remove any events for this userid
6026 if (command.removeEvents === true) { db.RemoveAllUserEvents(newuserdomain.id, newuserid); }
6027
6028 // Create a new user
6029 var newuser = { type: 'user', _id: newuserid, name: newusername, creation: Math.floor(Date.now() / 1000), domain: newuserdomain.id };
6030 if (command.siteadmin != null) { newuser.siteadmin = command.siteadmin; }
6031 else if (newuserdomain.newaccountsrights) { newuser.siteadmin = newuserdomain.newaccountsrights; }
6032 if (command.email != null) { newuser.email = command.email.toLowerCase(); if (command.emailVerified === true) { newuser.emailVerified = true; } } // Email
6033 if (command.resetNextLogin === true) { newuser.passchange = -1; } else { newuser.passchange = Math.floor(Date.now() / 1000); }
6034 if (user.groups) { newuser.groups = user.groups; } // New accounts are automatically part of our groups (Realms).
6035 if (common.validateString(command.realname, 1, 256)) { newuser.realname = command.realname; }
6036 if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { newuser.consent = command.consent; } change = 1; }
6037 if ((command.phone != null) && (typeof command.phone == 'string') && ((command.phone == '') || isPhoneNumber(command.phone))) { if (command.phone == '') { delete newuser.phone; } else { newuser.phone = command.phone; } change = 1; }
6038
6039 // Auto-join any user groups
6040 if (typeof newuserdomain.newaccountsusergroups == 'object') {
6041 for (var i in newuserdomain.newaccountsusergroups) {
6042 var ugrpid = newuserdomain.newaccountsusergroups[i];
6043 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + newuserdomain.id + '/' + ugrpid; }
6044 var ugroup = parent.userGroups[ugrpid];
6045 if (ugroup != null) {
6046 // Add group to the user
6047 if (newuser.links == null) { newuser.links = {}; }
6048 newuser.links[ugroup._id] = { rights: 1 };
6049
6050 // Add user to the group
6051 ugroup.links[newuser._id] = { userid: newuser._id, name: newuser.name, rights: 1 };
6052 db.Set(ugroup);
6053
6054 // Notify user group change
6055 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 80, msgArgs: [newuser.name, ugroup.name], msg: 'Added user ' + newuser.name + ' to user group ' + ugroup.name, addUserDomain: newuserdomain.id };
6056 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
6057 parent.parent.DispatchEvent(['*', ugroup._id, user._id, newuser._id], obj, event);
6058 }
6059 }
6060 }
6061
6062 parent.users[newuserid] = newuser;
6063
6064 // Create a user, generate a salt and hash the password
6065 require('./pass').hash(command.pass, function (err, salt, hash, tag) {
6066 if (err == null) {
6067 newuser.salt = salt;
6068 newuser.hash = hash;
6069 db.SetUser(newuser);
6070
6071 var event, targets = ['*', 'server-users'];
6072 if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
6073 if (command.email == null) {
6074 event = { etype: 'user', userid: newuser._id, username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 64, msgArgs: [command.username], msg: 'Account created, username is ' + command.username, domain: newuserdomain.id };
6075 } else {
6076 event = { etype: 'user', userid: newuser._id, username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 65, msgArgs: [command.email.toLowerCase()], msg: 'Account created, email is ' + command.email.toLowerCase(), domain: newuserdomain.id };
6077 }
6078 if (parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
6079 parent.parent.DispatchEvent(targets, obj, event);
6080
6081 // Perform email invitation
6082 if ((command.emailInvitation == true) && (command.emailVerified == true) && command.email && domain.mailserver) {
6083 domain.mailserver.sendAccountInviteMail(newuserdomain, (user.realname ? user.realname : user.name), newusername, command.email.toLowerCase(), command.pass, parent.getLanguageCodes(req), req.query.key);
6084 }
6085
6086 // Log in the auth log
6087 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created a user account ' + newuser.name); }
6088
6089 // OK Response
6090 if (command.responseid != null) { obj.send({ action: 'adduser', responseid: command.responseid, result: 'ok' }); }
6091 } else {
6092 if (command.responseid != null) { obj.send({ action: 'adduser', responseid: command.responseid, result: 'passwordHashError' }); }
6093 }
6094 }, 0);
6095 }
6096 });
6097 }
6098
6099 function serverCommandAddUserBatch(command) {
6100 var err = null;
6101
6102 // Add many new user accounts
6103 if ((user.siteadmin & 2) == 0) { err = 'Access denied'; }
6104 else if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { err = 'Unable to create users when in SSPI or LDAP mode'; }
6105 else if (!Array.isArray(command.users)) { err = 'Invalid users'; }
6106 else {
6107 var userCount = 0;
6108 for (var i in command.users) {
6109 if (domain.usernameisemail) { if (command.users[i].email) { command.users[i].user = command.users[i].email; } else { command.users[i].email = command.users[i].user; } } // If the email is the username, set this here.
6110 if (common.validateUsername(command.users[i].user, 1, 256) == false) { err = 'Invalid username'; } // Username is between 1 and 64 characters, no spaces
6111 if ((command.users[i].user[0] == '~') || (command.users[i].user.indexOf('/') >= 0)) { err = 'Invalid username'; } // This is a reserved user name or invalid name
6112 if (common.validateString(command.users[i].pass, 1, 256) == false) { err = 'Invalid password'; } // Password is between 1 and 256 characters
6113 if (common.checkPasswordRequirements(command.users[i].pass, domain.passwordrequirements) == false) { err = 'Invalid password'; } // Password does not meet requirements
6114 if ((command.users[i].email != null) && (common.validateEmail(command.users[i].email, 1, 1024) == false)) { err = 'Invalid email'; } // Check if this is a valid email address
6115 userCount++;
6116 }
6117 }
6118
6119 // Handle any errors
6120 if (err != null) {
6121 if (command.responseid != null) { obj.send({ action: 'adduserbatch', responseid: command.responseid, result: err }); }
6122 return;
6123 }
6124
6125 // Check if we exceed the maximum number of user accounts
6126 db.isMaxType(domain.limits.maxuseraccounts + userCount, 'user', domain.id, function (maxExceed) {
6127 if (maxExceed) {
6128 // Account count exceed, do notification
6129
6130 // Create the notification message
6131 var notification = { action: 'msg', type: 'notify', id: Math.random(), value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: domain.id, titleid: 2, msgid: 10 };
6132
6133 // Get the list of sessions for this user
6134 var sessions = parent.wssessions[user._id];
6135 if (sessions != null) { for (var i in sessions) { try { if (sessions[i].domainid == domain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } }
6136 // TODO: Notify all sessions on other peers.
6137 } else {
6138 for (var i in command.users) {
6139 // Check if this is an existing user
6140 var newuserid = 'user/' + domain.id + '/' + command.users[i].user.toLowerCase();
6141 var newuser = { type: 'user', _id: newuserid, name: command.users[i].user, creation: Math.floor(Date.now() / 1000), domain: domain.id };
6142 if (domain.newaccountsrights) { newuser.siteadmin = domain.newaccountsrights; }
6143 if (common.validateString(command.users[i].realname, 1, 256)) { newuser.realname = command.users[i].realname; }
6144 if (command.users[i].email != null) { newuser.email = command.users[i].email.toLowerCase(); if (command.users[i].emailVerified === true) { newuser.emailVerified = true; } } // Email, always lowercase
6145 if (command.users[i].resetNextLogin === true) { newuser.passchange = -1; } else { newuser.passchange = Math.floor(Date.now() / 1000); }
6146 if (user.groups) { newuser.groups = user.groups; } // New accounts are automatically part of our groups (Realms).
6147
6148 if (parent.users[newuserid] == null) {
6149 parent.users[newuserid] = newuser;
6150
6151 // Create a user, generate a salt and hash the password
6152 require('./pass').hash(command.users[i].pass, function (err, salt, hash, newuser) {
6153 if (err) throw err;
6154 newuser.salt = salt;
6155 newuser.hash = hash;
6156 db.SetUser(newuser);
6157
6158 var event, targets = ['*', 'server-users'];
6159 if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
6160 if (newuser.email == null) {
6161 event = { etype: 'user', userid: newuser._id, username: newuser.name, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 64, msgArgs: [newuser.name], msg: 'Account created, username is ' + newuser.name, domain: domain.id };
6162 } else {
6163 event = { etype: 'user', userid: newuser._id, username: newuser.name, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 65, msgArgs: [newuser.email], msg: 'Account created, email is ' + newuser.email, domain: domain.id };
6164 }
6165 if (parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
6166 parent.parent.DispatchEvent(targets, obj, event);
6167
6168 // Log in the auth log
6169 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created user account ' + newuser.name); }
6170 }, newuser);
6171 }
6172 }
6173 }
6174 });
6175 }
6176
6177 function serverCommandAddUserToUserGroup(command) {
6178 var err = null;
6179 try {
6180 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; }
6181 else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid
6182 else if (common.validateStrArray(command.usernames, 1, 128) == false) { err = 'Invalid usernames'; } // Username is between 1 and 128 characters
6183 else {
6184 var ugroupidsplit = command.ugrpid.split('/');
6185 if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = 'Invalid groupid'; }
6186 }
6187 } catch (ex) { err = 'Validation exception: ' + ex; }
6188
6189 // Fetch the domain
6190 var addUserDomain = domain;
6191 if (obj.crossDomain === true) { addUserDomain = parent.parent.config.domains[ugroupidsplit[1]]; }
6192 if (addUserDomain == null) { err = 'Invalid domain'; }
6193
6194 // Handle any errors
6195 if (err != null) {
6196 if (command.responseid != null) { obj.send({ action: 'addusertousergroup', responseid: command.responseid, result: err }); }
6197 return;
6198 }
6199
6200 // Get the user group
6201 var group = parent.userGroups[command.ugrpid];
6202 if (group != null) {
6203 // If this user group is an externally managed user group, we can't add users to it.
6204 if ((group != null) && (group.membershipType != null)) return;
6205
6206 if (group.links == null) { group.links = {}; }
6207
6208 var unknownUsers = [], addedCount = 0, failCount = 0, knownUsers = [];
6209 for (var i in command.usernames) {
6210 // Check if the user exists
6211 var chguserid = 'user/' + addUserDomain.id + '/' + command.usernames[i].toLowerCase();
6212 var chguser = parent.users[chguserid];
6213 if (chguser == null) { chguserid = 'user/' + addUserDomain.id + '/' + command.usernames[i]; chguser = parent.users[chguserid]; }
6214 if (chguser != null) {
6215 // Add usr group to user
6216 if (chguser.links == null) { chguser.links = {}; }
6217 chguser.links[group._id] = { rights: 1 };
6218 db.SetUser(chguser);
6219 parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
6220
6221 knownUsers.push(chguser);
6222 // Notify user change
6223 var targets = ['*', 'server-users', user._id, chguser._id];
6224 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 67, msgArgs: [chguser.name], msg: 'User group membership changed: ' + chguser.name, domain: addUserDomain.id };
6225 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6226 parent.parent.DispatchEvent(targets, obj, event);
6227
6228 // Add a user to the user group
6229 group.links[chguserid] = { userid: chguser._id, name: chguser.name, rights: 1 };
6230 addedCount++;
6231 } else {
6232 unknownUsers.push(command.usernames[i]);
6233 failCount++;
6234 }
6235 }
6236
6237 if (addedCount > 0) {
6238 // Save the new group to the database
6239 db.Set(group);
6240
6241 // Notify user group change
6242 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, action: 'usergroupchange', links: group.links, msgid: 71, msgArgs: [knownUsers.map((u)=>u.name), group.name], msg: 'Added user(s) ' + knownUsers.map((u)=>u.name) + ' to user group ' + group.name, addUserDomain: domain.id };
6243 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
6244 parent.parent.DispatchEvent(['*', group._id, user._id, chguserid], obj, event);
6245 }
6246
6247 if (unknownUsers.length > 0) {
6248 // Send error back, user not found.
6249 displayNotificationMessage('User' + ((unknownUsers.length > 1) ? 's' : '') + ' ' + EscapeHtml(unknownUsers.join(', ')) + ' not found.', "Device Group", 'ServerNotify', 5, (unknownUsers.length > 1) ? 16 : 15, [EscapeHtml(unknownUsers.join(', '))]);
6250 }
6251 }
6252
6253 if (command.responseid != null) { obj.send({ action: 'addusertousergroup', responseid: command.responseid, result: 'ok', added: addedCount, failed: failCount }); }
6254 }
6255
6256 function serverCommandAgentDisconnect(command) {
6257 if (common.validateInt(command.disconnectMode) == false) return; // Check disconnect mode
6258
6259 // Get the node and the rights for this node
6260 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6261 if ((node == null) || (((rights & MESHRIGHT_AGENTCONSOLE) == 0) && (user.siteadmin != SITERIGHT_ADMIN))) return;
6262
6263 // Force mesh agent disconnection
6264 parent.forceMeshAgentDisconnect(user, domain, node._id, command.disconnectMode);
6265 });
6266 }
6267
6268 function serverCommandAuthCookie(command) {
6269 try {
6270 ws.send(JSON.stringify({
6271 action: 'authcookie',
6272 cookie: parent.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, parent.parent.loginCookieEncryptionKey),
6273 rcookie: parent.parent.encodeCookie({ ruserid: user._id, x: req.session.x }, parent.parent.loginCookieEncryptionKey)
6274 }));
6275 } catch (ex) { }
6276 }
6277
6278 function serverCommandChangeEmail(command) {
6279 // Do not allow this command when logged in using a login token
6280 if (req.session.loginToken != null) return;
6281
6282 // If the email is the username, this command is not allowed.
6283 if (domain.usernameisemail) return;
6284
6285 // If this account is settings locked, return here.
6286 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
6287
6288 // Change our own email address
6289 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
6290 if (common.validateEmail(command.email, 1, 1024) == false) return;
6291
6292 // Always lowercase the email address
6293 command.email = command.email.toLowerCase();
6294
6295 if (obj.user.email != command.email) {
6296 // Check if this email is already validated on a different account
6297 db.GetUserWithVerifiedEmail(domain.id, command.email, function (err, docs) {
6298 if ((docs != null) && (docs.length > 0)) {
6299 // Notify the duplicate email error
6300 obj.send({ action: 'msg', type: 'notify', title: 'Account Settings', id: Math.random(), tag: 'ServerNotify', value: 'Failed to change email address, another account already using: ' + command.email + '.', titleid: 4, msgid: 13, args: [command.email] });
6301 } else {
6302 // Update the user's email
6303 var oldemail = user.email;
6304 user.email = command.email;
6305 user.emailVerified = false;
6306 parent.db.SetUser(user);
6307
6308 // Event the change
6309 var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id };
6310 if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6311 if (oldemail != null) {
6312 message.msg = 'Changed email of user ' + user.name + ' from ' + oldemail + ' to ' + user.email;
6313 } else {
6314 message.msg = 'Set email of user ' + user.name + ' to ' + user.email;
6315 }
6316
6317 var targets = ['*', 'server-users', user._id];
6318 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
6319 parent.parent.DispatchEvent(targets, obj, message);
6320
6321 // Log in the auth log
6322 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed email from ' + oldemail + ' to ' + user.email); }
6323
6324 // Send the verification email
6325 if (domain.mailserver != null) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req), req.query.key); }
6326 }
6327 });
6328 }
6329 }
6330
6331 function serverCommandChangeLang(command) {
6332 // Do not allow this command when logged in using a login token
6333 if (req.session.loginToken != null) return;
6334
6335 // If this account is settings locked, return here.
6336 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
6337
6338 if (common.validateString(command.lang, 1, 6) == false) return;
6339
6340 // Always lowercase the language
6341 command.lang = command.lang.toLowerCase();
6342
6343 // Update the user's language
6344 var oldlang = user.lang;
6345 if (command.lang == '*') { delete user.lang; } else { user.lang = command.lang; }
6346 parent.db.SetUser(user);
6347
6348 // Event the change
6349 var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, msgid: 3, msgArgs: ['', (oldlang ? oldlang : 'default'), (user.lang ? user.lang : 'default')] };
6350 if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6351 message.msg = 'Changed language from ' + (oldlang ? oldlang : 'default') + ' to ' + (user.lang ? user.lang : 'default');
6352
6353 var targets = ['*', 'server-users', user._id];
6354 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
6355 parent.parent.DispatchEvent(targets, obj, message);
6356 }
6357
6358 function serverCommandClose(command) {
6359 // Close the web socket session
6360 try { if (obj.req.session.ws == ws) delete obj.req.session.ws; } catch (e) { }
6361 try { ws.close(); } catch (e) { }
6362 }
6363
6364 function serverCommandConfirmPhone(command) {
6365 // Do not allow this command when logged in using a login token
6366 if (req.session.loginToken != null) return;
6367
6368 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
6369 if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'string') || (obj.failedSmsCookieCheck == 1)) return; // Input checks
6370 var cookie = parent.parent.decodeCookie(command.cookie);
6371 if (cookie == null) return; // Invalid cookie
6372 if (cookie.s != ws.sessionId) return; // Invalid session
6373 if (cookie.c != command.code) {
6374 obj.failedSmsCookieCheck = 1;
6375 // Code does not match, delay the response to limit how many guesses we can make and don't allow more than 1 guess at any given time.
6376 setTimeout(function () {
6377 ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true }));
6378 delete obj.failedSmsCookieCheck;
6379 }, 2000 + (parent.crypto.randomBytes(2).readUInt16BE(0) % 4095));
6380 return;
6381 }
6382
6383 // Set the user's phone
6384 user.phone = cookie.p;
6385 db.SetUser(user);
6386
6387 // Event the change
6388 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 96, msgArgs: [user.name], msg: 'Verified phone number of user ' + EscapeHtml(user.name), domain: domain.id };
6389 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6390 parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
6391 }
6392
6393 function serverCommandConfirmMessaging(command) {
6394 // Do not allow this command when logged in using a login token
6395 if (req.session.loginToken != null) return;
6396
6397 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
6398 if ((parent.parent.msgserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'string') || (obj.failedMsgCookieCheck == 1)) return; // Input checks
6399 var cookie = parent.parent.decodeCookie(command.cookie);
6400 if (cookie == null) return; // Invalid cookie
6401 if (cookie.s != ws.sessionId) return; // Invalid session
6402 if (cookie.c != command.code) {
6403 obj.failedMsgCookieCheck = 1;
6404 // Code does not match, delay the response to limit how many guesses we can make and don't allow more than 1 guess at any given time.
6405 setTimeout(function () {
6406 ws.send(JSON.stringify({ action: 'verifyMessaging', cookie: command.cookie, success: true }));
6407 delete obj.failedMsgCookieCheck;
6408 }, 2000 + (parent.crypto.randomBytes(2).readUInt16BE(0) % 4095));
6409 return;
6410 }
6411
6412 // Set the user's messaging handle
6413 user.msghandle = cookie.p;
6414 db.SetUser(user);
6415
6416 // Event the change
6417 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 156, msgArgs: [user.name], msg: 'Verified messaging account of user ' + EscapeHtml(user.name), domain: domain.id };
6418 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6419 parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
6420 }
6421
6422 function serverCommandEmailUser(command) {
6423 var errMsg = null, emailuser = null;
6424 if (domain.mailserver == null) { errMsg = 'Email server not enabled'; }
6425 else if ((user.siteadmin & 2) == 0) { errMsg = 'No user management rights'; }
6426 else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = 'Invalid userid'; }
6427 else if (common.validateString(command.subject, 1, 1000) == false) { errMsg = 'Invalid subject message'; }
6428 else if (common.validateString(command.msg, 1, 10000) == false) { errMsg = 'Invalid message'; }
6429 else {
6430 emailuser = parent.users[command.userid];
6431 if (emailuser == null) { errMsg = 'Invalid userid'; }
6432 else if (emailuser.email == null) { errMsg = 'No validated email address for this user'; }
6433 else if (emailuser.emailVerified !== true) { errMsg = 'No validated email address for this user'; }
6434 }
6435
6436 if (errMsg != null) { displayNotificationMessage(errMsg); return; }
6437 domain.mailserver.sendMail(emailuser.email, command.subject, command.msg);
6438 displayNotificationMessage("Email sent.", null, null, null, 14);
6439 }
6440
6441 function serverCommandFiles(command) {
6442 // Send the full list of server files to the browser app
6443 updateUserFiles(user, ws, domain);
6444 }
6445
6446 function serverCommandGetClip(command) {
6447 if (common.validateString(command.nodeid, 1, 1024) == false) return; // Check nodeid
6448
6449 // Get the node and the rights for this node
6450 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6451 if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
6452
6453 // Ask for clipboard data from agent
6454 var agent = parent.wsagents[node._id];
6455 if (agent != null) { try { agent.send(JSON.stringify({ action: 'getClip' })); } catch (ex) { } }
6456 });
6457 }
6458
6459 function serverCommandGetCookie(command) {
6460 // Check if this user has rights on this nodeid
6461 if (common.validateString(command.nodeid, 1, 1024) == false) return; // Check nodeid
6462 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6463 if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) return; // Access denied.
6464
6465 // Add a user authentication cookie to a url
6466 var cookieContent = { userid: user._id, domainid: user.domain };
6467 if (command.nodeid) { cookieContent.nodeid = command.nodeid; }
6468 if (command.tcpaddr) { cookieContent.tcpaddr = command.tcpaddr; } // Indicates the browser want the agent to TCP connect to a remote address
6469 if (command.tcpport) { cookieContent.tcpport = command.tcpport; } // Indicates the browser want the agent to TCP connect to a remote port
6470 if (command.tag == 'novnc') { cookieContent.p = 12; } // If tag is novnc we must encode a protocol for meshrelay logging
6471 if (node.mtype == 3) { cookieContent.lc = 1; command.localRelay = true; } // Indicate this is for a local connection
6472 command.cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
6473 command.trustedCert = parent.isTrustedCert(domain);
6474 obj.send(command);
6475 });
6476 }
6477
6478 function serverCommandGetNetworkInfo(command) {
6479 if (!validNodeIdAndDomain(command)) return;
6480
6481 // Get the node and the rights for this node
6482 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6483 if ((visible == false) || ((rights & MESHRIGHT_DEVICEDETAILS) == 0)) { obj.send({ action: 'getnetworkinfo', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' }); return; }
6484
6485 // Get network information about this node
6486 db.Get('if' + node._id, function (err, netinfos) {
6487 if ((netinfos == null) || (netinfos.length != 1)) { obj.send({ action: 'getnetworkinfo', nodeid: node._id, netif: null, netif2: null }); return; }
6488 var netinfo = netinfos[0];
6489
6490 // Unescape any field names that have special characters if needed
6491 if (netinfo.netif2 != null) {
6492 for (var i in netinfo.netif2) {
6493 var esc = common.unEscapeFieldName(i);
6494 if (esc !== i) { netinfo.netif2[esc] = netinfo.netif2[i]; delete netinfo.netif2[i]; }
6495 }
6496 }
6497
6498 obj.send({ action: 'getnetworkinfo', nodeid: node._id, updateTime: netinfo.updateTime, netif: netinfo.netif, netif2: netinfo.netif2 });
6499 });
6500 });
6501 }
6502
6503 function serverCommandGetSysInfo(command) {
6504 if (!validNodeIdAndDomain(command)) return;
6505
6506 // Get the node and the rights for this node
6507 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6508 if ((visible == false) || ((rights & MESHRIGHT_DEVICEDETAILS) == 0)) { obj.send({ action: 'getsysinfo', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' }); return; }
6509 // Query the database system information
6510 db.Get('si' + command.nodeid, function (err, docs) {
6511 if ((docs != null) && (docs.length > 0)) {
6512 var doc = docs[0];
6513 doc.action = 'getsysinfo';
6514 doc.nodeid = node._id;
6515 doc.tag = command.tag;
6516 delete doc.type;
6517 delete doc.domain;
6518 delete doc._id;
6519
6520 // If this is not a device group admin users, don't send any BitLocker recovery passwords
6521 if ((rights != MESHRIGHT_ADMIN) && (doc.hardware) && (doc.hardware.windows) && (doc.hardware.windows.volumes)) {
6522 for (var i in doc.hardware.windows.volumes) { delete doc.hardware.windows.volumes[i].recoveryPassword; }
6523 }
6524
6525 if (command.nodeinfo === true) {
6526 doc.node = node;
6527 doc.rights = rights;
6528 // Remove any connectivity and power state information, that should not be in the database anyway.
6529 // TODO: Find why these are sometimes saved in the db.
6530 if (doc.node.conn != null) { delete doc.node.conn; }
6531 if (doc.node.pwr != null) { delete doc.node.pwr; }
6532 if (doc.node.agct != null) { delete doc.node.agct; }
6533 if (doc.node.cict != null) { delete doc.node.cict; }
6534 // Add the connection state
6535 var state = parent.parent.GetConnectivityState(doc.nodeid);
6536 if (state) {
6537 doc.node.conn = state.connectivity;
6538 doc.node.pwr = state.powerState;
6539 if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[doc.nodeid]; if (agent != null) { doc.node.agct = agent.connectTime; } }
6540 // Use the connection time of the CIRA/Relay connection
6541 if ((state.connectivity & 2) != 0) {
6542 var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(doc.nodeid, null, true);
6543 if ((ciraConnection != null) && (ciraConnection.tag != null)) { doc.node.cict = ciraConnection.tag.connectTime; }
6544 }
6545 }
6546 }
6547 obj.send(doc);
6548 } else {
6549 obj.send({ action: 'getsysinfo', nodeid: node._id, tag: command.tag, noinfo: true, result: 'Invalid device id' });
6550 }
6551 });
6552 });
6553 }
6554
6555 function serverCommandInterSession(command) {
6556 // Sends data between sessions of the same user
6557 var sessions = parent.wssessions[obj.user._id];
6558 if (sessions == null) return;
6559
6560 // Create the notification message and send on all sessions except our own (no echo back).
6561 var notification = JSON.stringify(command);
6562 for (var i in sessions) { if (sessions[i] != obj.ws) { try { sessions[i].send(notification); } catch (ex) { } } }
6563 // TODO: Send the message of user sessions connected to other servers.
6564 }
6565
6566 function serverCommandInterUser(command) {
6567 // Sends data between users only if allowed. Only a user in the "interUserMessaging": [] list, in the settings section of the config.json can receive and send inter-user messages from and to all users.
6568 if ((parent.parent.config.settings.interusermessaging == null) || (parent.parent.config.settings.interusermessaging == false) || (command.data == null)) return;
6569 if (typeof command.sessionid == 'string') { var userSessionId = command.sessionid.split('/'); if (userSessionId.length != 4) return; command.userid = userSessionId[0] + '/' + userSessionId[1] + '/' + userSessionId[2]; }
6570 if (common.validateString(command.userid, 0, 2014) == false) return;
6571 var userSplit = command.userid.split('/');
6572 if (userSplit.length == 1) { command.userid = 'user/' + domain.id + '/' + command.userid; userSplit = command.userid.split('/'); }
6573 if ((userSplit.length != 3) || (userSplit[0] != 'user') || (userSplit[1] != domain.id) || (parent.users[command.userid] == null)) return; // Make sure the target userid is valid and within the domain
6574 const allowed = ((parent.parent.config.settings.interusermessaging === true) || (parent.parent.config.settings.interusermessaging.indexOf(obj.user._id) >= 0) || (parent.parent.config.settings.interusermessaging.indexOf(command.userid) >= 0));
6575 if (allowed == false) return;
6576
6577 // Get sessions
6578 var sessions = parent.wssessions[command.userid];
6579 if (sessions == null) return;
6580
6581 // Create the notification message and send on all sessions except our own (no echo back).
6582 var notification = JSON.stringify({ action: 'interuser', sessionid: ws.sessionId, data: command.data, scope: (command.sessionid != null)?'session':'user' });
6583 for (var i in sessions) {
6584 if ((command.sessionid != null) && (sessions[i].sessionId != command.sessionid)) continue; // Send to a specific session
6585 if (sessions[i] != obj.ws) { try { sessions[i].send(notification); } catch (ex) { } }
6586 }
6587 // TODO: Send the message of user sessions connected to other servers.
6588 }
6589
6590 function serverCommandLastConnect(command) {
6591 if (!validNodeIdAndDomain(command)) return;
6592
6593 // Get the node and the rights for this node
6594 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6595 if (visible == false) { obj.send({ action: 'lastconnect', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' }); return; }
6596
6597 // Query the database for the last time this node connected
6598 db.Get('lc' + command.nodeid, function (err, docs) {
6599 if ((docs != null) && (docs.length > 0)) {
6600 obj.send({ action: 'lastconnect', nodeid: command.nodeid, time: docs[0].time, addr: docs[0].addr });
6601 } else {
6602 obj.send({ action: 'lastconnect', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'No data' });
6603 }
6604 });
6605 });
6606 }
6607
6608 function serverCommandLastConnects(command) {
6609 if (obj.visibleDevices == null) {
6610 // If we are not paging, get all devices visible to this user
6611 const links = parent.GetAllMeshIdWithRights(user);
6612 const extraids = getUserExtraIds();
6613 db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, obj.deviceSkip, obj.deviceLimit, function (err, docs) {
6614 if (docs == null) return;
6615
6616 // Create a list of node ids for this user and query them for last device connection time
6617 const ids = []
6618 for (var i in docs) { ids.push('lc' + docs[i]._id); }
6619
6620 // Pull list of last connections only for device owned by this user
6621 db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) {
6622 if (docs == null) return;
6623 const response = {};
6624 for (var j in docs) { response[docs[j]._id.substring(2)] = docs[j].time; }
6625 obj.send({ action: 'lastconnects', lastconnects: response, tag: command.tag });
6626 });
6627 });
6628 } else {
6629 // If we are paging, we know what devices the user is look at
6630 // Create a list of node ids for this user and query them for last device connection time
6631 const ids = []
6632 for (var i in obj.visibleDevices) { ids.push('lc' + i); }
6633
6634 // Pull list of last connections only for device owned by this user
6635 db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) {
6636 if (docs == null) return;
6637 const response = {};
6638 for (var j in docs) { response[docs[j]._id.substring(2)] = docs[j].time; }
6639 obj.send({ action: 'lastconnects', lastconnects: response, tag: command.tag });
6640 });
6641 }
6642 }
6643
6644 function serverCommandLoginCookie(command) {
6645 // If allowed, return a login cookie
6646 if (parent.parent.config.settings.allowlogintoken === true) {
6647 obj.send({ action: 'logincookie', cookie: parent.parent.encodeCookie({ u: user._id, a: 3 }, parent.parent.loginCookieEncryptionKey) });
6648 }
6649 }
6650
6651 function serverCommandMeshes(command) {
6652 // Request a list of all meshes this user as rights to
6653 obj.send({ action: 'meshes', meshes: parent.GetAllMeshWithRights(user).map(parent.CloneSafeMesh), tag: command.tag });
6654 }
6655
6656 function serverCommandPing(command) { try { ws.send('{"action":"pong"}'); } catch (ex) { } }
6657 function serverCommandPong(command) { } // NOP
6658
6659 function serverCommandPowerTimeline(command) {
6660 // Get the node and the rights for this node
6661 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6662 if (visible == false) return;
6663 // Query the database for the power timeline for a given node
6664 // The result is a compacted array: [ startPowerState, startTimeUTC, powerState ] + many[ deltaTime, powerState ]
6665 db.getPowerTimeline(node._id, function (err, docs) {
6666 if ((err == null) && (docs != null) && (docs.length > 0)) {
6667 var timeline = [], time = null, previousPower;
6668 for (i in docs) {
6669 var doc = docs[i], j = parseInt(i);
6670 doc.time = Date.parse(doc.time);
6671 if (time == null) { // First element
6672 // Skip all starting power 0 events.
6673 if ((doc.power == 0) && ((doc.oldPower == null) || (doc.oldPower == 0))) continue;
6674 time = doc.time;
6675 if (doc.oldPower) { timeline.push(doc.oldPower, time / 1000, doc.power); } else { timeline.push(0, time / 1000, doc.power); }
6676 } else if (previousPower != doc.power) { // Delta element
6677 // If this event is of a short duration (2 minutes or less), skip it.
6678 if ((docs.length > (j + 1)) && ((Date.parse(docs[j + 1].time) - doc.time) < 120000)) continue;
6679 timeline.push((doc.time - time) / 1000, doc.power);
6680 time = doc.time;
6681 }
6682 previousPower = doc.power;
6683 }
6684 obj.send({ action: 'powertimeline', nodeid: node._id, timeline: timeline, tag: command.tag });
6685 } else {
6686 // No records found, send current state if we have it
6687 var state = parent.parent.GetConnectivityState(command.nodeid);
6688 if (state != null) { obj.send({ action: 'powertimeline', nodeid: node._id, timeline: [state.powerState, Date.now(), state.powerState], tag: command.tag }); }
6689 }
6690 });
6691 });
6692 }
6693
6694 function serverCommandPrint(command) { console.log(command.value); }
6695
6696 function serverCommandRemovePhone(command) {
6697 // Do not allow this command when logged in using a login token
6698 if (req.session.loginToken != null) return;
6699
6700 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
6701 if (user.phone == null) return;
6702
6703 // Clear the user's phone
6704 delete user.phone;
6705 db.SetUser(user);
6706
6707 // Event the change
6708 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 97, msgArgs: [user.name], msg: 'Removed phone number of user ' + EscapeHtml(user.name), domain: domain.id };
6709 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6710 parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
6711 }
6712
6713 function serverCommandRemoveMessaging(command) {
6714 // Do not allow this command when logged in using a login token
6715 if (req.session.loginToken != null) return;
6716
6717 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
6718 if (user.msghandle == null) return;
6719
6720 // Clear the user's phone
6721 delete user.msghandle;
6722 db.SetUser(user);
6723
6724 // Event the change
6725 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 157, msgArgs: [user.name], msg: 'Removed messaging account of user ' + EscapeHtml(user.name), domain: domain.id };
6726 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6727 parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
6728 }
6729
6730 function serverCommandRemoveUserFromUserGroup(command) {
6731 var err = null;
6732 try {
6733 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; }
6734 else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = 'Invalid groupid'; }
6735 else if (common.validateString(command.userid, 1, 256) == false) { err = 'Invalid userid'; }
6736 else {
6737 var ugroupidsplit = command.ugrpid.split('/');
6738 if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = 'Invalid groupid'; }
6739 }
6740 } catch (ex) { err = 'Validation exception: ' + ex; }
6741
6742 // Fetch the domain
6743 var removeUserDomain = domain;
6744 if (obj.crossDomain !== true) { removeUserDomain = parent.parent.config.domains[ugroupidsplit[1]]; }
6745 if (removeUserDomain == null) { err = 'Invalid domain'; }
6746
6747 // Handle any errors
6748 if (err != null) {
6749 if (command.responseid != null) { obj.send({ action: 'removeuserfromusergroup', responseid: command.responseid, result: err }); }
6750 return;
6751 }
6752
6753 // Check if the user exists
6754 if (command.userid.startsWith('user/') == false) {
6755 if (parent.users['user/' + removeUserDomain.id + '/' + command.userid.toLowerCase()] != null) { command.userid = 'user/' + removeUserDomain.id + '/' + command.userid.toLowerCase(); }
6756 else if (parent.users['user/' + removeUserDomain.id + '/' + command.userid] != null) { command.userid = 'user/' + removeUserDomain.id + '/' + command.userid; }
6757 }
6758
6759 var chguser = parent.users[command.userid];
6760 if (chguser != null) {
6761 // Get the user group
6762 var group = parent.userGroups[command.ugrpid];
6763
6764 // If this user group is an externally managed user group, we can't remove a user from it.
6765 if ((group != null) && (group.membershipType != null)) return;
6766
6767 if ((chguser.links != null) && (chguser.links[command.ugrpid] != null)) {
6768 delete chguser.links[command.ugrpid];
6769
6770 // Notify user change
6771 var targets = ['*', 'server-users', user._id, chguser._id];
6772 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 67, msgArgs: [chguser.name], msg: 'User group membership changed: ' + chguser.name, domain: removeUserDomain.id };
6773 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
6774 parent.parent.DispatchEvent(targets, obj, event);
6775
6776 db.SetUser(chguser);
6777 parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
6778 }
6779
6780 if (group != null) {
6781 // Remove the user from the group
6782 if ((group.links != null) && (group.links[command.userid] != null)) {
6783 delete group.links[command.userid];
6784 db.Set(group);
6785
6786 // Notify user group change
6787 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, action: 'usergroupchange', links: group.links, msgid: 72, msgArgs: [chguser.name, group.name], msg: 'Removed user ' + chguser.name + ' from user group ' + group.name, domain: removeUserDomain.id };
6788 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
6789 parent.parent.DispatchEvent(['*', group._id, user._id, chguser._id], obj, event);
6790 }
6791 }
6792 }
6793
6794 if (command.responseid != null) { obj.send({ action: 'removeuserfromusergroup', responseid: command.responseid, result: 'ok' }); }
6795 }
6796
6797 function serverCommandReport(command) {
6798 if (common.validateInt(command.type, 1, 4) == false) return; // Validate type
6799 if (common.validateInt(command.groupBy, 1, 3) == false) return; // Validate groupBy: 1 = User, 2 = Device, 3 = Day
6800 if ((typeof command.start != 'number') || (typeof command.end != 'number') || (command.start >= command.end)) return; // Validate start and end time
6801 const manageAllDeviceGroups = ((user.siteadmin == 0xFFFFFFFF) && (parent.parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0 || (user.links && Object.keys(user.links).some(key => parent.parent.config.settings.managealldevicegroups.indexOf(key) >= 0))));
6802 if ((command.devGroup != null) && (manageAllDeviceGroups == false) && ((user.links == null) || (user.links[command.devGroup] == null))) return; // Asking for a device group that is not allowed
6803
6804 const msgIdFilter = [5, 10, 11, 12, 122, 123, 124, 125, 126, 144];
6805 switch (command.type) {
6806 case 1: {
6807 remoteSessionReport(command, manageAllDeviceGroups, msgIdFilter);
6808 break;
6809 }
6810 case 2: {
6811 trafficUsageReport(command, msgIdFilter);
6812 break;
6813 }
6814 case 3: {
6815 userLoginReport(command);
6816 break;
6817 }
6818 case 4: {
6819 databaseRecordsReport(command);
6820 break;
6821 }
6822 }
6823 }
6824
6825 function serverCommandServerClearErrorLog(command) {
6826 // Clear the server error log if user has site update permissions
6827 if (userHasSiteUpdate()) { fs.unlink(parent.parent.getConfigFilePath('mesherrors.txt'), function (err) { }); }
6828 }
6829
6830 function serverCommandServerConsole(command) {
6831 // Do not allow this command when logged in using a login token
6832 if (req.session.loginToken != null) return;
6833 // This is a server console message, only process this if full administrator
6834 if (user.siteadmin != SITERIGHT_ADMIN) return;
6835 // Only accept if the console is allowed for this domain
6836 if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.console !== true))) return;
6837
6838 var cmdargs = splitArgs(command.value);
6839 if (cmdargs.length == 0) return;
6840 const cmd = cmdargs[0].toLowerCase();
6841 cmdargs = parseArgs(cmdargs);
6842 var cmdData = { result: '', command: command, cmdargs: cmdargs };
6843
6844 // Find the command in the lookup table and run it.
6845 var cmdTableEntry = serverUserCommands[cmd];
6846 if (cmdTableEntry != null) { try { cmdTableEntry[0](cmdData); } catch (ex) { cmdData.result = '' + ex; }
6847 } else { cmdData.result = 'Unknown command \"' + cmd + '\", type \"help\" for list of available commands.'; }
6848
6849 // Send back the command result
6850 if (cmdData.result != '') { obj.send({ action: 'serverconsole', value: cmdData.result, tag: command.tag }); }
6851 }
6852
6853 function serverCommandServerErrors(command) {
6854 // Load the server error log
6855 if (userHasSiteUpdate() && domainHasMyServerErrorLog())
6856 fs.readFile(parent.parent.getConfigFilePath('mesherrors.txt'), 'utf8', function (err, data) { obj.send({ action: 'servererrors', data: data }); });
6857 }
6858
6859 function serverCommandServerConfig(command) {
6860 // Load the server config.json. This is a sensitive file so care must be taken to only send to trusted administrators.
6861 if (userHasSiteUpdate() && (domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.config === true))) {
6862 const configFilePath = common.joinPath(parent.parent.datapath, (parent.parent.args.configfile ? parent.parent.args.configfile : 'config.json'));
6863 fs.readFile(configFilePath, 'utf8', function (err, data) { obj.send({ action: 'serverconfig', data: data }); });
6864 }
6865 }
6866
6867 function serverCommandServerStats(command) {
6868 // Only accept if the "My Server" tab is allowed for this domain
6869 if (domain.myserver === false) return;
6870
6871 if ((user.siteadmin & 21) == 0) return; // Only site administrators with "site backup" or "site restore" or "site update" permissions can use this.
6872 if (common.validateInt(command.interval, 1000, 1000000) == false) {
6873 // Clear the timer
6874 if (obj.serverStatsTimer != null) { clearInterval(obj.serverStatsTimer); delete obj.serverStatsTimer; }
6875 } else {
6876 // Set the timer
6877 obj.SendServerStats();
6878 obj.serverStatsTimer = setInterval(obj.SendServerStats, command.interval);
6879 }
6880 }
6881
6882 function serverCommandServerTimelineStats(command) {
6883 // Only accept if the "My Server" tab is allowed for this domain
6884 if (domain.myserver === false) return;
6885
6886 if ((user.siteadmin & 21) == 0) return; // Only site administrators with "site backup" or "site restore" or "site update" permissions can use this.
6887 if (common.validateInt(command.hours, 0, 24 * 30) == false) return;
6888 db.GetServerStats(command.hours, function (err, docs) {
6889 if (err == null) { obj.send({ action: 'servertimelinestats', events: docs }); }
6890 });
6891 }
6892
6893 function serverCommandServerUpdate(command) {
6894 // Do not allow this command when logged in using a login token
6895 if (req.session.loginToken != null) return;
6896
6897 // Perform server update
6898 if (userHasSiteUpdate() && domainHasMyServerUpgrade() && !((command.version != null) && (typeof command.version != 'string')))
6899 parent.parent.performServerUpdate(command.version);
6900 }
6901
6902 function serverCommandServerVersion(command) {
6903 // Do not allow this command when logged in using a login token
6904 if (req.session.loginToken != null) return;
6905
6906 // Check the server version
6907 if (userHasSiteUpdate() && domainHasMyServerUpgrade())
6908 parent.parent.getServerTags(function (tags, err) { obj.send({ action: 'serverversion', tags: tags }); });
6909 }
6910
6911 function serverCommandSetClip(command) {
6912 if (common.validateString(command.data, 1, 65535) == false) return; // Check
6913
6914 // Get the node and the rights for this node
6915 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
6916 if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
6917
6918 // Send clipboard data to the agent
6919 var agent = parent.wsagents[node._id];
6920 if (agent != null) { try { agent.send(JSON.stringify({ action: 'setClip', data: command.data })); } catch (ex) { } }
6921 });
6922 }
6923
6924 function serverCommandSmsUser(command) {
6925 var errMsg = null, smsuser = null;
6926 if (parent.parent.smsserver == null) { errMsg = "SMS gateway not enabled"; }
6927 else if ((user.siteadmin & 2) == 0) { errMsg = "No user management rights"; }
6928 else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = "Invalid username"; }
6929 else if (common.validateString(command.msg, 1, 160) == false) { errMsg = "Invalid SMS message"; }
6930 else {
6931 smsuser = parent.users[command.userid];
6932 if (smsuser == null) { errMsg = "Invalid username"; }
6933 else if (smsuser.phone == null) { errMsg = "No phone number for this user"; }
6934 }
6935
6936 if (errMsg != null) { displayNotificationMessage(errMsg); return; }
6937
6938 parent.parent.smsserver.sendSMS(smsuser.phone, command.msg, function (success, msg) {
6939 if (success) {
6940 displayNotificationMessage("SMS succesfuly sent.", null, null, null, 27);
6941 } else {
6942 if (typeof msg == 'string') { displayNotificationMessage("SMS error: " + msg, null, null, null, 29, [msg]); } else { displayNotificationMessage("SMS error", null, null, null, 28); }
6943 }
6944 });
6945 }
6946
6947 function serverCommandMsgUser(command) {
6948 var errMsg = null, msguser = null;
6949 if ((parent.parent.msgserver == null) || (parent.parent.msgserver.providers == 0)) { errMsg = "Messaging server not enabled"; }
6950 else if ((user.siteadmin & 2) == 0) { errMsg = "No user management rights"; }
6951 else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = "Invalid username"; }
6952 else if (common.validateString(command.msg, 1, 160) == false) { errMsg = "Invalid message"; }
6953 else {
6954 msguser = parent.users[command.userid];
6955 if (msguser == null) { errMsg = "Invalid username"; }
6956 else if (msguser.msghandle == null) { errMsg = "No messaging service configured for this user"; }
6957 }
6958
6959 if (errMsg != null) { displayNotificationMessage(errMsg); return; }
6960
6961 parent.parent.msgserver.sendMessage(msguser.msghandle, command.msg, domain, function (success, msg) {
6962 if (success) {
6963 displayNotificationMessage("Message succesfuly sent.", null, null, null, 32);
6964 } else {
6965 if (typeof msg == 'string') { displayNotificationMessage("Messaging error: " + msg, null, null, null, 34, [msg]); } else { displayNotificationMessage("Messaging error", null, null, null, 33); }
6966 }
6967 });
6968 }
6969
6970 function serverCommandTrafficDelta(command) {
6971 const stats = parent.getTrafficDelta(obj.trafficStats);
6972 obj.trafficStats = stats.current;
6973 obj.send({ action: 'trafficdelta', delta: stats.delta });
6974 }
6975
6976 function serverCommandTrafficStats(command) {
6977 obj.send({ action: 'trafficstats', stats: parent.getTrafficStats() });
6978 }
6979
6980 function serverCommandUpdateAgents(command) {
6981 // Update agents for selected devices
6982 if (common.validateStrArray(command.nodeids, 1) == false) return; // Check nodeids
6983 for (var i in command.nodeids) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: command.nodeids[i], value: 'agentupdate' }, MESHRIGHT_ADMIN, 0); }
6984 }
6985
6986 function serverCommandUpdateUserImage(command) {
6987 if (req.session.loginToken != null) return; // Do not allow this command when logged in using a login token
6988
6989 var uid = user._id;
6990 if ((typeof command.userid == 'string') && ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0)) { uid = command.userid; }
6991
6992 var chguser = parent.users[uid], flags = 0, change = 0;
6993 if (chguser == null) return;
6994 if (typeof chguser.flags == 'number') { flags = chguser.flags; }
6995
6996 if (command.image == 0) {
6997 // Delete the image
6998 db.Remove('im' + uid);
6999 if ((flags & 1) != 0) { flags -= 1; change = 1; }
7000 } else if ((typeof command.image == 'string') && (command.image.length < 600000) && ((command.image.startsWith('data:image/png;base64,') || (command.image.startsWith('data:image/jpeg;base64,'))))) {
7001 // Save the new image
7002 db.Set({ _id: 'im' + uid, image: command.image });
7003 if ((flags & 1) == 0) { flags += 1; }
7004 change = 1;
7005 }
7006
7007 // Update the user if needed
7008 if (change == 1) {
7009 chguser.flags = flags;
7010 db.SetUser(chguser);
7011
7012 // Event the change
7013 var targets = ['*', 'server-users', user._id, chguser._id];
7014 var allTargetGroups = chguser.groups;
7015 if (allTargetGroups) { for (var i in allTargetGroups) { targets.push('server-users:' + i); } }
7016 var event = { etype: 'user', userid: uid, username: chguser.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 66, msgArgs: [chguser.name], msg: 'Account changed: ' + chguser.name, domain: domain.id, accountImageChange: 1 };
7017 if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
7018 parent.parent.DispatchEvent(targets, obj, event);
7019 }
7020 }
7021
7022 function serverCommandUrlArgs(command) {
7023 console.log(req.query);
7024 console.log(command.args);
7025 }
7026
7027 function serverCommandUsers(command) {
7028 // Request a list of all users
7029 if ((user.siteadmin & 2) == 0) { if (command.responseid != null) { obj.send({ action: 'users', responseid: command.responseid, result: 'Access denied' }); } return; }
7030 var docs = [];
7031 for (i in parent.users) {
7032 if (((obj.crossDomain === true) || (parent.users[i].domain == domain.id)) && (parent.users[i].name != '~')) {
7033 // If we are part of a user group, we can only see other members of our own group
7034 if ((obj.crossDomain === true) || (user.groups == null) || (user.groups.length == 0) || ((parent.users[i].groups != null) && (findOne(parent.users[i].groups, user.groups)))) {
7035 docs.push(parent.CloneSafeUser(parent.users[i]));
7036 }
7037 }
7038 }
7039 obj.send({ action: 'users', users: docs, tag: command.tag });
7040 }
7041
7042 function serverCommandVerifyEmail(command) {
7043 // Do not allow this command when logged in using a login token
7044 if (req.session.loginToken != null) return;
7045
7046 // If this account is settings locked, return here.
7047 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
7048
7049 // Send a account email verification email
7050 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
7051 if (common.validateString(command.email, 3, 1024) == false) return;
7052
7053 // Always lowercase the email address
7054 command.email = command.email.toLowerCase();
7055
7056 if ((domain.mailserver != null) && (obj.user.email.toLowerCase() == command.email)) {
7057 // Send the verification email
7058 domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req), req.query.key);
7059 }
7060 }
7061
7062 function serverCommandVerifyPhone(command) {
7063 // Do not allow this command when logged in using a login token
7064 if (req.session.loginToken != null) return;
7065
7066 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
7067 if (parent.parent.smsserver == null) return;
7068 if (common.validateString(command.phone, 1, 18) == false) return; // Check phone length
7069 if (isPhoneNumber(command.phone) == false) return; // Check phone
7070
7071 const code = common.zeroPad(getRandomSixDigitInteger(), 6);
7072 const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone, s: ws.sessionId });
7073 parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) {
7074 ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success }));
7075 });
7076 }
7077
7078 function serverCommandVerifyMessaging(command) {
7079 // Do not allow this command when logged in using a login token
7080 if (req.session.loginToken != null) return;
7081
7082 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
7083 if (parent.parent.msgserver == null) return;
7084 if (common.validateString(command.handle, 1, 1024) == false) return; // Check handle length
7085
7086 // Setup the handle for the right messaging service
7087 var handle = null;
7088 if ((command.service == 1) && ((parent.parent.msgserver.providers & 1) != 0)) { handle = 'telegram:@' + command.handle; }
7089 if ((command.service == 4) && ((parent.parent.msgserver.providers & 4) != 0)) { handle = 'discord:' + command.handle; }
7090 if ((command.service == 8) && ((parent.parent.msgserver.providers & 8) != 0)) { handle = 'xmpp:' + command.handle; }
7091 if ((command.service == 16) && ((parent.parent.msgserver.providers & 16) != 0)) { handle = parent.parent.msgserver.callmebotUrlToHandle(command.handle); }
7092 if ((command.service == 32) && ((parent.parent.msgserver.providers & 32) != 0)) { handle = 'pushover:' + command.handle; }
7093 if ((command.service == 64) && ((parent.parent.msgserver.providers & 64) != 0)) { handle = 'ntfy:' + command.handle; }
7094 if ((command.service == 128) && ((parent.parent.msgserver.providers & 128) != 0)) { handle = 'zulip:' + command.handle; }
7095 if ((command.service == 256) && ((parent.parent.msgserver.providers & 256) != 0)) { handle = 'slack:' + command.handle; }
7096 if (handle == null) return;
7097
7098 // Send a verification message
7099 const code = common.zeroPad(getRandomSixDigitInteger(), 6);
7100 const messagingCookie = parent.parent.encodeCookie({ a: 'verifyMessaging', c: code, p: handle, s: ws.sessionId });
7101 parent.parent.msgserver.sendMessagingCheck(domain, handle, code, parent.getLanguageCodes(req), function (success) {
7102 ws.send(JSON.stringify({ action: 'verifyMessaging', cookie: messagingCookie, success: success }));
7103 });
7104 }
7105
7106 function serverUserCommandHelp(cmdData) {
7107 var fin = '', f = '', availcommands = [];
7108 for (var i in serverUserCommands) { availcommands.push(i); }
7109 availcommands = availcommands.sort();
7110 while (availcommands.length > 0) { if (f.length > 80) { fin += (f + ',\r\n'); f = ''; } f += (((f != '') ? ', ' : ' ') + availcommands.shift()); }
7111 if (f != '') { fin += f; }
7112
7113 if (cmdData.cmdargs['_'].length == 0) {
7114 cmdData.result = 'Available commands: \r\n' + fin + '\r\nType help <command> for details.';
7115 } else {
7116 var cmd2 = cmdData.cmdargs['_'][0].toLowerCase();
7117 var cmdTableEntry = serverUserCommands[cmd2];
7118 if (cmdTableEntry) {
7119 if (cmdTableEntry[1] == '') {
7120 cmdData.result = 'No help available for this command.';
7121 } else {
7122 cmdData.result = cmdTableEntry[1]; }
7123 } else {
7124 cmdData.result = "This command does not exist.";
7125 }
7126 }
7127 }
7128
7129 function serverUserCommandCertExpire(cmdData) {
7130 const now = Date.now();
7131 for (var i in parent.webCertificateExpire) {
7132 const domainName = (i == '') ? '[Default]' : i;
7133 cmdData.result += domainName + ', expires in ' + Math.floor((parent.webCertificateExpire[i] - now) / 86400000) + ' day(s)\r\n';
7134 }
7135 }
7136
7137 function serverUserCommandWebPush(cmdData) {
7138 if (parent.parent.webpush == null) {
7139 cmdData.result = "Web push not supported.";
7140 } else {
7141 if (cmdData.cmdargs['_'].length != 1) {
7142 cmdData.result = "Usage: WebPush \"Message\"";
7143 } else {
7144 const pushSubscription = { "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABgIkO9hjXHWhMPiuk-ppNRw7r_pUZitddwCEK4ykdzeIxOIjFnYhIt_nr-qUca2mpZziwQsSEhYTUCiuYrhWnVDRweMtiUj16yJJq8V5jneaEaUYjEIe5jp3DOMNpoTm1aHgX74gCR8uTXSITcM97bNi-hRxcQ4f6Ie4WSAmoXpd89B_g", "keys": { "auth": "UB2sbLVK7ALnSHw5P1dahg", "p256dh": "BIoRbcNSxBuTjN39CCCUCHo1f4NxBJ1YDdu_k4MbPW_q3NK1_RufnydUzLPDp8ibBVItSI72-s48QJvOjQ_S8Ok" } }
7145 parent.parent.webpush.sendNotification(pushSubscription, cmdData.cmdargs['_'][0]).then(
7146 function (value) { try { ws.send(JSON.stringify({ action: 'OK', value: cmdData.result, tag: cmdData.command.tag })); } catch (ex) { } },
7147 function (error) { try { ws.send(JSON.stringify({ action: 'Error', value: cmdData.result, tag: cmdData.command.tag })); } catch (ex) { } }
7148 );
7149 }
7150 }
7151 }
7152
7153 function serverUserCommandAmtManager(cmdData) {
7154 if (parent.parent.amtManager == null) {
7155 cmdData.result = 'Intel AMT Manager not active.';
7156 } else {
7157 cmdData.result = parent.parent.amtManager.getStatusString();
7158 }
7159 }
7160
7161 function serverUserCommandCertHashes(cmdData) {
7162 cmdData.result += 'AgentCertHash: ' + parent.agentCertificateHashHex;
7163 for (var i in parent.webCertificateHashs) { cmdData.result += '\r\nwebCertificateHash (' + i + '): ' + common.rstr2hex(parent.webCertificateHashs[i]); }
7164 for (var i in parent.webCertificateFullHashs) { cmdData.result += '\r\nwebCertificateFullHash (' + i + '): ' + common.rstr2hex(parent.webCertificateFullHashs[i]); }
7165 cmdData.result += '\r\ndefaultWebCertificateHash: ' + common.rstr2hex(parent.defaultWebCertificateHash);
7166 cmdData.result += '\r\ndefaultWebCertificateFullHash: ' + common.rstr2hex(parent.defaultWebCertificateFullHash);
7167 }
7168
7169 function serverUserCommandAmtAcm(cmdData) {
7170 if ((domain.amtacmactivation == null) || (domain.amtacmactivation.acmmatch == null) || (domain.amtacmactivation.acmmatch.length == 0)) {
7171 cmdData.result = 'No Intel AMT activation certificates.';
7172 } else {
7173 if (domain.amtacmactivation.log != null) { cmdData.result += '--- Activation Log ---\r\nFile : ' + domain.amtacmactivation.log + '\r\n'; }
7174 for (var i in domain.amtacmactivation.acmmatch) {
7175 var acmcert = domain.amtacmactivation.acmmatch[i];
7176 cmdData.result += '--- Activation Certificate ' + (parseInt(i) + 1) + ' ---\r\nName : ' + acmcert.cn + '\r\nSHA1 : ' + acmcert.sha1 + '\r\nSHA256: ' + acmcert.sha256 + '\r\n';
7177 }
7178 }
7179 }
7180
7181 function serverUserCommandHeapDump(cmdData) {
7182 // Heapdump support, see example at:
7183 // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
7184 if (parent.parent.config.settings.heapdump === true) {
7185 var dumpFileName = parent.path.join(parent.parent.datapath, `heapDump-${Date.now()}.heapsnapshot`);
7186 try { ws.send(JSON.stringify({ action: 'serverconsole', value: "Generating dump file at: " + dumpFileName, tag: cmdData.command.tag })); } catch (ex) { }
7187 require('heapdump').writeSnapshot(dumpFileName, (err, filename) => {
7188 try { ws.send(JSON.stringify({ action: 'serverconsole', value: "Done.", tag: cmdData.command.tag })); } catch (ex) { }
7189 });
7190 } else {
7191 cmdData.result = "Heapdump not supported, add \"heapdump\":true to settings section of config.json.";
7192 }
7193 }
7194
7195 function serverUserCommandHeapDump2(cmdData) {
7196 var heapdump = null;
7197 try { heapdump = require('heapdump'); } catch (ex) { }
7198 if (heapdump == null) {
7199 cmdData.result = 'Heapdump module not installed, run "npm install heapdump".';
7200 } else {
7201 heapdump.writeSnapshot(function (err, filename) {
7202 if (err != null) {
7203 try { ws.send(JSON.stringify({ action: 'serverconsole', value: 'Unable to write heapdump: ' + err })); } catch (ex) { }
7204 } else {
7205 try { ws.send(JSON.stringify({ action: 'serverconsole', value: 'Wrote heapdump at ' + filename })); } catch (ex) { }
7206 }
7207 });
7208 }
7209 }
7210
7211 function serverUserCommandSMS(cmdData) {
7212 if (parent.parent.smsserver == null) {
7213 cmdData.result = "No SMS gateway in use.";
7214 } else {
7215 if (cmdData.cmdargs['_'].length != 2) {
7216 cmdData.result = "Usage: SMS \"PhoneNumber\" \"Message\".";
7217 } else {
7218 parent.parent.smsserver.sendSMS(cmdData.cmdargs['_'][0], cmdData.cmdargs['_'][1], function (status, msg) {
7219 if (typeof msg == 'string') {
7220 try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? ('Success: ' + msg) : ('Failed: ' + msg), tag: cmdData.command.tag })); } catch (ex) { }
7221 } else {
7222 try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? 'Success' : 'Failed', tag: cmdData.command.tag })); } catch (ex) { }
7223 }
7224 });
7225 }
7226 }
7227 }
7228
7229 function serverUserCommandMsg(cmdData) {
7230 if ((parent.parent.msgserver == null) || (parent.parent.msgserver.providers == 0)) {
7231 cmdData.result = "No messaging providers configured.";
7232 } else {
7233 if (cmdData.cmdargs['_'].length != 2) {
7234 var r = [];
7235 if ((parent.parent.msgserver.providers & 1) != 0) { r.push("Usage: MSG \"telegram:[@UserHandle]\" \"Message\"."); }
7236 if ((parent.parent.msgserver.providers & 2) != 0) { r.push("Usage: MSG \"signal:[UserHandle]\" \"Message\"."); }
7237 if ((parent.parent.msgserver.providers & 4) != 0) { r.push("Usage: MSG \"discord:[Username#0000]\" \"Message\"."); }
7238 if ((parent.parent.msgserver.providers & 8) != 0) { r.push("Usage: MSG \"xmpp:[username@server.com]\" \"Message\"."); }
7239 if ((parent.parent.msgserver.providers & 32) != 0) { r.push("Usage: MSG \"pushover:[userkey]\" \"Message\"."); }
7240 if ((parent.parent.msgserver.providers & 64) != 0) { r.push("Usage: MSG \"ntfy:[topic]\" \"Message\"."); }
7241 if ((parent.parent.msgserver.providers & 128) != 0) { r.push("Usage: MSG \"zulip:[topic]\" \"Message\"."); }
7242 if ((parent.parent.msgserver.providers & 256) != 0) { r.push("Usage: MSG \"slack:[webhook]\" \"Message\"."); }
7243 cmdData.result = r.join('\r\n');
7244 } else {
7245 parent.parent.msgserver.sendMessage(cmdData.cmdargs['_'][0], cmdData.cmdargs['_'][1], domain, function (status, msg) {
7246 if (typeof msg == 'string') {
7247 try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? ('Success: ' + msg) : ('Failed: ' + msg), tag: cmdData.command.tag })); } catch (ex) { }
7248 } else {
7249 try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? 'Success' : 'Failed', tag: cmdData.command.tag })); } catch (ex) { }
7250 }
7251 });
7252 }
7253 }
7254 }
7255
7256 function serverUserCommandEmail(cmdData) {
7257 if (domain.mailserver == null) {
7258 cmdData.result = "No email service enabled.";
7259 } else {
7260 if (cmdData.cmdargs['_'].length != 3) {
7261 cmdData.result = "Usage: email \"user@sample.com\" \"Subject\" \"Message\".";
7262 } else {
7263 domain.mailserver.sendMail(cmdData.cmdargs['_'][0], cmdData.cmdargs['_'][1], cmdData.cmdargs['_'][2]);
7264 cmdData.result = "Done.";
7265 }
7266 }
7267 }
7268
7269 function serverUserCommandEmailNotifications(cmdData) {
7270 if (domain.mailserver == null) {
7271 cmdData.result = "No email service enabled.";
7272 } else {
7273 var x = '';
7274 for (var userid in domain.mailserver.deviceNotifications) {
7275 x += userid + '\r\n';
7276 for (var nodeid in domain.mailserver.deviceNotifications[userid].nodes) {
7277 const info = domain.mailserver.deviceNotifications[userid].nodes[nodeid];
7278 x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n';
7279 }
7280 }
7281 cmdData.result = ((x == '') ? 'None' : x);
7282 }
7283 }
7284
7285 function serverUserCommandMessageNotifications(cmdData) {
7286 if (parent.parent.msgserver == null) {
7287 cmdData.result = "No messaging service enabled.";
7288 } else {
7289 var x = '';
7290 for (var userid in parent.parent.msgserver.deviceNotifications) {
7291 x += userid + '\r\n';
7292 for (var nodeid in parent.parent.msgserver.deviceNotifications[userid].nodes) {
7293 const info = parent.parent.msgserver.deviceNotifications[userid].nodes[nodeid];
7294 x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n';
7295 }
7296 }
7297 cmdData.result = ((x == '') ? 'None' : x);
7298 }
7299 }
7300
7301 function serverUserCommandLe(cmdData) {
7302 if (parent.parent.letsencrypt == null) {
7303 cmdData.result = "Let's Encrypt not in use.";
7304 } else {
7305 cmdData.result = JSON.stringify(parent.parent.letsencrypt.getStats(), null, 4);
7306 }
7307 }
7308
7309 function serverUserCommandLeCheck(cmdData) {
7310 if (parent.parent.letsencrypt == null) {
7311 cmdData.result = "Let's Encrypt not in use.";
7312 } else {
7313 cmdData.result = ["CertOK", "Request:NoCert", "Request:Expire", "Request:MissingNames"][parent.parent.letsencrypt.checkRenewCertificate()];
7314 }
7315 }
7316
7317 function serverUserCommandLeEvents(cmdData) {
7318 if (parent.parent.letsencrypt == null) {
7319 cmdData.result = "Let's Encrypt not in use.";
7320 } else {
7321 cmdData.result = parent.parent.letsencrypt.events.join('\r\n');
7322 }
7323 }
7324
7325 function serverUserCommandBadLogins(cmdData) {
7326 if (parent.parent.config.settings.maxinvalidlogin == false) {
7327 cmdData.result = 'Bad login filter is disabled.';
7328 } else {
7329 if (cmdData.cmdargs['_'] == 'reset') {
7330 // Reset bad login table
7331 parent.badLoginTable = {};
7332 parent.badLoginTableLastClean = 0;
7333 cmdData.result = 'Done.';
7334 } else if (cmdData.cmdargs['_'] == '') {
7335 // Show current bad login table
7336 if (typeof parent.parent.config.settings.maxinvalidlogin.coolofftime == 'number') {
7337 cmdData.result = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s), " + parent.parent.config.settings.maxinvalidlogin.coolofftime + " minute(s) cooloff.\r\n";
7338 } else {
7339 cmdData.result = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s).\r\n";
7340 }
7341 var badLoginCount = 0;
7342 parent.cleanBadLoginTable();
7343 for (var i in parent.badLoginTable) {
7344 badLoginCount++;
7345 if (typeof parent.badLoginTable[i] == 'number') {
7346 cmdData.result += (i + " - Cooloff for " + Math.floor((parent.badLoginTable[i] - Date.now()) / 60000) + " minute(s)\r\n");
7347 } else {
7348 cmdData.result += (i + ' - ' + parent.badLoginTable[i].length + " attempt(s) until Cooloff ban\r\n");
7349 }
7350 }
7351 if (badLoginCount == 0) { cmdData.result += 'No bad logins.'; }
7352 } else {
7353 cmdData.result = 'Usage: badlogin [reset]';
7354 }
7355 }
7356 }
7357
7358 function serverUserCommandBad2fa(cmdData) {
7359 if (parent.parent.config.settings.maxinvalid2fa == false) {
7360 cmdData.result = 'Bad 2FA filter is disabled.';
7361 } else {
7362 if (cmdData.cmdargs['_'] == 'reset') {
7363 // Reset bad login table
7364 parent.bad2faTable = {};
7365 parent.bad2faTableLastClean = 0;
7366 cmdData.result = 'Done.';
7367 } else if (cmdData.cmdargs['_'] == '') {
7368 // Show current bad login table
7369 if (typeof parent.parent.config.settings.maxinvalid2fa.coolofftime == 'number') {
7370 cmdData.result = "Max is " + parent.parent.config.settings.maxinvalid2fa.count + " bad 2FA(s) in " + parent.parent.config.settings.maxinvalid2fa.time + " minute(s), " + parent.parent.config.settings.maxinvalid2fa.coolofftime + " minute(s) cooloff.\r\n";
7371 } else {
7372 cmdData.result = "Max is " + parent.parent.config.settings.maxinvalid2fa.count + " bad 2FA(s) in " + parent.parent.config.settings.maxinvalid2fa.time + " minute(s).\r\n";
7373 }
7374 var bad2faCount = 0;
7375 parent.cleanBad2faTable();
7376 for (var i in parent.bad2faTable) {
7377 bad2faCount++;
7378 if (typeof parent.bad2faTable[i] == 'number') {
7379 cmdData.result += "Cooloff for " + Math.floor((parent.bad2faTable[i] - Date.now()) / 60000) + " minute(s)\r\n";
7380 } else {
7381 if (parent.bad2faTable[i].length > 1) {
7382 cmdData.result += (i + ' - ' + parent.bad2faTable[i].length + " records\r\n");
7383 } else {
7384 cmdData.result += (i + ' - ' + parent.bad2faTable[i].length + " record\r\n");
7385 }
7386 }
7387 }
7388 if (bad2faCount == 0) { cmdData.result += 'No bad 2FA.'; }
7389 } else {
7390 cmdData.result = 'Usage: bad2fa [reset]';
7391 }
7392 }
7393 }
7394
7395 function serverUserCommandDispatchTable(cmdData) {
7396 for (var i in parent.parent.eventsDispatch) {
7397 cmdData.result += (i + ', ' + parent.parent.eventsDispatch[i].length + '\r\n');
7398 }
7399 }
7400
7401 function serverUserCommandDropAllCira(cmdData) {
7402 if (parent.parent.mpsserver == null) { cmdData.result = 'MPS not setup.'; return; }
7403 const dropCount = parent.parent.mpsserver.dropAllConnections();
7404 cmdData.result = 'Dropped ' + dropCount + ' connection(s).';
7405 }
7406
7407 function serverUserCommandDupAgents(cmdData) {
7408 for (var i in parent.duplicateAgentsLog) {
7409 cmdData.result += JSON.stringify(parent.duplicateAgentsLog[i]) + '\r\n';
7410 }
7411 if (cmdData.result == '') { cmdData.result = 'No duplicate agents in log.'; }
7412 }
7413
7414 function serverUserCommandAgentStats(cmdData) {
7415 var stats = parent.getAgentStats();
7416 for (var i in stats) {
7417 if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
7418 }
7419 }
7420
7421 function serverUserCommandAgentIssues(cmdData) {
7422 var stats = parent.getAgentIssues();
7423 if (stats.length == 0) {
7424 cmdData.result = "No agent issues.";
7425 } else {
7426 for (var i in stats) { cmdData.result += stats[i].join(', ') + '\r\n'; }
7427 }
7428 }
7429
7430 function serverUserCommandWebStats(cmdData) {
7431 var stats = parent.getStats();
7432 for (var i in stats) {
7433 if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
7434 }
7435 }
7436
7437 function serverUserCommandTrafficStats(cmdData) {
7438 var stats = parent.getTrafficStats();
7439 for (var i in stats) {
7440 if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
7441 }
7442 }
7443
7444 function serverUserCommandTrafficDelta(cmdData) {
7445 const stats = parent.getTrafficDelta(obj.trafficStats);
7446 obj.trafficStats = stats.current;
7447 for (var i in stats.delta) {
7448 if (typeof stats.delta[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats.delta[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats.delta[i] + '\r\n'); }
7449 }
7450 }
7451
7452 function serverUserCommandWatchdog(cmdData) {
7453 if (parent.parent.watchdog == null) {
7454 cmdData.result = 'Server watchdog not active.';
7455 } else {
7456 cmdData.result = 'Server watchdog active.\r\n';
7457 if (parent.parent.watchdogmaxtime != null) { cmdData.result += 'Largest timeout was ' + parent.parent.watchdogmax + 'ms on ' + parent.parent.watchdogmaxtime + '\r\n'; }
7458 for (var i in parent.parent.watchdogtable) { cmdData.result += parent.parent.watchdogtable[i] + '\r\n'; }
7459 }
7460 }
7461
7462 function serverUserCommand2faLock(cmdData) {
7463 var arg = null;
7464 if (cmdData.cmdargs['_'].length > 0) { arg = cmdData.cmdargs['_'][0]; }
7465 if (domain.passwordrequirements == null) { domain.passwordrequirements = {}; }
7466 if (arg == 'set') {
7467 // TODO: Change 2FA lock for peer servers
7468 domain.passwordrequirements.lock2factor = true;
7469 cmdData.result = "2FA lock is set";
7470 parent.parent.DispatchEvent(['server-allusers'], obj, { action: 'serverinfochange', lock2factor: true, nolog: 1, domain: domain.id });
7471 } else if (arg == 'clear') {
7472 // TODO: Change 2FA lock for peer servers
7473 delete domain.passwordrequirements.lock2factor;
7474 cmdData.result = "2FA lock is cleared";
7475 parent.parent.DispatchEvent(['server-allusers'], obj, { action: 'serverinfochange', lock2factor: false, nolog: 1, domain: domain.id });
7476 } else {
7477 cmdData.result = (domain.passwordrequirements.lock2factor == true) ? "2FA lock is set" : "2FA lock is cleared";
7478 cmdData.result += ", use '2falock [set/clear]' to change the lock state."
7479 }
7480 }
7481
7482 function serverUserCommandAcceleratorsStats(cmdData) {
7483 var stats = parent.parent.certificateOperations.getAcceleratorStats();
7484 for (var i in stats) {
7485 if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
7486 }
7487 }
7488
7489 function serverUserCommandMpsStats(cmdData) {
7490 if (parent.parent.mpsserver == null) {
7491 cmdData.result = 'MPS not enabled.';
7492 } else {
7493 var stats = parent.parent.mpsserver.getStats();
7494 for (var i in stats) {
7495 if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
7496 }
7497 }
7498 }
7499
7500 function serverUserCommandMps(cmdData) {
7501 if (parent.parent.mpsserver == null) {
7502 cmdData.result = 'MPS not enabled.';
7503 } else {
7504 const connectionTypes = ['CIRA', 'Relay', 'LMS'];
7505 for (var nodeid in parent.parent.mpsserver.ciraConnections) {
7506 cmdData.result += nodeid;
7507 var connections = parent.parent.mpsserver.ciraConnections[nodeid];
7508 for (var i in connections) { cmdData.result += ', ' + connectionTypes[connections[i].tag.connType]; }
7509 cmdData.result += '\r\n';
7510 }
7511 if (cmdData.result == '') { cmdData.result = 'MPS has not connections.'; }
7512 }
7513 }
7514
7515 function serverUserCommandDbStats(cmdData) {
7516 parent.parent.db.getDbStats(function (stats) {
7517 var r2 = '';
7518 for (var i in stats) { r2 += (i + ': ' + stats[i] + '\r\n'); }
7519 try { ws.send(JSON.stringify({ action: 'serverconsole', value: r2, tag: cmdData.command.tag })); } catch (ex) { }
7520 });
7521 }
7522
7523 function serverUserCommandDbCounters(cmdData) {
7524 try { ws.send(JSON.stringify({ action: 'serverconsole', value: JSON.stringify(parent.parent.db.dbCounters, null, 2), tag: cmdData.command.tag })); } catch (ex) { }
7525 }
7526
7527 function serverUserCommandServerUpdate(cmdData) {
7528 cmdData.result = 'Performing server update...';
7529 var version = null;
7530
7531 if (cmdData.cmdargs['_'].length > 0) {
7532 version = cmdData.cmdargs['_'][0];
7533
7534 // This call is SLOW. We only want to validate version if we have to
7535 if (version != 'stable' && version != 'latest') {
7536 parent.parent.getServerVersions((data) => {
7537 var versions = JSON.parse(data);
7538
7539 if (versions.includes(version)) {
7540 if (parent.parent.performServerUpdate(version) == false) {
7541 try {
7542 ws.send(JSON.stringify({ action: 'serverconsole',
7543 value: 'Server self-update not possible.'}));
7544 } catch (ex) { }
7545 }
7546 } else {
7547 try {
7548 ws.send(JSON.stringify({ action: 'serverconsole',
7549 value: 'Invalid version. Aborting update'}));
7550 } catch (ex) { }
7551 }
7552 });
7553 } else {
7554 if (parent.parent.performServerUpdate(version) == false) {
7555 cmdData.result = 'Server self-update not possible.';
7556 }
7557 }
7558 } else {
7559 if (parent.parent.performServerUpdate(version) == false) {
7560 cmdData.result = 'Server self-update not possible.';
7561 }
7562 }
7563 }
7564
7565 function serverUserCommandPrint(cmdData) {
7566 console.log(cmdData.cmdargs['_'][0]);
7567 }
7568
7569 function serverUserCommandAmtPasswords(cmdData) {
7570 if (parent.parent.amtPasswords == null) {
7571 cmdData.result = "No Intel AMT password table."
7572 } else {
7573 for (var i in parent.parent.amtPasswords) { cmdData.result += (i + ' - ' + parent.parent.amtPasswords[i].join(', ') + '\r\n'); }
7574 }
7575 }
7576
7577 function serverUserCommandAmtStats(cmdData) {
7578 parent.parent.db.GetAllType('node', function (err, docs) {
7579 var r = '';
7580 if (err != null) {
7581 r = "Error occured.";
7582 } else if ((docs == null) || (docs.length == 0)) {
7583 r = "No devices in database"
7584 } else {
7585 var amtData = { total: 0, versions: {}, state: {} };
7586 for (var i in docs) {
7587 const node = docs[i];
7588 if (node.intelamt != null) {
7589 amtData['total']++;
7590 if (node.intelamt.ver != null) { if (amtData.versions[node.intelamt.ver] == null) { amtData.versions[node.intelamt.ver] = 1; } else { amtData.versions[node.intelamt.ver]++; } }
7591 if (node.intelamt.state != null) { if (amtData.state[node.intelamt.state] == null) { amtData.state[node.intelamt.state] = 1; } else { amtData.state[node.intelamt.state]++; } }
7592 }
7593 }
7594 if (amtData.total == 0) {
7595 r = "No Intel AMT devices found"
7596 } else {
7597 r = "Total Intel AMT devices: " + amtData['total'] + '\r\n';
7598 r += "Un-provisionned: " + amtData['state'][0] + '\r\n';
7599 r += "Provisionned: " + amtData['state'][2] + '\r\n';
7600 r += "Versions: " + '\r\n';
7601
7602 // Sort the Intel AMT versions
7603 var amtVersions = [];
7604 for (var i in amtData.versions) { if (amtVersions.indexOf(i) == -1) { amtVersions.push(i); } }
7605 var collator = new Intl.Collator([], { numeric: true });
7606 amtVersions.sort((a, b) => collator.compare(a, b));
7607 for (var i in amtVersions) { r += ' ' + amtVersions[i] + ': ' + amtData.versions[amtVersions[i]] + '\r\n'; }
7608 }
7609 }
7610 try { ws.send(JSON.stringify({ action: 'serverconsole', value: r, tag: cmdData.command.tag })); } catch (ex) { }
7611 });
7612 }
7613
7614 function serverUserCommandUpdateCheck(cmdData) {
7615 parent.parent.getServerTags(function (tags, error) {
7616 var r2 = '';
7617 if (error != null) { r2 += 'Exception: ' + error + '\r\n'; }
7618 else { for (var i in tags) { r2 += i + ': ' + tags[i] + '\r\n'; } }
7619 try { ws.send(JSON.stringify({ action: 'serverconsole', value: r2, tag: cmdData.command.tag })); } catch (ex) { }
7620 });
7621 cmdData.result = "Checking server update...";
7622 }
7623
7624 function serverUserCommandMaintenance(cmdData) {
7625 var arg = null, changed = false;
7626 if ((cmdData.cmdargs['_'] != null) && (cmdData.cmdargs['_'][0] != null)) { arg = cmdData.cmdargs['_'][0].toLowerCase(); }
7627 if (arg == 'enabled') { parent.parent.config.settings.maintenancemode = 1; changed = true; }
7628 else if (arg == 'disabled') { delete parent.parent.config.settings.maintenancemode; changed = true; }
7629 cmdData.result = 'Maintenance mode: ' + ((parent.parent.config.settings.maintenancemode == null) ? 'Disabled' : 'Enabled');
7630 if (changed == false) { cmdData.result += '\r\nTo change type: maintenance [enabled|disabled]'; }
7631 }
7632
7633 function serverUserCommandInfo(cmdData) {
7634 function convertSeconds (s, form) {
7635 if (!['long', 'shortprecise'].includes(form)) {
7636 form = 'shortprecise';
7637 }
7638 let t = {}, r = '';
7639 t.d = Math.floor(s / (24 * 3600));
7640 s %= 24 * 3600;
7641 t.h= Math.floor(s / 3600);
7642 s %= 3600;
7643 t.m = Math.floor(s / 60);
7644 t.s =(s%60).toFixed(0);
7645 if ( form == 'long') {
7646 r = t.d + ((t.d == 1) ? ' day, ' : ' days, ') + t.h + ((t.h == 1) ? ' hour, ' : ' hours, ') + t.m + ((t.m == 1) ? ' minute, ' : ' minutes, ') + t.s+ ((t.s == 1) ? ' second' : ' seconds');
7647 } else if (form == 'shortprecise') {
7648 r = String(t.d).padStart(2, '0') + ':' + String(t.h).padStart(2, '0') + ':' + String(t.m).padStart(2, '0') + ':' + String((s%60).toFixed(2)).padStart(5, '0') + 's';
7649 }
7650 return r;
7651 }
7652 var info = {}, arg = null, t = {}, r = '';
7653 if ((cmdData.cmdargs['_'] != null) && (cmdData.cmdargs['_'][0] != null)) { arg = cmdData.cmdargs['_'][0].toLowerCase(); }
7654 try { info.meshVersion = 'v' + parent.parent.currentVer; } catch (ex) { }
7655 try { info.nodeVersion = process.version; } catch (ex) { }
7656 try { info.runMode = (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][(args.lanonly ? 2 : (args.wanonly ? 1 : 0))]); } catch (ex) { }
7657 try { info.productionMode = ((process.env.NODE_ENV != null) && (process.env.NODE_ENV == 'production')); } catch (ex) { }
7658 try { info.database = ["Unknown", "NeDB", "MongoJS", "MongoDB", "MariaDB", "MySQL", "PostgreSQL", "AceBase", "SQLite"][parent.parent.db.databaseType]; } catch (ex) { }
7659 try { if (parent.db.databaseType == 3) { info.dbChangeStream = parent.db.changeStream; info.dbBulkOperations = (parent.parent.config.settings.mongodbbulkoperations === true); } } catch (ex) { }
7660 try { if (parent.parent.multiServer != null) { info.serverId = parent.parent.multiServer.serverid; } } catch (ex) { }
7661 try { if (parent.parent.pluginHandler != null) { info.plugins = []; for (var i in parent.parent.pluginHandler.plugins) { info.plugins.push(i); } } } catch (ex) { }
7662 try { info.platform = process.platform; } catch (ex) { }
7663 try { info.arch = process.arch; } catch (ex) { }
7664 try { info.pid = process.pid; } catch (ex) { }
7665 if (arg == 'h') {
7666 try {
7667 info.uptime = convertSeconds(process.uptime(), 'long');
7668 info.cpuUsage = {
7669 system: (convertSeconds(process.cpuUsage().system /1000000)),
7670 user: (convertSeconds(process.cpuUsage().user /1000000))
7671 }
7672 info.memoryUsage = {};
7673 for (const [key,value] of Object.entries(process.memoryUsage())){
7674 info.memoryUsage[key] = ([value]/1048576).toFixed(2) + 'Mb';
7675 }
7676 } catch (ex) { }
7677 }
7678 else {
7679 try { info.uptime = process.uptime(); } catch (ex) { }
7680 try { info.cpuUsage = process.cpuUsage(); } catch (ex) { }
7681 try { info.memoryUsage = process.memoryUsage(); } catch (ex) { }
7682 }
7683 try { info.warnings = parent.parent.getServerWarnings(); } catch (ex) { console.log(ex); }
7684 try { info.allDevGroupManagers = parent.parent.config.settings.managealldevicegroups; } catch (ex) { }
7685 try { if (process.traceDeprecation == true) { info.traceDeprecation = true; } } catch (ex) { }
7686 cmdData.result = JSON.stringify(info, null, 4);
7687 }
7688
7689 function serverUserCommandNodeConfig(cmdData) {
7690 cmdData.result = JSON.stringify(process.config, null, 4);
7691 }
7692
7693 function serverUserCommandVersions(cmdData) {
7694 cmdData.result = JSON.stringify(process.versions, null, 4);
7695 }
7696
7697 function serverUserCommandArgs(cmdData) {
7698 cmdData.result = 'args: ' + JSON.stringify(cmdData.cmdargs);
7699 }
7700
7701 function serverUserCommandUserSessions(cmdData) {
7702 var userSessionCount = 0;
7703 var filter = null;
7704 var arg = cmdData.cmdargs['_'][0];
7705 if (typeof arg == 'string') { if (arg.indexOf('/') >= 0) { filter = arg; } else { filter = ('user/' + domain.id + '/' + arg); } }
7706 for (var i in parent.wssessions) {
7707 if ((filter == null) || (filter == i)) {
7708 userSessionCount++;
7709 cmdData.result += (i + ', ' + parent.wssessions[i].length + ' session' + ((parent.wssessions[i].length > 1) ? 's' : '') + '.\r\n');
7710 for (var j in parent.wssessions[i]) {
7711 var extras = "";
7712 if (parent.wssessions[i][j].satelliteFlags) { extras += ', Satellite'; }
7713 cmdData.result += ' ' + parent.wssessions[i][j].clientIp + ' --> ' + parent.wssessions[i][j].sessionId + extras + ((parent.wssessions[i][j].xclosed) ? (', CLOSED-' + parent.wssessions[i][j].xclosed):'') + '\r\n';
7714 }
7715 }
7716 }
7717 if (userSessionCount == 0) { cmdData.result = 'None.'; }
7718 }
7719
7720 function serverUserCommandCloseUserSessions(cmdData) {
7721 var userSessionCount = 0;
7722 var filter = null;
7723 var arg = cmdData.cmdargs['_'][0];
7724 if (typeof arg == 'string') { if (arg.indexOf('/') >= 0) { filter = arg; } else { filter = ('user/' + domain.id + '/' + arg); } }
7725 if (filter == null) {
7726 cmdData.result += "Usage: closeusersessions <username>";
7727 } else {
7728 cmdData.result += "Closing user sessions for: " + filter + '\r\n';
7729 for (var i in parent.wssessions) {
7730 if (filter == i) {
7731 userSessionCount++;
7732 for (var j in parent.wssessions[i]) {
7733 parent.wssessions[i][j].send(JSON.stringify({ action: 'stopped', msg: "Administrator forced disconnection" }));
7734 parent.wssessions[i][j].close();
7735 }
7736 }
7737 }
7738 if (userSessionCount < 2) { cmdData.result += 'Disconnected ' + userSessionCount + ' session.'; } else { cmdData.result += 'Disconnected ' + userSessionCount + ' sessions.'; };
7739 }
7740 }
7741
7742 function serverUserCommandResetServer(cmdData) {
7743 console.log("Server restart...");
7744 process.exit(0);
7745 }
7746
7747 function serverUserCommandTaskLimiter(cmdData) {
7748 if (parent.parent.taskLimiter != null) {
7749 //var obj = { maxTasks: maxTasks, maxTaskTime: (maxTaskTime * 1000), nextTaskId: 0, currentCount: 0, current: {}, pending: [[], [], []], timer: null };
7750 const tl = parent.parent.taskLimiter;
7751 cmdData.result += 'MaxTasks: ' + tl.maxTasks + ', NextTaskId: ' + tl.nextTaskId + '\r\n';
7752 cmdData.result += 'MaxTaskTime: ' + (tl.maxTaskTime / 1000) + ' seconds, Timer: ' + (tl.timer != null) + '\r\n';
7753 var c = [];
7754 for (var i in tl.current) { c.push(i); }
7755 cmdData.result += 'Current (' + tl.currentCount + '): [' + c.join(', ') + ']\r\n';
7756 cmdData.result += 'Pending (High/Med/Low): ' + tl.pending[0].length + ', ' + tl.pending[1].length + ', ' + tl.pending[2].length + '\r\n';
7757 }
7758 }
7759
7760 function serverUserCommandSetMaxTasks(cmdData) {
7761 if ((cmdData.cmdargs["_"].length != 1) || (parseInt(cmdData.cmdargs["_"][0]) < 1) || (parseInt(cmdData.cmdargs["_"][0]) > 1000)) {
7762 cmdData.result = 'Usage: setmaxtasks [1 to 1000]';
7763 } else {
7764 parent.parent.taskLimiter.maxTasks = parseInt(cmdData.cmdargs["_"][0]);
7765 cmdData.result = 'MaxTasks set to ' + parent.parent.taskLimiter.maxTasks + '.';
7766 }
7767 }
7768
7769 function serverUserCommandCores(cmdData) {
7770 if (parent.parent.defaultMeshCores != null) {
7771 for (var i in parent.parent.defaultMeshCores) {
7772 cmdData.result += i + ': ' + parent.parent.defaultMeshCores[i].length + ' bytes\r\n';
7773 }
7774 }
7775 }
7776
7777 function serverUserCommandShowPaths(cmdData) {
7778 cmdData.result = 'Parent: ' + parent.parent.parentpath + '\r\n';
7779 cmdData.result += 'Data: ' + parent.parent.datapath + '\r\n';
7780 cmdData.result += 'Files: ' + parent.parent.filespath + '\r\n';
7781 cmdData.result += 'Backup: ' + parent.parent.backuppath + '\r\n';
7782 cmdData.result += 'Record: ' + parent.parent.recordpath + '\r\n';
7783 cmdData.result += 'WebPublic: ' + parent.parent.webPublicPath + '\r\n';
7784 cmdData.result += 'WebViews: ' + parent.parent.webViewsPath + '\r\n';
7785 cmdData.result += 'WebEmails: ' + parent.parent.webEmailsPath + '\r\n';
7786 if (parent.parent.webPublicOverridePath) { cmdData.result += 'XWebPublic: ' + parent.parent.webPublicOverridePath + '\r\n'; }
7787 if (parent.parent.webViewsOverridePath) { cmdData.result += 'XWebViews: ' + parent.parent.webViewsOverridePath + '\r\n'; }
7788 if (parent.parent.webEmailsOverridePath) { cmdData.result += 'XWebEmails: ' + parent.parent.webEmailsOverridePath + '\r\n'; }
7789 if (domain.webpublicpath) { cmdData.result += 'DomainWebPublic: ' + domain.webpublicpath + '\r\n'; }
7790 if (domain.webviewspath) { cmdData.result += 'DomainWebViews: ' + domain.webviewspath + '\r\n'; }
7791 if (domain.webemailspath) { cmdData.result += 'DomainWebEmails: ' + domain.webemailspath + '\r\n'; }
7792 }
7793
7794 function serverUserCommandMigrationAgents(cmdData) {
7795 if (parent.parent.swarmserver == null) {
7796 cmdData.result = 'Swarm server not running.';
7797 } else {
7798 for (var i in parent.parent.swarmserver.migrationAgents) {
7799 var arch = parent.parent.swarmserver.migrationAgents[i];
7800 for (var j in arch) { var agent = arch[j]; cmdData.result += 'Arch ' + agent.arch + ', Ver ' + agent.ver + ', Size ' + ((agent.binary == null) ? 0 : agent.binary.length) + '<br />'; }
7801 }
7802 }
7803 }
7804
7805 function serverUserCommandSwarmStats(cmdData) {
7806 if (parent.parent.swarmserver == null) {
7807 cmdData.result = 'Swarm server not running.';
7808 } else {
7809 for (var i in parent.parent.swarmserver.stats) {
7810 if (typeof parent.parent.swarmserver.stats[i] == 'object') {
7811 cmdData.result += i + ': ' + JSON.stringify(parent.parent.swarmserver.stats[i]) + '\r\n';
7812 } else {
7813 cmdData.result += i + ': ' + parent.parent.swarmserver.stats[i] + '\r\n';
7814 }
7815 }
7816 }
7817 }
7818
7819 function serverUserCommandRelays(cmdData) {
7820 for (var i in parent.wsrelays) {
7821 cmdData.result += 'id: ' + i + ', ' + ((parent.wsrelays[i].state == 2) ? 'connected' : 'pending');
7822 if (parent.wsrelays[i].peer1 != null) {
7823 cmdData.result += ', ' + cleanRemoteAddr(parent.wsrelays[i].peer1.req.clientIp);
7824 if (parent.wsrelays[i].peer1.user) { cmdData.result += ' (User:' + parent.wsrelays[i].peer1.user.name + ')' }
7825 }
7826 if (parent.wsrelays[i].peer2 != null) {
7827 cmdData.result += ' to ' + cleanRemoteAddr(parent.wsrelays[i].peer2.req.clientIp);
7828 if (parent.wsrelays[i].peer2.user) { cmdData.result += ' (User:' + parent.wsrelays[i].peer2.user.name + ')' }
7829 }
7830 cmdData.result += '\r\n';
7831 }
7832 if (cmdData.result == '') { cmdData.result = 'No relays.'; }
7833 }
7834
7835 // removeinactivedevices showall|showremoved
7836 function serverUserCommandRemoveInactiveDevices(cmdData) {
7837 var arg = cmdData.cmdargs['_'][0];
7838 if ((arg == null) && (arg != 'showremoved') && (arg != 'showall')) {
7839 cmdData.result = 'Usage: removeinactivedevices [showremoved|showall]';
7840 } else {
7841 parent.db.removeInactiveDevices((arg == 'showall'), function (msg) { try { ws.send(JSON.stringify({ action: 'serverconsole', value: msg, tag: cmdData.command.tag })); } catch (ex) { } });
7842 }
7843 }
7844
7845 function serverUserCommandAutoBackup(cmdData) {
7846 cmdData.result = parent.db.performBackup(function (msg) {
7847 try { ws.send(JSON.stringify({ action: 'serverconsole', value: msg, tag: cmdData.command.tag })); } catch (ex) { }
7848 });
7849 }
7850
7851 function serverUserCommandBackupConfig(cmdData) {
7852 cmdData.result = parent.db.getBackupConfig();
7853 }
7854
7855 function serverUserCommandFirebase(cmdData) {
7856 if (parent.parent.firebase == null) {
7857 cmdData.result = "Firebase push messaging not supported";
7858 } else {
7859 cmdData.result = JSON.stringify(parent.parent.firebase.stats, null, 2);
7860 }
7861 }
7862
7863
7864 function validNodeIdAndDomain(command) {
7865 if (common.validateString(command.nodeid, 1, 1024) == false) return false; // Check nodeid
7866 if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
7867 if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return false; // Invalid domain, operation only valid for current domain
7868 return true;
7869 }
7870
7871 function getUserExtraIds() {
7872 var extraids = null;
7873 if (obj.user.links != null) {
7874 for (var i in obj.user.links) {
7875 if (i.startsWith('node/')) { if (extraids == null) { extraids = []; } extraids.push(i); }
7876 else if (i.startsWith('ugrp/')) {
7877 const g = parent.userGroups[i];
7878 if ((g != null) && (g.links != null)) {
7879 for (var j in g.links) { if (j.startsWith('node/')) { if (extraids == null) { extraids = []; } extraids.push(j); } }
7880 }
7881 }
7882 }
7883 }
7884 return extraids;
7885 }
7886
7887 function userHasSiteUpdate() { return ((user.siteadmin & SITERIGHT_SERVERUPDATE) > 0); }
7888 function domainHasMyServerErrorLog() { return !((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.errorlog !== true))); }
7889 function domainHasMyServerUpgrade() { return !((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.upgrade !== true))); }
7890
7891 function csvClean(s) { return '\"' + s.split('\"').join('').split(',').join('').split('\r').join('').split('\n').join('') + '\"'; }
7892
7893 function remoteSessionReport(command, manageAllDeviceGroups, msgIdFilter) {
7894 // If we are not user administrator on this site, only search for events with our own user id.
7895 var ids = [user._id];
7896 if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) {
7897 if (command.devGroup != null) {
7898 ids = [ user._id, command.devGroup ];
7899 } else {
7900 if (manageAllDeviceGroups) { ids = ['*']; } else if (user.links) { for (var i in user.links) { ids.push(i); } }
7901 }
7902 }
7903
7904 // Get the events in the time range
7905 // MySQL or MariaDB query will ignore the MsgID filter.
7906 db.GetEventsTimeRange(ids, domain.id, msgIdFilter, new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
7907 if (err != null) return;
7908 var data = { groups: {} };
7909 var guestNamePresent = false;
7910
7911 // Columns
7912 if (command.groupBy == 1) {
7913 data.groupFormat = 'user';
7914 data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: 'nodeid', title: "device", format: 'node' }, { id: 'meshid', title: "devgroup", format: 'mesh' }, { id: 'guestname', title: "guest", align: 'center' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
7915 } else if (command.groupBy == 2) {
7916 data.groupFormat = 'nodemesh';
7917 data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: 'userid', title: "user", format: 'user' }, { id: 'guestname', title: "guest", align: 'center' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
7918 } else if (command.groupBy == 3) {
7919 data.columns = [{ id: 'time', title: "time", format: 'time' }, { id: 'nodeid', title: "device", format: 'node' }, { id: 'meshid', title: "devgroup", format: 'mesh' }, { id: 'guestname', title: "guest", align: 'center' }, { id: 'userid', title: "user", format: 'user' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
7920 }
7921
7922 // Add traffic columns
7923 if (command.showTraffic) {
7924 data.columns.push({ id: 'bytesin', title: "bytesin", format: 'bytes', align: 'center', sumBy: 'protocol' });
7925 data.columns.push({ id: 'bytesout', title: "bytesout", format: 'bytes', align: 'center', sumBy: 'protocol' });
7926 }
7927
7928 // Rows
7929 for (var i in docs) {
7930 // If MySQL or MariaDB query, we can't filter on MsgID, so we have to do it here.
7931 if (msgIdFilter.indexOf(docs[i].msgid) < 0) continue;
7932 if ((command.devGroup != null) && (docs[i].ids != null) && (docs[i].ids.indexOf(command.devGroup) == -1)) continue;
7933
7934 var entry = { time: docs[i].time.valueOf() };
7935
7936 // UserID
7937 if (command.groupBy != 1) { entry.userid = docs[i].userid; }
7938 if (command.groupBy != 2) { entry.nodeid = docs[i].nodeid; }
7939 entry.protocol = docs[i].protocol;
7940
7941 // Device Group
7942 if (docs[i].ids != null) { for (var j in docs[i].ids) { if (docs[i].ids[j].startsWith('mesh/')) { entry.meshid = docs[i].ids[j]; } } }
7943
7944 // Add traffic data
7945 if (command.showTraffic) { entry.bytesin = docs[i].bytesin; entry.bytesout = docs[i].bytesout; }
7946
7947 // Add guest name if present
7948 if (docs[i].guestname != null) { entry.guestname = docs[i].guestname; guestNamePresent = true; }
7949
7950 // Session length
7951 if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { entry.length = docs[i].msgArgs[3]; }
7952 else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { entry.length = docs[i].msgArgs[0]; }
7953 else if ((docs[i].msgid == 144) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[1] == 'number')) { entry.length = docs[i].msgArgs[1]; }
7954
7955 if (command.groupBy == 1) { // Add entry to per user
7956 if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; }
7957 data.groups[docs[i].userid].entries.push(entry);
7958 } else if (command.groupBy == 2) { // Add entry to per mesh+device
7959 if (entry.meshid != null) {
7960 var k = docs[i].nodeid + '/' + entry.meshid;
7961 if (data.groups[k] == null) { data.groups[k] = { entries: [] }; }
7962 data.groups[k].entries.push(entry);
7963 } else {
7964 if (data.groups[docs[i].nodeid] == null) { data.groups[docs[i].nodeid] = { entries: [] }; }
7965 data.groups[docs[i].nodeid].entries.push(entry);
7966 }
7967 } else if (command.groupBy == 3) { // Add entry to per day
7968 var day;
7969 if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
7970 day = new Date(docs[i].time).toLocaleDateString(command.l, { timeZone: command.tz });
7971 } else {
7972 day = docs[i].time; // TODO
7973 }
7974 if (data.groups[day] == null) { data.groups[day] = { entries: [] }; }
7975 data.groups[day].entries.push(entry);
7976 }
7977 }
7978
7979 // Remove guest column if not needed
7980 if (guestNamePresent == false) {
7981 if ((command.groupBy == 1) || (command.groupBy == 3)) {
7982 data.columns.splice(3, 1);
7983 } else if (command.groupBy == 2) {
7984 data.columns.splice(2, 1);
7985 }
7986 }
7987
7988 try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
7989 });
7990 }
7991
7992 function trafficUsageReport(command, msgIdFilter) {
7993 // If we are not user administrator on this site, only search for events with our own user id.
7994 var ids = [user._id]; // If we are nto user administrator, only count our own traffic.
7995 if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) { ids = ['*']; } // If user administrator, count traffic of all users.
7996
7997 // Get the events in the time range
7998 // MySQL or MariaDB query will ignore the MsgID filter.
7999 db.GetEventsTimeRange(ids, domain.id, msgIdFilter, new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
8000 if (err != null) return;
8001 var data = { groups: { 0: { entries: [] } } };
8002 data.columns = [{ id: 'userid', title: "user", format: 'user' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: true }, { id: 'bytesin', title: "bytesin", format: 'bytes', align: 'center', sumBy: true }, { id: 'bytesout', title: "bytesout", format: 'bytes', align: 'center', sumBy: true }];
8003 var userEntries = {};
8004
8005 // Sum all entry logs for each user
8006 for (var i in docs) {
8007 // If MySQL or MariaDB query, we can't filter on MsgID, so we have to do it here.
8008 if (msgIdFilter.indexOf(docs[i].msgid) < 0) continue;
8009 if ((command.devGroup != null) && (docs[i].ids != null) && (docs[i].ids.indexOf(command.devGroup) == -1)) continue;
8010
8011 // Fetch or create the user entry
8012 var userEntry = userEntries[docs[i].userid];
8013 if (userEntry == null) { userEntry = { userid: docs[i].userid, length: 0, bytesin: 0, bytesout: 0 }; }
8014 if (docs[i].bytesin) { userEntry.bytesin += docs[i].bytesin; }
8015 if (docs[i].bytesout) { userEntry.bytesout += docs[i].bytesout; }
8016
8017 // Session length
8018 if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { userEntry.length += docs[i].msgArgs[3]; }
8019 else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { userEntry.length += docs[i].msgArgs[0]; }
8020 else if ((docs[i].msgid == 144) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[1] == 'number')) { userEntry.length += docs[i].msgArgs[1]; }
8021
8022 // Set the user entry
8023 userEntries[docs[i].userid] = userEntry;
8024 }
8025
8026 var userEntries2 = [];
8027 for (var i in userEntries) { userEntries2.push(userEntries[i]); }
8028 data.groups[0].entries = userEntries2;
8029
8030 try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
8031 });
8032 }
8033
8034
8035 function userLoginReport(command) {
8036 // If we are not user administrator on this site, only search for events with our own user id.
8037 var ids = [user._id]; // If we are nto user administrator, only count our own traffic.
8038 if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) { ids = ['*']; } // If user administrator, count traffic of all users.
8039
8040 var showInvalidLoginAttempts = true;
8041
8042 // Get the events in the time range
8043 // MySQL or MariaDB query will ignore the MsgID filter.
8044 var msgIdFilter = [107];
8045 if (showInvalidLoginAttempts) { msgIdFilter = [107, 108, 109, 110]; } // Includes invalid login attempts
8046
8047 db.GetEventsTimeRange(ids, domain.id, msgIdFilter, new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
8048 if (err != null) return;
8049
8050 // Columns
8051 var data = { groups: {} };
8052 if (command.groupBy == 1) {
8053 data.groupFormat = 'user';
8054 data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: 'ip', title: "ip" }, { id: 'browser', title: "browser" }, { id: 'os', title: "os" }, { id: 'twofactor', title: "twofactor", format: '2fa' }];
8055 } else if (command.groupBy == 3) {
8056 data.columns = [{ id: 'time', title: "time", format: 'time' }, { id: 'userid', title: "user", format: 'user' }, { id: 'ip', title: "ip" }, { id: 'browser', title: "browser" }, { id: 'os', title: "os" }, { id: 'twofactor', title: "twofactor", format: '2fa' }];
8057 }
8058 if (showInvalidLoginAttempts) { data.columns.push({ id: 'msg', title: "msg", format: 'msg' }); }
8059
8060 // Add all log entries
8061 var entries = [];
8062 for (var i in docs) {
8063 // If MySQL or MariaDB query, we can't filter on MsgID, so we have to do it here.
8064 if (msgIdFilter.indexOf(docs[i].msgid) < 0) continue;
8065
8066 if (command.groupBy == 1) { // Add entry per user
8067 if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; }
8068 const entry = { time: docs[i].time.valueOf(), ip: docs[i].msgArgs[0], browser: docs[i].msgArgs[1], os: docs[i].msgArgs[2], twofactor: docs[i].twoFactorType ? docs[i].twoFactorType : '' };
8069 if (showInvalidLoginAttempts) { entry.msg = docs[i].msgid }
8070 data.groups[docs[i].userid].entries.push(entry);
8071 } else if (command.groupBy == 3) { // Add entry per day
8072 var day;
8073 if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
8074 day = new Date(docs[i].time).toLocaleDateString(command.l, { timeZone: command.tz });
8075 } else {
8076 day = docs[i].time; // TODO
8077 }
8078 if (data.groups[day] == null) { data.groups[day] = { entries: [] }; }
8079 const entry = { time: docs[i].time.valueOf(), userid: docs[i].userid, ip: docs[i].msgArgs[0], browser: docs[i].msgArgs[1], os: docs[i].msgArgs[2], twofactor: docs[i].twoFactorType ? docs[i].twoFactorType : '' };
8080 if (showInvalidLoginAttempts) { entry.msg = docs[i].msgid }
8081 data.groups[day].entries.push(entry);
8082 }
8083 }
8084
8085 try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
8086 });
8087 }
8088
8089 function databaseRecordsReport(command) {
8090 if (user.siteadmin != 0xFFFFFFFF) return; // This report is only available to full administrators
8091 parent.parent.db.getDbStats(function (stats) {
8092 var data = { groups: { 0: { entries: [] } } };
8093 data.columns = [{ id: 'record', title: "Record", format: 'records' }, { id: 'recordcount', title: "Count", align: 'center', sumBy: true }];
8094 for (var i in stats) { if ((i != 'total') && (stats[i] > 0)) { data.groups[0].entries.push({ record: i, recordcount: stats[i] }); } }
8095 try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
8096 });
8097 }
8098
8099 // Return detailed information about an array of nodeid's
8100 function getDeviceDetailedInfo(nodeids, type, func) {
8101 if (nodeids == null) { getAllDeviceDetailedInfo(type, func); return; }
8102 var results = [], resultPendingCount = 0;
8103 for (var i in nodeids) {
8104 // Fetch the node from the database
8105 resultPendingCount++;
8106 const getNodeFunc = function (node, rights, visible) {
8107 if ((node != null) && (visible == true)) {
8108 const getNodeSysInfoFunc = function (err, docs) {
8109 const getNodeNetInfoFunc = function (err, docs) {
8110 var netinfo = null;
8111 if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; }
8112 resultPendingCount--;
8113 getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo });
8114 if (resultPendingCount == 0) { func(getNodeFunc.results, type); }
8115 }
8116 getNodeNetInfoFunc.results = getNodeSysInfoFunc.results;
8117 getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid;
8118 getNodeNetInfoFunc.node = getNodeSysInfoFunc.node;
8119 if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; }
8120
8121 // Query the database for network information
8122 db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc);
8123 }
8124 getNodeSysInfoFunc.results = getNodeFunc.results;
8125 getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid;
8126 getNodeSysInfoFunc.node = node;
8127
8128 // Query the database for system information
8129 db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc);
8130 } else { resultPendingCount--; }
8131 if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); }
8132 }
8133 getNodeFunc.results = results;
8134 getNodeFunc.nodeid = nodeids[i];
8135 parent.GetNodeWithRights(domain, user, nodeids[i], getNodeFunc);
8136 }
8137 }
8138
8139 // Update all device shares for a nodeid list to a new meshid
8140 // This is used when devices move to a new device group, changes are not evented.
8141 function changeDeviceShareMeshId(nodes, meshid) {
8142 parent.db.GetAllTypeNoTypeField('deviceshare', domain.id, function (err, docs) {
8143 if (err != null) return;
8144 for (var i = 0; i < docs.length; i++) {
8145 const doc = docs[i];
8146 if (nodes.indexOf(doc.nodeid) >= 0) {
8147 doc.xmeshid = meshid;
8148 doc.type = 'deviceshare';
8149 db.Set(doc);
8150 }
8151 }
8152 });
8153 }
8154
8155 // Return detailed information about all nodes this user has access to
8156 function getAllDeviceDetailedInfo(type, func) {
8157 // If we are not paging, get all devices visible to this user
8158 if (obj.visibleDevices == null) {
8159
8160 // Get all device groups this user has access to
8161 var links = parent.GetAllMeshIdWithRights(user);
8162
8163 // Add any nodes with direct rights or any nodes with user group direct rights
8164 var extraids = getUserExtraIds();
8165
8166 // Request a list of all nodes
8167 db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, obj.deviceSkip, obj.deviceLimit, function (err, docs) {
8168 if (docs == null) { docs = []; }
8169 parent.common.unEscapeAllLinksFieldName(docs);
8170
8171 var results = [], resultPendingCount = 0;
8172 if (docs.length == 0) { // no results return blank array
8173 func(docs, type);
8174 } else {
8175 for (i in docs) {
8176 // Check device links, if a link points to an unknown user, remove it.
8177 parent.cleanDevice(docs[i]);
8178
8179 // Fetch the node from the database
8180 resultPendingCount++;
8181 const getNodeFunc = function (node, rights, visible) {
8182 if ((node != null) && (visible == true)) {
8183 const getNodeSysInfoFunc = function (err, docs) {
8184 const getNodeNetInfoFunc = function (err, docs) {
8185 var netinfo = null;
8186 if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; }
8187 resultPendingCount--;
8188 getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo });
8189 if (resultPendingCount == 0) { func(getNodeFunc.results, type); }
8190 }
8191 getNodeNetInfoFunc.results = getNodeSysInfoFunc.results;
8192 getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid;
8193 getNodeNetInfoFunc.node = getNodeSysInfoFunc.node;
8194 if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; }
8195
8196 // Query the database for network information
8197 db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc);
8198 }
8199 getNodeSysInfoFunc.results = getNodeFunc.results;
8200 getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid;
8201 getNodeSysInfoFunc.node = node;
8202
8203 // Query the database for system information
8204 db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc);
8205 } else { resultPendingCount--; }
8206 if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); }
8207 }
8208 getNodeFunc.results = results;
8209 getNodeFunc.nodeid = docs[i]._id;
8210 parent.GetNodeWithRights(domain, user, docs[i]._id, getNodeFunc);
8211 }
8212 }
8213 });
8214 } else {
8215 // If we are paging, we know what devices the user is look at
8216 for (var id in obj.visibleDevices) {
8217 // Fetch the node from the database
8218 resultPendingCount++;
8219 const getNodeFunc = function (node, rights, visible) {
8220 if ((node != null) && (visible == true)) {
8221 const getNodeSysInfoFunc = function (err, docs) {
8222 const getNodeNetInfoFunc = function (err, docs) {
8223 var netinfo = null;
8224 if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; }
8225 resultPendingCount--;
8226 getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo });
8227 if (resultPendingCount == 0) { func(getNodeFunc.results, type); }
8228 }
8229 getNodeNetInfoFunc.results = getNodeSysInfoFunc.results;
8230 getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid;
8231 getNodeNetInfoFunc.node = getNodeSysInfoFunc.node;
8232 if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; }
8233
8234 // Query the database for network information
8235 db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc);
8236 }
8237 getNodeSysInfoFunc.results = getNodeFunc.results;
8238 getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid;
8239 getNodeSysInfoFunc.node = node;
8240
8241 // Query the database for system information
8242 db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc);
8243 } else { resultPendingCount--; }
8244 if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); }
8245 }
8246 getNodeFunc.results = results;
8247 getNodeFunc.nodeid = id;
8248 parent.GetNodeWithRights(domain, user, id, getNodeFunc);
8249 }
8250 }
8251 }
8252
8253 // Display a notification message for this session only.
8254 function displayNotificationMessage(msg, title, tag, titleid, msgid, args) {
8255 ws.send(JSON.stringify({ 'action': 'msg', 'type': 'notify', id: Math.random(), 'value': msg, 'title': title, 'userid': user._id, 'username': user.name, 'tag': tag, 'titleid': titleid, 'msgid': msgid, 'args': args }));
8256 }
8257
8258 // Read the folder and all sub-folders and serialize that into json.
8259 function readFilesRec(path) {
8260 var r = {}, dir = fs.readdirSync(path);
8261 for (var i in dir) {
8262 var f = { t: 3, d: 111 }, stat = null;
8263 try { stat = fs.statSync(path + '/' + dir[i]); } catch (ex) { }
8264 if (stat != null) {
8265 if ((stat.mode & 0x004000) == 0) { f.s = stat.size; f.d = stat.mtime.getTime(); } else { f.t = 2; f.f = readFilesRec(path + '/' + dir[i]); }
8266 r[dir[i]] = f;
8267 }
8268 }
8269 return r;
8270 }
8271
8272 // Delete a directory with a files and directories within it
8273 // TODO, make this an async function
8274 function deleteFolderRecursive(path) {
8275 if (fs.existsSync(path)) {
8276 fs.readdirSync(path).forEach(function (file, index) {
8277 var curPath = parent.path.join(path, file);;
8278 if (fs.lstatSync(curPath).isDirectory()) { // recurse
8279 deleteFolderRecursive(curPath);
8280 } else { // delete file
8281 fs.unlinkSync(curPath);
8282 }
8283 });
8284 fs.rmdirSync(path);
8285 }
8286 };
8287
8288 function updateUserFiles(user, ws, domain) {
8289 if ((user == null) || (user.siteadmin == null) || ((user.siteadmin & 8) == 0)) return;
8290
8291 // Request the list of server files
8292 var files = { action: 'files', filetree: { n: 'Root', f: {} } };
8293
8294 // Add user files
8295 files.filetree.f[user._id] = { t: 1, n: 'My Files', f: {} };
8296 files.filetree.f[user._id].maxbytes = parent.getQuota(user._id, domain);
8297 var usersplit = user._id.split('/'), domainx = 'domain';
8298 if (usersplit[1].length > 0) domainx = 'domain-' + usersplit[1];
8299
8300 // Read all files recursively
8301 try {
8302 files.filetree.f[user._id].f = readFilesRec(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2]));
8303 } catch (e) {
8304 // TODO: We may want to fake this file structure until it's needed.
8305 // Got an error, try to create all the folders and try again...
8306 try { fs.mkdirSync(parent.filespath); } catch (e) { }
8307 try { fs.mkdirSync(parent.path.join(parent.filespath, domainx)); } catch (e) { }
8308 try { fs.mkdirSync(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2])); } catch (e) { }
8309 try { fs.mkdirSync(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2] + '/Public')); } catch (e) { }
8310 try { files.filetree.f[user._id].f = readFilesRec(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2])); } catch (e) { }
8311 }
8312
8313 // Add files for each mesh
8314 const meshes = parent.GetAllMeshWithRights(user, MESHRIGHT_SERVERFILES);
8315 for (var i in meshes) {
8316 const mesh = meshes[i];
8317 var meshsplit = mesh._id.split('/');
8318 files.filetree.f[mesh._id] = { t: 4, n: mesh.name, f: {} };
8319 files.filetree.f[mesh._id].maxbytes = parent.getQuota(mesh._id, domain);
8320
8321 // Read all files recursively
8322 try {
8323 files.filetree.f[mesh._id].f = readFilesRec(parent.path.join(parent.filespath, domainx + '/mesh-' + meshsplit[2]));
8324 } catch (e) {
8325 files.filetree.f[mesh._id].f = {}; // Got an error, return empty folder. We will create the folder only when needed.
8326 }
8327 }
8328
8329 // Respond
8330 try { ws.send(JSON.stringify(files)); } catch (ex) { }
8331 }
8332
8333 function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
8334 //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
8335
8336 // Split a string taking into account the quoats. Used for command line parsing
8337 function splitArgs(str) { var myArray = [], myRegexp = /[^\s"]+|"([^"]*)"/gi; do { var match = myRegexp.exec(str); if (match != null) { myArray.push(match[1] ? match[1] : match[0]); } } while (match != null); return myArray; }
8338 function toNumberIfNumber(x) { if ((typeof x == 'string') && (+parseInt(x) === x)) { x = parseInt(x); } return x; }
8339
8340 function isPhoneNumber(x) {
8341 var ok = true;
8342 if (x.startsWith('+')) { x = x.substring(1); }
8343 for (var i = 0; i < x.length; i++) { var c = x.charCodeAt(i); if (((c < 48) || (c > 57)) && (c != 32) && (c != 45) && (c != 46)) { ok = false; } }
8344 return ok && (x.length >= 10);
8345 }
8346
8347 function removeAllUnderScore(obj) {
8348 if (typeof obj != 'object') return obj;
8349 for (var i in obj) { if (i.startsWith('_')) { delete obj[i]; } else if (typeof obj[i] == 'object') { removeAllUnderScore(obj[i]); } }
8350 return obj;
8351 }
8352
8353 // Generate a 8 digit integer with even random probability for each value.
8354 function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
8355 function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
8356
8357 // Parse arguments string array into an object
8358 function parseArgs(argv) {
8359 var results = { '_': [] }, current = null;
8360 for (var i = 1, len = argv.length; i < len; i++) {
8361 var x = argv[i];
8362 if (x.length > 2 && x[0] == '-' && x[1] == '-') {
8363 if (current != null) { results[current] = true; }
8364 current = x.substring(2);
8365 } else {
8366 if (current != null) { results[current] = toNumberIfNumber(x); current = null; } else { results['_'].push(toNumberIfNumber(x)); }
8367 }
8368 }
8369 if (current != null) { results[current] = true; }
8370 return results;
8371 }
8372
8373 // Return true if at least one element of arr2 is in arr1
8374 function findOne(arr1, arr2) { if ((arr1 == null) || (arr2 == null)) return false; return arr2.some(function (v) { return arr1.indexOf(v) >= 0; }); };
8375
8376 function getRandomPassword() { return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
8377
8378 function handleAmtCommand(cmd, node) {
8379 if (cmd == null) return;
8380 var host = cmd.nodeid;
8381 if (cmd.mode == 1) { host = node.host; }
8382 var tlsoptions = null;
8383 var wsman = new Wsman(WsmanComm, host, node.intelamt.tls ? 16993 : 16992, node.intelamt.user, node.intelamt.pass,
8384 node.intelamt.tls, tlsoptions, parent.parent, cmd.mode);
8385 var amt = new Amt(wsman);
8386 switch (cmd.command) {
8387 case 'Get-GeneralSettings': {
8388 amt.Get('AMT_GeneralSettings', function (obj, name, response, status) {
8389 if (status == 200) {
8390 var resp = { action: 'amt', nodeid: cmd.nodeid, command: 'Get-GeneralSettings', value: response.Body }
8391 ws.send(JSON.stringify(resp));
8392 } else {
8393 ws.send(JSON.stringify({ 'error': error }));
8394 }
8395 });
8396 break;
8397 }
8398 default: {
8399 // Do nothing
8400 }
8401 }
8402 }
8403
8404 // Return the number of 2nd factor for this account
8405 function count2factoraAuths() {
8406 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null));
8407 var sms2fa = ((parent.parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)));
8408 var msg2fa = ((parent.parent.msgserver != null) && (parent.parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)));
8409 var duo2fa = ((typeof domain.passwordrequirements != 'object') || (typeof domain.passwordrequirements.duo2factor == 'object'));
8410 var authFactorCount = 0;
8411 if (typeof user.otpsecret == 'string') { authFactorCount++; } // Authenticator time factor
8412 if (email2fa && (user.otpekey != null)) { authFactorCount++; } // EMail factor
8413 if (sms2fa && (user.phone != null)) { authFactorCount++; } // SMS factor
8414 if (msg2fa && (user.msghandle != null)) { authFactorCount++; } // Messaging factor
8415 if (duo2fa && (user.otpduo != null)) { authFactorCount++; } // Duo authentication factor
8416 if (user.otphkeys != null) { authFactorCount += user.otphkeys.length; } // FIDO hardware factor
8417 if ((authFactorCount > 0) && (user.otpkeys != null)) { authFactorCount++; } // Backup keys
8418 return authFactorCount;
8419 }
8420
8421 // Return true if the event is for a device that is part of the currently visible page
8422 function isEventWithinPage(ids) {
8423 if (obj.visibleDevices == null) return true; // Add devices are visible
8424 var r = true;
8425 for (var i in ids) {
8426 // If the event is for a visible device, return true
8427 if (ids[i].startsWith('node/')) { r = false; if (obj.visibleDevices[ids[i]] != null) return true; }
8428 }
8429 return r; // If this event is not for any specific device, return true
8430 }
8431
8432 return obj;
8433};