master
js 8,636 lines 589 KB
Raw
Large file — syntax highlighting disabled.
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
17 module.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 = {
556 domain: domain.id,
557 name: domain.dns ? domain.dns : parent.certificates.CommonName,
558 mpsname: parent.certificates.AmtMpsName,
559 mpsport: mpsport,
560 mpspass: args.mpspass,
561 port: httpport,
562 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)),
563 domainauth: (domain.auth == 'sspi'),
564 serverTime: Date.now(),
565 features: allFeatures.features,
566 features2: allFeatures.features2,
567 features3: allFeatures.features3
568 };
569 serverinfo.languages = parent.renderLanguages;
570 serverinfo.tlshash = Buffer.from(parent.webCertificateFullHashs[domain.id], 'binary').toString('hex').toUpperCase(); // SHA384 of server HTTPS certificate
571 serverinfo.agentCertHash = parent.agentCertificateHashBase64;
572 if (typeof domain.sessionrecording == 'object') {
573 if (domain.sessionrecording.onlyselectedusers === true) { serverinfo.usersSessionRecording = 1; } // Allow enabling of session recording for users
574 if (domain.sessionrecording.onlyselectedusergroups === true) { serverinfo.userGroupsSessionRecording = 1; } // Allow enabling of session recording for user groups
575 if (domain.sessionrecording.onlyselecteddevicegroups === true) { serverinfo.devGroupSessionRecording = 1; } // Allow enabling of session recording for device groups
576 }
577 if ((parent.parent.config.domains[domain.id].amtacmactivation != null) && (parent.parent.config.domains[domain.id].amtacmactivation.acmmatch != null)) {
578 var matchingDomains = [];
579 for (var i in parent.parent.config.domains[domain.id].amtacmactivation.acmmatch) {
580 var cn = parent.parent.config.domains[domain.id].amtacmactivation.acmmatch[i].cn;
581 if ((cn != '*') && (matchingDomains.indexOf(cn) == -1)) { matchingDomains.push(cn); }
582 }
583 if (matchingDomains.length > 0) { serverinfo.amtAcmFqdn = matchingDomains; }
584 }
585 if (typeof domain.devicemeshrouterlinks == 'object') { serverinfo.devicemeshrouterlinks = domain.devicemeshrouterlinks; }
586 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 }]; }
587 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 }); } } }
588 serverinfo.https = true;
589 serverinfo.redirport = args.redirport;
590 if (parent.parent.webpush != null) { serverinfo.vapidpublickey = parent.parent.webpush.vapidPublicKey; } // Web push public key
591 if (parent.parent.amtProvisioningServer != null) { serverinfo.amtProvServerMeshId = parent.parent.amtProvisioningServer.meshid; } // Device group that allows for bare-metal Intel AMT activation
592 if ((typeof domain.autoremoveinactivedevices == 'number') && (domain.autoremoveinactivedevices > 0)) { serverinfo.autoremoveinactivedevices = domain.autoremoveinactivedevices; } // Default number of days before inactive devices are removed
593 if (domain.passwordrequirements) {
594 if (domain.passwordrequirements.lock2factor == true) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed
595 if (typeof domain.passwordrequirements.maxfidokeys == 'number') { serverinfo.maxfidokeys = domain.passwordrequirements.maxfidokeys; }
596 }
597 if (parent.parent.msgserver != null) { // Setup messaging providers information
598 serverinfo.userMsgProviders = parent.parent.msgserver.providers;
599 if (parent.parent.msgserver.discordUrl != null) { serverinfo.discordUrl = parent.parent.msgserver.discordUrl; }
600 }
601 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
602 serverinfo.userMsgNftyUrl = parent.parent.config.messaging.ntfy.userurl;
603 }
604
605 // Build the mobile agent URL, this is used to connect mobile devices
606 var agentServerName = parent.getWebServerName(domain, req);
607 if (typeof parent.args.agentaliasdns == 'string') { agentServerName = parent.args.agentaliasdns; }
608 var xdomain = (domain.dns == null) ? domain.id : '';
609 var agentHttpsPort = ((parent.args.aliasport == null) ? parent.args.port : parent.args.aliasport); // Use HTTPS alias port is specified
610 if (parent.args.agentport != null) { agentHttpsPort = parent.args.agentport; } // If an agent only port is enabled, use that.
611 if (parent.args.agentaliasport != null) { agentHttpsPort = parent.args.agentaliasport; } // If an agent alias port is specified, use that.
612 serverinfo.magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '');
613 serverinfo.domainsuffix = xdomain;
614
615 if (domain.guestdevicesharing === false) { serverinfo.guestdevicesharing = false; } else {
616 if (typeof domain.guestdevicesharing == 'object') {
617 if (typeof domain.guestdevicesharing.maxsessiontime == 'number') { serverinfo.guestdevicesharingmaxtime = domain.guestdevicesharing.maxsessiontime; }
618 }
619 }
620 if (typeof domain.userconsentflags == 'number') { serverinfo.consent = domain.userconsentflags; }
621 if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) {serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); }
622 if (typeof domain.logoutonidlesessiontimeout == 'boolean') {
623 serverinfo.logoutonidlesessiontimeout = domain.logoutonidlesessiontimeout;
624 } else {
625 // Default
626 serverinfo.logoutonidlesessiontimeout = true;
627 }
628 if (user.siteadmin === SITERIGHT_ADMIN) {
629 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; }
630 if (obj.crossDomain === true) { serverinfo.crossDomain = []; for (var i in parent.parent.config.domains) { serverinfo.crossDomain.push(i); } }
631 if (typeof parent.webCertificateExpire[domain.id] == 'number') { serverinfo.certExpire = parent.webCertificateExpire[domain.id]; }
632 }
633 if (typeof domain.terminal == 'object') { // Settings used for remote terminal feature
634 if ((typeof domain.terminal.linuxshell == 'string') && (domain.terminal.linuxshell != 'any')) { serverinfo.linuxshell = domain.terminal.linuxshell; }
635 }
636 if (Array.isArray(domain.preconfiguredremoteinput)) { serverinfo.preConfiguredRemoteInput = domain.preconfiguredremoteinput; }
637 if (Array.isArray(domain.preconfiguredscripts)) {
638 const r = [];
639 for (var i in domain.preconfiguredscripts) {
640 const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent
641 const script = domain.preconfiguredscripts[i];
642 if ((typeof script.name == 'string') && (script.name.length <= 32) && (typeof script.type == 'string') && ((typeof script.file == 'string') || (typeof script.cmd == 'string'))) {
643 const s = { name: script.name, type: types.indexOf(script.type.toLowerCase()) };
644 if (s.type > 0) { r.push(s); }
645 }
646 }
647 serverinfo.preConfiguredScripts = r;
648 }
649 serverinfo.softwareinventory = domain?.softwareinventory === true;
650 if (domain.maxdeviceview != null) { serverinfo.maxdeviceview = domain.maxdeviceview; } // Maximum number of devices a user can view at any given time
651
652 // Send server information
653 try { ws.send(JSON.stringify({ action: 'serverinfo', serverinfo: serverinfo })); } catch (ex) { }
654
655 // Send user information to web socket, this is the first thing we send
656 try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: parent.CloneSafeUser(parent.users[user._id]) })); } catch (ex) { }
657
658 if (user.siteadmin === SITERIGHT_ADMIN) {
659 // Check if tracing is allowed for this domain
660 if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.trace === true))) {
661 // Send server tracing information
662 try { ws.send(JSON.stringify({ action: 'traceinfo', traceSources: parent.parent.debugRemoteSources })); } catch (ex) { }
663 }
664
665 // Send any server warnings if any
666 var serverWarnings = parent.parent.getServerWarnings();
667 if (serverWarnings.length > 0) { try { ws.send(JSON.stringify({ action: 'serverwarnings', warnings: serverWarnings })); } catch (ex) { } }
668 }
669
670 // See how many times bad login attempts where made since the last login
671 const lastLoginTime = parent.users[user._id].pastlogin;
672 if (lastLoginTime != null) {
673 db.GetFailedLoginCount(user._id, user.domain, new Date(lastLoginTime * 1000), function (count) {
674 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; }
675 });
676 }
677
678 // If we are site administrator and Google Drive backup is setup, send out the status.
679 if ((user.siteadmin === SITERIGHT_ADMIN) && (domain.id == '') && (typeof parent.parent.config.settings.autobackup == 'object') && (typeof parent.parent.config.settings.autobackup.googledrive == 'object')) {
680 db.Get('GoogleDriveBackup', function (err, docs) {
681 if (err != null) return;
682 if (docs.length == 0) { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 1 })); } catch (ex) { } }
683 else { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: docs[0].state })); } catch (ex) { } }
684 });
685 }
686
687 // We are all set, start receiving data
688 ws._socket.resume();
689 if (parent.parent.pluginHandler != null) parent.parent.pluginHandler.callHook('hook_userLoggedIn', user);
690 });
691 } catch (ex) { console.log(ex); }
692
693 // Process incoming web socket data from the browser
694 function processWebSocketData(msg) {
695 var command, i = 0, mesh = null, meshid = null, nodeid = null, meshlinks = null, change = 0;
696 try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
697 if (common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
698
699 var commandHandler = serverCommands[command.action];
700 if (commandHandler != null) {
701 try { commandHandler(command); return; }
702 catch (e) {
703 console.log('Unhandled error while processing ' + command.action + ' for user ' + user.name + ':\n' + e);
704 parent.parent.logError(e.stack); return; // todo: remove returns when switch is gone
705 }
706 } else { }
707 // console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
708 // pass through to switch statement until refactoring complete
709
710 switch (command.action) {
711 case 'nodes':
712 {
713 // If in paging mode, look to set the skip and limit values
714 if (domain.maxdeviceview != null) {
715 if ((typeof command.skip == 'number') && (command.skip >= 0)) { obj.deviceSkip = command.skip; }
716 if ((typeof command.limit == 'number') && (command.limit > 0)) { obj.deviceLimit = command.limit; }
717 if (obj.deviceLimit > domain.maxdeviceview) { obj.deviceLimit = domain.maxdeviceview; }
718 }
719
720 var links = [], extraids = null, err = null;
721
722 // Resolve the device group name if needed
723 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
724 for (var i in parent.meshes) {
725 var m = parent.meshes[i];
726 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
727 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
728 }
729 }
730 if (command.meshid == null) { err = 'Invalid group id'; }
731 }
732
733 // Check if command.id is set with a domain id, if not, add the domain id to it.
734 if (typeof command.id == 'string' && command.id !== '') {
735 if (common.validateString(command.id, 1, 1024) == false) { err = 'Invalid device identifier'; }
736 else if (command.id.indexOf('/') == -1) { command.id = 'node/' + domain.id + '/' + command.id; }
737 }
738
739 if (err == null) {
740 try {
741 if (command.meshid == null) {
742 // Request a list of all meshes this user as rights to
743 links = parent.GetAllMeshIdWithRights(user);
744
745 // Add any nodes with direct rights or any nodes with user group direct rights
746 extraids = getUserExtraIds();
747 } else {
748 // Request list of all nodes for one specific meshid
749 meshid = command.meshid;
750 if (common.validateString(meshid, 0, 128) == false) { err = 'Invalid group id'; } else {
751 if (meshid.split('/').length == 1) { meshid = 'mesh/' + domain.id + '/' + command.meshid; }
752 if (parent.IsMeshViewable(user, meshid)) { links.push(meshid); } else { err = 'Invalid group id'; }
753 }
754 }
755 } catch (ex) { err = 'Validation exception: ' + ex; }
756 }
757
758 // Handle any errors
759 if (err != null) {
760 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'nodes', responseid: command.responseid, result: err })); } catch (ex) { } }
761 break;
762 }
763
764 // Request a list of all nodes
765 db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, obj.deviceSkip, obj.deviceLimit, function (err, docs) {
766
767 //console.log(err, docs, links, extraids, domain.id, 'node', command.id);
768
769 if (docs == null) { docs = []; }
770 parent.common.unEscapeAllLinksFieldName(docs);
771
772 var r = {}, nodeCount = docs.length;
773 if (domain.maxdeviceview != null) { obj.visibleDevices = {}; }
774 for (i in docs) {
775 // Check device links, if a link points to an unknown user, remove it.
776 parent.cleanDevice(docs[i]); // TODO: This will make the total device count incorrect and will affect device paging.
777
778 // If we are paging, add the device to the page here
779 if (domain.maxdeviceview != null) { obj.visibleDevices[docs[i]._id] = 1; }
780
781 // Remove any connectivity and power state information, that should not be in the database anyway.
782 // TODO: Find why these are sometimes saved in the db.
783 if (docs[i].conn != null) { delete docs[i].conn; }
784 if (docs[i].pwr != null) { delete docs[i].pwr; }
785 if (docs[i].agct != null) { delete docs[i].agct; }
786 if (docs[i].cict != null) { delete docs[i].cict; }
787
788 // Add the connection state
789 var state = parent.parent.GetConnectivityState(docs[i]._id);
790 if (state) {
791 docs[i].conn = state.connectivity;
792 docs[i].pwr = state.powerState;
793 if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[docs[i]._id]; if (agent != null) { docs[i].agct = agent.connectTime; } }
794
795 // Use the connection time of the CIRA/Relay connection
796 if ((state.connectivity & 2) != 0) {
797 var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(docs[i]._id, null, true);
798 if ((ciraConnection != null) && (ciraConnection.tag != null)) { docs[i].cict = ciraConnection.tag.connectTime; }
799 }
800 }
801
802 // Compress the meshid's
803 meshid = docs[i].meshid;
804 if (!r[meshid]) { r[meshid] = []; }
805 delete docs[i].meshid;
806
807 // Remove push messaging token if present
808 if (docs[i].pmt != null) { docs[i].pmt = 1; }
809
810 // Remove SSH credentials if present
811 if (docs[i].ssh != null) {
812 if ((docs[i].ssh[user._id] != null) && (docs[i].ssh[user._id].u)) {
813 if (docs[i].ssh.k && docs[i].ssh[user._id].kp) { docs[i].ssh = 2; } // Username, key and password
814 else if (docs[i].ssh[user._id].k) { docs[i].ssh = 3; } // Username and key. No password.
815 else if (docs[i].ssh[user._id].p) { docs[i].ssh = 1; } // Username and password
816 else { delete docs[i].ssh; }
817 } else {
818 delete docs[i].ssh;
819 }
820 }
821
822 // Remove RDP credentials if present, only set to 1 if our userid has RDP credentials
823 if ((docs[i].rdp != null) && (docs[i].rdp[user._id] != null)) { docs[i].rdp = 1; } else { delete docs[i].rdp; }
824
825 // Remove Intel AMT credential if present
826 if (docs[i].intelamt != null) {
827 if (docs[i].intelamt.pass != null) { docs[i].intelamt.pass = 1; }
828 if (docs[i].intelamt.mpspass != null) { docs[i].intelamt.mpspass = 1; }
829 }
830
831 // If GeoLocation not enabled, remove any node location information
832 if (domain.geolocation != true) {
833 if (docs[i].iploc != null) { delete docs[i].iploc; }
834 if (docs[i].wifiloc != null) { delete docs[i].wifiloc; }
835 if (docs[i].gpsloc != null) { delete docs[i].gpsloc; }
836 if (docs[i].userloc != null) { delete docs[i].userloc; }
837 }
838
839 // Add device sessions
840 const xagent = parent.wsagents[docs[i]._id];
841 if ((xagent != null) && (xagent.sessions != null)) { docs[i].sessions = xagent.sessions; }
842
843 // Add IP-KVM sessions
844 if (parent.parent.ipKvmManager != null) {
845 const xipkvmport = parent.parent.ipKvmManager.managedPorts[docs[i]._id];
846 if ((xipkvmport != null) && (xipkvmport.sessions != null)) { docs[i].sessions = xipkvmport.sessions; }
847 }
848
849 // Patch node links with names, like meshes links with names
850 for (var a in docs[i].links) {
851 if (!docs[i].links[a].name) {
852 if (parent.users[a] && parent.users[a].realname) { docs[i].links[a].name = parent.users[a].realname; }
853 else if (parent.users[a] && parent.users[a].name) { docs[i].links[a].name = parent.users[a].name; }
854 }
855 }
856
857 r[meshid].push(docs[i]);
858 }
859 const response = { action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag };
860 if (domain.maxdeviceview != null) {
861 // If in paging mode, report back the skip and limit values
862 response.skip = obj.deviceSkip;
863 response.limit = obj.deviceLimit;
864
865 // Add total device count
866 // Only set response.totalcount if we need to be in paging mode
867 if (nodeCount < response.limit) {
868 if (obj.deviceSkip > 0) { response.totalcount = obj.deviceSkip + nodeCount; } else { obj.visibleDevices = null; }
869 try { ws.send(JSON.stringify(response)); } catch (ex) { }
870 } else {
871 // Ask the database for the total device count
872 if (db.CountAllTypeNoTypeFieldMeshFiltered) {
873 db.CountAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, function (err, count) {
874 if ((err != null) || (typeof count != 'number') || ((obj.deviceSkip == 0) && (count < obj.deviceLimit))) {
875 obj.visibleDevices = null;
876 } else {
877 response.totalcount = count;
878 }
879 try { ws.send(JSON.stringify(response)); } catch (ex) { }
880 });
881 } else {
882 // The database does not support device counting
883 obj.visibleDevices = null; // We are not in paging mode
884 try { ws.send(JSON.stringify(response)); } catch (ex) { }
885 }
886 }
887 } else {
888 obj.visibleDevices = null; // We are not in paging mode
889 try { ws.send(JSON.stringify(response)); } catch (ex) { }
890 }
891 });
892 break;
893 }
894 case 'fileoperation':
895 {
896 // Check permissions
897 if ((user.siteadmin & 8) != 0) {
898 // Perform a file operation (Create Folder, Delete Folder, Delete File...)
899 if (common.validateString(command.fileop, 3, 16) == false) return;
900 var sendUpdate = true, path = meshPathToRealPath(command.path, user); // This will also check access rights
901 if (path == null) break;
902
903 if ((command.fileop == 'createfolder') && (common.IsFilenameValid(command.newfolder) == true)) {
904 // Create a new folder
905 try { fs.mkdirSync(parent.path.join(path, command.newfolder)); } catch (ex) {
906 try { fs.mkdirSync(path); } catch (ex) { }
907 try { fs.mkdirSync(parent.path.join(path, command.newfolder)); } catch (ex) { }
908 }
909 }
910 else if (command.fileop == 'delete') {
911 // Delete a file
912 if (common.validateArray(command.delfiles, 1) == false) return;
913 for (i in command.delfiles) {
914 if (common.IsFilenameValid(command.delfiles[i]) == true) {
915 var fullpath = parent.path.join(path, command.delfiles[i]);
916 if (command.rec == true) {
917 try { deleteFolderRecursive(fullpath); } catch (ex) { } // TODO, make this an async function
918 } else {
919 try { fs.rmdirSync(fullpath); } catch (ex) { try { fs.unlinkSync(fullpath); } catch (xe) { } }
920 }
921 }
922 }
923
924 // If we deleted something in the mesh root folder and the entire mesh folder is empty, remove it.
925 if (command.path.length == 1) {
926 try {
927 if (command.path[0].startsWith('mesh//')) {
928 path = meshPathToRealPath([command.path[0]], user);
929 fs.readdir(path, function (err, dir) { if ((err == null) && (dir.length == 0)) { fs.rmdir(path, function (err) { }); } });
930 }
931 } catch (ex) { }
932 }
933 }
934 else if ((command.fileop == 'rename') && (common.IsFilenameValid(command.oldname) === true) && (common.IsFilenameValid(command.newname) === true)) {
935 // Rename
936 try { fs.renameSync(parent.path.join(path, command.oldname), parent.path.join(path, command.newname)); } catch (e) { }
937 }
938 else if ((command.fileop == 'copy') || (command.fileop == 'move')) {
939 // Copy or move of one or many files
940 if (common.validateArray(command.names, 1) == false) return;
941 var scpath = meshPathToRealPath(command.scpath, user); // This will also check access rights
942 if (scpath == null) break;
943 // TODO: Check quota if this is a copy
944 for (i in command.names) {
945 if (common.IsFilenameValid(command.names[i]) === true) {
946 var s = parent.path.join(scpath, command.names[i]), d = parent.path.join(path, command.names[i]);
947 sendUpdate = false;
948 try { fs.mkdirSync(path); } catch (ex) { } // try to create folder first incase folder is missing
949 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));
950 }
951 }
952 } else if (command.fileop == 'get') {
953 // Get a short file and send it back on the web socket
954 if (common.validateString(command.file, 1, 4096) == false) return;
955 const scpath = meshPathToRealPath(command.path, user); // This will also check access rights
956 if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break;
957 const filePath = parent.path.join(scpath, command.file);
958 fs.stat(filePath, function (err, stat) {
959 if ((err != null) || (stat == null) || (stat.size >= 204800)) return;
960 fs.readFile(filePath, function (err, data) {
961 if ((err != null) || (data == null)) return;
962 command.data = data.toString('base64');
963 ws.send(JSON.stringify(command)); // Send the file data back, base64 encoded.
964 });
965 });
966 } else if (command.fileop == 'set') {
967 // Set a short file transfered on the web socket
968 if (common.validateString(command.file, 1, 4096) == false) return;
969 if (typeof command.data != 'string') return;
970 const scpath = meshPathToRealPath(command.path, user); // This will also check access rights
971 if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break;
972 const filePath = parent.path.join(scpath, command.file);
973 var data = null;
974 try { data = Buffer.from(command.data, 'base64'); } catch (ex) { return; }
975 fs.writeFile(filePath, data, function (err) { if (err == null) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } });
976 }
977 if (sendUpdate == true) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } // Fire an event causing this user to update this files
978 }
979 break;
980 }
981 case 'software': {
982 if (domain.softwareinventory !== true) {
983 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
984 break;
985 }
986 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
987 var mesh = parent.meshes[node.meshid];
988 if ((node != null) && (mesh != null) && ((rights & MESHRIGHT_DEVICEDETAILS) != 0)) {
989 var agent = parent.wsagents[command.nodeid];
990 if (agent != null) {
991 console.log(command);
992 routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions);
993 } else {
994 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Agent offline' })); } catch (ex) { } }
995 }
996 } else {
997 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
998 }
999 });
1000 break;
1001 }
1002 case 'msg':
1003 {
1004 // Check the nodeid
1005 if (common.validateString(command.nodeid, 1, 1024) == false) {
1006 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'msg', result: 'Unable to route', tag: command.tag, responseid: command.responseid })); } catch (ex) { } }
1007 return;
1008 }
1009
1010 // Rights check
1011 var requiredRights = null, requiredNonRights = null, routingOptions = null;
1012
1013 // Complete the nodeid if needed
1014 if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
1015
1016 // Check if getting / setting clipboard data is allowed
1017 if ((command.type == 'getclip') && (domain.clipboardget == false)) { console.log('CG-EXIT'); break; }
1018 if ((command.type == 'setclip') && (domain.clipboardset == false)) { console.log('CS-EXIT'); break; }
1019
1020 // Before routing this command, let's do some security checking.
1021 // If this is a tunnel request, we need to make sure the NodeID in the URL matches the NodeID in the command.
1022 if (command.type == 'tunnel') {
1023 if ((typeof command.value != 'string') || (typeof command.nodeid != 'string')) break;
1024 var url = null;
1025 try { url = new URL(command.value, 'http://localhost'); } catch (ex) { }
1026 if (url == null) break; // Bad URL
1027 if (url.searchParams.get('nodeid') && (url.searchParams.get('nodeid') != command.nodeid)) break; // Bad NodeID in URL query string
1028
1029 // Check rights
1030 if (url.searchParams.get('p') == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; }
1031 else if ((url.searchParams.get('p') == '4') || (url.searchParams.get('p') == '5')) { requiredNonRights = MESHRIGHT_NOFILES; }
1032
1033 // If we are using the desktop multiplexor, remove the VIEWONLY limitation. The multiplexor will take care of enforcing that limitation when needed.
1034 if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.searchParams.get('p') == '2')) { routingOptions = { removeViewOnlyLimitation: true }; }
1035
1036 // Add server TLS cert hash
1037 var tlsCertHash = null;
1038 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.
1039 tlsCertHash = parent.webCertificateFullHashs[domain.id];
1040 if (tlsCertHash != null) { command.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
1041 }
1042
1043 // Add user consent messages
1044 command.soptions = {};
1045 if (typeof domain.consentmessages == 'object') {
1046 if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
1047 if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
1048 if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; }
1049 if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
1050 if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
1051 if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
1052 if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
1053 if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
1054 if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
1055 if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
1056 if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
1057 if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
1058 if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
1059 if (domain.consentmessages.autoacceptiffilelocked === true) { command.soptions.consentAutoAcceptIfFileLocked = true; }
1060 if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
1061 }
1062 if (typeof domain.notificationmessages == 'object') {
1063 if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
1064 if (typeof domain.notificationmessages.desktop == 'string') { command.soptions.notifyMsgDesktop = domain.notificationmessages.desktop; }
1065 if (typeof domain.notificationmessages.terminal == 'string') { command.soptions.notifyMsgTerminal = domain.notificationmessages.terminal; }
1066 if (typeof domain.notificationmessages.files == 'string') { command.soptions.notifyMsgFiles = domain.notificationmessages.files; }
1067 }
1068 if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; }
1069
1070 // Add userid
1071 command.userid = user._id;
1072
1073 // Add tunnel pre-message deflate
1074 if (typeof parent.parent.config.settings.agentwscompression == 'boolean') { command.perMessageDeflate = parent.parent.config.settings.agentwscompression; }
1075 }
1076
1077 // If a response is needed, set a callback function
1078 var func = null;
1079 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) { } } }
1080
1081 // Route this command to a target node
1082 routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions);
1083 break;
1084 }
1085 case 'events':
1086 {
1087 // User filtered events
1088 if ((command.userid != null) && ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0)) {
1089 const userSplit = command.userid.split('/');
1090 if ((userSplit.length != 3) || (userSplit[1] != domain.id)) return;
1091
1092 // TODO: Add the meshes command.userid has access to (???)
1093 var filter = [command.userid];
1094
1095 var actionfilter = null;
1096 if (command.filter != null) {
1097 if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter;
1098 }
1099
1100 if ((command.limit == null) || (typeof command.limit != 'number')) {
1101 // Send the list of all events for this session
1102 db.GetUserEvents(filter, domain.id, command.userid, actionfilter, function (err, docs) {
1103 if (err != null) return;
1104 try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { }
1105 });
1106 } else {
1107 // Send the list of most recent events for this session, up to 'limit' count
1108 db.GetUserEventsWithLimit(filter, domain.id, command.userid, command.limit, actionfilter, function (err, docs) {
1109 if (err != null) return;
1110 try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { }
1111 });
1112 }
1113 } else if (command.nodeid != null) { // Device filtered events
1114 // Check that the user has access to this nodeid
1115
1116 const nodeSplit = command.nodeid.split('/');
1117 if (nodeSplit.length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
1118
1119 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
1120 if (node == null) { try { ws.send(JSON.stringify({ action: 'events', events: [], nodeid: command.nodeid, tag: command.tag })); } catch (ex) { } return; }
1121
1122 // Put a limit on the number of returned entries if present
1123 var limit = 10000;
1124 if (common.validateInt(command.limit, 1, 1000000) == true) { limit = command.limit; }
1125
1126 var filter = null;
1127 if (command.filter != null) {
1128 if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) filter = command.filter;
1129 }
1130
1131 if (((rights & MESHRIGHT_LIMITEVENTS) != 0) && (rights != MESHRIGHT_ADMIN)) {
1132 // Send the list of most recent events for this nodeid that only apply to us, up to 'limit' count
1133 db.GetNodeEventsSelfWithLimit(node._id, domain.id, user._id, limit, filter, function (err, docs) {
1134 if (err != null) return;
1135 try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { }
1136 });
1137 } else {
1138 // Send the list of most recent events for this nodeid, up to 'limit' count
1139 db.GetNodeEventsWithLimit(node._id, domain.id, limit, filter, function (err, docs) {
1140 if (err != null) return;
1141 try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { }
1142 });
1143 }
1144 });
1145 } else {
1146 // Create a filter for device groups
1147 if ((obj.user == null) || (obj.user.links == null)) return;
1148
1149 // All events
1150 var exGroupFilter2 = [], filter = [], filter2 = user.subscriptions;
1151
1152 // Add all meshes for groups this user is part of
1153 // TODO (UserGroups)
1154
1155 // Remove MeshID's that we do not have rights to see events for
1156 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); } }
1157 for (var i in filter2) { if (exGroupFilter2.indexOf(filter2[i]) == -1) { filter.push(filter2[i]); } }
1158
1159 var actionfilter = null;
1160 if (command.filter != null) {
1161 if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter;
1162 }
1163
1164 if ((command.limit == null) || (typeof command.limit != 'number')) {
1165 // Send the list of all events for this session
1166 db.GetEvents(filter, domain.id, actionfilter, function (err, docs) {
1167 if (err != null) return;
1168 try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { }
1169 });
1170 } else {
1171 // Send the list of most recent events for this session, up to 'limit' count
1172 db.GetEventsWithLimit(filter, domain.id, command.limit, actionfilter, function (err, docs) {
1173 if (err != null) return;
1174 try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { }
1175 });
1176 }
1177 }
1178 break;
1179 }
1180 case 'recordings': {
1181 if (((user.siteadmin & SITERIGHT_RECORDINGS) == 0) || (domain.sessionrecording == null)) return; // Check if recordings is enabled and we have rights to do this.
1182 var recordingsPath = null;
1183 if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.parent.recordpath; }
1184 if (recordingsPath == null) return;
1185 fs.readdir(recordingsPath, function (err, files) {
1186 if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 1, tag: command.tag })); } catch (ex) { } return; }
1187 if ((command.limit == null) || (typeof command.limit != 'number')) {
1188 // Send the list of all recordings
1189 db.GetEvents(['recording'], domain.id, null, function (err, docs) {
1190 if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; }
1191 for (var i in docs) {
1192 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.
1193 if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
1194 }
1195 try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
1196 });
1197 } else {
1198 // Send the list of most recent recordings, up to 'limit' count
1199 db.GetEventsWithLimit(['recording'], domain.id, command.limit, null, function (err, docs) {
1200 if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; }
1201 for (var i in docs) {
1202 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.
1203 if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
1204 }
1205 try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
1206 });
1207 }
1208 });
1209 break;
1210 }
1211 case 'wssessioncount':
1212 {
1213 // Request a list of all web socket user session count
1214 var wssessions = {};
1215 if ((user.siteadmin & 2) == 0) { try { ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: {}, tag: command.tag })); } catch (ex) { } break; }
1216 if (parent.parent.multiServer == null) {
1217 // No peering, use simple session counting
1218 for (i in parent.wssessions) {
1219 if ((obj.crossDomain === true) || (parent.wssessions[i][0].domainid == domain.id)) {
1220 if ((user.groups == null) || (user.groups.length == 0)) {
1221 // No user groups, count everything
1222 wssessions[i] = parent.wssessions[i].length;
1223 } else {
1224 // Only count if session is for a user in our user groups
1225 var sessionUser = parent.users[parent.wssessions[i][0].userid];
1226 if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
1227 wssessions[i] = parent.wssessions[i].length;
1228 }
1229 }
1230 }
1231 }
1232 } else {
1233 // We have peer servers, use more complex session counting
1234 for (i in parent.sessionsCount) {
1235 if ((obj.crossDomain === true) || (i.split('/')[1] == domain.id)) {
1236 if ((user.groups == null) || (user.groups.length == 0)) {
1237 // No user groups, count everything
1238 wssessions[i] = parent.sessionsCount[i];
1239 } else {
1240 // Only count if session is for a user in our user groups
1241 var sessionUser = parent.users[i];
1242 if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
1243 wssessions[i] = parent.sessionsCount[i];
1244 }
1245 }
1246 }
1247 }
1248 }
1249 try { ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: wssessions, tag: command.tag })); } catch (ex) { } // wssessions is: userid --> count
1250 break;
1251 }
1252 case 'deleteuser':
1253 {
1254 // Delete a user account
1255 var err = null, delusersplit, deluserid, deluser, deluserdomain;
1256 try {
1257 if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
1258 else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
1259 else {
1260 if (command.userid.indexOf('/') < 0) { command.userid = 'user/' + domain.id + '/' + command.userid; }
1261 delusersplit = command.userid.split('/');
1262 deluserid = command.userid;
1263 deluser = parent.users[deluserid];
1264 if (deluser == null) { err = 'User does not exists'; }
1265 else if ((obj.crossDomain !== true) && ((delusersplit.length != 3) || (delusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
1266 else if ((deluser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Permission denied'; } // Need full admin to remote another administrator
1267 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.
1268 }
1269 } catch (ex) { err = 'Validation exception: ' + ex; }
1270
1271 // Get domain
1272 deluserdomain = domain;
1273 if (obj.crossDomain === true) { deluserdomain = parent.parent.config.domains[delusersplit[1]]; }
1274 if (deluserdomain == null) { err = 'Invalid domain'; }
1275
1276 // Handle any errors
1277 if (err != null) {
1278 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: err })); } catch (ex) { } }
1279 break;
1280 }
1281
1282 // Remove all links to this user
1283 if (deluser.links != null) {
1284 for (var i in deluser.links) {
1285 if (i.startsWith('mesh/')) {
1286 // Get the device group
1287 mesh = parent.meshes[i];
1288 if (mesh) {
1289 // Remove user from the mesh
1290 if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
1291
1292 // Notify mesh change
1293 change = 'Removed user ' + deluser.name + ' from device group ' + mesh.name;
1294 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 };
1295 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.
1296 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [deluser._id, user._id]), obj, event);
1297 }
1298 } else if (i.startsWith('node/')) {
1299 // Get the node and the rights for this node
1300 parent.GetNodeWithRights(deluserdomain, deluser, i, function (node, rights, visible) {
1301 if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
1302
1303 // Remove the link and save the node to the database
1304 delete node.links[deluser._id];
1305 if (Object.keys(node.links).length == 0) { delete node.links; }
1306 db.Set(parent.cleanDevice(node));
1307
1308 // Event the node change
1309 var event;
1310 if (command.rights == 0) {
1311 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) }
1312 } else {
1313 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) }
1314 }
1315 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.
1316 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
1317 });
1318 } else if (i.startsWith('ugrp/')) {
1319 // Get the device group
1320 var ugroup = parent.userGroups[i];
1321 if (ugroup) {
1322 // Remove user from the user group
1323 if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
1324
1325 // Notify user group change
1326 change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
1327 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 };
1328 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.
1329 parent.parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
1330 }
1331 }
1332 }
1333 }
1334
1335 db.Remove('ws' + deluser._id); // Remove user web state
1336 db.Remove('nt' + deluser._id); // Remove notes for this user
1337 db.Remove('ntp' + deluser._id); // Remove personal notes for this user
1338 db.Remove('im' + deluser._id); // Remove image for this user
1339
1340 // Delete any login tokens
1341 parent.parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
1342 if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.parent.db.Remove(docs[i]._id, function () { }); } }
1343 });
1344
1345 // Delete all files on the server for this account
1346 try {
1347 var deluserpath = parent.getServerRootFilePath(deluser);
1348 if (deluserpath != null) { parent.deleteFolderRec(deluserpath); }
1349 } catch (e) { }
1350
1351 db.Remove(deluserid);
1352 delete parent.users[deluserid];
1353
1354 var targets = ['*', 'server-users'];
1355 if (deluser.groups) { for (var i in deluser.groups) { targets.push('server-users:' + i); } }
1356 parent.parent.DispatchEvent(targets, obj, { etype: 'user', userid: deluserid, username: deluser.name, action: 'accountremove', msgid: 63, msg: 'Account removed', domain: deluserdomain.id });
1357 parent.parent.DispatchEvent([deluserid], obj, 'close');
1358
1359 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1360
1361 // Log in the auth log
1362 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted user account ' + deluser.name); }
1363
1364 break;
1365 }
1366 case 'userbroadcast':
1367 {
1368 var err = null;
1369 try {
1370 // Broadcast a message to all currently connected users.
1371 if ((user.siteadmin & 2) == 0) { err = "Permission denied"; }
1372 else if (common.validateString(command.msg, 1, 512) == false) { err = "Message is too long"; } // Notification message is between 1 and 256 characters
1373 } catch (ex) { err = "Validation exception: " + ex; }
1374
1375 // Handle any errors
1376 if (err != null) {
1377 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: err })); } catch (ex) { } }
1378 break;
1379 }
1380
1381 // Create the notification message
1382 var notification = { action: 'msg', type: 'notify', domain: domain.id, value: command.msg, title: user.name, icon: 0, tag: 'broadcast', id: Math.random() };
1383 if ((typeof command.maxtime == 'number') && (command.maxtime > 0)) { notification.maxtime = command.maxtime; }
1384
1385 // Send the notification on all user sessions for this server
1386 for (var i in parent.wssessions2) {
1387 try {
1388 if (parent.wssessions2[i].domainid == domain.id) {
1389 var sessionUser = parent.users[parent.wssessions2[i].userid];
1390 if ((command.userid != null) && (command.userid != sessionUser._id) && (command.userid != sessionUser._id.split('/')[2])) { continue; }
1391 if ((command.target == null) || ((sessionUser.links) != null && (sessionUser.links[command.target] != null))) {
1392 if ((user.groups == null) || (user.groups.length == 0)) {
1393 // We are part of no user groups, send to everyone.
1394 parent.wssessions2[i].send(JSON.stringify(notification));
1395 } else {
1396 // We are part of user groups, only send to sessions of users in our groups.
1397 if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
1398 parent.wssessions2[i].send(JSON.stringify(notification));
1399 }
1400 }
1401 }
1402 }
1403 } catch (ex) { }
1404 }
1405
1406 // TODO: Notify all sessions on other peers.
1407
1408 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1409 break;
1410 }
1411 case 'edituser':
1412 {
1413 // Must be user administrator or edit self.
1414 if (((user.siteadmin & 2) == 0) && (user._id != command.id)) break;
1415
1416 // User the username as userid if needed
1417 if ((typeof command.username == 'string') && (command.userid == null)) { command.userid = command.username; }
1418 if ((typeof command.id == 'string') && (command.userid == null)) { command.userid = command.id; }
1419
1420 // Edit a user account
1421 var err = null, editusersplit, edituserid, edituser, edituserdomain;
1422 try {
1423 if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
1424 else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
1425 else {
1426 if (command.userid.indexOf('/') < 0) { command.userid = 'user/' + domain.id + '/' + command.userid; }
1427 editusersplit = command.userid.split('/');
1428 edituserid = command.userid;
1429 edituser = parent.users[edituserid];
1430 if (edituser == null) { err = 'User does not exists'; }
1431 else if ((obj.crossDomain !== true) && ((editusersplit.length != 3) || (editusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
1432 else if ((edituser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Permission denied'; } // Need full admin to remote another administrator
1433 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.
1434 }
1435 } catch (ex) { err = 'Validation exception: ' + ex; }
1436
1437 // Handle any errors
1438 if (err != null) {
1439 if (command.responseid != null) {
1440 try { ws.send(JSON.stringify({ action: 'edituser', responseid: command.responseid, result: err })); } catch (ex) { }
1441 }
1442 break;
1443 }
1444
1445 // Edit a user account, may involve changing email or administrator permissions
1446 var chguser = parent.users[edituserid];
1447 change = 0;
1448 if (chguser) {
1449 // If the target user is admin and we are not admin, no changes can be made.
1450 if ((chguser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) return;
1451
1452 // Can only perform this operation on other users of our group.
1453 if (user.siteadmin != SITERIGHT_ADMIN) {
1454 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) return;
1455 }
1456
1457 // Fetch and validate the user domain
1458 var edituserdomainid = edituserid.split('/')[1];
1459 if ((obj.crossDomain !== true) && (edituserdomainid != domain.id)) break;
1460 var edituserdomain = parent.parent.config.domains[edituserdomainid];
1461 if (edituserdomain == null) break;
1462
1463 // Validate and change email
1464 if (edituserdomain.usernameisemail !== true) {
1465 if (common.validateString(command.email, 0, 1024) && (chguser.email != command.email)) {
1466 if (command.email == '') { command.emailVerified = false; delete chguser.email; } else { chguser.email = command.email.toLowerCase(); }
1467 change = 1;
1468 }
1469 }
1470
1471 // Validate and change real name
1472 if (common.validateString(command.realname, 0, 256) && (chguser.realname != command.realname)) {
1473 if (command.realname == '') { delete chguser.realname; } else { chguser.realname = command.realname; }
1474 change = 1;
1475 }
1476
1477 // Make changes
1478 if ((command.emailVerified === true || command.emailVerified === false) && (chguser.emailVerified != command.emailVerified)) { chguser.emailVerified = command.emailVerified; change = 1; }
1479 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; }
1480 if (command.resetNextLogin === true) { chguser.passchange = -1; }
1481 if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { chguser.consent = command.consent; } change = 1; }
1482 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; }
1483 if ((command.msghandle != null) && (typeof command.msghandle == 'string')) {
1484 if (command.msghandle.startsWith('callmebot:http')) { const h = parent.parent.msgserver.callmebotUrlToHandle(command.msghandle.substring(10)); if (h) { command.msghandle = h; } else { command.msghandle = ''; } }
1485 if (command.msghandle == '') { delete chguser.msghandle; } else { chguser.msghandle = command.msghandle; }
1486 change = 1;
1487 }
1488 if ((command.flags != null) && (typeof command.flags == 'number')) {
1489 // Flags: 1 = Account Image, 2 = Session Recording
1490 if ((command.flags == 0) && (chguser.flags != null)) { delete chguser.flags; change = 1; } else { if (command.flags !== chguser.flags) { chguser.flags = command.flags; change = 1; } }
1491 }
1492 if ((command.removeRights != null) && (typeof command.removeRights == 'number')) {
1493 if (command.removeRights == 0) {
1494 if (chguser.removeRights != null) { delete chguser.removeRights; change = 1; }
1495 } else {
1496 if (command.removeRights !== chguser.removeRights) { chguser.removeRights = command.removeRights; change = 1; }
1497 }
1498 }
1499
1500 // Site admins can change any server rights, user managers can only change AccountLock, NoMeshCmd and NoNewGroups
1501 if (common.validateInt(command.siteadmin) && (chguser._id !== user._id) && (chguser.siteadmin != command.siteadmin)) { // We can't change our own siteadmin permissions.
1502 var chgusersiteadmin = chguser.siteadmin ? chguser.siteadmin : 0;
1503 if (user.siteadmin === SITERIGHT_ADMIN) { chguser.siteadmin = command.siteadmin; change = 1; }
1504 else if (user.siteadmin & 2) {
1505 var mask = 0xFFFFFF1D; // Mask: 2 (User Mangement) + 32 (Account locked) + 64 (No New Groups) + 128 (No Tools)
1506 if ((user.siteadmin & 256) != 0) { mask -= 256; } // Mask: Manage User Groups
1507 if ((user.siteadmin & 512) != 0) { mask -= 512; } // Mask: Manage Recordings
1508 if (((chgusersiteadmin ^ command.siteadmin) & mask) == 0) { chguser.siteadmin = command.siteadmin; change = 1; }
1509 }
1510 }
1511
1512 // When sending a notification about a group change, we need to send to all the previous and new groups.
1513 var allTargetGroups = chguser.groups;
1514 if ((Array.isArray(command.groups)) && ((user._id != command.id) || (user.siteadmin === SITERIGHT_ADMIN))) {
1515 if (command.groups.length == 0) {
1516 // Remove the user groups
1517 if (chguser.groups != null) { delete chguser.groups; change = 1; }
1518 } else {
1519 // Arrange the user groups
1520 var groups2 = [];
1521 for (var i in command.groups) {
1522 if (typeof command.groups[i] == 'string') {
1523 var gname = command.groups[i].trim().toLowerCase();
1524 if ((gname.length > 0) && (gname.length <= 64) && (groups2.indexOf(gname) == -1)) { groups2.push(gname); }
1525 }
1526 }
1527 groups2.sort();
1528
1529 // Set the user groups (Realms)
1530 if (chguser.groups != groups2) { chguser.groups = groups2; change = 1; }
1531
1532 // Add any missing groups in the target list
1533 if (allTargetGroups == null) { allTargetGroups = []; }
1534 for (var i in groups2) { if (allTargetGroups.indexOf(i) == -1) { allTargetGroups.push(i); } }
1535 }
1536 }
1537
1538 if (change == 1) {
1539 // Update the user
1540 db.SetUser(chguser);
1541 parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
1542
1543 var targets = ['*', 'server-users', user._id, chguser._id];
1544 if (allTargetGroups) { for (var i in allTargetGroups) { targets.push('server-users:' + i); } }
1545 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 };
1546 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.
1547 parent.parent.DispatchEvent(targets, obj, event);
1548 }
1549 if ((chguser.siteadmin) && (chguser.siteadmin !== SITERIGHT_ADMIN) && (chguser.siteadmin & 32)) {
1550 // If the user is locked out of this account, disconnect now
1551 parent.parent.DispatchEvent([chguser._id], obj, 'close'); // Disconnect all this user's sessions
1552 }
1553 }
1554
1555 // OK Response
1556 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'edituser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1557 break;
1558 }
1559 case 'usergroups':
1560 {
1561 // Return only groups in the same administrative domain
1562 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) {
1563 // We are not user group administrator, return a list with limited data for our domain.
1564 var groups = {}, groupCount = 0;
1565 for (var i in parent.userGroups) { if (parent.userGroups[i].domain == domain.id) { groupCount++; groups[i] = { name: parent.userGroups[i].name }; } }
1566 try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: groupCount ? groups : null, tag: command.tag })); } catch (ex) { }
1567 } else {
1568 // We are user group administrator, return a full user group list for our domain.
1569 var groups = {}, groupCount = 0;
1570 for (var i in parent.userGroups) { if ((obj.crossDomain == true) || (parent.userGroups[i].domain == domain.id)) { groupCount++; groups[i] = parent.userGroups[i]; } }
1571 try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: groupCount ? groups : null, tag: command.tag })); } catch (ex) { }
1572 }
1573 break;
1574 }
1575 case 'createusergroup':
1576 {
1577 var ugrpdomain, err = null;
1578 try {
1579 // Check if we are in a mode that does not allow manual user group creation
1580 if (
1581 (typeof domain.authstrategies == 'object') &&
1582 (typeof domain.authstrategies['oidc'] == 'object') &&
1583 (typeof domain.authstrategies['oidc'].groups == 'object') &&
1584 ((domain.authstrategies['oidc'].groups.sync == true) || ((typeof domain.authstrategies['oidc'].groups.sync == 'object') && (domain.authstrategies['oidc'].groups.sync.enabled == true)))
1585 ) {
1586 err = "Not allowed in OIDC mode with user group sync.";
1587 }
1588
1589 // Check if we have new group restriction
1590 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = "Permission denied"; }
1591
1592 // Create user group validation
1593 else if (common.validateString(command.name, 1, 64) == false) { err = "Invalid group name"; } // User group name is between 1 and 64 characters
1594 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
1595
1596 // If we are cloning from an existing user group, check that.
1597 if (command.clone) {
1598 if (common.validateString(command.clone, 1, 256) == false) { err = "Invalid clone groupid"; }
1599 else {
1600 var clonesplit = command.clone.split('/');
1601 if ((clonesplit.length != 3) || (clonesplit[0] != 'ugrp') || ((command.domain == null) && (clonesplit[1] != domain.id))) { err = "Invalid clone groupid"; }
1602 else if (parent.userGroups[command.clone] == null) { err = "Invalid clone groupid"; }
1603 }
1604
1605 if (err == null) {
1606 // Get new user group domain
1607 ugrpdomain = parent.parent.config.domains[clonesplit[1]];
1608 if (ugrpdomain == null) { err = "Invalid domain"; }
1609 }
1610 } else {
1611 // Get new user group domain
1612 ugrpdomain = domain;
1613 if ((obj.crossDomain === true) && (command.domain != null)) { ugrpdomain = parent.parent.config.domains[command.domain]; }
1614 if (ugrpdomain == null) { err = "Invalid domain"; }
1615 }
1616
1617 // In some situations, we need a verified email address to create a device group.
1618 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.
1619 } catch (ex) { err = "Validation exception: " + ex; }
1620
1621 // Handle any errors
1622 if (err != null) {
1623 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
1624 break;
1625 }
1626
1627 // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
1628 parent.crypto.randomBytes(48, function (err, buf) {
1629 // Create new device group identifier
1630 var ugrpid = 'ugrp/' + ugrpdomain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1631
1632 // Create the new device group
1633 var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: ugrpdomain.id, links: {} };
1634
1635 // Clone the existing group if required
1636 var pendingDispatchEvents = [];
1637 if (command.clone != null) {
1638 var cgroup = parent.userGroups[command.clone];
1639 if (cgroup.links) {
1640 for (var i in cgroup.links) {
1641 if (i.startsWith('user/')) {
1642 var xuser = parent.users[i];
1643 if ((xuser != null) && (xuser.links != null)) {
1644 ugrp.links[i] = { rights: cgroup.links[i].rights };
1645 xuser.links[ugrpid] = { rights: cgroup.links[i].rights };
1646 db.SetUser(xuser);
1647 parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
1648
1649 // Notify user change
1650 var targets = ['*', 'server-users', user._id, xuser._id];
1651 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 };
1652 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.
1653 //parent.parent.DispatchEvent(targets, obj, event);
1654 pendingDispatchEvents.push([targets, obj, event]);
1655 }
1656 } else if (i.startsWith('mesh/')) {
1657 var xmesh = parent.meshes[i];
1658 if (xmesh && xmesh.links) {
1659 ugrp.links[i] = { rights: cgroup.links[i].rights };
1660 xmesh.links[ugrpid] = { rights: cgroup.links[i].rights };
1661 db.Set(xmesh);
1662
1663 // Notify mesh change
1664 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 };
1665 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.
1666 //parent.parent.DispatchEvent(['*', xmesh._id, user._id], obj, event);
1667 pendingDispatchEvents.push([parent.CreateMeshDispatchTargets(xmesh, [user._id]), obj, event]);
1668 }
1669 }
1670 }
1671 }
1672 }
1673
1674 // Save the new group
1675 db.Set(ugrp);
1676 if (db.changeStream == false) { parent.userGroups[ugrpid] = ugrp; }
1677
1678 // Event the user group creation
1679 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 };
1680 parent.parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
1681
1682 // Event any pending events, these must be sent out after the group creation event is dispatched.
1683 for (var i in pendingDispatchEvents) { var ev = pendingDispatchEvents[i]; parent.parent.DispatchEvent(ev[0], ev[1], ev[2]); }
1684
1685 // Log in the auth log
1686 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created user group ' + ugrp.name); }
1687
1688 try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: 'ok', ugrpid: ugrpid, links: ugrp.links })); } catch (ex) { }
1689 });
1690 break;
1691 }
1692 case 'deleteusergroup':
1693 {
1694 var err = null;
1695
1696 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = "Permission denied"; }
1697
1698 // Change the name or description of a user group
1699 else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = "Invalid group id"; } // Check the user group id
1700 else {
1701 var ugroupidsplit = command.ugrpid.split('/');
1702 if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = "Invalid domain id"; }
1703 }
1704
1705 // Get the domain
1706 var delGroupDomain;
1707 if (ugroupidsplit != null) {
1708 delGroupDomain = parent.parent.config.domains[ugroupidsplit[1]];
1709 if (delGroupDomain == null) { err = "Invalid domain id"; }
1710 }
1711
1712 // Handle any errors
1713 if (err != null) {
1714 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
1715 break;
1716 }
1717
1718 db.Get(command.ugrpid, function (err, groups) {
1719 if ((err != null) || (groups.length != 1)) {
1720 try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { }
1721 return;
1722 }
1723 var group = groups[0];
1724
1725 // If this user group is an externally managed user group, it can't be deleted unless there are no users in it.
1726 if (group.membershipType != null) {
1727 var userCount = 0;
1728 if (group.links != null) { for (var i in group.links) { if (i.startsWith('user/')) { userCount++; } } }
1729 if (userCount > 0) return;
1730 }
1731
1732 // Unlink any user and meshes that have a link to this group
1733 if (group.links) {
1734 for (var i in group.links) {
1735 if (i.startsWith('user/')) {
1736 var xuser = parent.users[i];
1737 if ((xuser != null) && (xuser.links != null)) {
1738 delete xuser.links[group._id];
1739 db.SetUser(xuser);
1740 parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
1741
1742 // Notify user change
1743 var targets = ['*', 'server-users', user._id, xuser._id];
1744 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 };
1745 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.
1746 parent.parent.DispatchEvent(targets, obj, event);
1747 }
1748 } else if (i.startsWith('mesh/')) {
1749 var xmesh = parent.meshes[i];
1750 if (xmesh && xmesh.links) {
1751 delete xmesh.links[group._id];
1752 db.Set(xmesh);
1753
1754 // Notify mesh change
1755 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 };
1756 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.
1757 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(xmesh, [user._id]), obj, event);
1758 }
1759 }
1760 }
1761 }
1762
1763 // Remove the user group from the database
1764 db.Remove(group._id);
1765 if (db.changeStream == false) { delete parent.userGroups[group._id]; }
1766
1767 // Event the user group being removed
1768 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, domain: delGroupDomain.id };
1769 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.
1770 parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
1771
1772 // Log in the auth log
1773 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted user group ' + group.name); }
1774
1775 try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: 'ok', ugrpid: group._id })); } catch (ex) { }
1776 });
1777 break;
1778 }
1779 case 'editusergroup':
1780 {
1781 if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
1782
1783 // Change the name or description of a user group
1784 if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id
1785 var ugroupidsplit = command.ugrpid.split('/');
1786 if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break;
1787
1788 // Get the user group
1789 change = '';
1790 var group = parent.userGroups[command.ugrpid];
1791 if (group != null) {
1792 // If this user group is an externally managed user group, the name of the user group can't be edited
1793 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; }
1794 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; }
1795 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; }
1796
1797 if ((command.flags != null) && (typeof command.flags == 'number')) {
1798 // Flags: 2 = Session Recording
1799 if ((command.flags == 0) && (group.flags != null)) { delete group.flags; } else { if (command.flags !== group.flags) { group.flags = command.flags; } }
1800 if (change == '') { change = 'User group features changed.'; }
1801 }
1802
1803 if (change != '') {
1804 db.Set(group);
1805 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 };
1806 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.
1807 parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
1808 }
1809 }
1810 break;
1811 }
1812 case 'changemeshnotify':
1813 {
1814 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
1815
1816 // 2 = WebPage device connections
1817 // 4 = WebPage device disconnections
1818 // 8 = WebPage device desktop and serial events
1819 // 16 = Email device connections
1820 // 32 = Email device disconnections
1821 // 64 = Email device help request
1822 // 128 = Messaging device connections
1823 // 256 = Messaging device disconnections
1824 // 512 = Messaging device help request
1825
1826 var err = null;
1827 try {
1828 // Change the current user's notification flags for a meshid
1829 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
1830 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
1831 if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
1832 if (parent.IsMeshViewable(user, command.meshid) == false) err = 'Access denied';
1833 } catch (ex) { err = 'Validation exception: ' + ex; }
1834
1835 // Handle any errors
1836 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changemeshnotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
1837
1838 // Change the device group notification
1839 if (user.links == null) { user.links = {}; }
1840 if (user.links[command.meshid]) {
1841 // The user has direct rights for this device group
1842 if (command.notify == 0) {
1843 delete user.links[command.meshid].notify;
1844 } else {
1845 user.links[command.meshid].notify = command.notify;
1846 }
1847 }
1848
1849 // Change user notification if needed, this is needed then a user has device rights thru a user group
1850 if ((command.notify == 0) && (user.notify != null) && (user.notify[command.meshid] != null)) { delete user.notify[command.meshid]; }
1851 if ((command.notify != 0) && (user.links[command.meshid] == null)) { if (user.notify == null) { user.notify = {} } user.notify[command.meshid] = command.notify; }
1852
1853 // Save the user
1854 parent.db.SetUser(user);
1855
1856 // Notify change
1857 var targets = ['*', 'server-users', user._id];
1858 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1859 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 };
1860 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.
1861 parent.parent.DispatchEvent(targets, obj, event);
1862
1863 break;
1864 }
1865 case 'changeusernotify':
1866 {
1867 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
1868
1869 // 2 = WebPage device connections
1870 // 4 = WebPage device disconnections
1871 // 8 = WebPage device desktop and serial events
1872 // 16 = Email device connections
1873 // 32 = Email device disconnections
1874 // 64 = Email device help request
1875 // 128 = Messaging device connections
1876 // 256 = Messaging device disconnections
1877 // 512 = Messaging device help request
1878
1879 var err = null;
1880 try {
1881 // Change the current user's notification flags for a meshid
1882 if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid device identifier'; } // Check the meshid
1883 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
1884 if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
1885 //if (parent.IsMeshViewable(user, command.nodeid) == false) err = 'Access denied';
1886 } catch (ex) { err = 'Validation exception: ' + ex; }
1887
1888 // Handle any errors
1889 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeusernotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
1890
1891 // Check if nothing has changed
1892 if ((user.notify == null) && (command.notify == 0)) return;
1893 if ((user.notify != null) && (user.notify[command.nodeid] == command.notify)) return;
1894
1895 // Change the notification
1896 if (user.notify == null) { user.notify = {}; }
1897 if (command.notify == 0) { delete user.notify[command.nodeid]; } else { user.notify[command.nodeid] = command.notify; }
1898 if (Object.keys(user.notify).length == 0) { delete user.notify; }
1899
1900 // Save the user
1901 parent.db.SetUser(user);
1902
1903 // Notify change
1904 var targets = ['*', 'server-users', user._id];
1905 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1906 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 };
1907 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.
1908 parent.parent.DispatchEvent(targets, obj, event);
1909
1910 break;
1911 }
1912 case 'changepassword':
1913 {
1914 // Do not allow this command when logged in using a login token
1915 if (req.session.loginToken != null) break;
1916
1917 // If this account is settings locked, return here.
1918 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
1919
1920 // Do not allow change password if sspi or ldap
1921 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
1922
1923 // Change our own password
1924 if (common.validateString(command.oldpass, 1, 256) == false) break;
1925 if (common.validateString(command.newpass, 1, 256) == false) break;
1926 if ((command.hint != null) && (common.validateString(command.hint, 0, 256) == false)) break;
1927 if (common.checkPasswordRequirements(command.newpass, domain.passwordrequirements) == false) break; // Password does not meet requirements
1928
1929 // Start by checking the old password
1930 parent.checkUserPassword(domain, user, command.oldpass, function (result) {
1931 if (result == true) {
1932 parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
1933 if (result == 1) {
1934 // Send user notification of error
1935 displayNotificationMessage("Error, unable to change to previously used password.", "Account Settings", 'ServerNotify', 4, 17);
1936 } else if (result == 2) {
1937 // Send user notification of error
1938 displayNotificationMessage("Error, unable to change to commonly used password.", "Account Settings", 'ServerNotify', 4, 18);
1939 } else {
1940 // Update the password
1941 require('./pass').hash(command.newpass, function (err, salt, hash, tag) {
1942 if (err) {
1943 // Send user notification of error
1944 displayNotificationMessage("Error, password not changed.", "Account Settings", 'ServerNotify', 4, 19);
1945 } else {
1946 const nowSeconds = Math.floor(Date.now() / 1000);
1947
1948 // Change the password
1949 if (domain.passwordrequirements != null) {
1950 // Save password hint if this feature is enabled
1951 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; }
1952
1953 // Save previous password if this feature is enabled
1954 if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1955 if (user.oldpasswords == null) { user.oldpasswords = []; }
1956 user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1957 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1958 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1959 }
1960 }
1961 user.salt = salt;
1962 user.hash = hash;
1963 user.passchange = nowSeconds;
1964 delete user.passtype;
1965 db.SetUser(user);
1966
1967 var targets = ['*', 'server-users'];
1968 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1969 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 };
1970 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.
1971 parent.parent.DispatchEvent(targets, obj, event);
1972
1973 // Send user notification of password change
1974 displayNotificationMessage("Password changed.", "Account Settings", 'ServerNotify', 4, 20);
1975
1976 // Log in the auth log
1977 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed this password'); }
1978 }
1979 }, 0);
1980 }
1981 });
1982 } else {
1983 // Send user notification of error
1984 displayNotificationMessage("Current password not correct.", "Account Settings", 'ServerNotify', 4, 21);
1985 }
1986 });
1987 break;
1988 }
1989 case 'changeuserpass':
1990 {
1991 // Change a user's password
1992 if ((user.siteadmin & 2) == 0) break;
1993 if (common.validateString(command.userid, 1, 256) == false) break;
1994 if (common.validateString(command.pass, 0, 256) == false) break;
1995 if ((command.hint != null) && (common.validateString(command.hint, 0, 256) == false)) break;
1996 if (typeof command.removeMultiFactor != 'boolean') break;
1997 if ((command.pass != '') && (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false)) break; // Password does not meet requirements
1998
1999 var chguser = parent.users[command.userid];
2000 if (chguser) {
2001 // If we are not full administrator, we can't change anything on a different full administrator
2002 if ((user.siteadmin != SITERIGHT_ADMIN) & (chguser.siteadmin === SITERIGHT_ADMIN)) break;
2003
2004 // Can only perform this operation on other users of our group.
2005 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
2006
2007 // Compute the password hash & save it
2008 require('./pass').hash(command.pass, function (err, salt, hash, tag) {
2009 if (!err) {
2010 if (command.pass != '') { chguser.salt = salt; chguser.hash = hash; }
2011 if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true) && (command.hint != null)) {
2012 var hint = command.hint;
2013 if (hint.length > 250) { hint = hint.substring(0, 250); }
2014 chguser.passhint = hint;
2015 }
2016 if (command.resetNextLogin === true) { chguser.passchange = -1; } else { chguser.passchange = Math.floor(Date.now() / 1000); }
2017 delete chguser.passtype; // Remove the password type if one was present.
2018 if (command.removeMultiFactor === true) {
2019 delete chguser.otpkeys; // One time backup codes
2020 delete chguser.otpsecret; // OTP Google Authenticator
2021 delete chguser.otphkeys; // FIDO keys
2022 delete chguser.otpekey; // Email 2FA
2023 delete chguser.phone; // SMS 2FA
2024 delete chguser.otpdev; // Push notification 2FA
2025 delete chguser.otpduo; // Duo 2FA
2026 }
2027 db.SetUser(chguser);
2028
2029 var targets = ['*', 'server-users', user._id, chguser._id];
2030 if (chguser.groups) { for (var i in chguser.groups) { targets.push('server-users:' + i); } }
2031 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 };
2032 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.
2033 parent.parent.DispatchEvent(targets, obj, event);
2034
2035 // Log in the auth log
2036 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed account password of user ' + chguser.name); }
2037 } else {
2038 // Report that the password change failed
2039 // TODO
2040 }
2041 }, 0);
2042 }
2043 break;
2044 }
2045 case 'notifyuser':
2046 {
2047 // Send a notification message to a user
2048 if ((user.siteadmin & 2) == 0) break;
2049 if (common.validateString(command.userid, 1, 2048) == false) break;
2050 if (common.validateString(command.msg, 1, 4096) == false) break;
2051
2052 // Can only perform this operation on other users of our group.
2053 var chguser = parent.users[command.userid];
2054 if (chguser == null) break; // This user does not exists
2055 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
2056
2057 // Create the notification message
2058 var notification = { action: 'msg', type: 'notify', id: Math.random(), value: command.msg, title: user.name, icon: 8, userid: user._id, username: user.name };
2059 if (typeof command.url == 'string') { notification.url = command.url; }
2060 if ((typeof command.maxtime == 'number') && (command.maxtime > 0)) { notification.maxtime = command.maxtime; }
2061 if (command.msgid == 11) { notification.value = "Chat Request, Click here to accept."; notification.msgid = 11; } // Chat request
2062
2063 // Get the list of sessions for this user
2064 var sessions = parent.wssessions[command.userid];
2065 if (sessions != null) { for (i in sessions) { try { sessions[i].send(JSON.stringify(notification)); } catch (ex) { } } }
2066
2067 if (parent.parent.multiServer != null) {
2068 // TODO: Add multi-server support
2069 }
2070
2071 // If the user is not connected, use web push if available.
2072 if ((parent.wssessions[chguser._id] == null) && (parent.sessionsCount[chguser._id] == null)) {
2073 // Perform web push notification
2074 var payload = { body: command.msg, icon: 8 }; // Icon 8 is the user icon.
2075 if (command.url) { payload.url = command.url; }
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
2081 break;
2082 }
2083 case 'meshmessenger':
2084 {
2085 // Setup a user-to-user session
2086 if (common.validateString(command.userid, 1, 2048)) {
2087 // Send a notification message to a user
2088 if ((user.siteadmin & 2) == 0) break;
2089
2090 // Can only perform this operation on other users of our group.
2091 var chguser = parent.users[command.userid];
2092 if (chguser == null) break; // This user does not exists
2093 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
2094
2095 // Create the notification message
2096 var notification = {
2097 '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
2098 };
2099
2100 // Get the list of sessions for this user
2101 var sessions = parent.wssessions[command.userid];
2102 if (sessions != null) { for (i in sessions) { try { sessions[i].send(JSON.stringify(notification)); } catch (ex) { } } }
2103
2104 if (parent.parent.multiServer != null) {
2105 // TODO: Add multi-server support
2106 }
2107
2108 // If the user is not connected, use web push if available.
2109 if ((parent.wssessions[chguser._id] == null) && (parent.sessionsCount[chguser._id] == null)) {
2110 // Create the server url
2111 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
2112 var xdomain = (domain.dns == null) ? domain.id : '';
2113 if (xdomain != '') xdomain += "/";
2114 var url = "https://" + parent.getWebServerName(domain, req) + ":" + httpsPort + "/" + xdomain + "messenger?id=meshmessenger/" + encodeURIComponent(command.userid) + "/" + encodeURIComponent(user._id);
2115
2116 // Perform web push notification
2117 var payload = { body: "Chat Request, Click here to accept.", icon: 8, url: url }; // Icon 8 is the user icon.
2118 if (domain.title != null) { payload.title = domain.title; } else { payload.title = "MeshCentral"; }
2119 payload.title += ' - ' + user.name;
2120 parent.performWebPush(domain, chguser, payload, { TTL: 60 }); // For now, 1 minute TTL
2121 }
2122 return;
2123 }
2124
2125 // User-to-device chat is not support in LAN-only mode yet. We need the agent to replace the IP address of the server??
2126 if (args.lanonly == true) { return; }
2127
2128 // Setup a user-to-node session
2129 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
2130 // Check if this user has rights to do this
2131 if ((rights & MESHRIGHT_CHATNOTIFY) == 0) return;
2132
2133 // Create the server url
2134 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
2135 var xdomain = (domain.dns == null) ? domain.id : '';
2136 if (xdomain != '') xdomain += "/";
2137 var url = "https://" + parent.getWebServerName(domain, req) + ":" + httpsPort + "/" + xdomain + "messenger?id=meshmessenger/" + encodeURIComponent(command.nodeid) + "/" + encodeURIComponent(user._id);
2138
2139 // Open a web page on the remote device
2140 routeCommandToNode({ 'action': 'openUrl', 'nodeid': command.nodeid, 'userid': user._id, 'username': user.name, 'url': url });
2141 });
2142 break;
2143 }
2144 case 'createmesh':
2145 {
2146 var err = null;
2147 try {
2148 // Support for old web pages that sent the meshtype as a string.
2149 if (typeof command.meshtype == 'string') { command.meshtype = parseInt(command.meshtype); }
2150
2151 // Check if we have new group restriction
2152 if ((user.siteadmin != SITERIGHT_ADMIN) && ((user.siteadmin & 64) != 0)) { err = 'Permission denied'; }
2153
2154 // In some situations, we need a verified email address to create a device group.
2155 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.
2156
2157 // Create mesh
2158 else if (common.validateString(command.meshname, 1, 128) == false) { err = 'Invalid group name'; } // Meshname is between 1 and 128 characters
2159 else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = 'Invalid group description'; } // Mesh description is between 0 and 1024 characters
2160 else if ((command.meshtype < 1) || (command.meshtype > 4)) { err = 'Invalid group type'; } // Device group types are 1 = AMT, 2 = Agent, 3 = Local
2161 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
2162 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
2163 else if ((domain.ipkvm == null) && (command.meshtype == 4)) { err = 'Invalid group type'; } // IP KVM device group type is not allowed unless enabled
2164 else if ((command.parent != null) && (typeof command.parent !== 'string' || !parent.meshes[command.parent] || parent.meshes[command.parent].domain !== domain.id)) { err = 'Invalid parent group'; }
2165 if ((err == null) && (command.meshtype == 4)) {
2166 if ((command.kvmmodel < 1) || (command.kvmmodel > 2)) { err = 'Invalid KVM model'; }
2167 else if (common.validateString(command.kvmhost, 1, 128) == false) { err = 'Invalid KVM hostname'; }
2168 else if (common.validateString(command.kvmuser, 1, 128) == false) { err = 'Invalid KVM username'; }
2169 else if (common.validateString(command.kvmpass, 1, 128) == false) { err = 'Invalid KVM password'; }
2170 }
2171 } catch (ex) { err = 'Validation exception: ' + ex; }
2172
2173 // Handle any errors
2174 if (err != null) {
2175 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2176 break;
2177 }
2178
2179 // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
2180 parent.crypto.randomBytes(48, function (err, buf) {
2181 // Create new device group identifier
2182 meshid = 'mesh/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
2183
2184 // Create the new device group
2185 var links = {};
2186 links[user._id] = { name: user.name, rights: 4294967295 };
2187 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 };
2188
2189 // Set parent mesh if provided
2190 if (command.parent && parent.meshes[command.parent]) { mesh.parent = command.parent; }
2191
2192 // Add flags and consent if present
2193 if (typeof command.flags == 'number') { mesh.flags = command.flags; }
2194 if (typeof command.consent == 'number') { mesh.consent = command.consent; }
2195
2196 // Add KVM information if needed
2197 if (command.meshtype == 4) { mesh.kvm = { model: command.kvmmodel, host: command.kvmhost, user: command.kvmuser, pass: command.kvmpass }; }
2198
2199 // If this is device group that requires a relay device, store that now
2200 if ((parent.args.lanonly != true) && ((command.meshtype == 3) || (command.meshtype == 4)) && (typeof command.relayid == 'string')) {
2201 // Check the relay id
2202 var relayIdSplit = command.relayid.split('/');
2203 if ((relayIdSplit[0] == 'node') && (relayIdSplit[1] == domain.id)) { mesh.relayid = command.relayid; }
2204 }
2205
2206 // Save the new device group
2207 db.Set(mesh);
2208 parent.meshes[meshid] = mesh;
2209 parent.parent.AddEventDispatch([meshid], ws);
2210
2211 // Change the user to make him administration of the new device group
2212 if (user.links == null) user.links = {};
2213 user.links[meshid] = { rights: 4294967295 };
2214 user.subscriptions = parent.subscribe(user._id, ws);
2215 db.SetUser(user);
2216
2217 // Event the user change
2218 var targets = ['*', 'server-users', user._id];
2219 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2220 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
2221 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.
2222 parent.parent.DispatchEvent(targets, obj, event);
2223
2224 // Event the device group creation
2225 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 };
2226 parent.parent.DispatchEvent(['*', 'server-createmesh', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
2227
2228 // Log in the auth log
2229 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created device group ' + mesh.name); }
2230
2231 try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: 'ok', meshid: meshid, links: links })); } catch (ex) { }
2232
2233 // If needed, event that a device is now a device group relay
2234 if (mesh.relayid != null) {
2235 // Get the node and the rights for this node
2236 parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
2237 if (node == null) return;
2238 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) };
2239 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.
2240 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2241 });
2242 }
2243 });
2244 break;
2245 }
2246 case 'deletemesh':
2247 {
2248 // Delete a mesh and all computers within it
2249 var err = null;
2250
2251 // Resolve the device group name if needed
2252 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
2253 for (var i in parent.meshes) {
2254 var m = parent.meshes[i];
2255 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
2256 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2257 }
2258 }
2259 }
2260
2261 // Validate input
2262 try {
2263 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
2264 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2265 } catch (ex) { err = 'Validation exception: ' + ex; }
2266
2267 // Handle any errors
2268 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
2269
2270 // Get the device group reference we are going to delete
2271 var mesh = parent.meshes[command.meshid];
2272 if (mesh == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { } } return; }
2273
2274 // Check if this user has rights to do this
2275 var err = null;
2276 if (parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN) { err = 'Access denied'; }
2277 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group'; } // Invalid domain, operation only valid for current domain
2278
2279 // Handle any errors
2280 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } return; }
2281
2282 // Fire the removal event first, because after this, the event will not route
2283 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 };
2284 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.
2285
2286 // Remove all user links to this mesh
2287 for (var j in mesh.links) {
2288 if (j.startsWith('user/')) {
2289 var xuser = parent.users[j];
2290 if (xuser && xuser.links) {
2291 delete xuser.links[mesh._id];
2292 db.SetUser(xuser);
2293 parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
2294
2295 // Notify user change
2296 var targets = ['*', 'server-users', user._id, xuser._id];
2297 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 };
2298 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.
2299 parent.parent.DispatchEvent(targets, obj, event);
2300 }
2301 } else if (j.startsWith('ugrp/')) {
2302 var xgroup = parent.userGroups[j];
2303 if (xgroup && xgroup.links) {
2304 delete xgroup.links[mesh._id];
2305 db.Set(xgroup);
2306
2307 // Notify user group change
2308 var targets = ['*', 'server-ugroups', user._id, xgroup._id];
2309 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 };
2310 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.
2311 parent.parent.DispatchEvent(targets, obj, event);
2312 }
2313 }
2314 }
2315
2316 // Delete any invitation codes
2317 delete mesh.invite;
2318
2319 // Delete all files on the server for this mesh
2320 try {
2321 var meshpath = parent.getServerRootFilePath(mesh);
2322 if (meshpath != null) { parent.deleteFolderRec(meshpath); }
2323 } catch (e) { }
2324
2325 parent.parent.RemoveEventDispatchId(command.meshid); // Remove all subscriptions to this mesh
2326
2327 // Notify the devices that they have changed relay roles
2328 if (mesh.relayid != null) {
2329 // Get the node and the rights for this node
2330 parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
2331 if (node == null) return;
2332 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) };
2333 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.
2334 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2335 });
2336 }
2337
2338 // Mark the mesh as deleted
2339 mesh.deleted = new Date(); // Mark the time this mesh was deleted, we can expire it at some point.
2340 db.Set(mesh); // We don't really delete meshes because if a device connects to is again, we will un-delete it.
2341
2342 // Delete all devices attached to this mesh in the database
2343 db.RemoveMeshDocuments(command.meshid);
2344 // TODO: We are possibly deleting devices that users will have links to. We need to clean up the broken links from on occasion.
2345
2346 // Log in the auth log
2347 if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted device group ' + mesh.name); }
2348
2349 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2350 break;
2351 }
2352 case 'editmesh':
2353 {
2354 // Change the name or description of a device group (mesh)
2355 var err = null;
2356
2357 // Resolve the device group name if needed
2358 if ((typeof command.meshidname == 'string') && (command.meshid == null)) {
2359 for (var i in parent.meshes) {
2360 var m = parent.meshes[i];
2361 if ((m.mtype == 2) && (m.name == command.meshidname) && parent.IsMeshViewable(user, m)) {
2362 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2363 }
2364 }
2365 }
2366
2367 // Validate input
2368 try {
2369 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
2370 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2371 if (err == null) {
2372 mesh = parent.meshes[command.meshid];
2373 if (mesh == null) { err = 'Invalid group identifier '; }
2374 }
2375 } catch (ex) { err = 'Validation exception: ' + ex; }
2376
2377 // Handle any errors
2378 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
2379
2380 change = '';
2381
2382 // Check if this user has rights to do this
2383 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return;
2384 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
2385
2386 var changesids = [];
2387 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; }
2388 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; }
2389 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; }
2390 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; }
2391 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; } }
2392
2393 var oldRelayNodeId = null, newRelayNodeId = null;
2394 if ((typeof command.relayid == 'string') && ((mesh.mtype == 3) || (mesh.mtype == 4)) && (mesh.relayid != null) && (command.relayid != mesh.relayid)) {
2395 var relayIdSplit = command.relayid.split('/');
2396 if ((relayIdSplit.length == 3) && (relayIdSplit[0] = 'node') && (relayIdSplit[1] == domain.id)) {
2397 if (change != '') { change += ' and device relay changed'; } else { change = 'Device relay changed'; }
2398 changesids.push(7);
2399 oldRelayNodeId = mesh.relayid;
2400 newRelayNodeId = mesh.relayid = command.relayid;
2401 }
2402 }
2403
2404 // See if we need to change device group invitation codes
2405 if (mesh.mtype == 2) {
2406 if (command.invite === '*') {
2407 // Clear invite codes
2408 if (mesh.invite != null) { delete mesh.invite; }
2409 if (change != '') { change += ' and invite code changed'; } else { change += 'Device group "' + mesh.name + '" invite code changed'; }
2410 changesids.push(6);
2411 } else if ((typeof command.invite == 'object') && (Array.isArray(command.invite.codes)) && (typeof command.invite.flags == 'number')) {
2412 // Set invite codes
2413 if ((mesh.invite == null) || (mesh.invite.codes != command.invite.codes) || (mesh.invite.flags != command.invite.flags)) {
2414 // Check if an invite code is not already in use.
2415 var dup = null;
2416 for (var i in command.invite.codes) {
2417 for (var j in parent.meshes) {
2418 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; }
2419 }
2420 }
2421 if (dup != null) {
2422 // A duplicate was found, don't allow this change.
2423 displayNotificationMessage("Error, invite code \"" + dup + "\" already in use.", "Invite Codes", null, 6, 22, [dup]);
2424 return;
2425 }
2426 mesh.invite = { codes: command.invite.codes, flags: command.invite.flags };
2427 if (typeof command.invite.ag == 'number') { mesh.invite.ag = command.invite.ag; }
2428 if (change != '') { change += ' and invite code changed'; } else { change += 'Device group "' + mesh.name + '" invite code changed'; }
2429 changesids.push(6);
2430 }
2431 }
2432 }
2433
2434 if (change != '') {
2435 db.Set(mesh);
2436 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 };
2437 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.
2438 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, 'server-editmesh']), obj, event);
2439 }
2440
2441 // Notify the devices that they have changed relay roles
2442 if (oldRelayNodeId != null) {
2443 // Get the node and the rights for this node
2444 parent.GetNodeWithRights(domain, user, oldRelayNodeId, function (node, rights, visible) {
2445 if (node == null) return;
2446 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) };
2447 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.
2448 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2449 });
2450 }
2451 if (newRelayNodeId != null) {
2452 // Get the node and the rights for this node
2453 parent.GetNodeWithRights(domain, user, newRelayNodeId, function (node, rights, visible) {
2454 if (node == null) return;
2455 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) };
2456 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.
2457 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2458 });
2459 } else if ((mesh.relayid != null) && (changesids.indexOf(1) >= 0)) {
2460 // Notify of node name change, get the node and the rights for this node, we just want to trigger a device update.
2461 parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
2462 if (node == null) return;
2463 var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, node: parent.CloneSafeNode(node), nolog: 1 };
2464 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.
2465 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
2466 });
2467 }
2468
2469 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2470 break;
2471 }
2472 case 'removemeshuser':
2473 {
2474 var xdomain, err = null;
2475
2476 // Resolve the device group name if needed
2477 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
2478 for (var i in parent.meshes) {
2479 var m = parent.meshes[i];
2480 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
2481 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2482 }
2483 }
2484 }
2485
2486 try {
2487 if (common.validateString(command.userid, 1, 1024) == false) { err = "Invalid userid"; } // Check userid
2488 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid
2489 if (command.userid.indexOf('/') == -1) { command.userid = 'user/' + domain.id + '/' + command.userid; }
2490 if (command.userid == obj.user._id) { err = "Can't remove self"; } // Can't add of modify self
2491 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
2492 else {
2493 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2494 mesh = parent.meshes[command.meshid];
2495 var meshIdSplit = command.meshid.split('/');
2496 if (mesh == null) { err = "Unknown device group"; }
2497 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGEUSERS) == 0) { err = "Permission denied"; }
2498 else if (meshIdSplit.length != 3) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2499 else {
2500 xdomain = domain;
2501 if (obj.crossDomain !== true) { xdomain = parent.parent.config.domains[meshIdSplit[1]]; }
2502 if (xdomain == null) { err = "Invalid domain"; }
2503 }
2504 }
2505 } catch (ex) { err = "Validation exception: " + ex; }
2506
2507 // Handle any errors
2508 if (err != null) {
2509 console.log(err);
2510 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: err })); } catch (ex) { } }
2511 break;
2512 }
2513
2514 // 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.
2515 var deluserid = command.userid, deluser = null;
2516 if (deluserid.startsWith('user/')) { deluser = parent.users[deluserid]; }
2517 else if (deluserid.startsWith('ugrp/')) { deluser = parent.userGroups[deluserid]; }
2518
2519 // Search for a user name in that windows domain is the username starts with *\
2520 if ((deluser == null) && (deluserid.startsWith('user/' + xdomain.id + '/*\\')) == true) {
2521 var search = deluserid.split('/')[2].substring(1);
2522 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; } }
2523 }
2524
2525 if (deluser != null) {
2526 // Remove mesh from user
2527 if (deluser.links != null && deluser.links[command.meshid] != null) {
2528 var delmeshrights = deluser.links[command.meshid].rights;
2529 if ((delmeshrights == MESHRIGHT_ADMIN) && (parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN)) return; // A non-admin can't kick out an admin
2530 delete deluser.links[command.meshid];
2531 if (deluserid.startsWith('user/')) { db.SetUser(deluser); }
2532 else if (deluserid.startsWith('ugrp/')) { db.Set(deluser); }
2533 parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe');
2534
2535 if (deluserid.startsWith('user/')) {
2536 // Notify user change
2537 var targets = ['*', 'server-users', user._id, deluser._id];
2538 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 };
2539 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.
2540 parent.parent.DispatchEvent(targets, obj, event);
2541 } else if (deluserid.startsWith('ugrp/')) {
2542 // Notify user group change
2543 var targets = ['*', 'server-ugroups', user._id, deluser._id];
2544 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 };
2545 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.
2546 parent.parent.DispatchEvent(targets, obj, event);
2547 }
2548 }
2549 }
2550
2551 // Remove user from the mesh
2552 if (mesh.links[command.userid] != null) {
2553 delete mesh.links[command.userid];
2554 db.Set(mesh);
2555
2556 // Notify mesh change
2557 var event;
2558 if (deluser != null) {
2559 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 };
2560 } else {
2561 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 };
2562 }
2563 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, command.userid]), obj, event);
2564 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2565 } else {
2566 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'User not in group' })); } catch (ex) { } }
2567 }
2568 break;
2569 }
2570 case 'meshamtpolicy':
2571 {
2572 // Change a mesh Intel AMT policy
2573 if (common.validateString(command.meshid, 8, 134) == false) break; // Check the meshid
2574 if (common.validateObject(command.amtpolicy) == false) break; // Check the amtpolicy
2575 if (common.validateInt(command.amtpolicy.type, 0, 4) == false) break; // Check the amtpolicy.type
2576 if (command.amtpolicy.type === 2) {
2577 if ((command.amtpolicy.password != null) && (common.validateString(command.amtpolicy.password, 0, 32) == false)) break; // Check the amtpolicy.password
2578 if ((command.amtpolicy.badpass != null) && common.validateInt(command.amtpolicy.badpass, 0, 1) == false) break; // Check the amtpolicy.badpass
2579 if (common.validateInt(command.amtpolicy.cirasetup, 0, 2) == false) break; // Check the amtpolicy.cirasetup
2580 } else if (command.amtpolicy.type === 3) {
2581 if ((command.amtpolicy.password != null) && (common.validateString(command.amtpolicy.password, 0, 32) == false)) break; // Check the amtpolicy.password
2582 if ((command.amtpolicy.badpass != null) && common.validateInt(command.amtpolicy.badpass, 0, 1) == false) break; // Check the amtpolicy.badpass
2583 if ((command.amtpolicy.ccm != null) && common.validateInt(command.amtpolicy.ccm, 0, 2) == false) break; // Check the amtpolicy.ccm
2584 if (common.validateInt(command.amtpolicy.cirasetup, 0, 2) == false) break; // Check the amtpolicy.cirasetup
2585 }
2586
2587 mesh = parent.meshes[command.meshid];
2588 if (mesh) {
2589 // Check if this user has rights to do this
2590 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return;
2591 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
2592
2593 // TODO: Check if this is a change from the existing policy
2594
2595 // Perform the Intel AMT policy change
2596 var amtpolicy = { type: command.amtpolicy.type };
2597 if ((command.amtpolicy.type === 2) || (command.amtpolicy.type === 3)) {
2598 amtpolicy = { type: command.amtpolicy.type, badpass: command.amtpolicy.badpass, cirasetup: command.amtpolicy.cirasetup };
2599 if (command.amtpolicy.type === 3) { amtpolicy.ccm = command.amtpolicy.ccm; }
2600 if ((command.amtpolicy.password == null) && (mesh.amt != null) && (typeof mesh.amt.password == 'string')) { amtpolicy.password = mesh.amt.password; } // Keep the last password
2601 if ((typeof command.amtpolicy.password == 'string') && (command.amtpolicy.password.length >= 8)) { amtpolicy.password = command.amtpolicy.password; } // Set a new password
2602 }
2603 mesh.amt = amtpolicy;
2604 db.Set(mesh);
2605 var amtpolicy2 = Object.assign({}, amtpolicy); // Shallow clone
2606 if (amtpolicy2.password != null) { amtpolicy2.password = 1; }
2607 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 };
2608 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.
2609 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id]), obj, event);
2610
2611 // If we have peer servers, inform them of the new Intel AMT policy for this device group
2612 if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'newIntelAmtPolicy', meshid: command.meshid, amtpolicy: amtpolicy }); }
2613
2614 // See if any agents for the affected device group is connected, if so, update the Intel AMT policy
2615 for (var nodeid in parent.wsagents) {
2616 const agent = parent.wsagents[nodeid];
2617 if (agent.dbMeshKey == command.meshid) { agent.sendUpdatedIntelAmtPolicy(amtpolicy); }
2618 }
2619 }
2620 break;
2621 }
2622 case 'addlocaldevice':
2623 {
2624 var err = null;
2625 // Perform input validation
2626 try {
2627 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid
2628 if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name
2629 if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname
2630 if (typeof command.type != 'number') { err = "Invalid type"; } // Type must be a number
2631 if ((command.type != 4) && (command.type != 6) && (command.type != 29)) { err = "Invalid type"; } // Check device type
2632 else {
2633 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2634 mesh = parent.meshes[command.meshid];
2635 if (mesh == null) { err = "Unknown device group"; }
2636 if (mesh.mtype != 3) { err = "Local device agentless mesh only allowed" } // This operation is only allowed for mesh type 3, local device agentless mesh.
2637 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
2638 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2639 }
2640 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
2641 // Handle any errors
2642 if (err != null) {
2643 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2644 break;
2645 }
2646
2647 // Create a new nodeid
2648 parent.crypto.randomBytes(48, function (err, buf) {
2649 // Create the new node
2650 nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
2651 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 } };
2652 db.Set(device);
2653
2654 // Event the new node
2655 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 });
2656 // Send response if required
2657 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addlocaldevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2658 });
2659 break;
2660 }
2661 case 'addamtdevice':
2662 {
2663 if (args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added
2664 var err = null;
2665 // Perform input validation
2666 try {
2667 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid
2668 if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name
2669 if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname
2670 if (common.validateString(command.amtusername, 0, 16) == false) { err = "Invalid amtusername"; } // Check username
2671 if (common.validateString(command.amtpassword, 0, 16) == false) { err = "Invalid amtpassword"; } // Check password
2672 if (command.amttls == '0') { command.amttls = 0; } else if (command.amttls == '1') { command.amttls = 1; } // Check TLS flag
2673 if ((command.amttls != 1) && (command.amttls != 0)) { err = "Invalid amttls"; }
2674 else {
2675 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2676 // Get the mesh
2677 mesh = parent.meshes[command.meshid];
2678 if (mesh == null) { err = "Unknown device group"; }
2679 if (mesh.mtype != 1) { err = "Intel AMT agentless mesh only allowed"; } // This operation is only allowed for mesh type 1, Intel AMT agentless mesh.
2680 // Check if this user has rights to do this
2681 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
2682 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2683 }
2684 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
2685
2686 // Handle any errors
2687 if (err != null) {
2688 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2689 break;
2690 }
2691
2692 // If we are in WAN-only mode, hostname is not used
2693 if ((args.wanonly == true) && (command.hostname)) { delete command.hostname; }
2694
2695 // Create a new nodeid
2696 parent.crypto.randomBytes(48, function (err, buf) {
2697 // Create the new node
2698 nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
2699 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 } };
2700
2701 // Add optional feilds
2702 if (common.validateInt(command.state, 0, 3)) { device.intelamt.state = command.state; }
2703 if (common.validateString(command.ver, 1, 16)) { device.intelamt.ver = command.ver; }
2704 if (common.validateString(command.hash, 1, 256)) { device.intelamt.hash = command.hash; }
2705 if (common.validateString(command.realm, 1, 256)) { device.intelamt.realm = command.realm; }
2706
2707 // Save the device to the database
2708 db.Set(device);
2709
2710 // Event the new node
2711 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 });
2712 // Send response if required
2713 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addamtdevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2714 });
2715
2716 break;
2717 }
2718 case 'scanamtdevice':
2719 {
2720 if (args.wanonly == true) return; // This is a WAN-only server, this type of scanning is not allowed.
2721 if (common.validateString(command.range, 1, 256) == false) break; // Check range string
2722
2723 // Ask the RMCP scanning to scan a range of IP addresses
2724 if (parent.parent.amtScanner) {
2725 if (parent.parent.amtScanner.performRangeScan(user._id, command.range) == false) {
2726 parent.parent.DispatchEvent(['*', user._id], obj, { action: 'scanamtdevice', range: command.range, results: null, nolog: 1 });
2727 }
2728 }
2729 break;
2730 }
2731 case 'changeDeviceMesh':
2732 {
2733 var err = null;
2734
2735 // Resolve the device group name if needed
2736 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
2737 for (var i in parent.meshes) {
2738 var m = parent.meshes[i];
2739 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
2740 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
2741 }
2742 }
2743 }
2744
2745 // Perform input validation
2746 try {
2747 if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
2748 if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid
2749 else {
2750 if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
2751 mesh = parent.meshes[command.meshid];
2752 if (mesh == null) { err = "Unknown device group"; }
2753 else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
2754 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
2755 }
2756 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
2757
2758 // Handle any errors
2759 if (err != null) {
2760 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
2761 break;
2762 }
2763
2764 // This is to change device guest sharing to the new device group
2765 var changeDeviceShareMeshIdNodeCount = command.nodeids.length;
2766 var changeDeviceShareMeshIdNodeList = [];
2767
2768 // For each nodeid, change the group
2769 for (var i = 0; i < command.nodeids.length; i++) {
2770 var xnodeid = command.nodeids[i];
2771 if (xnodeid.indexOf('/') == -1) { xnodeid = 'node/' + domain.id + '/' + xnodeid; }
2772
2773 // Get the node and the rights for this node
2774 parent.GetNodeWithRights(domain, user, xnodeid, function (node, rights, visible) {
2775 // Check if we found this device
2776 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; }
2777
2778 // Check if already in the right mesh
2779 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; }
2780
2781 // Make sure both source and target mesh are the same type
2782 try { if (parent.meshes[node.meshid].mtype != parent.meshes[command.meshid].mtype) { changeDeviceShareMeshIdNodeCount--; return; } } catch (e) {
2783 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Device groups are of different types' })); } catch (ex) { } }
2784 changeDeviceShareMeshIdNodeCount--;
2785 return;
2786 };
2787
2788 // Make sure that we have rights on both source and destination mesh
2789 const targetMeshRights = parent.GetMeshRights(user, command.meshid);
2790 if (((rights & MESHRIGHT_EDITMESH) == 0) || ((targetMeshRights & MESHRIGHT_EDITMESH) == 0)) {
2791 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Permission denied' })); } catch (ex) { } }
2792 changeDeviceShareMeshIdNodeCount--;
2793 return;
2794 }
2795
2796 // Perform the switch, start by saving the node with the new meshid.
2797 changeDeviceShareMeshIdNodeList.push(node._id);
2798 changeDeviceShareMeshIdNodeCount--;
2799 if (changeDeviceShareMeshIdNodeCount == 0) { changeDeviceShareMeshId(changeDeviceShareMeshIdNodeList, command.meshid); }
2800 const oldMeshId = node.meshid;
2801 node.meshid = command.meshid;
2802 db.Set(parent.cleanDevice(node));
2803
2804 // If the device is connected on this server, switch it now.
2805 var agentSession = parent.wsagents[node._id];
2806 if (agentSession != null) {
2807 agentSession.dbMeshKey = command.meshid; // Switch the agent mesh
2808 agentSession.meshid = command.meshid.split('/')[2]; // Switch the agent mesh
2809 agentSession.sendUpdatedIntelAmtPolicy(); // Send the new Intel AMT policy
2810 }
2811
2812 // If any MQTT sessions are connected on this server, switch it now.
2813 if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.changeDeviceMesh(node._id, command.meshid); }
2814
2815 // If any CIRA sessions are connected on this server, switch it now.
2816 if (parent.parent.mpsserver != null) { parent.parent.mpsserver.changeDeviceMesh(node._id, command.meshid); }
2817
2818 // Add the connection state
2819 const state = parent.parent.GetConnectivityState(node._id);
2820 if (state) {
2821 node.conn = state.connectivity;
2822 node.pwr = state.powerState;
2823 if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[node._id]; if (agent != null) { node.agct = agent.connectTime; } }
2824
2825 // Uuse the connection time of the CIRA/Relay connection
2826 if ((state.connectivity & 2) != 0) {
2827 var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(node._id, null, true);
2828 if ((ciraConnection != null) && (ciraConnection.tag != null)) { node.cict = ciraConnection.tag.connectTime; }
2829 }
2830 }
2831
2832 // Update lastconnect meshid for this node
2833 db.Get('lc' + node._id, function (err, xnodes) {
2834 if ((xnodes != null) && (xnodes.length == 1) && (xnodes[0].meshid != command.meshid)) { xnodes[0].meshid = command.meshid; db.Set(xnodes[0]); }
2835 });
2836
2837 // Event the node change
2838 var newMesh = parent.meshes[command.meshid];
2839 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 };
2840 // 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.
2841 parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [oldMeshId, node._id]), obj, event);
2842
2843 // Send response if required
2844 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2845 });
2846 }
2847 break;
2848 }
2849 case 'removedevices':
2850 {
2851 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
2852 for (i in command.nodeids) {
2853 var nodeid = command.nodeids[i], err = null;
2854
2855 // Argument validation
2856 if (common.validateString(nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check nodeid
2857 else {
2858 if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
2859 if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
2860 }
2861 if (err != null) {
2862 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: err })); } catch (ex) { } }
2863 continue;
2864 }
2865 // Get the node and the rights for this node
2866 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
2867 // Check we have the rights to delete this device
2868 if ((rights & MESHRIGHT_UNINSTALL) == 0) {
2869 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
2870 return;
2871 }
2872
2873 // Delete this node including network interface information, events and timeline
2874 db.Remove(node._id); // Remove node with that id
2875 db.Remove('if' + node._id); // Remove interface information
2876 db.Remove('nt' + node._id); // Remove notes
2877 db.Remove('lc' + node._id); // Remove last connect time
2878 db.Remove('si' + node._id); // Remove system information
2879 db.Remove('al' + node._id); // Remove error log last time
2880 if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
2881 db.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node
2882 db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
2883 if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
2884 db.Get('ra' + node._id, function (err, nodes) {
2885 if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
2886 db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
2887 });
2888
2889 // Remove any user node links
2890 if (node.links != null) {
2891 for (var i in node.links) {
2892 if (i.startsWith('user/')) {
2893 var cuser = parent.users[i];
2894 if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
2895 // Remove the user link & save the user
2896 delete cuser.links[node._id];
2897 if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
2898 db.SetUser(cuser);
2899
2900 // Notify user change
2901 var targets = ['*', 'server-users', cuser._id];
2902 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) };
2903 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.
2904 parent.parent.DispatchEvent(targets, obj, event);
2905 }
2906 } else if (i.startsWith('ugrp/')) {
2907 var cusergroup = parent.userGroups[i];
2908 if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
2909 // Remove the user link & save the user
2910 delete cusergroup.links[node._id];
2911 if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
2912 db.Set(cusergroup);
2913
2914 // Notify user change
2915 var targets = ['*', 'server-users', cusergroup._id];
2916 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 };
2917 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.
2918 parent.parent.DispatchEvent(targets, obj, event);
2919 }
2920 }
2921 }
2922 }
2923
2924 // Event node deletion
2925 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 };
2926 // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
2927 //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.
2928 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
2929
2930 // Disconnect all connections if needed
2931 var state = parent.parent.GetConnectivityState(nodeid);
2932 if ((state != null) && (state.connectivity != null)) {
2933 if ((state.connectivity & 1) != 0) { parent.wsagents[nodeid].close(); } // Disconnect mesh agent
2934 if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(nodeid); } // Disconnect CIRA/Relay/LMS connections
2935 }
2936
2937 // Send response if required
2938 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
2939 });
2940 }
2941
2942 break;
2943 }
2944 case 'wakedevices':
2945 {
2946 // TODO: We can optimize this a lot.
2947 // - We should get a full list of all MAC's to wake first.
2948 // - We should try to only have one agent per subnet (using Gateway MAC) send a wake-on-lan.
2949 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
2950
2951 // Event wakeup, this will cause Intel AMT wake operations on this and other servers.
2952 parent.parent.DispatchEvent('*', obj, { action: 'wakedevices', userid: user._id, username: user.name, nodeids: command.nodeids, domain: domain.id, nolog: 1 });
2953
2954 // Perform wake-on-lan
2955 for (i in command.nodeids) {
2956 var nodeid = command.nodeids[i];
2957
2958 // Argument validation
2959 if (common.validateString(nodeid, 8, 128) == false) { // Check the nodeid
2960 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
2961 continue;
2962 }
2963 else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
2964 else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { // Invalid domain, operation only valid for current domain
2965 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid domain' })); } catch (ex) { } }
2966 continue;
2967 }
2968
2969 // Get the node and the rights for this node
2970 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
2971 // Check we have the rights to wake this device
2972 if ((node == null) || (visible == false) || (rights & MESHRIGHT_WAKEDEVICE) == 0) {
2973 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
2974 return;
2975 }
2976
2977 // If this device is connected on MQTT, send a wake action.
2978 if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.publish(node._id, 'powerAction', 'wake'); }
2979
2980 // If this is a IP-KVM or Power Distribution Unit (PDU), dispatch an action event
2981 if (node.mtype == 4) {
2982 // Send out an event to perform turn off command on the port
2983 const targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['devport-operation', 'server-users', user._id]);
2984 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." };
2985 parent.parent.DispatchEvent(targets, obj, event);
2986 return;
2987 }
2988
2989 // Get the device interface information
2990 db.Get('if' + node._id, function (err, nodeifs) {
2991 if ((nodeifs != null) && (nodeifs.length == 1)) {
2992 var macs = [], nodeif = nodeifs[0];
2993 if (nodeif.netif) {
2994 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); } }
2995 } else if (nodeif.netif2) {
2996 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); } } }
2997 }
2998 if (macs.length == 0) {
2999 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) { } }
3000 return;
3001 }
3002
3003 // Have the server send a wake-on-lan packet (Will not work in WAN-only)
3004 if (parent.parent.meshScanner != null) { parent.parent.meshScanner.wakeOnLan(macs, node.host); }
3005
3006 // Get the list of device groups this user as wake permissions on
3007 var targets = [], targetDeviceGroups = parent.GetAllMeshWithRights(user, MESHRIGHT_WAKEDEVICE);
3008 for (j in targetDeviceGroups) { targets.push(targetDeviceGroups[j]._id); }
3009 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); } }
3010
3011 // Go thru all the connected agents and send wake-on-lan on all the ones in the target mesh list
3012 var wakeCount = 0;
3013 for (j in parent.wsagents) {
3014 var agent = parent.wsagents[j];
3015 if ((agent.authenticated == 2) && ((targets.indexOf(agent.dbMeshKey) >= 0) || (targets.indexOf(agent.dbNodeKey) >= 0))) {
3016 //console.log('Asking agent ' + agent.dbNodeKey + ' to wake ' + macs.join(','));
3017 try { agent.send(JSON.stringify({ action: 'wakeonlan', macs: macs })); wakeCount++; } catch (ex) { }
3018 }
3019 }
3020 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) { } }
3021 } else {
3022 if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'No network information for this device' })); } catch (ex) { } }
3023 }
3024 });
3025 });
3026
3027 if (command.nodeids.length > 1) {
3028 // If we are waking multiple devices, confirm we got the command.
3029 try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
3030 }
3031 }
3032 break;
3033 }
3034 case 'webrelay':
3035 {
3036 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
3037 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
3038 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
3039 else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present
3040 else {
3041 if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
3042 var snode = command.nodeid.split('/');
3043 if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; }
3044 }
3045 // Handle any errors
3046 if (err != null) {
3047 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: err })); } catch (ex) { } }
3048 break;
3049 }
3050 // Get the device rights
3051 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3052 // If node not found or we don't have remote control, reject.
3053 if (node == null) {
3054 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
3055 return;
3056 }
3057 var relayid = null;
3058 var addr = null;
3059 if (node.mtype == 3) { // Setup device relay if needed
3060 var mesh = parent.meshes[node.meshid];
3061 if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
3062 }
3063 var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
3064 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));
3065 if (webRelayPort == 0) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'WebRelay Disabled' })); return; } catch (ex) { } }
3066 const authRelayCookie = parent.parent.encodeCookie({ ruserid: user._id, x: req.session.x }, parent.parent.loginCookieEncryptionKey);
3067 var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + command.nodeid + '&p=' + command.port + '&appid=' + command.appid + '&c=' + authRelayCookie;
3068 if (addr != null) { url += '&addr=' + addr; }
3069 if (relayid != null) { url += '&relayid=' + relayid }
3070 command.url = url;
3071 if (command.responseid != null) { command.result = 'OK'; }
3072 try { ws.send(JSON.stringify(command)); } catch (ex) { }
3073 });
3074 break;
3075 }
3076 case 'runcommands':
3077 {
3078 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3079 if (typeof command.presetcmd != 'number') {
3080 if (typeof command.type != 'number') break; // Check command type
3081 if (typeof command.runAsUser != 'number') { command.runAsUser = 0; } // Check runAsUser
3082 }
3083
3084 const processRunCommand = function (command) {
3085 for (i in command.nodeids) {
3086 var nodeid = command.nodeids[i], err = null;
3087
3088 // Argument validation
3089 if (common.validateString(nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check nodeid
3090 else {
3091 if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
3092 if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
3093 }
3094 if (err != null) {
3095 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: err })); } catch (ex) { } }
3096 continue;
3097 }
3098
3099 // Get the node and the rights for this node
3100 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3101 // Check if this node was found
3102 if (node == null) {
3103 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
3104 return;
3105 }
3106
3107 if (command.type == 4) {
3108 // This is an agent console command
3109
3110 // Check we have the rights to run commands on this device, MESHRIGHT_REMOTECONTROL & MESHRIGHT_AGENTCONSOLE are needed
3111 if ((rights & 24) != 24) {
3112 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
3113 return;
3114 }
3115
3116 var theCommand = { action: 'msg', type: 'console', value: command.cmds, rights: rights, sessionid: ws.sessionId };
3117 if (parent.parent.multiServer != null) { // peering setup
3118 parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand});
3119 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3120 } else {
3121 // Send the commands to the agent
3122 var agent = parent.wsagents[node._id];
3123 if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
3124 try { agent.send(JSON.stringify(theCommand)); } catch (ex) { }
3125 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3126 } else {
3127 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
3128 }
3129 }
3130 } else {
3131 // This is a standard (bash/shell/powershell) command.
3132
3133 // Check we have the rights to run commands on this device
3134 if ((rights & MESHRIGHT_REMOTECOMMAND) == 0) {
3135 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
3136 return;
3137 }
3138
3139 if (typeof command.reply != 'boolean') command.reply = false;
3140 if (typeof command.responseid != 'string') command.responseid = null;
3141 var msgid = 24; // "Running commands"
3142 if (command.type == 1) { msgid = 99; } // "Running commands as user"
3143 if (command.type == 2) { msgid = 100; } // "Running commands as user if possible"
3144 // Check if this agent is correct for this command type
3145 // command.type 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux/BSD/macOS
3146 var commandsOk = false;
3147 if ((node.agent.id > 0) && (node.agent.id < 5) || (node.agent.id > 41 && node.agent.id < 44)) {
3148 // Windows Agent
3149 if ((command.type == 1) || (command.type == 2)) { commandsOk = true; }
3150 else if (command.type === 0) { command.type = 1; commandsOk = true; } // Set the default type of this agent
3151 } else {
3152 // Non-Windows Agent
3153 if (command.type == 3) { commandsOk = true; }
3154 else if (command.type === 0) { command.type = 3; commandsOk = true; } // Set the default type of this agent
3155 }
3156 if (commandsOk == true) {
3157 var theCommand = { action: 'runcommands', type: command.type, cmds: command.cmds, runAsUser: command.runAsUser, reply: command.reply, responseid: command.responseid };
3158 var agent = parent.wsagents[node._id];
3159 if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
3160 // Send the commands to the agent
3161 try { agent.send(JSON.stringify(theCommand)); } catch (ex) { }
3162 if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3163 // Send out an event that these commands where run on this device
3164 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
3165 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 };
3166 parent.parent.DispatchEvent(targets, obj, event);
3167 } else if (parent.parent.multiServer != null) { // peering setup
3168 // Send the commands to the agent
3169 parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand});
3170 if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
3171 // Send out an event that these commands where run on this device
3172 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
3173 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 };
3174 parent.parent.multiServer.DispatchEvent(targets, obj, event);
3175 } else {
3176 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
3177 }
3178 } else {
3179 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid command type' })); } catch (ex) { } }
3180 }
3181 }
3182 });
3183 }
3184 }
3185
3186 if (typeof command.presetcmd == 'number') {
3187 // If a pre-set command is used, load the command
3188 if (Array.isArray(domain.preconfiguredscripts) == false) return;
3189 const script = domain.preconfiguredscripts[command.presetcmd];
3190 if (script == null) return;
3191 delete command.presetcmd;
3192
3193 // Decode script type
3194 const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent
3195 if (typeof script.type == 'string') { const stype = types.indexOf(script.type.toLowerCase()); if (stype > 0) { command.type = stype; } }
3196 if (command.type == null) return;
3197
3198 // Decode script runas
3199 if (command.type != 4) {
3200 const runAsModes = ['agent', 'userfirst', 'user']; // 0 = AsAgent, 1 = UserFirst, 2 = UserOnly
3201 if (typeof script.runas == 'string') { const srunas = runAsModes.indexOf(script.runas.toLowerCase()); if (srunas >= 0) { command.runAsUser = srunas; } }
3202 }
3203
3204 if (typeof script.file == 'string') {
3205 // The pre-defined script commands are in a file, load it
3206 const scriptPath = parent.common.joinPath(parent.parent.datapath, script.file);
3207 fs.readFile(scriptPath, function (err, data) {
3208 // If loaded correctly, run loaded commands
3209 if ((err != null) || (data == null) || (data.length == 0) || (data.length > 65535)) return;
3210 command.cmds = data.toString();
3211 processRunCommand(command);
3212 });
3213 } else if (typeof script.cmd == 'string') {
3214 // The pre-defined script commands are right in the config.json, use that
3215 command.cmds = script.cmd;
3216 processRunCommand(command);
3217 }
3218 } else if (typeof command.cmdpath == 'string') {
3219 // If a server command path is used, load the script from the path
3220 var file = parent.getServerFilePath(user, domain, command.cmdpath);
3221 if (file != null) {
3222 fs.readFile(file.fullpath, function (err, data) {
3223 // If loaded correctly, run loaded commands
3224 if ((err != null) || (data == null) || (data.length == 0) || (data.length > 65535)) return;
3225 command.cmds = data.toString();
3226 delete command.cmdpath;
3227 processRunCommand(command);
3228 });
3229 }
3230 } else if (typeof command.cmds == 'string') {
3231 // Run provided commands
3232 if (command.cmds.length > 65535) return;
3233 processRunCommand(command);
3234 }
3235 break;
3236 }
3237 case 'uninstallagent':
3238 {
3239 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3240 for (i in command.nodeids) {
3241 // Get the node and the rights for this node
3242 parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
3243 // Check we have the rights to delete this device
3244 if ((rights & MESHRIGHT_UNINSTALL) == 0) return;
3245
3246 // Send uninstall command to connected agent
3247 const agent = parent.wsagents[node._id];
3248 if (agent != null) {
3249 //console.log('Asking agent ' + agent.dbNodeKey + ' to uninstall.');
3250 try { agent.send(JSON.stringify({ action: 'uninstallagent' })); } catch (ex) { }
3251 }
3252 });
3253 }
3254 break;
3255 }
3256 case 'poweraction':
3257 {
3258 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3259 if (common.validateInt(command.actiontype, 2, 401) == false) break; // Check actiontype
3260 for (i in command.nodeids) {
3261 var nodeid = command.nodeids[i];
3262
3263 // Argument validation
3264 if (common.validateString(nodeid, 8, 128) == false) { continue; } // Check the nodeid
3265 else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
3266 else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { continue; } // Invalid domain, operation only valid for current domain
3267
3268 // Get the node and the rights for this node
3269 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3270 if ((command.actiontype >= 400) && ((rights & MESHRIGHT_REMOTECONTROL) != 0)) {
3271 // Flash and vibrate
3272 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); }
3273 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); }
3274 } else {
3275 // Check we have the rights to perform this operation
3276 if ((command.actiontype == 302) && ((rights & MESHRIGHT_WAKEDEVICE) == 0)) return; // This is a Intel AMT power on operation, check if we have WAKE rights
3277 if ((command.actiontype != 302) && ((rights & MESHRIGHT_RESETOFF) == 0)) return; // For all other operations, check that we have RESET/OFF rights
3278
3279 // If this device is connected on MQTT, send a power action.
3280 if ((parent.parent.mqttbroker != null) && (command.actiontype >= 0) && (command.actiontype <= 4)) { parent.parent.mqttbroker.publish(node._id, 'powerAction', ['', '', 'poweroff', 'reset', 'sleep'][command.actiontype]); }
3281
3282 // If this is a IP-KVM or Power Distribution Unit (PDU), dispatch an action event
3283 if (node.mtype == 4) {
3284 // Send out an event to perform turn off command on the port
3285 const targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['devport-operation', 'server-users', user._id]);
3286 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." };
3287 parent.parent.DispatchEvent(targets, obj, event);
3288 return;
3289 }
3290
3291 if ((command.actiontype >= 300) && (command.actiontype < 400)) {
3292 if ((command.actiontype != 302) && (command.actiontype != 308) && (command.actiontype < 310) && (command.actiontype > 316)) return; // Invalid action type.
3293 // 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
3294 parent.parent.DispatchEvent('*', obj, { action: 'amtpoweraction', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, actiontype: command.actiontype - 300 });
3295 } else {
3296 if ((command.actiontype < 2) && (command.actiontype > 4)) return; // Invalid action type.
3297 // Mesh Agent power command, get this device and send the power command
3298 const agent = parent.wsagents[node._id];
3299 if (agent != null) {
3300 try { agent.send(JSON.stringify({ action: 'poweraction', actiontype: command.actiontype, userid: user._id, username: user.name, remoteaddr: req.clientIp })); } catch (ex) { }
3301 }
3302 }
3303 }
3304 });
3305
3306 // Confirm we may be doing something (TODO)
3307 if (command.responseid != null) {
3308 try { ws.send(JSON.stringify({ action: 'poweraction', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
3309 } else {
3310 try { ws.send(JSON.stringify({ action: 'poweraction' })); } catch (ex) { }
3311 }
3312 }
3313 break;
3314 }
3315 case 'toast':
3316 {
3317 var err = null;
3318
3319 // Perform input validation
3320 try {
3321 if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
3322 else if (common.validateString(command.msg, 1, 4096) == false) { err = "Invalid message"; } // Check message
3323 else {
3324 var nodeids = [];
3325 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]); } }
3326 command.nodeids = nodeids;
3327 }
3328 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
3329
3330 // Handle any errors
3331 if (err != null) {
3332 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: err })); } catch (ex) { } }
3333 break;
3334 }
3335
3336 // Check the title, if needed, use a default one
3337 if (common.validateString(command.title, 1, 512) == false) { delete command.title } // Check title
3338 if ((command.title == null) && (typeof domain.notificationmessages == 'object') && (typeof domain.notificationmessages.title == 'string')) { command.title = domain.notificationmessages.title; }
3339 if ((command.title == null) && (typeof domain.title == 'string')) { command.title = domain.title; }
3340 if (command.title == null) { command.title = "MeshCentral"; }
3341
3342 for (i in command.nodeids) {
3343 // Get the node and the rights for this node
3344 parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
3345 // Check we have the rights to notify this device
3346 if ((rights & MESHRIGHT_CHATNOTIFY) == 0) {
3347 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } }
3348 return;
3349 }
3350
3351 // Get this device and send toast command
3352 const agent = parent.wsagents[node._id];
3353 if (agent != null) {
3354 try { agent.send(JSON.stringify({ action: 'toast', title: command.title, msg: command.msg, sessionid: ws.sessionId, username: user.name, userid: user._id })); } catch (ex) { }
3355 }
3356 });
3357 }
3358
3359 // Send response if required
3360 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
3361 break;
3362 }
3363 case 'changedevice':
3364 {
3365 var err = null;
3366
3367 // Argument validation
3368 try {
3369 if (common.validateString(command.nodeid, 1, 1024) == false) { err = "Invalid nodeid"; } // Check nodeid
3370 else {
3371 if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
3372 if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = "Invalid nodeid"; } // Invalid domain, operation only valid for current domain
3373 else if ((command.userloc) && (command.userloc.length != 2) && (command.userloc.length != 0)) { err = "Invalid user location"; }
3374 }
3375 } catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
3376
3377 // Handle any errors
3378 if (err != null) {
3379 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: err })); } catch (ex) { } }
3380 break;
3381 }
3382
3383 // Get the node and the rights for this node
3384 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3385 if ((rights & MESHRIGHT_MANAGECOMPUTERS) == 0) {
3386 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } }
3387 return;
3388 }
3389 node = common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials
3390 var mesh = parent.meshes[node.meshid], amtchange = 0;
3391
3392 // Ready the node change event
3393 var changes = [], event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id };
3394 change = 0;
3395 event.msg = ': ';
3396
3397 // If we are in WAN-only mode, host is not used
3398 if ((args.wanonly == true) && (command.host) && (node.mtype != 3) && (node.mtype != 4)) { delete command.host; }
3399
3400 // Look for a change
3401 if ((typeof command.icon == 'number') && (command.icon != node.icon)) { change = 1; node.icon = command.icon; changes.push('icon'); }
3402 if ((typeof command.name == 'string') && (command.name != node.name)) { change = 1; node.name = command.name; changes.push('name'); }
3403 if ((typeof command.host == 'string') && (command.host != node.host)) { change = 1; node.host = command.host; changes.push('host'); }
3404 if (typeof command.consent == 'number') {
3405 var oldConsent = node.consent;
3406 if (command.consent != node.consent) { node.consent = command.consent; }
3407 if (command.consent == 0) { delete node.consent; }
3408 if (oldConsent != node.consent) { change = 1; changes.push('consent'); }
3409 }
3410
3411 if ((typeof command.rdpport == 'number') && (command.rdpport > 0) && (command.rdpport < 65536)) {
3412 if ((command.rdpport == 3389) && (node.rdpport != null)) {
3413 delete node.rdpport; change = 1; changes.push('rdpport'); // Delete the RDP port
3414 } else {
3415 node.rdpport = command.rdpport; change = 1; changes.push('rdpport'); // Set the RDP port
3416 }
3417 }
3418
3419 if ((typeof command.rfbport == 'number') && (command.rfbport > 0) && (command.rfbport < 65536)) {
3420 if ((command.rfbport == 5900) && (node.rfbport != null)) {
3421 delete node.rfbport; change = 1; changes.push('rfbport'); // Delete the RFB port
3422 } else {
3423 node.rfbport = command.rfbport; change = 1; changes.push('rfbport'); // Set the RFB port
3424 }
3425 }
3426
3427 if ((typeof command.sshport == 'number') && (command.sshport > 0) && (command.sshport < 65536)) {
3428 if ((command.sshport == 22) && (node.sshport != null)) {
3429 delete node.sshport; change = 1; changes.push('sshport'); // Delete the SSH port
3430 } else {
3431 node.sshport = command.sshport; change = 1; changes.push('sshport'); // Set the SSH port
3432 }
3433 }
3434
3435 if ((typeof command.httpport == 'number') && (command.httpport > 0) && (command.httpport < 65536)) {
3436 if ((command.httpport == 80) && (node.httpport != null)) {
3437 delete node.httpport; change = 1; changes.push('httpport'); // Delete the HTTP port
3438 } else {
3439 node.httpport = command.httpport; change = 1; changes.push('httpport'); // Set the HTTP port
3440 }
3441 }
3442
3443 if ((typeof command.httpsport == 'number') && (command.httpsport > 0) && (command.httpsport < 65536)) {
3444 if ((command.httpsport == 443) && (node.httpsport != null)) {
3445 delete node.httpsport; change = 1; changes.push('httpsport'); // Delete the HTTPS port
3446 } else {
3447 node.httpsport = command.httpsport; change = 1; changes.push('httpsport'); // Set the HTTPS port
3448 }
3449 }
3450
3451 if ((typeof command.ssh == 'number') && (command.ssh == 0)) {
3452 if ((node.ssh != null) && (node.ssh[user._id] != null)) { delete node.ssh[user._id]; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
3453 }
3454
3455 if ((typeof command.rdp == 'number') && (command.rdp == 0)) {
3456 if ((node.rdp != null) && (node.rdp[user._id] != null)) { delete node.rdp[user._id]; change = 1; changes.push('rdp'); } // Delete the RDP cendentials
3457 }
3458
3459 // Clean up any legacy RDP and SSH credentials
3460 if (node.rdp != null) { delete node.rdp.d; delete node.rdp.u; delete node.rdp.p; }
3461 if (node.ssh != null) { delete node.ssh.u; delete node.ssh.p; delete node.ssh.k; delete node.ssh.kp; }
3462
3463 if (domain.geolocation && command.userloc && ((node.userloc == null) || (command.userloc[0] != node.userloc[0]) || (command.userloc[1] != node.userloc[1]))) {
3464 change = 1;
3465 if ((command.userloc.length == 0) && (node.userloc)) {
3466 delete node.userloc;
3467 changes.push('location removed');
3468 } else {
3469 command.userloc.push((Math.floor((new Date()) / 1000)));
3470 node.userloc = command.userloc.join(',');
3471 changes.push('location');
3472 }
3473 }
3474 if (command.desc != null && (command.desc != node.desc)) { change = 1; node.desc = command.desc; changes.push('description'); }
3475 if (command.intelamt != null) {
3476 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.
3477 if ((command.intelamt.user != null) && (command.intelamt.pass != null) && ((command.intelamt.user != node.intelamt.user) || (command.intelamt.pass != node.intelamt.pass))) {
3478 change = 1;
3479 node.intelamt.user = command.intelamt.user;
3480 node.intelamt.pass = command.intelamt.pass;
3481 node.intelamt.warn |= 8; // Change warning to "Trying". Bit flags: 1 = Unknown credentials, 2 = Realm Mismatch, 4 = TLS Cert Mismatch, 8 = Trying credentials
3482 changes.push('Intel AMT credentials');
3483 amtchange = 1;
3484 }
3485 }
3486 // Only allow the user to set Intel AMT TLS state if AMT Manager is not active. AMT manager will auto-detect TLS state.
3487 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'); }
3488 }
3489 if (command.tags) { // Node grouping tag, this is a array of strings that can't be empty and can't contain a comma
3490 var ok = true, group2 = [];
3491 if (common.validateString(command.tags, 0, 4096) == true) { command.tags = command.tags.split(','); }
3492 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); } }
3493 group2.sort();
3494 if (node.tags != group2) { node.tags = group2; change = 1; }
3495 } else if ((command.tags === '') && node.tags) { delete node.tags; change = 1; }
3496
3497 if (change == 1) {
3498 // Save the node
3499 db.Set(parent.cleanDevice(node));
3500
3501 // Event the node change. Only do this if the database will not do it.
3502 event.msg = 'Changed device ' + node.name + ' from group ' + mesh.name + ': ' + changes.join(', ');
3503 event.node = parent.CloneSafeNode(node);
3504 event.msgid = 140;
3505 event.msgArgs = [ node.name, mesh.name, changes.join(', ') ];
3506 if (amtchange == 1) { event.amtchange = 1; } // This will give a hint to the AMT Manager to reconnect using new AMT credentials
3507 if (command.rdpport == 3389) { event.node.rdpport = 3389; }
3508 if (command.rfbport == 5900) { event.node.rfbport = 5900; }
3509 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.
3510 parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
3511 }
3512
3513 // Send response if required
3514 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
3515 });
3516 break;
3517 }
3518 case 'uploadagentcore':
3519 {
3520 if (common.validateString(command.type, 1, 40) == false) break; // Check path
3521 if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
3522
3523 // Go thru all node identifiers and run the operation
3524 for (var i in command.nodeids) {
3525 var nodeid = command.nodeids[i];
3526 if (typeof nodeid != 'string') return;
3527
3528 // Get the node and the rights for this node
3529 parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3530 if ((node == null) || (((rights & MESHRIGHT_AGENTCONSOLE) == 0) && (user.siteadmin != SITERIGHT_ADMIN))) return;
3531
3532 // TODO: If we have peer servers, inform...
3533 //if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'uploadagentcore', sessionid: ws.sessionId }); }
3534
3535 if (command.type == 'default') {
3536 // Send the default core to the agent
3537 parent.parent.updateMeshCore(function () { parent.sendMeshAgentCore(user, domain, node._id, 'default'); });
3538 } else if (command.type == 'clear') {
3539 // Clear the mesh agent core on the mesh agent
3540 parent.sendMeshAgentCore(user, domain, node._id, 'clear');
3541 } else if (command.type == 'recovery') {
3542 // Send the recovery core to the agent
3543 parent.sendMeshAgentCore(user, domain, node._id, 'recovery');
3544 } else if (command.type == 'tiny') {
3545 // Send the tiny core to the agent
3546 parent.sendMeshAgentCore(user, domain, node._id, 'tiny');
3547 } else if ((command.type == 'custom') && (common.validateString(command.path, 1, 2048) == true)) {
3548 // Send a mesh agent core to the mesh agent
3549 var file = parent.getServerFilePath(user, domain, command.path);
3550 if (file != null) {
3551 fs.readFile(file.fullpath, 'utf8', function (err, data) {
3552 if (err != null) {
3553 data = common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
3554 parent.sendMeshAgentCore(user, domain, node._id, 'custom', data);
3555 }
3556 });
3557 }
3558 }
3559 });
3560 }
3561 break;
3562 }
3563 case 'inviteAgent':
3564 {
3565 var err = null, mesh = null;
3566
3567 // Resolve the device group name if needed
3568 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
3569 for (var i in parent.meshes) {
3570 var m = parent.meshes[i];
3571 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
3572 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
3573 }
3574 }
3575 }
3576
3577 try {
3578 if ((domain.mailserver == null) || (args.lanonly == true)) { err = 'Unsupported feature'; } // This operation requires the email server
3579 else if ((parent.parent.certificates.CommonName == null) || (parent.parent.certificates.CommonName.indexOf('.') == -1)) { err = 'Unsupported feature'; } // Server name must be configured
3580 else if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check meshid
3581 else {
3582 if (command.meshid.split('/').length == 1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
3583 if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group identifier'; } // Invalid domain, operation only valid for current domain
3584 else if (common.validateString(command.email, 4, 1024) == false) { err = 'Invalid email'; } // Check email
3585 else if (command.email.split('@').length != 2) { err = 'Invalid email'; } // Check email
3586 else {
3587 mesh = parent.meshes[command.meshid];
3588 if (mesh == null) { err = 'Unknown device group'; } // Check if the group exists
3589 else if (mesh.mtype != 2) { err = 'Invalid group type'; } // Check if this is the correct group type
3590 else if (parent.IsMeshViewable(user, mesh) == false) { err = 'Not allowed'; } // Check if this user has rights to do this
3591 }
3592 }
3593 } catch (ex) { err = 'Validation exception: ' + ex; }
3594
3595 // Handle any errors
3596 if (err != null) {
3597 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: err })); } catch (ex) { } }
3598 break;
3599 }
3600
3601 // Perform email invitation
3602 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);
3603
3604 // Send a response if needed
3605 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
3606 break;
3607 }
3608 case 'setDeviceEvent':
3609 {
3610 // Argument validation
3611 if (common.validateString(command.msg, 1, 4096) == false) break; // Check event
3612
3613 // Get the node and the rights for this node
3614 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
3615 if (rights == 0) return;
3616
3617 // Add an event for this device
3618 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
3619 var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'manual', msg: decodeURIComponent(command.msg), domain: domain.id };
3620 parent.parent.DispatchEvent(targets, obj, event);
3621 });
3622 break;
3623 }
3624 case 'setNotes':
3625 {
3626 // Argument validation
3627 if (common.validateString(command.id, 1, 1024) == false) break; // Check id
3628 var splitid = command.id.split('/');
3629 if ((splitid.length != 3) || (splitid[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
3630 var idtype = splitid[0];
3631 if ((idtype != 'puser') && (idtype != 'user') && (idtype != 'mesh') && (idtype != 'node')) return;
3632
3633 if (idtype == 'node') {
3634 // Get the node and the rights for this node
3635 parent.GetNodeWithRights(domain, user, command.id, function (node, rights, visible) {
3636 if ((rights & MESHRIGHT_SETNOTES) != 0) {
3637 // Set the id's notes
3638 if (common.validateString(command.notes, 1) == false) {
3639 db.Remove('nt' + node._id); // Delete the note for this node
3640 } else {
3641 db.Set({ _id: 'nt' + node._id, type: 'note', value: command.notes }); // Set the note for this node
3642 }
3643 }
3644 });
3645 } else if (idtype == 'mesh') {
3646 // Get the mesh for this device
3647 mesh = parent.meshes[command.id];
3648 if (mesh) {
3649 // Check if this user has rights to do this
3650 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return; // Must have rights to edit the mesh
3651
3652 // Set the id's notes
3653 if (common.validateString(command.notes, 1) == false) {
3654 db.Remove('nt' + command.id); // Delete the note for this node
3655 } else {
3656 db.Set({ _id: 'nt' + command.id, type: 'note', value: command.notes }); // Set the note for this mesh
3657 }
3658 }
3659 } else if ((idtype == 'user') && ((user.siteadmin & 2) != 0)) {
3660 // Set the id's notes
3661 if (common.validateString(command.notes, 1) == false) {
3662 db.Remove('nt' + command.id); // Delete the note for this node
3663 } else {
3664 // Can only perform this operation on other users of our group.
3665 var chguser = parent.users[command.id];
3666 if (chguser == null) break; // This user does not exists
3667 if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
3668 db.Set({ _id: 'nt' + command.id, type: 'note', value: command.notes }); // Set the note for this user
3669 }
3670 } else if (idtype == 'puser') {
3671 // Set the user's personal note, starts with 'ntp' + userid.
3672 if (common.validateString(command.notes, 1) == false) {
3673 db.Remove('ntp' + user._id); // Delete the note for this node
3674 } else {
3675 db.Set({ _id: 'ntp' + user._id, type: 'note', value: command.notes }); // Set the note for this user
3676 }
3677 }
3678
3679 break;
3680 }
3681 case 'otpemail':
3682 {
3683 // Do not allow this command if 2FA's are locked
3684 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3685
3686 // Do not allow this command when logged in using a login token
3687 if (req.session.loginToken != null) break;
3688
3689 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3690
3691 // Check input
3692 if (typeof command.enabled != 'boolean') return;
3693
3694 // See if we really need to change the state
3695 if ((command.enabled === true) && (user.otpekey != null)) return;
3696 if ((command.enabled === false) && (user.otpekey == null)) return;
3697
3698 // Change the email 2FA of this user
3699 if (command.enabled === true) { user.otpekey = {}; } else { delete user.otpekey; }
3700 parent.db.SetUser(user);
3701 ws.send(JSON.stringify({ action: 'otpemail', success: true, enabled: command.enabled })); // Report success
3702
3703 // Notify change
3704 var targets = ['*', 'server-users', user._id];
3705 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3706 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 };
3707 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.
3708 parent.parent.DispatchEvent(targets, obj, event);
3709 break;
3710 }
3711 case 'otpduo':
3712 {
3713 // Do not allow this command if 2FA's are locked
3714 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3715
3716 // Do not allow if Duo is not supported
3717 if ((typeof domain.duo2factor != 'object') || (typeof domain.duo2factor.integrationkey != 'string') || (typeof domain.duo2factor.secretkey != 'string') || (typeof domain.duo2factor.apihostname != 'string')) return;
3718
3719 // Do not allow if Duo is disabled
3720 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) return;
3721
3722 // Do not allow this command when logged in using a login token
3723 if (req.session.loginToken != null) break;
3724
3725 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3726
3727 // Check input
3728 if ((typeof command.enabled != 'boolean') || (command.enabled != false)) return;
3729
3730 // See if we really need to change the state
3731 if ((command.enabled === false) && (user.otpduo == null)) return;
3732
3733 // Change the duo 2FA of this user
3734 delete user.otpduo;
3735 parent.db.SetUser(user);
3736 ws.send(JSON.stringify({ action: 'otpduo', success: true, enabled: command.enabled })); // Report success
3737
3738 // Notify change
3739 var targets = ['*', 'server-users', user._id];
3740 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3741 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 };
3742 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.
3743 parent.parent.DispatchEvent(targets, obj, event);
3744 break;
3745 }
3746 case 'otpauth-request':
3747 {
3748 // Do not allow this command if 2FA's are locked
3749 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 1 })); return; }
3750
3751 // Do not allow this command when logged in using a login token
3752 if (req.session.loginToken != null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 3 })); return; }
3753
3754 // Check of OTP 2FA is allowed
3755 if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 4 })); return; }
3756
3757 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.
3758
3759 // Check if 2-step login is supported
3760 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3761 if (twoStepLoginSupported) {
3762 // Request a one time password to be setup
3763 var otplib = null;
3764 try { otplib = require('otplib'); } catch (ex) { }
3765 if (otplib == null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 6 })); return; }
3766 const secret = otplib.generateSecret(); // TODO: Check the random source of this value.
3767
3768 var domainName = parent.certificates.CommonName;
3769 if (domain.dns != null) {
3770 domainName = domain.dns;
3771 } else if (domain.dns == null && domain.id != '') {
3772 domainName += "/" + domain.id;
3773 }
3774 ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.generateURI({ issuer: domainName, label: user.name, secret: secret }) }));
3775 }
3776 break;
3777 }
3778 case 'otpauth-setup':
3779 {
3780 // Do not allow this command if 2FA's are locked
3781 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3782
3783 // Do not allow this command when logged in using a login token
3784 if (req.session.loginToken != null) break;
3785
3786 // Check of OTP 2FA is allowed
3787 if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) break;
3788
3789 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3790
3791 // Check if 2-step login is supported
3792 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3793 if (twoStepLoginSupported) {
3794 // Perform the one time password setup
3795 var otplib = null;
3796 try { otplib = require('otplib'); } catch (ex) { }
3797 if (otplib == null) { break; }
3798 const verified = require('otplib').verifySync({
3799 epochTolerance: 60,
3800 token: command.token,
3801 secret: command.secret,
3802 guardrails: otplib.createGuardrails({
3803 MIN_SECRET_BYTES: 10, // https://github.com/yeojz/otplib/issues/671#issuecomment-4368647105
3804 })
3805 });
3806 if (verified.valid === true) {
3807 // Token is valid, activate 2-step login on this account.
3808 user.otpsecret = command.secret;
3809 parent.db.SetUser(user);
3810 ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // Report success
3811
3812 // Notify change
3813 var targets = ['*', 'server-users', user._id];
3814 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3815 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 };
3816 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.
3817 parent.parent.DispatchEvent(targets, obj, event);
3818 } else {
3819 ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
3820 }
3821 }
3822 break;
3823 }
3824 case 'otpauth-clear':
3825 {
3826 // Do not allow this command if 2FA's are locked
3827 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3828
3829 // Do not allow this command when logged in using a login token
3830 if (req.session.loginToken != null) break;
3831
3832 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3833
3834 // Check if 2-step login is supported
3835 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3836 if (twoStepLoginSupported) {
3837 // Clear the one time password secret
3838 if (user.otpsecret) {
3839 delete user.otpsecret;
3840 parent.db.SetUser(user);
3841 ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // Report success
3842
3843 // Notify change
3844 var targets = ['*', 'server-users', user._id];
3845 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3846 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 };
3847 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.
3848 parent.parent.DispatchEvent(targets, obj, event);
3849 } else {
3850 ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
3851 }
3852 }
3853 break;
3854 }
3855 case 'otpauth-getpasswords':
3856 {
3857 // Do not allow this command if 2FA's are locked
3858 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3859
3860 // Do not allow this command if backup codes are not allowed
3861 if ((domain.passwordrequirements) && (domain.passwordrequirements.backupcode2factor == false)) return;
3862
3863 // Do not allow this command when logged in using a login token
3864 if (req.session.loginToken != null) break;
3865
3866 // Check if 2-step login is supported
3867 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3868 if (twoStepLoginSupported == false) break;
3869
3870 var actionTaken = false, actionText = null, actionId = 0;
3871 if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 1024) == 0)) { // Don't allow generation of tokens if the account is settings locked
3872 // Perform a sub-action
3873 if (command.subaction == 1) { // Generate a new set of tokens
3874 var randomNumbers = [], v;
3875 for (var i = 0; i < 10; i++) { do { v = getRandomEightDigitInteger(); } while (randomNumbers.indexOf(v) >= 0); randomNumbers.push(v); }
3876 user.otpkeys = { keys: [] };
3877 for (var i = 0; i < 10; i++) { user.otpkeys.keys[i] = { p: randomNumbers[i], u: true } }
3878 actionTaken = true;
3879 actionId = 92;
3880 actionText = "New 2FA backup codes generated";
3881 } else if (command.subaction == 2) { // Clear all tokens
3882 actionTaken = (user.otpkeys != null);
3883 delete user.otpkeys;
3884 if (actionTaken) {
3885 actionId = 93;
3886 actionText = "2FA backup codes cleared";
3887 }
3888 }
3889
3890 // Save the changed user
3891 if (actionTaken) { parent.db.SetUser(user); }
3892 }
3893
3894 // Return one time passwords for this user
3895 if (count2factoraAuths() > 0) {
3896 ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys ? user.otpkeys.keys : null }));
3897 }
3898
3899 // Notify change
3900 if (actionText != null) {
3901 var targets = ['*', 'server-users', user._id];
3902 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3903 var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: actionId, msg: actionText, domain: domain.id };
3904 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.
3905 parent.parent.DispatchEvent(targets, obj, event);
3906 }
3907 break;
3908 }
3909 case 'otp-hkey-get':
3910 {
3911 // Do not allow this command if 2FA's are locked
3912 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3913
3914 // Do not allow this command when logged in using a login token
3915 if (req.session.loginToken != null) break;
3916
3917 // Check if 2-step login is supported
3918 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3919 if (twoStepLoginSupported == false) break;
3920
3921 // Send back the list of keys we have, just send the list of names and index
3922 var hkeys = [];
3923 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 }); } }
3924
3925 ws.send(JSON.stringify({ action: 'otp-hkey-get', keys: hkeys }));
3926 break;
3927 }
3928 case 'otp-hkey-remove':
3929 {
3930 // Do not allow this command if 2FA's are locked
3931 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
3932
3933 // Do not allow this command when logged in using a login token
3934 if (req.session.loginToken != null) break;
3935
3936 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3937
3938 // Check if 2-step login is supported
3939 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3940 if (twoStepLoginSupported == false || command.index == null) break;
3941
3942 // Remove a key
3943 var foundAtIndex = -1;
3944 if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyIndex == command.index) { foundAtIndex = i; } } }
3945 if (foundAtIndex != -1) {
3946 user.otphkeys.splice(foundAtIndex, 1);
3947 parent.db.SetUser(user);
3948 }
3949
3950 // Notify change
3951 var targets = ['*', 'server-users', user._id];
3952 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3953 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 };
3954 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.
3955 parent.parent.DispatchEvent(targets, obj, event);
3956 break;
3957 }
3958 case 'otp-hkey-yubikey-add':
3959 {
3960 // Do not allow this command if 2FA's are locked or max keys reached
3961 if (domain.passwordrequirements) {
3962 if (domain.passwordrequirements.lock2factor == true) return;
3963 if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return;
3964 }
3965
3966 // Do not allow this command when logged in using a login token
3967 if (req.session.loginToken != null) break;
3968
3969 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
3970
3971 // Yubico API id and signature key can be requested from https://upgrade.yubico.com/getapikey/
3972 var yub = null;
3973 try { yub = require('yub'); } catch (ex) { }
3974
3975 // Check if 2-step login is supported
3976 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
3977 if ((yub == null) || (twoStepLoginSupported == false) || (typeof command.otp != 'string')) {
3978 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
3979 break;
3980 }
3981
3982 // Check if Yubikey support is present or OTP no exactly 44 in length
3983 if ((typeof domain.yubikey != 'object') || (typeof domain.yubikey.id != 'string') || (typeof domain.yubikey.secret != 'string') || (command.otp.length != 44)) {
3984 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
3985 break;
3986 }
3987
3988 // TODO: Check if command.otp is modhex encoded, reject if not.
3989
3990 // Query the YubiKey server to validate the OTP
3991 yub.init(domain.yubikey.id, domain.yubikey.secret);
3992 yub.verify(command.otp, function (err, results) {
3993 if ((results != null) && (results.status == 'OK')) {
3994 var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
3995 var keyId = command.otp.substring(0, 12);
3996 if (user.otphkeys == null) { user.otphkeys = []; }
3997
3998 // Check if this key was already registered, if so, remove it.
3999 var foundAtIndex = -1;
4000 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyid == keyId) { foundAtIndex = i; } }
4001 if (foundAtIndex != -1) { user.otphkeys.splice(foundAtIndex, 1); }
4002
4003 // Add the new key and notify
4004 user.otphkeys.push({ name: command.name, type: 2, keyid: keyId, keyIndex: keyIndex });
4005 parent.db.SetUser(user);
4006 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: true, name: command.name, index: keyIndex }));
4007
4008 // Notify change TODO: Should be done on all sessions/servers for this user.
4009 var targets = ['*', 'server-users', user._id];
4010 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
4011 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 };
4012 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.
4013 parent.parent.DispatchEvent(targets, obj, event);
4014 } else {
4015 ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
4016 }
4017 });
4018
4019 break;
4020 }
4021 case 'otpdev-clear':
4022 {
4023 // Do not allow this command if 2FA's are locked
4024 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
4025
4026 // Do not allow this command when logged in using a login token
4027 if (req.session.loginToken != null) break;
4028
4029 // Remove the authentication push notification device
4030 if (user.otpdev != null) {
4031 // Change the user
4032 user.otpdev = obj.dbNodeKey;
4033 parent.db.SetUser(user);
4034
4035 // Notify change
4036 var targets = ['*', 'server-users', user._id];
4037 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
4038 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 };
4039 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.
4040 parent.parent.DispatchEvent(targets, obj, event);
4041 }
4042 break;
4043 }
4044 case 'otpdev-set':
4045 {
4046 // Do not allow this command if 2FA's are locked
4047 if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
4048
4049 // Do not allow this command when logged in using a login token
4050 if (req.session.loginToken != null) break;
4051
4052 // Attempt to add a authentication push notification device
4053 // This will only send a push notification to the device, the device needs to confirm for the auth device to be added.
4054 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
4055 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4056 // Only allow use of devices with full rights
4057 if ((node == null) || (visible == false) || (rights != 0xFFFFFFFF) || (node.agent == null) || (node.agent.id != 14) || (node.pmt == null)) return;
4058
4059 // Encode the cookie
4060 const code = Buffer.from(user.name).toString('base64');
4061 const authCookie = parent.parent.encodeCookie({ a: 'addAuth', c: code, u: user._id, n: node._id });
4062
4063 // Send out a push message to the device
4064 var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
4065 var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
4066 parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
4067 if (err == null) {
4068 parent.parent.debug('email', 'Successfully auth addition send push message to device ' + node.name);
4069 } else {
4070 parent.parent.debug('email', 'Failed auth addition push message to device ' + node.name + ', error: ' + errdesc);
4071 }
4072 });
4073 });
4074 break;
4075 }
4076 case 'webauthn-startregister':
4077 {
4078 // Do not allow this command if 2FA's are locked or max keys reached
4079 if (domain.passwordrequirements) {
4080 if (domain.passwordrequirements.lock2factor == true) return;
4081 if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return;
4082 }
4083
4084 // Do not allow this command when logged in using a login token
4085 if (req.session.loginToken != null) break;
4086
4087 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
4088
4089 // Check if 2-step login is supported
4090 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
4091 if ((twoStepLoginSupported == false) || (command.name == null)) break;
4092
4093 // Send the registration request
4094 var registrationOptions = parent.webauthn.generateRegistrationChallenge("Anonymous Service", { id: Buffer.from(user._id, 'binary').toString('base64'), name: user._id, displayName: user._id.split('/')[2] });
4095 //console.log('registrationOptions', registrationOptions);
4096 registrationOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.
4097 obj.webAuthnReqistrationRequest = { action: 'webauthn-startregister', keyname: command.name, request: registrationOptions };
4098 ws.send(JSON.stringify(obj.webAuthnReqistrationRequest));
4099 break;
4100 }
4101 case 'webauthn-endregister':
4102 {
4103 // Do not allow this command if 2FA's are locked or max keys reached
4104 if (domain.passwordrequirements) {
4105 if (domain.passwordrequirements.lock2factor == true) return;
4106 if ((typeof domain.passwordrequirements.maxfidokeys == 'number') && (user.otphkeys) && (user.otphkeys.length >= domain.passwordrequirements.maxfidokeys)) return;
4107 }
4108
4109 // Do not allow this command when logged in using a login token
4110 if (req.session.loginToken != null) break;
4111
4112 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
4113 const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
4114 if ((twoStepLoginSupported == false) || (obj.webAuthnReqistrationRequest == null)) return;
4115
4116 // Figure out the origin
4117 var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
4118 var origin = "https://" + (domain.dns ? domain.dns : parent.certificates.CommonName);
4119 if (httpport != 443) { origin += ':' + httpport; }
4120
4121 // Use internal WebAuthn module to check the response
4122 var regResult = null;
4123 try { regResult = parent.webauthn.verifyAuthenticatorAttestationResponse(command.response.response); } catch (ex) { regResult = { verified: false, error: ex }; }
4124 if (regResult.verified === true) {
4125 // Since we are registering a WebAuthn/FIDO2 key, remove all U2F keys (Type 1).
4126 var otphkeys2 = [];
4127 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]); } } }
4128 user.otphkeys = otphkeys2;
4129
4130 // Add the new WebAuthn/FIDO2 keys
4131 var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
4132 if (user.otphkeys == null) { user.otphkeys = []; }
4133 user.otphkeys.push({ name: obj.webAuthnReqistrationRequest.keyname, type: 3, publicKey: regResult.authrInfo.publicKey, counter: regResult.authrInfo.counter, keyIndex: keyIndex, keyId: regResult.authrInfo.keyId });
4134 parent.db.SetUser(user);
4135 ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: true, name: command.name, index: keyIndex }));
4136
4137 // Notify change
4138 var targets = ['*', 'server-users', user._id];
4139 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
4140 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 };
4141 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.
4142 parent.parent.DispatchEvent(targets, obj, event);
4143 } else {
4144 //console.log('webauthn-endregister-error', regResult.error);
4145 ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: false, error: regResult.error, name: command.name, index: keyIndex }));
4146 }
4147
4148 delete obj.hardwareKeyRegistrationRequest;
4149 break;
4150 }
4151 case 'userWebState': {
4152 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
4153 if (common.validateString(command.state, 1, 30000) == false) break; // Check state size, no more than 30k
4154 command.state = parent.filterUserWebState(command.state); // Filter the state to remove anything bad
4155 if ((command.state == null) || (typeof command.state !== 'string')) break; // If state did not validate correctly, quit here.
4156 command.domain = domain.id;
4157 db.Set({ _id: 'ws' + user._id, state: command.state });
4158 parent.parent.DispatchEvent([user._id], obj, { action: 'userWebState', nolog: 1, domain: domain.id, state: command.state });
4159 break;
4160 }
4161 case 'getNotes':
4162 {
4163 // Argument validation
4164 if (common.validateString(command.id, 1, 1024) == false) break; // Check id
4165 var splitid = command.id.split('/');
4166 if ((splitid.length != 3) || (splitid[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
4167 var idtype = splitid[0];
4168 if ((idtype != 'puser') && (idtype != 'user') && (idtype != 'mesh') && (idtype != 'node')) return;
4169
4170 if (idtype == 'node') {
4171 // Get the node and the rights for this node
4172 parent.GetNodeWithRights(domain, user, command.id, function (node, rights, visible) {
4173 if (visible == false) return;
4174
4175 // Get the notes about this node
4176 db.Get('nt' + command.id, function (err, notes) {
4177 try {
4178 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4179 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4180 } catch (ex) { }
4181 });
4182 });
4183 } else if (idtype == 'mesh') {
4184 // Get the mesh for this device
4185 mesh = parent.meshes[command.id];
4186 if (mesh) {
4187 // Check if this user has rights to do this
4188 if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return; // Must have rights to edit the mesh
4189
4190 // Get the notes about this node
4191 db.Get('nt' + command.id, function (err, notes) {
4192 try {
4193 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4194 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4195 } catch (ex) { }
4196 });
4197 }
4198 } else if ((idtype == 'user') && ((user.siteadmin & 2) != 0)) {
4199 // Get the notes about this node
4200 db.Get('nt' + command.id, function (err, notes) {
4201 try {
4202 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4203 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4204 } catch (ex) { }
4205 });
4206 } else if (idtype == 'puser') {
4207 // Get personal note, starts with 'ntp' + userid
4208 db.Get('ntp' + user._id, function (err, notes) {
4209 try {
4210 if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
4211 ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
4212 } catch (ex) { }
4213 });
4214 }
4215
4216 break;
4217 }
4218 case 'createInviteLink': {
4219 var err = null;
4220
4221 // Resolve the device group name if needed
4222 if ((typeof command.meshname == 'string') && (command.meshid == null)) {
4223 for (var i in parent.meshes) {
4224 var m = parent.meshes[i];
4225 if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
4226 if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
4227 }
4228 }
4229 }
4230
4231 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group id'; } // Check the meshid (Max length of a meshid is 134 bytes).
4232 else if (common.validateInt(command.expire, 0, 99999) == false) { err = 'Invalid expire time'; } // Check the expire time in hours
4233 else if (common.validateInt(command.flags, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
4234 else {
4235 if (command.meshid.split('/').length == 1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
4236 var smesh = command.meshid.split('/');
4237 if ((smesh.length != 3) || (smesh[0] != 'mesh') || (smesh[1] != domain.id)) { err = 'Invalid group id'; }
4238 mesh = parent.meshes[command.meshid];
4239 if ((mesh == null) || (parent.IsMeshViewable(user, mesh) == false)) { err = 'Invalid group id'; }
4240 }
4241 var serverName = parent.getWebServerName(domain, req);
4242
4243 // Handle any errors
4244 if (err != null) {
4245 console.log(err, command.meshid);
4246 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createInviteLink', responseid: command.responseid, result: err })); } catch (ex) { } }
4247 break;
4248 }
4249
4250 const cookie = { a: 4, mid: command.meshid, f: command.flags, expire: command.expire * 60 };
4251 if ((typeof command.agents == 'number') && (command.agents != 0)) { cookie.ag = command.agents; }
4252 const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
4253 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; }
4254
4255 // Create the server url
4256 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
4257 var xdomain = (domain.dns == null) ? domain.id : '';
4258 if (xdomain != '') xdomain += '/';
4259 var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'agentinvite?c=' + inviteCookie;
4260 if (serverName.split('.') == 1) { url = '/' + xdomain + 'agentinvite?c=' + inviteCookie; }
4261
4262 ws.send(JSON.stringify({ action: 'createInviteLink', meshid: command.meshid, url: url, expire: command.expire, cookie: inviteCookie, responseid: command.responseid, tag: command.tag }));
4263 break;
4264 }
4265 case 'deviceMeshShares': {
4266 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4267 var err = null;
4268
4269 // Argument validation
4270 if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid device group id'; } // Check the meshid
4271 else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
4272 else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4273 else {
4274 // Check if we have rights on this device group
4275 mesh = parent.meshes[command.meshid];
4276 if (mesh == null) { err = 'Invalid device group id'; } // Check the meshid
4277 else if (parent.GetMeshRights(user, mesh) == 0) { err = 'Access denied'; }
4278 }
4279
4280 // Handle any errors
4281 if (err != null) {
4282 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: err })); } catch (ex) { } }
4283 break;
4284 }
4285
4286 // Get all device shares
4287 parent.db.GetAllTypeNoTypeField('deviceshare', domain.id, function (err, docs) {
4288 if (err != null) return;
4289 var now = Date.now(), okDocs = [];
4290 for (var i = 0; i < docs.length; i++) {
4291 const doc = docs[i];
4292 if ((doc.expireTime != null) && (doc.expireTime < now)) {
4293 // This share is expired.
4294 parent.db.Remove(doc._id, function () { });
4295
4296 // Send device share update
4297 var targets = parent.CreateNodeDispatchTargets(doc.xmeshid, doc.nodeid, ['server-users', user._id]);
4298 parent.parent.DispatchEvent(targets, obj, { etype: 'node', meshid: doc.xmeshid, nodeid: doc.nodeid, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
4299 } else {
4300 if (doc.xmeshid == null) {
4301 // This is an old share with missing meshid, fix it here.
4302 const f = function fixShareMeshId(err, nodes) {
4303 if (err != null) return;
4304 if (nodes.length == 1) {
4305 // Add the meshid to the device share
4306 fixShareMeshId.xdoc.xmeshid = nodes[0].meshid;
4307 fixShareMeshId.xdoc.type = 'deviceshare';
4308 delete fixShareMeshId.xdoc.meshid;
4309 parent.db.Set(fixShareMeshId.xdoc);
4310 } else {
4311 // This node no longer exists, remove the device share.
4312 parent.db.Remove(fixShareMeshId.xdoc._id);
4313 }
4314 }
4315 f.xdoc = doc;
4316 db.Get(doc.nodeid, f);
4317 } else if (doc.xmeshid == command.meshid) {
4318 // This share is ok, remove extra data we don't need to send.
4319 delete doc._id; delete doc.domain; delete doc.type; delete doc.xmeshid;
4320 if (doc.userid != user._id) { delete doc.url; } // If this is not the user who created this link, don't give the link.
4321 okDocs.push(doc);
4322 }
4323 }
4324 }
4325 try { ws.send(JSON.stringify({ action: 'deviceMeshShares', meshid: command.meshid, deviceShares: okDocs })); } catch (ex) { }
4326 });
4327 break;
4328 }
4329 case 'deviceShares': {
4330 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4331 var err = null;
4332
4333 // Argument validation
4334 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
4335 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4336 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4337
4338 // Handle any errors
4339 if (err != null) {
4340 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: err })); } catch (ex) { } }
4341 break;
4342 }
4343
4344 // Get the device rights
4345 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4346 // If node not found or we don't have remote control, reject.
4347 if (node == null) {
4348 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
4349 return;
4350 }
4351
4352 // If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
4353 if (rights != MESHRIGHT_ADMIN) {
4354 // If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
4355 if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
4356 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4357 return;
4358 }
4359 }
4360
4361 parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
4362 if (err != null) return;
4363 var now = Date.now(), removed = false, okDocs = [];
4364 for (var i = 0; i < docs.length; i++) {
4365 const doc = docs[i];
4366 if ((doc.expireTime != null) && (doc.expireTime < now)) {
4367 // This share is expired.
4368 parent.db.Remove(doc._id, function () { }); removed = true;
4369 } else {
4370 // This share is ok, remove extra data we don't need to send.
4371 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.xmeshid;
4372 if ((user.siteadmin !== SITERIGHT_ADMIN) && (doc.userid != user._id)) { delete doc.url; } // If this is not the user who created this link AND the user is not a site admin, don't give the link.
4373 okDocs.push(doc);
4374 }
4375 }
4376 try { ws.send(JSON.stringify({ action: 'deviceShares', nodeid: command.nodeid, deviceShares: okDocs })); } catch (ex) { }
4377
4378 // If we removed any shares, send device share update
4379 if (removed == true) {
4380 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4381 parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
4382 }
4383 });
4384 });
4385
4386 break;
4387 }
4388 case 'removeDeviceShare': {
4389 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4390 var err = null;
4391
4392 // Argument validation
4393 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
4394 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4395 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4396 if (common.validateString(command.publicid, 1, 128) == false) { err = 'Invalid public id'; } // Check the public identifier
4397
4398 // Handle any errors
4399 if (err != null) {
4400 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: err })); } catch (ex) { } }
4401 break;
4402 }
4403
4404 // Get the device rights
4405 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4406 // If node not found or we don't have remote control, reject.
4407 if (node == null) {
4408 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
4409 return;
4410 }
4411
4412 // If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
4413 if (rights != MESHRIGHT_ADMIN) {
4414 // If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
4415 if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
4416 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4417 return;
4418 }
4419 }
4420
4421 parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
4422 if (err != null) return;
4423
4424 // Remove device sharing
4425 var now = Date.now(), removedExact = null, removed = false, okDocs = [];
4426 for (var i = 0; i < docs.length; i++) {
4427 const doc = docs[i];
4428 if (doc.publicid == command.publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
4429 else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
4430 // This share is ok, remove extra data we don't need to send.
4431 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
4432 okDocs.push(doc);
4433 }
4434 }
4435
4436 // Confirm removal if requested
4437 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, nodeid: command.nodeid, publicid: command.publicid, removed: removedExact })); } catch (ex) { } }
4438
4439 // Event device share removal
4440 if (removedExact != null) {
4441 // Send out an event that we removed a device share
4442 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', 'server-shareremove', user._id]);
4443 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 };
4444 parent.parent.DispatchEvent(targets, obj, event);
4445
4446 // If this is an agent self-sharing link, notify the agent
4447 if (command.publicid.startsWith('AS:node/')) { routeCommandToNode({ action: 'msg', type: 'guestShare', nodeid: command.publicid.substring(3), flags: 0, url: null, viewOnly: false }); }
4448 }
4449
4450 // If we removed any shares, send device share update
4451 if (removed == true) {
4452 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4453 parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
4454 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
4455 } else {
4456 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'Invalid device share identifier.' })); } catch (ex) { } }
4457 }
4458 });
4459 });
4460 break;
4461 }
4462 case 'createDeviceShareLink': {
4463 if (domain.guestdevicesharing === false) return; // This feature is not allowed.
4464 var err = null;
4465
4466 // Argument validation
4467 if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
4468 else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4469 else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
4470 if (common.validateString(command.guestname, 1, 128) == false) { err = 'Invalid guest name'; } // Check the guest name
4471 else if ((command.expire != null) && (typeof command.expire != 'number')) { err = 'Invalid expire time'; } // Check the expire time in minutes
4472 else if ((command.start != null) && (typeof command.start != 'number')) { err = 'Invalid start time'; } // Check the start time in UTC seconds
4473 else if ((command.end != null) && (typeof command.end != 'number')) { err = 'Invalid end time'; } // Check the end time in UTC seconds
4474 else if (common.validateInt(command.consent, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
4475 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
4476 else if ((command.recurring != null) && (common.validateInt(command.recurring, 1, 2) == false)) { err = 'Invalid recurring value'; } // Check the recurring value, 1 = Daily, 2 = Weekly
4477 else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present
4478 else if ((command.recurring != null) && ((command.end != null) || (command.start == null) || (command.expire == null))) { err = 'Invalid recurring command'; }
4479 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
4480 else {
4481 if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
4482 var snode = command.nodeid.split('/');
4483 if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; }
4484 }
4485
4486 // Handle any errors
4487 if (err != null) {
4488 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createDeviceShareLink', responseid: command.responseid, result: err })); } catch (ex) { } }
4489 break;
4490 }
4491
4492 // Correct maximum session length if needed
4493 if ((typeof domain.guestdevicesharing == 'object') && (typeof domain.guestdevicesharing.maxsessiontime == 'number') && (domain.guestdevicesharing.maxsessiontime > 0)) {
4494 const maxtime = domain.guestdevicesharing.maxsessiontime;
4495 if ((command.expire != null) && (command.expire > maxtime)) { command.expire = maxtime; }
4496 if ((command.start != null) && (command.end != null)) { if ((command.end - command.start) > (maxtime * 60)) { command.end = (command.start + (maxtime * 60)); } }
4497 }
4498
4499 // Get the device rights
4500 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4501 // If node not found or we don't have remote control, reject.
4502 if (node == null) {
4503 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
4504 return;
4505 }
4506
4507 // If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
4508 if (rights != MESHRIGHT_ADMIN) {
4509 // If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
4510 if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
4511 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4512 return;
4513 }
4514 }
4515
4516 // If we are limited to no terminal, don't allow terminal sharing
4517 if (((command.p & 1) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOTERMINAL) != 0)) {
4518 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4519 return;
4520 }
4521
4522 // If we are limited to no desktop, don't allow desktop sharing
4523 if (((command.p & 2) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NODESKTOP) != 0)) {
4524 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4525 return;
4526 }
4527
4528 // If we are limited to no files, don't allow file sharing
4529 if (((command.p & 4) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOFILES) != 0)) {
4530 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
4531 return;
4532 }
4533
4534 // If we have view only remote desktop rights, force view-only on the guest share.
4535 if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_REMOTEVIEWONLY) != 0)) { command.viewOnly = true; }
4536
4537 // Create cookie
4538 var publicid = getRandomPassword(), startTime = null, expireTime = null, duration = null;
4539 if (command.recurring) {
4540 // Recurring share
4541 startTime = command.start * 1000;
4542 duration = command.expire;
4543 } else if (command.expire != null) {
4544 if (command.expire !== 0) {
4545 // Now until expire in hours
4546 startTime = Date.now();
4547 expireTime = Date.now() + (60000 * command.expire);
4548 } else {
4549 delete command.expire;
4550 }
4551 } else {
4552 // Time range in seconds
4553 startTime = command.start * 1000;
4554 expireTime = command.end * 1000;
4555 }
4556
4557 //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
4558 var cookie = { a: 6, pid: publicid }; // New style sharing cookie
4559 if ((startTime != null) && (expireTime != null)) { command.start = startTime; command.expire = cookie.e = expireTime; }
4560 else if ((startTime != null) && (duration != null)) { command.start = startTime; }
4561 const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
4562 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; }
4563
4564 // Create the server url
4565 var serverName = parent.getWebServerName(domain, req);
4566 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
4567 var xdomain = (domain.dns == null) ? domain.id : '';
4568 if (xdomain != '') xdomain += '/';
4569 var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
4570 if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
4571 command.url = url;
4572 command.publicid = publicid;
4573 if (command.responseid != null) { command.result = 'OK'; }
4574 try { ws.send(JSON.stringify(command)); } catch (ex) { }
4575
4576 // Create a device sharing database entry
4577 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 };
4578 if ((startTime != null) && (expireTime != null)) { shareEntry.startTime = startTime; shareEntry.expireTime = expireTime; }
4579 else if ((startTime != null) && (duration != null)) { shareEntry.startTime = startTime; shareEntry.duration = duration; }
4580 if (command.recurring) { shareEntry.recurring = command.recurring; }
4581 if (command.viewOnly === true) { shareEntry.viewOnly = true; }
4582 parent.db.Set(shareEntry);
4583
4584 // Send out an event that we added a device share
4585 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4586 var event;
4587 if (command.recurring == 1) {
4588 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 };
4589 } else if (command.recurring == 2) {
4590 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 };
4591 } else if ((startTime != null) && (expireTime != null)) {
4592 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 };
4593 } else {
4594 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 };
4595 }
4596 parent.parent.DispatchEvent(targets, obj, event);
4597
4598 // Send device share update
4599 parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
4600 if (err != null) return;
4601
4602 // Check device sharing
4603 var now = Date.now();
4604 for (var i = 0; i < docs.length; i++) {
4605 const doc = docs[i];
4606 if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
4607 // This share is ok, remove extra data we don't need to send.
4608 delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.xmeshid;
4609 }
4610 }
4611
4612 // Send device share update
4613 var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
4614 parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
4615 });
4616 });
4617 break;
4618 }
4619 case 'traceinfo': {
4620 // Only accept if the tracing tab is allowed for this domain
4621 if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.trace !== true))) break;
4622
4623 if ((user.siteadmin === SITERIGHT_ADMIN) && (typeof command.traceSources == 'object')) {
4624 parent.parent.debugRemoteSources = command.traceSources;
4625 parent.parent.DispatchEvent(['*'], obj, { action: 'traceinfo', userid: user._id, username: user.name, traceSources: command.traceSources, nolog: 1, domain: domain.id });
4626 }
4627 break;
4628 }
4629 case 'sendmqttmsg': {
4630 if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }; // MQTT not available
4631 if (common.validateArray(command.nodeids, 1) == false) { err = 'Invalid nodeids'; }; // Check nodeid's
4632 if (common.validateString(command.topic, 1, 64) == false) { err = 'Invalid topic'; } // Check the topic
4633 if (common.validateString(command.msg, 1, 4096) == false) { err = 'Invalid msg'; } // Check the message
4634
4635 // Handle any errors
4636 if (err != null) {
4637 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'sendmqttmsg', responseid: command.responseid, result: err })); } catch (ex) { } }
4638 break;
4639 }
4640
4641 // Send the MQTT message
4642 for (i in command.nodeids) {
4643 // Get the node and the rights for this node
4644 parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
4645 // If this device is connected on MQTT, send a wake action.
4646 if (rights != 0) {
4647 parent.parent.mqttbroker.publish(node._id, command.topic, command.msg);
4648 }
4649 });
4650 }
4651
4652 break;
4653 }
4654 case 'getmqttlogin': {
4655 var err = null;
4656 if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }
4657 if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid
4658
4659 // Handle any errors
4660 if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
4661
4662 // Get the node and the rights for this node
4663 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4664 // Check if this user has rights to do this
4665 if (rights == MESHRIGHT_ADMIN) {
4666 var token = parent.parent.mqttbroker.generateLogin(node.meshid, node._id);
4667 var r = { action: 'getmqttlogin', responseid: command.responseid, nodeid: node._id, user: token.user, pass: token.pass };
4668 const serverName = parent.getWebServerName(domain, req);
4669
4670 // Add MPS URL
4671 if (parent.parent.mpsserver != null) {
4672 r.mpsCertHashSha384 = parent.parent.certificateOperations.getCertHash(parent.parent.mpsserver.certificates.mps.cert);
4673 r.mpsCertHashSha1 = parent.parent.certificateOperations.getCertHashSha1(parent.parent.mpsserver.certificates.mps.cert);
4674 r.mpsUrl = 'mqtts://' + serverName + ':' + ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport) + '/';
4675 }
4676
4677 // Add WS URL
4678 var xdomain = (domain.dns == null) ? domain.id : '';
4679 if (xdomain != '') xdomain += '/';
4680 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
4681 r.wsUrl = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'mqtt.ashx';
4682 r.wsTrustedCert = parent.isTrustedCert(domain);
4683
4684 try { ws.send(JSON.stringify(r)); } catch (ex) { }
4685 } else {
4686 if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Unable to perform this operation' })); } catch (ex) { } }
4687 }
4688 });
4689 break;
4690 }
4691 case 'amt': {
4692 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
4693 if (common.validateInt(command.mode, 0, 3) == false) break; // Check connection mode
4694 // Validate if communication mode is possible
4695 if (command.mode == null || command.mode == 0) {
4696 break; //unsupported
4697 } else if (command.mode == 1) {
4698 var state = parent.parent.GetConnectivityState(command.nodeid);
4699 if ((state == null) || (state.connectivity & 4) == 0) break;
4700 } else if (command.mode == 2) {
4701 if (parent.parent.mpsserver.ciraConnections[command.nodeid] == null) break;
4702 }
4703 /*
4704 else if (command.mode == 3) {
4705 if (parent.parent.apfserver.apfConnections[command.nodeid] == null) break;
4706 }
4707 */
4708
4709 // Get the node and the rights for this node
4710 parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
4711 if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
4712 handleAmtCommand(command, node);
4713 });
4714 break;
4715 }
4716 case 'distributeCore': {
4717 // This is only available when plugins are enabled since it could cause stress on the server
4718 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4719 for (var i in command.nodes) {
4720 parent.sendMeshAgentCore(user, domain, command.nodes[i]._id, 'default');
4721 }
4722 break;
4723 }
4724 case 'plugins': {
4725 // Since plugin actions generally require a server restart, use the Full admin permission
4726 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4727 parent.db.getPlugins(function(err, docs) {
4728 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4729 });
4730 break;
4731 }
4732 case 'pluginLatestCheck': {
4733 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4734 parent.parent.pluginHandler.getPluginLatest()
4735 .then(function(latest) {
4736 try { ws.send(JSON.stringify({ action: 'pluginVersionsAvailable', list: latest })); } catch (ex) { }
4737 });
4738 break;
4739 }
4740 case 'addplugin': {
4741 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4742 try {
4743 parent.parent.pluginHandler.getPluginConfig(command.url)
4744 .then(parent.parent.pluginHandler.addPlugin)
4745 .then(function(docs){
4746 var targets = ['*', 'server-users'];
4747 parent.parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs });
4748 })
4749 .catch(function(err) {
4750 if (typeof err == 'object') err = err.message;
4751 try { ws.send(JSON.stringify({ action: 'pluginError', msg: err })); } catch (er) { }
4752 });
4753
4754 } catch(ex) { console.log('Cannot add plugin: ' + e); }
4755 break;
4756 }
4757 case 'installplugin': {
4758 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4759 parent.parent.pluginHandler.installPlugin(command.id, command.version_only, null, function(){
4760 parent.db.getPlugins(function(err, docs) {
4761 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4762 });
4763 var targets = ['*', 'server-users'];
4764 parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
4765 });
4766 break;
4767 }
4768 case 'disableplugin': {
4769 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4770 parent.parent.pluginHandler.disablePlugin(command.id, function(){
4771 parent.db.getPlugins(function(err, docs) {
4772 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4773 var targets = ['*', 'server-users'];
4774 parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
4775 });
4776 });
4777 break;
4778 }
4779 case 'removeplugin': {
4780 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4781 parent.parent.pluginHandler.removePlugin(command.id, function(){
4782 parent.db.getPlugins(function(err, docs) {
4783 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
4784 });
4785 });
4786 break;
4787 }
4788 case 'reloadplugin': {
4789 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4790 if (command.plugin == "ALL") {
4791 // Reload all plugins
4792 parent.parent.pluginHandler.reloadAllPlugins(function(result) {
4793 try { ws.send(JSON.stringify({ action: 'pluginReloaded', result: result })); } catch (ex) { }
4794 });
4795 } else {
4796 // Reload specific plugin
4797 parent.parent.pluginHandler.reloadPlugin(command.plugin, function(result) {
4798 try { ws.send(JSON.stringify({ action: 'pluginReloaded', result: result })); } catch (ex) { }
4799 });
4800 }
4801 break;
4802 }
4803 case 'getpluginpermissions': {
4804 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin
4805 var perms = parent.parent.pluginHandler.getPluginPermissions(command.plugin);
4806 try { ws.send(JSON.stringify({ action: 'pluginPermissions', plugin: command.plugin, permissions: perms })); } catch (ex) { }
4807 break;
4808 }
4809 case 'setpluginpermissions': {
4810 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin
4811 parent.parent.pluginHandler.setPluginPermissions(command.plugin, command.data, function(err) {
4812 try { ws.send(JSON.stringify({ action: 'pluginPermissionsSet', plugin: command.plugin, success: !err, error: err })); } catch (ex) { }
4813 });
4814 break;
4815 }
4816 case 'getpluginpermissionlist': {
4817 // Return list of users, user groups, meshes, nodes for permission assignment UI
4818 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break;
4819
4820 var result = { users: [], userGroups: [], meshes: [], nodes: [] };
4821
4822 // Get all users
4823 parent.db.GetAllType('user', function(err, docs) {
4824 if (docs) {
4825 docs.forEach(function(u) {
4826 if (u.name && u._id) {
4827 result.users.push({ _id: u._id, name: u.name, email: u.email });
4828 }
4829 });
4830 }
4831
4832 // Get all user groups
4833 parent.db.GetAllType('ugrp', function(err, ugrps) {
4834 if (ugrps) {
4835 ugrps.forEach(function(ug) {
4836 if (ug.name && ug._id) {
4837 result.userGroups.push({ _id: ug._id, name: ug.name });
4838 }
4839 });
4840 }
4841
4842 // Get all meshes (device groups)
4843 parent.db.GetAllType('mesh', function(err, meshes) {
4844 if (meshes) {
4845 meshes.forEach(function(m) {
4846 if (m.name && m._id && !m.deleted) {
4847 result.meshes.push({ _id: m._id, name: m.name });
4848 }
4849 });
4850 }
4851
4852 // Get all nodes (devices)
4853 parent.db.GetAllType('node', function(err, nodes) {
4854 if (nodes) {
4855 // Create a map of meshid to meshname for grouping
4856 var meshMap = {};
4857 if (meshes) {
4858 meshes.forEach(function(m) {
4859 meshMap[m._id] = m.name;
4860 });
4861 }
4862
4863 nodes.forEach(function(n) {
4864 if (n.name && n._id && !n.deleted) {
4865 var meshname = meshMap[n.meshid] || 'Ungrouped';
4866 result.nodes.push({ _id: n._id, name: n.name, meshid: n.meshid, meshname: meshname });
4867 }
4868 });
4869 }
4870
4871 try { ws.send(JSON.stringify({ action: 'pluginPermissionList', list: result })); } catch (ex) { }
4872 });
4873 });
4874 });
4875 });
4876 break;
4877 }
4878 case 'getpluginversions': {
4879 if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
4880 parent.parent.pluginHandler.getPluginVersions(command.id)
4881 .then(function (versionInfo) {
4882 try { ws.send(JSON.stringify({ action: 'downgradePluginVersions', info: versionInfo, error: null })); } catch (ex) { }
4883 })
4884 .catch(function (e) {
4885 try { ws.send(JSON.stringify({ action: 'pluginError', msg: e })); } catch (ex) { }
4886 });
4887
4888 break;
4889 }
4890 case 'plugin': {
4891 if (parent.parent.pluginHandler == null) break; // If the plugin's are not supported, reject this command.
4892 command.userid = user._id;
4893 if (command.routeToNode === true) {
4894 routeCommandToNode(command);
4895 } else {
4896 try {
4897 parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
4898 } catch (ex) { console.log('Error loading plugin handler (' + ex + ')'); }
4899 }
4900 break;
4901 }
4902 case 'uicustomevent': {
4903 if ((command.src != null) && (Array.isArray(command.src.selectedDevices))) {
4904 // Contains a list of nodeid's, check that we have permissions for them.
4905 parent.GetNodesWithRights(domain, user, command.src.selectedDevices, function (nodes) {
4906 var nodeids = [];
4907 for (var i in nodes) { nodeids.push(i); }
4908 if (nodeids.length == 0) return;
4909
4910 // Event the custom UI action
4911 var message = { etype: 'user', userid: user._id, username: user.name, action: 'uicustomevent', domain: domain.id, uisection: command.section, element: command.element };
4912 if (nodeids.length == 1) { message.nodeid = nodeids[0]; }
4913 if (command.selectedDevices != null) { message.selectedDevices = command.selectedDevices; }
4914 if (command.src != null) { message.src = command.src; }
4915 if (command.values != null) { message.values = command.values; }
4916 if (typeof command.logmsg == 'string') { message.msg = command.logmsg; } else { message.nolog = 1; }
4917 parent.parent.DispatchEvent(['*', user._id], obj, message);
4918 });
4919 } else {
4920 // Event the custom UI action
4921 var message = { etype: 'user', userid: user._id, username: user.name, action: 'uicustomevent', domain: domain.id, uisection: command.section, element: command.element };
4922 if (command.selectedDevices != null) { message.selectedDevices = command.selectedDevices; }
4923 if (command.src != null) { message.src = command.src; }
4924 if (command.values != null) { message.values = command.values; }
4925 if (typeof command.logmsg == 'string') { message.msg = command.logmsg; } else { message.nolog = 1; }
4926 parent.parent.DispatchEvent(['*', user._id], obj, message);
4927 }
4928
4929 if (parent.parent.pluginHandler != null) // If the plugin's are not supported, reject this command.
4930 {
4931 command.userid = user._id;
4932 try {
4933 for( var pluginName in parent.parent.pluginHandler.plugins)
4934 if( typeof parent.parent.pluginHandler.plugins[pluginName].uiCustomEvent === 'function' )
4935 parent.parent.pluginHandler.plugins[pluginName].uiCustomEvent(command, obj);
4936 } catch (ex) { console.log('Error loading plugin handler (' + ex + ')'); }
4937 }
4938 break;
4939 }
4940 case 'serverBackup': {
4941 // Do not allow this command when logged in using a login token
4942 if (req.session.loginToken != null) break;
4943
4944 if ((user.siteadmin != SITERIGHT_ADMIN) || (typeof parent.parent.config.settings.autobackup.googledrive != 'object')) return;
4945 if (command.service == 'googleDrive') {
4946 if (command.state == 0) {
4947 parent.db.Remove('GoogleDriveBackup', function () { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 1 })); } catch (ex) { } });
4948 } else if (command.state == 1) {
4949 const {google} = require('googleapis');
4950 obj.oAuth2Client = new google.auth.OAuth2(command.clientid, command.clientsecret, "urn:ietf:wg:oauth:2.0:oob");
4951 obj.oAuth2Client.xxclientid = command.clientid;
4952 obj.oAuth2Client.xxclientsecret = command.clientsecret;
4953 const authUrl = obj.oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/drive.file'] });
4954 try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 2, url: authUrl })); } catch (ex) { }
4955 } else if ((command.state == 2) && (obj.oAuth2Client != null)) {
4956 obj.oAuth2Client.getToken(command.code, function (err, token) {
4957 if (err != null) { console.log('GoogleDrive (getToken) error: ', err); return; }
4958 parent.db.Set({ _id: 'GoogleDriveBackup', state: 3, clientid: obj.oAuth2Client.xxclientid, clientsecret: obj.oAuth2Client.xxclientsecret, token: token });
4959 try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 3 })); } catch (ex) { }
4960 });
4961 }
4962 }
4963 break;
4964 }
4965 case 'twoFactorCookie': {
4966 try {
4967 // Do not allow this command when logged in using a login token
4968 if (req.session.loginToken != null) break;
4969
4970 // Do not allows this command is 2FA cookie duration is set to zero
4971 if (domain.twofactorcookiedurationdays === 0) break;
4972
4973 // Generate a two-factor cookie
4974 var maxCookieAge = domain.twofactorcookiedurationdays;
4975 if ((typeof maxCookieAge != 'number') || (maxCookieAge < 1)) { maxCookieAge = 30; }
4976 const twoFactorCookie = parent.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, parent.parent.loginCookieEncryptionKey);
4977 try { ws.send(JSON.stringify({ action: 'twoFactorCookie', cookie: twoFactorCookie })); } catch (ex) { }
4978 } catch (ex) { console.log(ex); }
4979 break;
4980 }
4981 case 'amtsetupbin': {
4982 if ((command.oldmebxpass != 'admin') && (common.validateString(command.oldmebxpass, 8, 16) == false)) break; // Check password
4983 if (common.validateString(command.newmebxpass, 8, 16) == false) break; // Check password
4984 if ((command.baremetal) && (parent.parent.amtProvisioningServer != null)) {
4985 // Create bare metal setup.bin
4986 var bin = parent.parent.certificateOperations.GetBareMetalSetupBinFile(domain.amtacmactivation, command.oldmebxpass, command.newmebxpass, domain, user);
4987 try { ws.send(JSON.stringify({ action: 'amtsetupbin', file: Buffer.from(bin, 'binary').toString('base64') })); } catch (ex) { }
4988 } else {
4989 // Create standard setup.bin
4990 var bin = parent.parent.certificateOperations.GetSetupBinFile(domain.amtacmactivation, command.oldmebxpass, command.newmebxpass, domain, user);
4991 try { ws.send(JSON.stringify({ action: 'amtsetupbin', file: Buffer.from(bin, 'binary').toString('base64') })); } catch (ex) { }
4992 }
4993 break;
4994 }
4995 case 'meshToolInfo': {
4996 if (typeof command.name != 'string') break;
4997 var info = parent.parent.meshToolsBinaries[command.name];
4998 var responseCmd = { action: 'meshToolInfo', name: command.name, hash: info.hash, size: info.size, url: info.url };
4999 if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
5000 try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
Showing first 5,000 of 8,636 lines. View raw