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