meshuser.js
Rama master · réplica de lectura del repositorio original.
/**
* @description MeshCentral MeshAgent
* @author Ylian Saint-Hilaire & Bryan Roe
* @copyright Intel Corporation 2018-2022
* @license Apache-2.0
* @version v0.0.1
*/
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict";
// volume & bitlocker statuses
const encMethod = { 0: '', 1: "AES-128 with diffuser", 2: "AES-256 with diffuser", 3: 'AES-128', 4: 'AES-256', 5: "Hardware encryption", 6: 'XTS-AES-128', 7: 'XTS-AES-256' };
const driveType = { 0: "Unknown", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" };
const conversionStatus = { "-1": "Unknown", 0: "Fully Decrypted", 1: "Fully Encrypted", 2: "Encryption In Progress", 3: "Decryption In Progress", 4: "Encryption Paused", 5: "Decryption Paused" };
const protectionStatus = { 0: "Off", 1: "On", 2: "Locked"};
// Construct a MeshAgent object, called upon connection
module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, user) {
const fs = require('fs');
const path = require('path');
const common = parent.common;
// Cross domain messages, for cross-domain administrators only.
const allowedCrossDomainMessages = ['accountcreate', 'accountremove', 'accountchange', 'createusergroup', 'deleteusergroup', 'usergroupchange'];
// User Consent Flags
const USERCONSENT_DesktopNotifyUser = 1;
const USERCONSENT_TerminalNotifyUser = 2;
const USERCONSENT_FilesNotifyUser = 4;
const USERCONSENT_DesktopPromptUser = 8;
const USERCONSENT_TerminalPromptUser = 16;
const USERCONSENT_FilesPromptUser = 32;
const USERCONSENT_ShowConnectionToolbar = 64;
// Mesh Rights
const MESHRIGHT_EDITMESH = 0x00000001; // 1
const MESHRIGHT_MANAGEUSERS = 0x00000002; // 2
const MESHRIGHT_MANAGECOMPUTERS = 0x00000004; // 4
const MESHRIGHT_REMOTECONTROL = 0x00000008; // 8
const MESHRIGHT_AGENTCONSOLE = 0x00000010; // 16
const MESHRIGHT_SERVERFILES = 0x00000020; // 32
const MESHRIGHT_WAKEDEVICE = 0x00000040; // 64
const MESHRIGHT_SETNOTES = 0x00000080; // 128
const MESHRIGHT_REMOTEVIEWONLY = 0x00000100; // 256
const MESHRIGHT_NOTERMINAL = 0x00000200; // 512
const MESHRIGHT_NOFILES = 0x00000400; // 1024
const MESHRIGHT_NOAMT = 0x00000800; // 2048
const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000; // 4096
const MESHRIGHT_LIMITEVENTS = 0x00002000; // 8192
const MESHRIGHT_CHATNOTIFY = 0x00004000; // 16384
const MESHRIGHT_UNINSTALL = 0x00008000; // 32768
const MESHRIGHT_NODESKTOP = 0x00010000; // 65536
const MESHRIGHT_REMOTECOMMAND = 0x00020000; // 131072
const MESHRIGHT_RESETOFF = 0x00040000; // 262144
const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
const MESHRIGHT_RELAY = 0x00200000; // 2097152
const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304
const MESHRIGHT_NOSOFTWARE = 0x00800000; // 8388608
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
const MESHRIGHT_RESTRICTIONS = MESHRIGHT_REMOTEVIEWONLY | MESHRIGHT_NOTERMINAL | MESHRIGHT_NOFILES | MESHRIGHT_NOAMT | MESHRIGHT_DESKLIMITEDINPUT | MESHRIGHT_LIMITEVENTS | MESHRIGHT_NODESKTOP | MESHRIGHT_NOREGISTRY | MESHRIGHT_NOSOFTWARE;
// Site rights
const SITERIGHT_SERVERBACKUP = 0x00000001; // 1
const SITERIGHT_MANAGEUSERS = 0x00000002; // 2
const SITERIGHT_SERVERRESTORE = 0x00000004; // 4
const SITERIGHT_FILEACCESS = 0x00000008; // 8
const SITERIGHT_SERVERUPDATE = 0x00000010; // 16
const SITERIGHT_LOCKED = 0x00000020; // 32
const SITERIGHT_NONEWGROUPS = 0x00000040; // 64
const SITERIGHT_NOMESHCMD = 0x00000080; // 128
const SITERIGHT_USERGROUPS = 0x00000100; // 256
const SITERIGHT_RECORDINGS = 0x00000200; // 512
const SITERIGHT_LOCKSETTINGS = 0x00000400; // 1024
const SITERIGHT_ALLEVENTS = 0x00000800; // 2048
const SITERIGHT_NONEWDEVICES = 0x00001000; // 4096
const SITERIGHT_ADMIN = 0xFFFFFFFF;
// Protocol Numbers
const PROTOCOL_TERMINAL = 1;
const PROTOCOL_DESKTOP = 2;
const PROTOCOL_FILES = 5;
const PROTOCOL_AMTWSMAN = 100;
const PROTOCOL_AMTREDIR = 101;
const PROTOCOL_MESSENGER = 200;
const PROTOCOL_WEBRDP = 201;
const PROTOCOL_WEBSSH = 202;
const PROTOCOL_WEBSFTP = 203;
const PROTOCOL_WEBVNC = 204;
// MeshCentral Satellite
const SATELLITE_PRESENT = 1; // This session is a MeshCentral Satellite session
const SATELLITE_802_1x = 2; // This session supports 802.1x profile checking and creation
// Events
/*
var eventsMessageId = {
1: "Account login",
2: "Account logout",
3: "Changed language from {1} to {2}",
4: "Joined desktop multiplex session",
5: "Left the desktop multiplex session",
6: "Started desktop multiplex session",
7: "Finished recording session, {0} second(s)",
8: "Closed desktop multiplex session, {0} second(s)"
};
*/
var obj = {};
obj.user = user;
obj.domain = domain;
obj.ws = ws;
// Information related to the current page the user is looking at
obj.deviceSkip = 0; // How many devices to skip
obj.deviceLimit = 0; // How many devices to view
obj.visibleDevices = null; // An object of visible nodeid's if the user is in paging mode
obj.deviceFilter = null; // Device name filter applied by the server before paging, only used in paging mode
if (domain.maxdeviceview != null) { obj.deviceLimit = domain.maxdeviceview; }
// Check if we are a cross-domain administrator
if (parent.parent.config.settings.managecrossdomain && (parent.parent.config.settings.managecrossdomain.indexOf(user._id) >= 0)) { obj.crossDomain = true; }
// Server side Intel AMT stack
const WsmanComm = require('./amt/amt-wsman-comm.js');
const Wsman = require('./amt/amt-wsman.js');
const Amt = require('./amt/amt.js');
// If this session has an expire time, setup a timer now.
if ((req.session != null) && (typeof req.session.expire == 'number')) {
var delta = (req.session.expire - Date.now());
if (delta <= 0) { req.session = {}; try { ws.close(); } catch (ex) { } return; } // Session is already expired, close now.
obj.expireTimer = setTimeout(function () { for (var i in req.session) { delete req.session[i]; } obj.close(); }, delta);
}
// Send data through the websocket
obj.send = function (object) { try { ws.send(JSON.stringify(object)); } catch(ex) {} }
// Clean a IPv6 address that encodes a IPv4 address
function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } }
// Send a PING/PONG message
function sendPing() { try { obj.ws.send('{"action":"ping"}'); } catch (ex) { } }
function sendPong() { try { obj.ws.send('{"action":"pong"}'); } catch (ex) { } }
// Setup the agent PING/PONG timers
if ((typeof args.browserping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.browserping * 1000); }
else if ((typeof args.browserpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.browserpong * 1000); }
// Disconnect this user
obj.close = function (arg) {
obj.ws.xclosed = 1; // This is for testing. Will be displayed when running "usersessions" server console command.
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
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
obj.ws.xclosed = 2; // DEBUG
// Perform timer cleanup
if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
obj.ws.xclosed = 3; // DEBUG
// Clear expire timeout
if (obj.expireTimer != null) { clearTimeout(obj.expireTimer); delete obj.expireTimer; }
obj.ws.xclosed = 4; // DEBUG
// Perform cleanup
parent.parent.RemoveAllEventDispatch(obj.ws);
if (obj.serverStatsTimer != null) { clearInterval(obj.serverStatsTimer); delete obj.serverStatsTimer; }
if (req.session && req.session.ws && req.session.ws == obj.ws) { delete req.session.ws; }
if (parent.wssessions2[ws.sessionId]) { delete parent.wssessions2[ws.sessionId]; }
obj.ws.xclosed = 5; // DEBUG
if ((obj.user != null) && (parent.wssessions[obj.user._id])) {
obj.ws.xclosed = 6; // DEBUG
var i = parent.wssessions[obj.user._id].indexOf(obj.ws);
if (i >= 0) {
obj.ws.xclosed = 7; // DEBUG
parent.wssessions[obj.user._id].splice(i, 1);
var user = parent.users[obj.user._id];
if (user) {
obj.ws.xclosed = 8; // DEBUG
if (parent.parent.multiServer == null) {
var targets = ['*', 'server-users'];
if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } }
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 });
} else {
parent.recountSessions(ws.sessionId); // Recount sessions
}
}
if (parent.wssessions[obj.user._id].length == 0) { delete parent.wssessions[obj.user._id]; }
}
}
obj.ws.xclosed = 9; // DEBUG
// If we have peer servers, inform them of the disconnected session
if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'sessionEnd', sessionid: ws.sessionId }); }
obj.ws.xclosed = 10; // DEBUG
// Update user last access time
if (obj.user != null) {
const timeNow = Math.floor(Date.now() / 1000);
if (obj.user.access < (timeNow - 300)) { // Only update user access time if longer than 5 minutes
obj.user.access = timeNow;
parent.db.SetUser(user);
// Event the change
var message = { etype: 'user', userid: obj.user._id, username: obj.user.name, account: parent.CloneSafeUser(obj.user), action: 'accountchange', domain: domain.id, nolog: 1 };
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.
var targets = ['*', 'server-users', obj.user._id];
if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } }
parent.parent.DispatchEvent(targets, obj, message);
}
}
// Aggressive cleanup
delete obj.user;
delete obj.domain;
delete obj.ws.userid;
delete obj.ws.domainid;
delete obj.ws.clientIp;
delete obj.ws.sessionId;
delete obj.ws.HandleEvent;
obj.ws.removeAllListeners(['message', 'close', 'error']);
obj.ws.xclosed = 11; // DEBUG
};
// Convert a mesh path array into a real path on the server side
function meshPathToRealPath(meshpath, user) {
if (common.validateArray(meshpath, 1) == false) return null;
var splitid = meshpath[0].split('/');
if (splitid[0] == 'user') {
// Check user access
if (meshpath[0] != user._id) return null; // Only allow own user folder
} else if (splitid[0] == 'mesh') {
// Check mesh access
if ((parent.GetMeshRights(user, meshpath[0]) & MESHRIGHT_SERVERFILES) == 0) return null; // This user must have mesh rights to "server files"
} else return null;
var rootfolder = meshpath[0], rootfoldersplit = rootfolder.split('/'), domainx = 'domain';
if (rootfoldersplit[1].length > 0) domainx = 'domain-' + rootfoldersplit[1];
var path = parent.path.join(parent.filespath, domainx, rootfoldersplit[0] + '-' + rootfoldersplit[2]);
for (var i = 1; i < meshpath.length; i++) { if (common.IsFilenameValid(meshpath[i]) == false) { path = null; break; } path += ("/" + meshpath[i]); }
return path;
}
// Copy a file using the best technique available
function copyFile(src, dest, func, tag) {
if (fs.copyFile) {
// NodeJS v8.5 and higher
fs.copyFile(src, dest, function (err) { func(tag); })
} else {
// Older NodeJS
try {
var ss = fs.createReadStream(src), ds = fs.createWriteStream(dest);
ss.on('error', function () { func(tag); });
ds.on('error', function () { func(tag); });
ss.pipe(ds);
ds.ss = ss;
if (arguments.length == 3 && typeof arguments[2] === 'function') { ds.on('close', arguments[2]); }
else if (arguments.length == 4 && typeof arguments[3] === 'function') { ds.on('close', arguments[3]); }
ds.on('close', function () { func(tag); });
} catch (ex) { }
}
}
// Route a command to a target node
function routeCommandToNode(command, requiredRights, requiredNonRights, func, options) {
if (common.validateString(command.nodeid, 8, 128) == false) { if (func) { func(false); } return false; }
var splitnodeid = command.nodeid.split('/');
// Check that we are in the same domain and the user has rights over this node.
if ((splitnodeid[0] == 'node') && (splitnodeid[1] == domain.id)) {
// See if the node is connected
var agent = parent.wsagents[command.nodeid];
if (agent != null) {
// Check if we have permission to send a message to that node
parent.GetNodeWithRights(domain, user, agent.dbNodeKey, function (node, rights, visible) {
var mesh = parent.meshes[agent.dbMeshKey];
if ((node != null) && (mesh != null) && ((rights & MESHRIGHT_REMOTECONTROL) || (rights & MESHRIGHT_REMOTEVIEWONLY))) { // 8 is remote control permission, 256 is desktop read only
if ((requiredRights != null) && ((rights & requiredRights) == 0)) { if (func) { func(false); } return; } // Check Required Rights
if ((requiredNonRights != null) && (rights != MESHRIGHT_ADMIN) && ((rights & requiredNonRights) != 0)) { if (func) { func(false); } return; } // Check Required None Rights
command.sessionid = ws.sessionId; // Set the session id, required for responses
command.rights = rights; // Add user rights flags to the message
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
command.consent = 0;
if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags
if (typeof mesh.consent == 'number') { command.consent |= mesh.consent; } // Add device group user consent
if (typeof node.consent == 'number') { command.consent |= node.consent; } // Add node user consent
if (typeof user.consent == 'number') { command.consent |= user.consent; } // Add user consent
// If desktop is viewonly, add this here.
if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { command.desktopviewonly = true; }
// Check if we need to add consent flags because of a user group link
if ((user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
// This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
for (var i in user.links) {
var ugrp = parent.userGroups[i];
if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
command.consent |= ugrp.consent; // Add user group consent flags
}
}
}
command.username = user.name; // Add user name
command.realname = user.realname; // Add real name
command.userid = user._id; // Add user id
command.remoteaddr = req.clientIp; // User's IP address
if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text
if ((typeof domain.desktopprivacybarmaxwidth == 'number') && (domain.desktopprivacybarmaxwidth > 0)) { command.privacybarmaxwidth = domain.desktopprivacybarmaxwidth; } // Privacy bar maximum width
delete command.nodeid; // Remove the nodeid since it's implied
try { agent.send(JSON.stringify(command)); } catch (ex) { }
if (func) { func(true); }
} else { if (func) { func(false); } }
});
} else {
// Check if a peer server is connected to this agent
var routing = parent.parent.GetRoutingServerIdNotSelf(command.nodeid, 1); // 1 = MeshAgent routing type
if (routing != null) {
// Check if we have permission to send a message to that node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((requiredRights != null) && ((rights & requiredRights) == 0)) { if (func) { func(false); } return; } // Check Required Rights
if ((requiredNonRights != null) && (rights != MESHRIGHT_ADMIN) && ((rights & requiredNonRights) != 0)) { if (func) { func(false); } return; } // Check Required None Rights
var mesh = parent.meshes[routing.meshid];
if ((node != null) && (mesh != null) && ((rights & MESHRIGHT_REMOTECONTROL) || (rights & MESHRIGHT_REMOTEVIEWONLY))) { // 8 is remote control permission
command.fromSessionid = ws.sessionId; // Set the session id, required for responses
command.rights = rights; // Add user rights flags to the message
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
command.consent = 0;
if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags
if (typeof mesh.consent == 'number') { command.consent |= mesh.consent; } // Add device group user consent
if (typeof node.consent == 'number') { command.consent |= node.consent; } // Add node user consent
if (typeof user.consent == 'number') { command.consent |= user.consent; } // Add user consent
// Check if we need to add consent flags because of a user group link
if ((user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
// This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
for (var i in user.links) {
var ugrp = parent.userGroups[i];
if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
command.consent |= ugrp.consent; // Add user group consent flags
}
}
}
command.username = user.name; // Add user name
command.realname = user.realname; // Add real name
command.userid = user._id; // Add user id
command.remoteaddr = req.clientIp; // User's IP address
if (typeof domain.desktopprivacybartext == 'string') { command.privacybartext = domain.desktopprivacybartext; } // Privacy bar text
if ((typeof domain.desktopprivacybarmaxwidth == 'number') && (domain.desktopprivacybarmaxwidth > 0)) { command.privacybarmaxwidth = domain.desktopprivacybarmaxwidth; } // Privacy bar maximum width
parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid);
if (func) { func(true); }
} else { if (func) { func(false); } }
});
} else { if (func) { func(false); } return false; }
}
} else { if (func) { func(false); } return false; }
return true;
}
// Route a command to all targets in a mesh
function routeCommandToMesh(meshid, command) {
// If we have peer servers, inform them of this command to send to all agents of this device group
if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'agentMsgByMeshId', meshid: meshid, command: command }); }
// See if the node is connected
for (var nodeid in parent.wsagents) {
var agent = parent.wsagents[nodeid];
if (agent.dbMeshKey == meshid) { try { agent.send(JSON.stringify(command)); } catch (ex) { } }
}
return true;
}
try {
// Check if the user is logged in
if (user == null) { try { ws.close(); } catch (e) { } return; }
// Check if we have exceeded the user session limit
if ((typeof domain.limits.maxusersessions == 'number') || (typeof domain.limits.maxsingleusersessions == 'number')) {
// Count the number of user sessions for this domain
var domainUserSessionCount = 0, selfUserSessionCount = 0;
for (var i in parent.wssessions2) {
if (parent.wssessions2[i].domainid == domain.id) {
domainUserSessionCount++; if (parent.wssessions2[i].userid == user._id) { selfUserSessionCount++; }
}
}
// Check if we have too many user sessions
if (((typeof domain.limits.maxusersessions == 'number') && (domainUserSessionCount >= domain.limits.maxusersessions)) || ((typeof domain.limits.maxsingleusersessions == 'number') && (selfUserSessionCount >= domain.limits.maxsingleusersessions))) {
try { ws.send(JSON.stringify({ action: 'stopped', msg: 'Session count exceed' })); } catch (ex) { }
try { ws.close(); } catch (e) { }
return;
}
}
// Associate this websocket session with the web session
ws.userid = user._id;
ws.domainid = domain.id;
ws.clientIp = req.clientIp;
// Create a new session id for this user.
parent.crypto.randomBytes(20, function (err, randombuf) {
ws.sessionId = user._id + '/' + randombuf.toString('hex');
// Add this web socket session to session list
parent.wssessions2[ws.sessionId] = ws;
if (!parent.wssessions[user._id]) { parent.wssessions[user._id] = [ws]; } else { parent.wssessions[user._id].push(ws); }
if (parent.parent.multiServer == null) {
var targets = ['*', 'server-users'];
if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } }
parent.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: user._id, username: user.name, count: parent.wssessions[user._id].length, nolog: 1, domain: domain.id });
} else {
parent.recountSessions(ws.sessionId); // Recount sessions
}
// If we have peer servers, inform them of the new session
if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'sessionStart', sessionid: ws.sessionId }); }
// Handle events
ws.HandleEvent = function (source, event, ids, id) {
// If this session is logged in using a loginToken and the token is removed, disconnect.
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; }
// If this user is not viewing all devices and paging, check if this event is in the current page
if (isEventWithinPage(ids) == false) return;
// Normally, only allow this user to receive messages from it's own domain.
// If the user is a cross domain administrator, allow some select messages from different domains.
if ((event.domain == null) || (event.domain == domain.id) || ((obj.crossDomain === true) && (allowedCrossDomainMessages.indexOf(event.action) >= 0))) {
try {
if (event == 'close') { try { delete req.session; } catch (ex) { } obj.close(); return; }
else if (event == 'resubscribe') { user.subscriptions = parent.subscribe(user._id, ws); }
else if (event == 'updatefiles') { updateUserFiles(user, ws, domain); }
else {
// If updating guest device shares, if we are updating a user that is not creator of the share, remove the URL.
if (((event.action == 'deviceShareUpdate') && (Array.isArray(event.deviceShares))) || ((event.action == 'changenode') && (event.node != null) && ((event.node.rdp != null) || (event.node.ssh != null)))) {
event = common.Clone(event);
if ((event.action == 'deviceShareUpdate') && (Array.isArray(event.deviceShares))) {
for (var i in event.deviceShares) { if (event.deviceShares[i].userid != user._id) { delete event.deviceShares[i].url; } }
}
if ((event.action == 'changenode') && (event.node != null) && ((event.node.rdp != null) || (event.node.ssh != null))) {
// Clean up RDP & SSH credentials
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; }
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; }
}
}
// This is a MeshCentral Satellite message
if (event.action == 'satellite') { if ((obj.ws.satelliteFlags & event.satelliteFlags) != 0) { try { ws.send(JSON.stringify(event)); } catch (ex) { } return; } }
// Because of the device group "Show Self Events Only", we need to do more checks here.
if (id.startsWith('mesh/')) {
// 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.
var meshrights = parent.GetMeshRights(user, id);
if ((meshrights === MESHRIGHT_ADMIN) || ((meshrights & MESHRIGHT_LIMITEVENTS) == 0) || (ids.indexOf(user._id) >= 0)) {
// We have the device group rights to see this event or we are directly targetted by the event
try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { }
} else {
// Check if no other users are targeted by the event, if not, we can get this event.
var userTarget = false;
for (var i in ids) { if (ids[i].startsWith('user/')) { userTarget = true; } }
if (userTarget == false) { ws.send(JSON.stringify({ action: 'event', event: event })); }
}
} else if (event.ugrpid != null) {
if ((user.siteadmin & SITERIGHT_USERGROUPS) != 0) {
// If we have the rights to see users in a group, send the group as is.
try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { }
} else {
// We don't have the rights to see otehr users in the user group, remove the links that are not for ourselves.
var links = {};
if (event.links) { for (var i in event.links) { if ((i == user._id) || i.startsWith('mesh/') || i.startsWith('node/')) { links[i] = event.links[i]; } } }
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) { }
}
} else {
// This is not a device group event, we can get this event.
try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { }
}
}
} catch (ex) { console.log(ex); }
}
};
user.subscriptions = parent.subscribe(user._id, ws); // Subscribe to events
try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
// Send current server statistics
obj.SendServerStats = function () {
// Take a look at server stats
var os = require('os');
var stats = { action: 'serverstats', totalmem: os.totalmem(), freemem: os.freemem() };
try { stats.cpuavg = os.loadavg(); } catch (ex) { }
if (parent.parent.platform != 'win32') {
try { stats.availablemem = 1024 * Number(/MemAvailable:[ ]+(\d+)/.exec(fs.readFileSync('/proc/meminfo', 'utf8'))[1]); } catch (ex) { }
}
// Count the number of device groups that are not deleted
var activeDeviceGroups = 0;
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.
var serverStats = {
UserAccounts: Object.keys(parent.users).length,
DeviceGroups: activeDeviceGroups,
AgentSessions: Object.keys(parent.wsagents).length,
ConnectedUsers: Object.keys(parent.wssessions).length,
UsersSessions: Object.keys(parent.wssessions2).length,
RelaySessions: parent.relaySessionCount,
RelayCount: Object.keys(parent.wsrelays).length,
ConnectedIntelAMT: 0
};
if (parent.relaySessionErrorCount != 0) { serverStats.RelayErrors = parent.relaySessionErrorCount; }
if (parent.parent.mpsserver != null) {
serverStats.ConnectedIntelAMTCira = 0;
for (var i in parent.parent.mpsserver.ciraConnections) { serverStats.ConnectedIntelAMTCira += parent.parent.mpsserver.ciraConnections[i].length; }
}
for (var i in parent.parent.connectivityByNode) {
const node = parent.parent.connectivityByNode[i];
if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { serverStats.ConnectedIntelAMT++; }
}
// Take a look at agent errors
var agentstats = parent.getAgentStats();
var errorCounters = {}, errorCountersCount = 0;
if (agentstats.meshDoesNotExistCount > 0) { errorCountersCount++; errorCounters.UnknownGroup = agentstats.meshDoesNotExistCount; }
if (agentstats.invalidPkcsSignatureCount > 0) { errorCountersCount++; errorCounters.InvalidPKCSsignature = agentstats.invalidPkcsSignatureCount; }
if (agentstats.invalidRsaSignatureCount > 0) { errorCountersCount++; errorCounters.InvalidRSAsignature = agentstats.invalidRsaSignatureCount; }
if (agentstats.invalidJsonCount > 0) { errorCountersCount++; errorCounters.InvalidJSON = agentstats.invalidJsonCount; }
if (agentstats.unknownAgentActionCount > 0) { errorCountersCount++; errorCounters.UnknownAction = agentstats.unknownAgentActionCount; }
if (agentstats.agentBadWebCertHashCount > 0) { errorCountersCount++; errorCounters.BadWebCertificate = agentstats.agentBadWebCertHashCount; }
if ((agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count) > 0) { errorCountersCount++; errorCounters.BadSignature = (agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count); }
if (agentstats.agentMaxSessionHoldCount > 0) { errorCountersCount++; errorCounters.MaxSessionsReached = agentstats.agentMaxSessionHoldCount; }
if ((agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count) > 0) { errorCountersCount++; errorCounters.UnknownDeviceGroup = (agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count); }
if ((agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count) > 0) { errorCountersCount++; errorCounters.InvalidDeviceGroupType = (agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count); }
//if (agentstats.duplicateAgentCount > 0) { errorCountersCount++; errorCounters.DuplicateAgent = agentstats.duplicateAgentCount; }
// Send out the stats
stats.values = { ServerState: serverStats }
if (errorCountersCount > 0) { stats.values.AgentErrorCounters = errorCounters; }
try { ws.send(JSON.stringify(stats)); } catch (ex) { }
}
// When data is received from the web socket
ws.on('message', processWebSocketData);
// If error, do nothing
ws.on('error', function (err) { console.log(err); obj.close(0); });
// If the web socket is closed
ws.on('close', function (req) { obj.close(0); });
// Figure out the MPS port, use the alias if set
var mpsport = ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport);
var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
// Build server information object
const allFeatures = parent.getDomainUserFeatures(domain, user, req);
var serverinfo = {
domain: domain.id,
name: domain.dns ? domain.dns : parent.certificates.CommonName,
mpsname: parent.certificates.AmtMpsName,
mpsport: mpsport,
mpspass: args.mpspass,
port: httpport,
emailcheck: ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (args.lanonly != true) && (parent.certificates.CommonName != null) && (parent.certificates.CommonName.indexOf('.') != -1) && (user._id.split('/')[2].startsWith('~') == false)),
domainauth: (domain.auth == 'sspi'),
serverTime: Date.now(),
features: allFeatures.features,
features2: allFeatures.features2,
features3: allFeatures.features3
};
serverinfo.languages = parent.renderLanguages;
serverinfo.tlshash = Buffer.from(parent.webCertificateFullHashs[domain.id], 'binary').toString('hex').toUpperCase(); // SHA384 of server HTTPS certificate
serverinfo.agentCertHash = parent.agentCertificateHashBase64;
if (typeof domain.sessionrecording == 'object') {
if (domain.sessionrecording.onlyselectedusers === true) { serverinfo.usersSessionRecording = 1; } // Allow enabling of session recording for users
if (domain.sessionrecording.onlyselectedusergroups === true) { serverinfo.userGroupsSessionRecording = 1; } // Allow enabling of session recording for user groups
if (domain.sessionrecording.onlyselecteddevicegroups === true) { serverinfo.devGroupSessionRecording = 1; } // Allow enabling of session recording for device groups
}
if ((parent.parent.config.domains[domain.id].amtacmactivation != null) && (parent.parent.config.domains[domain.id].amtacmactivation.acmmatch != null)) {
var matchingDomains = [];
for (var i in parent.parent.config.domains[domain.id].amtacmactivation.acmmatch) {
var cn = parent.parent.config.domains[domain.id].amtacmactivation.acmmatch[i].cn;
if ((cn != '*') && (matchingDomains.indexOf(cn) == -1)) { matchingDomains.push(cn); }
}
if (matchingDomains.length > 0) { serverinfo.amtAcmFqdn = matchingDomains; }
}
if (typeof domain.devicemeshrouterlinks == 'object') { serverinfo.devicemeshrouterlinks = domain.devicemeshrouterlinks; }
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 }]; }
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 }); } } }
serverinfo.https = true;
serverinfo.redirport = args.redirport;
if (parent.parent.webpush != null) { serverinfo.vapidpublickey = parent.parent.webpush.vapidPublicKey; } // Web push public key
if (parent.parent.amtProvisioningServer != null) { serverinfo.amtProvServerMeshId = parent.parent.amtProvisioningServer.meshid; } // Device group that allows for bare-metal Intel AMT activation
if ((typeof domain.autoremoveinactivedevices == 'number') && (domain.autoremoveinactivedevices > 0)) { serverinfo.autoremoveinactivedevices = domain.autoremoveinactivedevices; } // Default number of days before inactive devices are removed
if (domain.passwordrequirements) {
if (domain.passwordrequirements.lock2factor == true) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed
if (typeof domain.passwordrequirements.maxfidokeys == 'number') { serverinfo.maxfidokeys = domain.passwordrequirements.maxfidokeys; }
}
if (parent.parent.msgserver != null) { // Setup messaging providers information
serverinfo.userMsgProviders = parent.parent.msgserver.providers;
if (parent.parent.msgserver.discordUrl != null) { serverinfo.discordUrl = parent.parent.msgserver.discordUrl; }
}
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
serverinfo.userMsgNftyUrl = parent.parent.config.messaging.ntfy.userurl;
}
// Build the mobile agent URL, this is used to connect mobile devices
var agentServerName = parent.getWebServerName(domain, req);
if (typeof parent.args.agentaliasdns == 'string') { agentServerName = parent.args.agentaliasdns; }
var xdomain = (domain.dns == null) ? domain.id : '';
var agentHttpsPort = ((parent.args.aliasport == null) ? parent.args.port : parent.args.aliasport); // Use HTTPS alias port is specified
if (parent.args.agentport != null) { agentHttpsPort = parent.args.agentport; } // If an agent only port is enabled, use that.
if (parent.args.agentaliasport != null) { agentHttpsPort = parent.args.agentaliasport; } // If an agent alias port is specified, use that.
serverinfo.magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '');
serverinfo.domainsuffix = xdomain;
if (domain.guestdevicesharing === false) { serverinfo.guestdevicesharing = false; } else {
if (typeof domain.guestdevicesharing == 'object') {
if (typeof domain.guestdevicesharing.maxsessiontime == 'number') { serverinfo.guestdevicesharingmaxtime = domain.guestdevicesharing.maxsessiontime; }
}
}
if (typeof domain.userconsentflags == 'number') { serverinfo.consent = domain.userconsentflags; }
if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) {serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); }
if (typeof domain.logoutonidlesessiontimeout == 'boolean') {
serverinfo.logoutonidlesessiontimeout = domain.logoutonidlesessiontimeout;
} else {
// Default
serverinfo.logoutonidlesessiontimeout = true;
}
if (user.siteadmin === SITERIGHT_ADMIN) {
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; }
if (obj.crossDomain === true) { serverinfo.crossDomain = []; for (var i in parent.parent.config.domains) { serverinfo.crossDomain.push(i); } }
if (typeof parent.webCertificateExpire[domain.id] == 'number') { serverinfo.certExpire = parent.webCertificateExpire[domain.id]; }
}
if (typeof domain.terminal == 'object') { // Settings used for remote terminal feature
if ((typeof domain.terminal.linuxshell == 'string') && (domain.terminal.linuxshell != 'any')) { serverinfo.linuxshell = domain.terminal.linuxshell; }
}
if (Array.isArray(domain.preconfiguredremoteinput)) { serverinfo.preConfiguredRemoteInput = domain.preconfiguredremoteinput; }
if (Array.isArray(domain.preconfiguredscripts)) {
const r = [];
for (var i in domain.preconfiguredscripts) {
const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent
const script = domain.preconfiguredscripts[i];
if ((typeof script.name == 'string') && (script.name.length <= 32) && (typeof script.type == 'string') && ((typeof script.file == 'string') || (typeof script.cmd == 'string'))) {
const s = { name: script.name, type: types.indexOf(script.type.toLowerCase()) };
if (s.type > 0) { r.push(s); }
}
}
serverinfo.preConfiguredScripts = r;
}
if (domain.maxdeviceview != null) { serverinfo.maxdeviceview = domain.maxdeviceview; } // Maximum number of devices a user can view at any given time
// Send server information
try { ws.send(JSON.stringify({ action: 'serverinfo', serverinfo: serverinfo })); } catch (ex) { }
// Send user information to web socket, this is the first thing we send
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: parent.CloneSafeUser(parent.users[user._id]) })); } catch (ex) { }
if (user.siteadmin === SITERIGHT_ADMIN) {
// Check if tracing is allowed for this domain
if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.trace === true))) {
// Send server tracing information
try { ws.send(JSON.stringify({ action: 'traceinfo', traceSources: parent.parent.debugRemoteSources })); } catch (ex) { }
}
// Send any server warnings if any
var serverWarnings = parent.parent.getServerWarnings();
if (serverWarnings.length > 0) { try { ws.send(JSON.stringify({ action: 'serverwarnings', warnings: serverWarnings })); } catch (ex) { } }
}
// See how many times bad login attempts where made since the last login
const lastLoginTime = parent.users[user._id].pastlogin;
if (lastLoginTime != null) {
db.GetFailedLoginCount(user._id, user.domain, new Date(lastLoginTime * 1000), function (count) {
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; }
});
}
// If we are site administrator and Google Drive backup is setup, send out the status.
if ((user.siteadmin === SITERIGHT_ADMIN) && (domain.id == '') && (typeof parent.parent.config.settings.autobackup == 'object') && (typeof parent.parent.config.settings.autobackup.googledrive == 'object')) {
db.Get('GoogleDriveBackup', function (err, docs) {
if (err != null) return;
if (docs.length == 0) { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 1 })); } catch (ex) { } }
else { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: docs[0].state })); } catch (ex) { } }
});
}
// We are all set, start receiving data
ws._socket.resume();
if (parent.parent.pluginHandler != null) parent.parent.pluginHandler.callHook('hook_userLoggedIn', user);
});
} catch (ex) { console.log(ex); }
// Commands still accepted while force2factor requires this account to set up a second factor: account security plus what the pages need to stay connected
const force2factorCommands = ['ping', 'pong', 'close', 'urlargs', 'userWebState', 'changelang', 'previousLogins', 'changepassword', 'changeemail', 'verifyemail', 'verifyPhone', 'confirmPhone', 'removePhone', 'verifyMessaging', 'confirmMessaging', 'removeMessaging', 'otpemail', 'otpduo', 'otpauth-request', 'otpauth-setup', 'otpauth-clear', 'otpauth-getpasswords', 'otp-hkey-get', 'otp-hkey-remove', 'otp-hkey-yubikey-add', 'otpdev-clear', 'otpdev-set', 'webauthn-startregister', 'webauthn-endregister'];
// Process incoming web socket data from the browser
function processWebSocketData(msg) {
var command, i = 0, mesh = null, meshid = null, nodeid = null, meshlinks = null, change = 0;
try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
if (command == null) return;
if (common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
// Until the account has a second factor under force2factor, only the account security commands get through
if ((force2factorCommands.indexOf(command.action) == -1) && parent.force2factorPending(domain, user, req)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: command.action, responseid: command.responseid, result: 'Two-factor authentication required' })); } catch (ex) { } }
return;
}
var commandHandler = serverCommands[command.action];
if (commandHandler != null) {
try { commandHandler(command); return; }
catch (e) {
console.log('Unhandled error while processing ' + command.action + ' for user ' + user.name + ':\n' + e);
parent.parent.logError(e.stack); return; // todo: remove returns when switch is gone
}
} else { }
// console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
// pass through to switch statement until refactoring complete
switch (command.action) {
case 'nodes':
{
// If in paging mode, look to set the skip and limit values
if (domain.maxdeviceview != null) {
if ((typeof command.skip == 'number') && (command.skip >= 0)) { obj.deviceSkip = command.skip; }
if ((typeof command.limit == 'number') && (command.limit > 0)) { obj.deviceLimit = command.limit; }
if (obj.deviceLimit > domain.maxdeviceview) { obj.deviceLimit = domain.maxdeviceview; }
// Device name filter, applied by the database before paging so it searches all devices, not only the current page.
// The filter stays set for the following requests (page changes, refreshes) until the client changes it.
if (typeof command.filter == 'string') {
var newFilter = command.filter.trim().toLowerCase().substring(0, 128);
if (newFilter == '') { newFilter = null; }
if (newFilter != obj.deviceFilter) { obj.deviceFilter = newFilter; if (typeof command.skip != 'number') { obj.deviceSkip = 0; } }
}
}
var links = [], extraids = null, err = null;
// Resolve the device group name if needed
if ((typeof command.meshname == 'string') && (command.meshid == null)) {
for (var i in parent.meshes) {
var m = parent.meshes[i];
if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
}
}
if (command.meshid == null) { err = 'Invalid group id'; }
}
// Check if command.id is set with a domain id, if not, add the domain id to it.
if (typeof command.id == 'string' && command.id !== '') {
if (common.validateString(command.id, 1, 1024) == false) { err = 'Invalid device identifier'; }
else if (command.id.indexOf('/') == -1) { command.id = 'node/' + domain.id + '/' + command.id; }
}
if (err == null) {
try {
if (command.meshid == null) {
// Request a list of all meshes this user as rights to
links = parent.GetAllMeshIdWithRights(user);
// Add any nodes with direct rights or any nodes with user group direct rights
extraids = getUserExtraIds();
} else {
// Request list of all nodes for one specific meshid
meshid = command.meshid;
if (common.validateString(meshid, 0, 128) == false) { err = 'Invalid group id'; } else {
if (meshid.split('/').length == 1) { meshid = 'mesh/' + domain.id + '/' + command.meshid; }
if (parent.IsMeshViewable(user, meshid)) { links.push(meshid); } else { err = 'Invalid group id'; }
}
}
} catch (ex) { err = 'Validation exception: ' + ex; }
}
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'nodes', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Request a list of all nodes
db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, obj.deviceSkip, obj.deviceLimit, obj.deviceFilter, function (err, docs) {
//console.log(err, docs, links, extraids, domain.id, 'node', command.id);
if (docs == null) { docs = []; }
parent.common.unEscapeAllLinksFieldName(docs);
var r = {}, nodeCount = docs.length;
if (domain.maxdeviceview != null) { obj.visibleDevices = {}; }
for (i in docs) {
// Check device links, if a link points to an unknown user, remove it.
parent.cleanDevice(docs[i]); // TODO: This will make the total device count incorrect and will affect device paging.
// If we are paging, add the device to the page here
if (domain.maxdeviceview != null) { obj.visibleDevices[docs[i]._id] = 1; }
// Remove any connectivity and power state information, that should not be in the database anyway.
// TODO: Find why these are sometimes saved in the db.
if (docs[i].conn != null) { delete docs[i].conn; }
if (docs[i].pwr != null) { delete docs[i].pwr; }
if (docs[i].agct != null) { delete docs[i].agct; }
if (docs[i].cict != null) { delete docs[i].cict; }
// Add the connection state
var state = parent.parent.GetConnectivityState(docs[i]._id);
if (state) {
docs[i].conn = state.connectivity;
docs[i].pwr = state.powerState;
if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[docs[i]._id]; if (agent != null) { docs[i].agct = agent.connectTime; } }
// Use the connection time of the CIRA/Relay connection
if ((state.connectivity & 2) != 0) {
var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(docs[i]._id, null, true);
if ((ciraConnection != null) && (ciraConnection.tag != null)) { docs[i].cict = ciraConnection.tag.connectTime; }
}
}
// Compress the meshid's
meshid = docs[i].meshid;
if (!r[meshid]) { r[meshid] = []; }
delete docs[i].meshid;
// Remove push messaging token if present
if (docs[i].pmt != null) { docs[i].pmt = 1; }
// Remove SSH credentials if present
if (docs[i].ssh != null) {
if ((docs[i].ssh[user._id] != null) && (docs[i].ssh[user._id].u)) {
if (docs[i].ssh.k && docs[i].ssh[user._id].kp) { docs[i].ssh = 2; } // Username, key and password
else if (docs[i].ssh[user._id].k) { docs[i].ssh = 3; } // Username and key. No password.
else if (docs[i].ssh[user._id].p) { docs[i].ssh = 1; } // Username and password
else { delete docs[i].ssh; }
} else {
delete docs[i].ssh;
}
}
// Remove RDP credentials if present, only set to 1 if our userid has RDP credentials
if ((docs[i].rdp != null) && (docs[i].rdp[user._id] != null)) { docs[i].rdp = 1; } else { delete docs[i].rdp; }
// Remove Intel AMT credential if present
if (docs[i].intelamt != null) {
if (docs[i].intelamt.pass != null) { docs[i].intelamt.pass = 1; }
if (docs[i].intelamt.mpspass != null) { docs[i].intelamt.mpspass = 1; }
}
// If GeoLocation not enabled, remove any node location information
if (domain.geolocation != true) {
if (docs[i].iploc != null) { delete docs[i].iploc; }
if (docs[i].wifiloc != null) { delete docs[i].wifiloc; }
if (docs[i].gpsloc != null) { delete docs[i].gpsloc; }
if (docs[i].userloc != null) { delete docs[i].userloc; }
}
// Add device sessions
const xagent = parent.wsagents[docs[i]._id];
if ((xagent != null) && (xagent.sessions != null)) { docs[i].sessions = xagent.sessions; }
// Add IP-KVM sessions
if (parent.parent.ipKvmManager != null) {
const xipkvmport = parent.parent.ipKvmManager.managedPorts[docs[i]._id];
if ((xipkvmport != null) && (xipkvmport.sessions != null)) { docs[i].sessions = xipkvmport.sessions; }
}
// Patch node links with names, like meshes links with names
for (var a in docs[i].links) {
if (!docs[i].links[a].name) {
if (parent.users[a] && parent.users[a].realname) { docs[i].links[a].name = parent.users[a].realname; }
else if (parent.users[a] && parent.users[a].name) { docs[i].links[a].name = parent.users[a].name; }
}
}
r[meshid].push(docs[i]);
}
const response = { action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag };
if (domain.maxdeviceview != null) {
// If in paging mode, report back the skip and limit values
response.skip = obj.deviceSkip;
response.limit = obj.deviceLimit;
if (obj.deviceFilter != null) { response.filter = obj.deviceFilter; }
// Add total device count
// Only set response.totalcount if we need to be in paging mode
if (nodeCount < response.limit) {
if (obj.deviceSkip > 0) { response.totalcount = obj.deviceSkip + nodeCount; } else { obj.visibleDevices = null; }
try { ws.send(JSON.stringify(response)); } catch (ex) { }
} else {
// Ask the database for the total device count
if (db.CountAllTypeNoTypeFieldMeshFiltered) {
db.CountAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, obj.deviceFilter, function (err, count) {
if ((err != null) || (typeof count != 'number') || ((obj.deviceSkip == 0) && (count < obj.deviceLimit))) {
obj.visibleDevices = null;
} else {
response.totalcount = count;
}
try { ws.send(JSON.stringify(response)); } catch (ex) { }
});
} else {
// The database does not support device counting
obj.visibleDevices = null; // We are not in paging mode
try { ws.send(JSON.stringify(response)); } catch (ex) { }
}
}
} else {
obj.visibleDevices = null; // We are not in paging mode
try { ws.send(JSON.stringify(response)); } catch (ex) { }
}
});
break;
}
case 'fileoperation':
{
// Check permissions
if ((user.siteadmin & 8) != 0) {
// Perform a file operation (Create Folder, Delete Folder, Delete File...)
if (common.validateString(command.fileop, 3, 16) == false) return;
var sendUpdate = true, path = meshPathToRealPath(command.path, user); // This will also check access rights
if (path == null) break;
if ((command.fileop == 'createfolder') && (common.IsFilenameValid(command.newfolder) == true)) {
// Create a new folder
try { fs.mkdirSync(parent.path.join(path, command.newfolder)); } catch (ex) {
try { fs.mkdirSync(path); } catch (ex) { }
try { fs.mkdirSync(parent.path.join(path, command.newfolder)); } catch (ex) { }
}
}
else if (command.fileop == 'delete') {
// Delete a file
if (common.validateArray(command.delfiles, 1) == false) return;
for (i in command.delfiles) {
if (common.IsFilenameValid(command.delfiles[i]) == true) {
var fullpath = parent.path.join(path, command.delfiles[i]);
if (command.rec == true) {
try { deleteFolderRecursive(fullpath); } catch (ex) { } // TODO, make this an async function
} else {
try { fs.rmdirSync(fullpath); } catch (ex) { try { fs.unlinkSync(fullpath); } catch (xe) { } }
}
}
}
// If we deleted something in the mesh root folder and the entire mesh folder is empty, remove it.
if (command.path.length == 1) {
try {
if (command.path[0].startsWith('mesh//')) {
path = meshPathToRealPath([command.path[0]], user);
fs.readdir(path, function (err, dir) { if ((err == null) && (dir.length == 0)) { fs.rmdir(path, function (err) { }); } });
}
} catch (ex) { }
}
}
else if ((command.fileop == 'rename') && (common.IsFilenameValid(command.oldname) === true) && (common.IsFilenameValid(command.newname) === true)) {
// Rename
try { fs.renameSync(parent.path.join(path, command.oldname), parent.path.join(path, command.newname)); } catch (e) { }
}
else if ((command.fileop == 'copy') || (command.fileop == 'move')) {
// Copy or move of one or many files
if (common.validateArray(command.names, 1) == false) return;
var scpath = meshPathToRealPath(command.scpath, user); // This will also check access rights
if (scpath == null) break;
// TODO: Check quota if this is a copy
for (i in command.names) {
if (common.IsFilenameValid(command.names[i]) === true) {
var s = parent.path.join(scpath, command.names[i]), d = parent.path.join(path, command.names[i]);
sendUpdate = false;
try { fs.mkdirSync(path); } catch (ex) { } // try to create folder first incase folder is missing
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));
}
}
} else if (command.fileop == 'get') {
// Get a short file and send it back on the web socket
if (common.validateString(command.file, 1, 4096) == false) return;
const scpath = meshPathToRealPath(command.path, user); // This will also check access rights
if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break;
const filePath = parent.path.join(scpath, command.file);
fs.stat(filePath, function (err, stat) {
if ((err != null) || (stat == null) || (stat.size >= 204800)) return;
fs.readFile(filePath, function (err, data) {
if ((err != null) || (data == null)) return;
command.data = data.toString('base64');
ws.send(JSON.stringify(command)); // Send the file data back, base64 encoded.
});
});
} else if (command.fileop == 'set') {
// Set a short file transfered on the web socket
if (common.validateString(command.file, 1, 4096) == false) return;
if (typeof command.data != 'string') return;
const scpath = meshPathToRealPath(command.path, user); // This will also check access rights
if ((scpath == null) || (command.file !== parent.path.basename(command.file))) break;
const filePath = parent.path.join(scpath, command.file);
var data = null;
try { data = Buffer.from(command.data, 'base64'); } catch (ex) { return; }
fs.writeFile(filePath, data, function (err) { if (err == null) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } });
}
if (sendUpdate == true) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } // Fire an event causing this user to update this files
}
break;
}
case 'software': {
// The inventory needs the software right, an uninstall needs full administrator rights on the device, same as the web UI.
var swfunc = null;
if (command.responseid != null) { swfunc = function (r) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: r ? 'OK' : 'Denied' })); } catch (ex) { } }; }
if (common.validateString(command.nodeid, 1, 1024) == false) { if (swfunc) { swfunc(false); } break; }
if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
var allowed = false;
if ((command.type == 'installedapps') || (command.type == 'installedstoreapps')) { allowed = ((rights == MESHRIGHT_ADMIN) || ((rights & MESHRIGHT_NOSOFTWARE) == 0)); }
else if ((command.type == 'uninstallapp') || (command.type == 'uninstallstoreapp')) { allowed = (rights == MESHRIGHT_ADMIN); }
if ((node == null) || (allowed == false)) { if (swfunc) { swfunc(false); } return; }
routeCommandToNode(command, null, null, swfunc);
});
break;
}
case 'msg':
{
// Check the nodeid
if (common.validateString(command.nodeid, 1, 1024) == false) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'msg', result: 'Unable to route', tag: command.tag, responseid: command.responseid })); } catch (ex) { } }
return;
}
// Rights check
var requiredRights = null, requiredNonRights = null, routingOptions = null;
// Complete the nodeid if needed
if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
// Check if getting / setting clipboard data is allowed
if ((command.type == 'getclip') && (domain.clipboardget == false)) { console.log('CG-EXIT'); break; }
if ((command.type == 'setclip') && (domain.clipboardset == false)) { console.log('CS-EXIT'); break; }
// Rights needed on top of the remote control or view only rights that routeCommandToNode() checks
switch (command.type) {
case 'console': { requiredRights = MESHRIGHT_AGENTCONSOLE; break; }
case 'messagebox':
case 'alertbox': { requiredRights = MESHRIGHT_CHATNOTIFY; break; }
case 'pskill':
case 'serviceStart':
case 'serviceStop':
case 'serviceRestart':
case 'setclip':
case 'openUrl':
case 'deskBackground': { requiredRights = MESHRIGHT_REMOTECONTROL; requiredNonRights = MESHRIGHT_REMOTEVIEWONLY; break; } // View only users can look, but not change anything
}
// Before routing this command, let's do some security checking.
// If this is a tunnel request, we need to make sure the NodeID in the URL matches the NodeID in the command.
if (command.type == 'tunnel') {
if ((typeof command.value != 'string') || (typeof command.nodeid != 'string')) break;
var url = null;
try { url = new URL(command.value, 'http://localhost'); } catch (ex) { }
if (url == null) break; // Bad URL
if (url.searchParams.get('nodeid') && (url.searchParams.get('nodeid') != command.nodeid)) break; // Bad NodeID in URL query string
// Check rights
if (url.searchParams.get('p') == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; }
else if (url.searchParams.get('p') == '4') { requiredNonRights = MESHRIGHT_NOREGISTRY; }
else if ((url.searchParams.get('p') == '5') || (url.searchParams.get('p') == '10')) { requiredNonRights = MESHRIGHT_NOFILES; }
else if (url.searchParams.get('p') == '6') { requiredNonRights = MESHRIGHT_NOSOFTWARE; }
// If we are using the desktop multiplexor, remove the VIEWONLY limitation. The multiplexor will take care of enforcing that limitation when needed.
if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.searchParams.get('p') == '2')) { routingOptions = { removeViewOnlyLimitation: true }; }
// Add server TLS cert hash
var tlsCertHash = null;
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.
tlsCertHash = parent.webCertificateFullHashs[domain.id];
if (tlsCertHash != null) { command.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
}
// Add user consent messages
command.soptions = {};
if (typeof domain.consentmessages == 'object') {
if (typeof domain.consentmessages.title == 'string') { command.soptions.consentTitle = domain.consentmessages.title; }
if (typeof domain.consentmessages.desktop == 'string') { command.soptions.consentMsgDesktop = domain.consentmessages.desktop; }
if (typeof domain.consentmessages.terminal == 'string') { command.soptions.consentMsgTerminal = domain.consentmessages.terminal; }
if (typeof domain.consentmessages.files == 'string') { command.soptions.consentMsgFiles = domain.consentmessages.files; }
if ((typeof domain.consentmessages.consenttimeout == 'number') && (domain.consentmessages.consenttimeout > 0)) { command.soptions.consentTimeout = domain.consentmessages.consenttimeout; }
if (domain.consentmessages.autoacceptontimeout === true) { command.soptions.consentAutoAccept = true; }
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
if (domain.consentmessages.autoacceptiffilelocked === true) { command.soptions.consentAutoAcceptIfFileLocked = true; }
if (domain.consentmessages.oldstyle === true) { command.soptions.oldStyle = true; }
}
if (typeof domain.notificationmessages == 'object') {
if (typeof domain.notificationmessages.title == 'string') { command.soptions.notifyTitle = domain.notificationmessages.title; }
if (typeof domain.notificationmessages.desktop == 'string') { command.soptions.notifyMsgDesktop = domain.notificationmessages.desktop; }
if (typeof domain.notificationmessages.terminal == 'string') { command.soptions.notifyMsgTerminal = domain.notificationmessages.terminal; }
if (typeof domain.notificationmessages.files == 'string') { command.soptions.notifyMsgFiles = domain.notificationmessages.files; }
}
if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; }
// Add userid
command.userid = user._id;
// Add tunnel pre-message deflate
if (typeof parent.parent.config.settings.agentwscompression == 'boolean') { command.perMessageDeflate = parent.parent.config.settings.agentwscompression; }
}
// If a response is needed, set a callback function
var func = null;
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) { } } }
// Route this command to a target node
routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions);
break;
}
case 'events':
{
// User filtered events
if ((command.userid != null) && ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0)) {
if (typeof command.userid != 'string') return;
const userSplit = command.userid.split('/');
if ((userSplit.length != 3) || (userSplit[1] != domain.id)) return;
// TODO: Add the meshes command.userid has access to (???)
var filter = [command.userid];
var actionfilter = null;
if (command.filter != null) {
if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter;
}
if ((command.limit == null) || (typeof command.limit != 'number')) {
// Send the list of all events for this session
db.GetUserEvents(filter, domain.id, command.userid, actionfilter, function (err, docs) {
if (err != null) return;
try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { }
});
} else {
// Send the list of most recent events for this session, up to 'limit' count
db.GetUserEventsWithLimit(filter, domain.id, command.userid, command.limit, actionfilter, function (err, docs) {
if (err != null) return;
try { ws.send(JSON.stringify({ action: 'events', events: docs, userid: command.userid, tag: command.tag })); } catch (ex) { }
});
}
} else if (command.nodeid != null) { // Device filtered events
// Check that the user has access to this nodeid
if (typeof command.nodeid != 'string') return;
const nodeSplit = command.nodeid.split('/');
if (nodeSplit.length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if (node == null) { try { ws.send(JSON.stringify({ action: 'events', events: [], nodeid: command.nodeid, tag: command.tag })); } catch (ex) { } return; }
// Put a limit on the number of returned entries if present
var limit = 10000;
if (common.validateInt(command.limit, 1, 1000000) == true) { limit = command.limit; }
var filter = null;
if (command.filter != null) {
if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) filter = command.filter;
}
if (((rights & MESHRIGHT_LIMITEVENTS) != 0) && (rights != MESHRIGHT_ADMIN)) {
// Send the list of most recent events for this nodeid that only apply to us, up to 'limit' count
db.GetNodeEventsSelfWithLimit(node._id, domain.id, user._id, limit, filter, function (err, docs) {
if (err != null) return;
try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { }
});
} else {
// Send the list of most recent events for this nodeid, up to 'limit' count
db.GetNodeEventsWithLimit(node._id, domain.id, limit, filter, function (err, docs) {
if (err != null) return;
try { ws.send(JSON.stringify({ action: 'events', events: docs, nodeid: node._id, tag: command.tag })); } catch (ex) { }
});
}
});
} else {
// Create a filter for device groups
if ((obj.user == null) || (obj.user.links == null)) return;
// All events
var exGroupFilter2 = [], filter = [], filter2 = user.subscriptions;
// Add all meshes for groups this user is part of
// TODO (UserGroups)
// Remove MeshID's that we do not have rights to see events for
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); } }
for (var i in filter2) { if (exGroupFilter2.indexOf(filter2[i]) == -1) { filter.push(filter2[i]); } }
var actionfilter = null;
if (command.filter != null) {
if (['agentlog','batchupload','changenode','manual','relaylog','removenode','runcommands'].includes(command.filter)) actionfilter = command.filter;
}
if ((command.limit == null) || (typeof command.limit != 'number')) {
// Send the list of all events for this session
db.GetEvents(filter, domain.id, actionfilter, function (err, docs) {
if (err != null) return;
try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { }
});
} else {
// Send the list of most recent events for this session, up to 'limit' count
db.GetEventsWithLimit(filter, domain.id, command.limit, actionfilter, function (err, docs) {
if (err != null) return;
try { ws.send(JSON.stringify({ action: 'events', events: docs, user: command.user, tag: command.tag })); } catch (ex) { }
});
}
}
break;
}
case 'recordings': {
if (((user.siteadmin & SITERIGHT_RECORDINGS) == 0) || (domain.sessionrecording == null)) return; // Check if recordings is enabled and we have rights to do this.
var recordingsPath = null;
if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.parent.recordpath; }
if (recordingsPath == null) return;
fs.readdir(recordingsPath, function (err, files) {
if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 1, tag: command.tag })); } catch (ex) { } return; }
if ((command.limit == null) || (typeof command.limit != 'number')) {
// Send the list of all recordings
db.GetEvents(['recording'], domain.id, null, function (err, docs) {
if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; }
for (var i in docs) {
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.
if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
}
try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
});
} else {
// Send the list of most recent recordings, up to 'limit' count
db.GetEventsWithLimit(['recording'], domain.id, command.limit, null, function (err, docs) {
if (err != null) { try { ws.send(JSON.stringify({ action: 'recordings', error: 2, tag: command.tag })); } catch (ex) { } return; }
for (var i in docs) {
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.
if (files.indexOf(docs[i].filename) >= 0) { docs[i].present = 1; }
}
try { ws.send(JSON.stringify({ action: 'recordings', events: docs, tag: command.tag })); } catch (ex) { }
});
}
});
break;
}
case 'wssessioncount':
{
// Request a list of all web socket user session count
var wssessions = {};
if ((user.siteadmin & 2) == 0) { try { ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: {}, tag: command.tag })); } catch (ex) { } break; }
if (parent.parent.multiServer == null) {
// No peering, use simple session counting
for (i in parent.wssessions) {
if ((obj.crossDomain === true) || (parent.wssessions[i][0].domainid == domain.id)) {
if ((user.groups == null) || (user.groups.length == 0)) {
// No user groups, count everything
wssessions[i] = parent.wssessions[i].length;
} else {
// Only count if session is for a user in our user groups
var sessionUser = parent.users[parent.wssessions[i][0].userid];
if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
wssessions[i] = parent.wssessions[i].length;
}
}
}
}
} else {
// We have peer servers, use more complex session counting
for (i in parent.sessionsCount) {
if ((obj.crossDomain === true) || (i.split('/')[1] == domain.id)) {
if ((user.groups == null) || (user.groups.length == 0)) {
// No user groups, count everything
wssessions[i] = parent.sessionsCount[i];
} else {
// Only count if session is for a user in our user groups
var sessionUser = parent.users[i];
if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
wssessions[i] = parent.sessionsCount[i];
}
}
}
}
}
try { ws.send(JSON.stringify({ action: 'wssessioncount', wssessions: wssessions, tag: command.tag })); } catch (ex) { } // wssessions is: userid --> count
break;
}
case 'deleteuser':
{
// Delete a user account
var err = null, delusersplit, deluserid, deluser, deluserdomain;
try {
if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
else {
if (command.userid.indexOf('/') < 0) { command.userid = 'user/' + domain.id + '/' + command.userid; }
delusersplit = command.userid.split('/');
deluserid = command.userid;
deluser = parent.users[deluserid];
if (deluser == null) { err = 'User does not exists'; }
else if ((obj.crossDomain !== true) && ((delusersplit.length != 3) || (delusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
else if ((deluser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Permission denied'; } // Need full admin to remote another administrator
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.
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Get domain
deluserdomain = domain;
if (obj.crossDomain === true) { deluserdomain = parent.parent.config.domains[delusersplit[1]]; }
if (deluserdomain == null) { err = 'Invalid domain'; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Remove all links to this user
if (deluser.links != null) {
for (var i in deluser.links) {
if (i.startsWith('mesh/')) {
// Get the device group
mesh = parent.meshes[i];
if (mesh) {
// Remove user from the mesh
if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
// Notify mesh change
change = 'Removed user ' + deluser.name + ' from device group ' + mesh.name;
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [deluser._id, user._id]), obj, event);
}
} else if (i.startsWith('node/')) {
// Get the node and the rights for this node
parent.GetNodeWithRights(deluserdomain, deluser, i, function (node, rights, visible) {
if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
// Remove the link and save the node to the database
delete node.links[deluser._id];
if (Object.keys(node.links).length == 0) { delete node.links; }
db.Set(parent.cleanDevice(node));
// Event the node change
var event;
if (command.rights == 0) {
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) }
} else {
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) }
}
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
});
} else if (i.startsWith('ugrp/')) {
// Get the device group
var ugroup = parent.userGroups[i];
if (ugroup) {
// Remove user from the user group
if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
// Notify user group change
change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
}
}
}
}
db.Remove('ws' + deluser._id); // Remove user web state
db.Remove('nt' + deluser._id); // Remove notes for this user
db.Remove('ntp' + deluser._id); // Remove personal notes for this user
db.Remove('im' + deluser._id); // Remove image for this user
// Delete any login tokens
parent.parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.parent.db.Remove(docs[i]._id, function () { }); } }
});
// Delete all files on the server for this account
try {
var deluserpath = parent.getServerRootFilePath(deluser);
if (deluserpath != null) { parent.deleteFolderRec(deluserpath); }
} catch (e) { }
db.Remove(deluserid);
delete parent.users[deluserid];
var targets = ['*', 'server-users'];
if (deluser.groups) { for (var i in deluser.groups) { targets.push('server-users:' + i); } }
parent.parent.DispatchEvent(targets, obj, { etype: 'user', userid: deluserid, username: deluser.name, action: 'accountremove', msgid: 63, msg: 'Account removed', domain: deluserdomain.id });
parent.parent.DispatchEvent([deluserid], obj, 'close');
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted user account ' + deluser.name); }
break;
}
case 'userbroadcast':
{
var err = null;
try {
// Broadcast a message to all currently connected users.
if ((user.siteadmin & 2) == 0) { err = "Permission denied"; }
else if (common.validateString(command.msg, 1, 512) == false) { err = "Message is too long"; } // Notification message is between 1 and 256 characters
} catch (ex) { err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Create the notification message
var notification = { action: 'msg', type: 'notify', domain: domain.id, value: command.msg, title: user.name, icon: 0, tag: 'broadcast', id: Math.random() };
if ((typeof command.maxtime == 'number') && (command.maxtime > 0)) { notification.maxtime = command.maxtime; }
// Send the notification on all user sessions for this server
for (var i in parent.wssessions2) {
try {
if (parent.wssessions2[i].domainid == domain.id) {
var sessionUser = parent.users[parent.wssessions2[i].userid];
if ((command.userid != null) && (command.userid != sessionUser._id) && (command.userid != sessionUser._id.split('/')[2])) { continue; }
if ((command.target == null) || ((sessionUser.links) != null && (sessionUser.links[command.target] != null))) {
if ((user.groups == null) || (user.groups.length == 0)) {
// We are part of no user groups, send to everyone.
parent.wssessions2[i].send(JSON.stringify(notification));
} else {
// We are part of user groups, only send to sessions of users in our groups.
if ((sessionUser != null) && findOne(sessionUser.groups, user.groups)) {
parent.wssessions2[i].send(JSON.stringify(notification));
}
}
}
}
} catch (ex) { }
}
// TODO: Notify all sessions on other peers.
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'edituser':
{
// Must be user administrator or edit self.
if (((user.siteadmin & 2) == 0) && (user._id != command.id)) break;
// User the username as userid if needed
if ((typeof command.username == 'string') && (command.userid == null)) { command.userid = command.username; }
if ((typeof command.id == 'string') && (command.userid == null)) { command.userid = command.id; }
// Edit a user account
var err = null, editusersplit, edituserid, edituser, edituserdomain;
try {
if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
else {
if (command.userid.indexOf('/') < 0) { command.userid = 'user/' + domain.id + '/' + command.userid; }
editusersplit = command.userid.split('/');
edituserid = command.userid;
edituser = parent.users[edituserid];
if (edituser == null) { err = 'User does not exists'; }
else if ((obj.crossDomain !== true) && ((editusersplit.length != 3) || (editusersplit[1] != domain.id))) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
else if ((edituser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Permission denied'; } // Need full admin to remote another administrator
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.
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) {
try { ws.send(JSON.stringify({ action: 'edituser', responseid: command.responseid, result: err })); } catch (ex) { }
}
break;
}
// Edit a user account, may involve changing email or administrator permissions
var chguser = parent.users[edituserid];
change = 0;
if (chguser) {
// If the target user is admin and we are not admin, no changes can be made.
if ((chguser.siteadmin === SITERIGHT_ADMIN) && (user.siteadmin != SITERIGHT_ADMIN)) return;
// Can only perform this operation on other users of our group.
if (user.siteadmin != SITERIGHT_ADMIN) {
if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) return;
}
// Fetch and validate the user domain
var edituserdomainid = edituserid.split('/')[1];
if ((obj.crossDomain !== true) && (edituserdomainid != domain.id)) break;
var edituserdomain = parent.parent.config.domains[edituserdomainid];
if (edituserdomain == null) break;
// Validate and change email
if (edituserdomain.usernameisemail !== true) {
if (common.validateString(command.email, 0, 1024) && (chguser.email != command.email)) {
if (command.email == '') { command.emailVerified = false; delete chguser.email; } else { chguser.email = command.email.toLowerCase(); }
change = 1;
}
}
// Validate and change real name
if (common.validateString(command.realname, 0, 256) && (chguser.realname != command.realname)) {
if (command.realname == '') { delete chguser.realname; } else { chguser.realname = command.realname; }
change = 1;
}
// Make changes
if ((command.emailVerified === true || command.emailVerified === false) && (chguser.emailVerified != command.emailVerified)) { chguser.emailVerified = command.emailVerified; change = 1; }
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; }
if (command.resetNextLogin === true) { chguser.passchange = -1; }
if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { chguser.consent = command.consent; } change = 1; }
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; }
if ((command.msghandle != null) && (typeof command.msghandle == 'string')) {
if (command.msghandle.startsWith('callmebot:http')) { const h = parent.parent.msgserver.callmebotUrlToHandle(command.msghandle.substring(10)); if (h) { command.msghandle = h; } else { command.msghandle = ''; } }
if (command.msghandle == '') { delete chguser.msghandle; } else { chguser.msghandle = command.msghandle; }
change = 1;
}
if ((command.flags != null) && (typeof command.flags == 'number')) {
// Flags: 1 = Account Image, 2 = Session Recording
if ((command.flags == 0) && (chguser.flags != null)) { delete chguser.flags; change = 1; } else { if (command.flags !== chguser.flags) { chguser.flags = command.flags; change = 1; } }
}
if ((command.removeRights != null) && (typeof command.removeRights == 'number')) {
if (command.removeRights == 0) {
if (chguser.removeRights != null) { delete chguser.removeRights; change = 1; }
} else {
if (command.removeRights !== chguser.removeRights) { chguser.removeRights = command.removeRights; change = 1; }
}
}
// Site admins can change any server rights, user managers can only change AccountLock, NoMeshCmd and NoNewGroups
if (common.validateInt(command.siteadmin) && (chguser._id !== user._id) && (chguser.siteadmin != command.siteadmin)) { // We can't change our own siteadmin permissions.
var chgusersiteadmin = chguser.siteadmin ? chguser.siteadmin : 0;
if (user.siteadmin === SITERIGHT_ADMIN) { chguser.siteadmin = command.siteadmin; change = 1; }
else if (user.siteadmin & 2) {
var mask = 0xFFFFFF1D; // Mask: 2 (User Mangement) + 32 (Account locked) + 64 (No New Groups) + 128 (No Tools)
if ((user.siteadmin & 256) != 0) { mask -= 256; } // Mask: Manage User Groups
if ((user.siteadmin & 512) != 0) { mask -= 512; } // Mask: Manage Recordings
if (((chgusersiteadmin ^ command.siteadmin) & mask) == 0) { chguser.siteadmin = command.siteadmin; change = 1; }
}
}
// When sending a notification about a group change, we need to send to all the previous and new groups.
var allTargetGroups = chguser.groups;
if ((Array.isArray(command.groups)) && ((user._id != command.id) || (user.siteadmin === SITERIGHT_ADMIN))) {
if (command.groups.length == 0) {
// Remove the user groups
if (chguser.groups != null) { delete chguser.groups; change = 1; }
} else {
// Arrange the user groups
var groups2 = [];
for (var i in command.groups) {
if (typeof command.groups[i] == 'string') {
var gname = command.groups[i].trim().toLowerCase();
if ((gname.length > 0) && (gname.length <= 64) && (groups2.indexOf(gname) == -1)) { groups2.push(gname); }
}
}
groups2.sort();
// Set the user groups (Realms)
if (chguser.groups != groups2) { chguser.groups = groups2; change = 1; }
// Add any missing groups in the target list
if (allTargetGroups == null) { allTargetGroups = []; }
for (var i in groups2) { if (allTargetGroups.indexOf(i) == -1) { allTargetGroups.push(i); } }
}
}
if (change == 1) {
// Update the user
db.SetUser(chguser);
parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
var targets = ['*', 'server-users', user._id, chguser._id];
if (allTargetGroups) { for (var i in allTargetGroups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
}
if ((chguser.siteadmin) && (chguser.siteadmin !== SITERIGHT_ADMIN) && (chguser.siteadmin & 32)) {
// If the user is locked out of this account, disconnect now
parent.parent.DispatchEvent([chguser._id], obj, 'close'); // Disconnect all this user's sessions
}
}
// OK Response
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'edituser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'usergroups':
{
// Return only groups in the same administrative domain
if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) {
// We are not user group administrator, return a list with limited data for our domain.
var groups = {}, groupCount = 0;
for (var i in parent.userGroups) { if (parent.userGroups[i].domain == domain.id) { groupCount++; groups[i] = { name: parent.userGroups[i].name }; } }
try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: groupCount ? groups : null, tag: command.tag })); } catch (ex) { }
} else {
// We are user group administrator, return a full user group list for our domain.
var groups = {}, groupCount = 0;
for (var i in parent.userGroups) { if ((obj.crossDomain == true) || (parent.userGroups[i].domain == domain.id)) { groupCount++; groups[i] = parent.userGroups[i]; } }
try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: groupCount ? groups : null, tag: command.tag })); } catch (ex) { }
}
break;
}
case 'createusergroup':
{
var ugrpdomain, err = null;
try {
// Check if we are in a mode that does not allow manual user group creation
if (
(typeof domain.authstrategies == 'object') &&
(typeof domain.authstrategies['oidc'] == 'object') &&
(typeof domain.authstrategies['oidc'].groups == 'object') &&
((domain.authstrategies['oidc'].groups.sync == true) || ((typeof domain.authstrategies['oidc'].groups.sync == 'object') && (domain.authstrategies['oidc'].groups.sync.enabled == true)))
) {
err = "Not allowed in OIDC mode with user group sync.";
}
// Check if we have new group restriction
if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = "Permission denied"; }
// Create user group validation
else if (common.validateString(command.name, 1, 64) == false) { err = "Invalid group name"; } // User group name is between 1 and 64 characters
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
// If we are cloning from an existing user group, check that.
if (command.clone) {
if (common.validateString(command.clone, 1, 256) == false) { err = "Invalid clone groupid"; }
else {
var clonesplit = command.clone.split('/');
if ((clonesplit.length != 3) || (clonesplit[0] != 'ugrp') || ((command.domain == null) && (clonesplit[1] != domain.id))) { err = "Invalid clone groupid"; }
else if (parent.userGroups[command.clone] == null) { err = "Invalid clone groupid"; }
}
if (err == null) {
// Get new user group domain
ugrpdomain = parent.parent.config.domains[clonesplit[1]];
if (ugrpdomain == null) { err = "Invalid domain"; }
}
} else {
// Get new user group domain
ugrpdomain = domain;
if ((obj.crossDomain === true) && (command.domain != null)) { ugrpdomain = parent.parent.config.domains[command.domain]; }
if (ugrpdomain == null) { err = "Invalid domain"; }
}
// In some situations, we need a verified email address to create a device group.
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.
} catch (ex) { err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
parent.crypto.randomBytes(48, function (err, buf) {
// Create new device group identifier
var ugrpid = 'ugrp/' + ugrpdomain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
// Create the new device group
var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: ugrpdomain.id, links: {} };
// Clone the existing group if required
var pendingDispatchEvents = [];
if (command.clone != null) {
var cgroup = parent.userGroups[command.clone];
if (cgroup.links) {
for (var i in cgroup.links) {
if (i.startsWith('user/')) {
var xuser = parent.users[i];
if ((xuser != null) && (xuser.links != null)) {
ugrp.links[i] = { rights: cgroup.links[i].rights };
xuser.links[ugrpid] = { rights: cgroup.links[i].rights };
db.SetUser(xuser);
parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
// Notify user change
var targets = ['*', 'server-users', user._id, xuser._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
//parent.parent.DispatchEvent(targets, obj, event);
pendingDispatchEvents.push([targets, obj, event]);
}
} else if (i.startsWith('mesh/')) {
var xmesh = parent.meshes[i];
if (xmesh && xmesh.links) {
ugrp.links[i] = { rights: cgroup.links[i].rights };
xmesh.links[ugrpid] = { rights: cgroup.links[i].rights };
db.Set(xmesh);
// Notify mesh change
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
//parent.parent.DispatchEvent(['*', xmesh._id, user._id], obj, event);
pendingDispatchEvents.push([parent.CreateMeshDispatchTargets(xmesh, [user._id]), obj, event]);
}
}
}
}
}
// Save the new group
db.Set(ugrp);
if (db.changeStream == false) { parent.userGroups[ugrpid] = ugrp; }
// Event the user group creation
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
// Event any pending events, these must be sent out after the group creation event is dispatched.
for (var i in pendingDispatchEvents) { var ev = pendingDispatchEvents[i]; parent.parent.DispatchEvent(ev[0], ev[1], ev[2]); }
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created user group ' + ugrp.name); }
try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: 'ok', ugrpid: ugrpid, links: ugrp.links })); } catch (ex) { }
});
break;
}
case 'deleteusergroup':
{
var err = null;
if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = "Permission denied"; }
// Change the name or description of a user group
else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = "Invalid group id"; } // Check the user group id
else {
var ugroupidsplit = command.ugrpid.split('/');
if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = "Invalid domain id"; }
}
// Get the domain
var delGroupDomain;
if (ugroupidsplit != null) {
delGroupDomain = parent.parent.config.domains[ugroupidsplit[1]];
if (delGroupDomain == null) { err = "Invalid domain id"; }
}
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
db.Get(command.ugrpid, function (err, groups) {
if ((err != null) || (groups.length != 1)) {
try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { }
return;
}
var group = groups[0];
// If this user group is an externally managed user group, it can't be deleted unless there are no users in it.
if (group.membershipType != null) {
var userCount = 0;
if (group.links != null) { for (var i in group.links) { if (i.startsWith('user/')) { userCount++; } } }
if (userCount > 0) return;
}
// Unlink any user and meshes that have a link to this group
if (group.links) {
for (var i in group.links) {
if (i.startsWith('user/')) {
var xuser = parent.users[i];
if ((xuser != null) && (xuser.links != null)) {
delete xuser.links[group._id];
db.SetUser(xuser);
parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
// Notify user change
var targets = ['*', 'server-users', user._id, xuser._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
}
} else if (i.startsWith('mesh/')) {
var xmesh = parent.meshes[i];
if (xmesh && xmesh.links) {
delete xmesh.links[group._id];
db.Set(xmesh);
// Notify mesh change
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(xmesh, [user._id]), obj, event);
}
}
}
}
// Remove the user group from the database
db.Remove(group._id);
if (db.changeStream == false) { delete parent.userGroups[group._id]; }
// Event the user group being removed
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, domain: delGroupDomain.id };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted user group ' + group.name); }
try { ws.send(JSON.stringify({ action: 'deleteusergroup', responseid: command.responseid, result: 'ok', ugrpid: group._id })); } catch (ex) { }
});
break;
}
case 'editusergroup':
{
if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; }
// Change the name or description of a user group
if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id
var ugroupidsplit = command.ugrpid.split('/');
if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break;
// Get the user group
change = '';
var group = parent.userGroups[command.ugrpid];
if (group != null) {
// If this user group is an externally managed user group, the name of the user group can't be edited
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; }
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; }
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; }
if ((command.flags != null) && (typeof command.flags == 'number')) {
// Flags: 2 = Session Recording
if ((command.flags == 0) && (group.flags != null)) { delete group.flags; } else { if (command.flags !== group.flags) { group.flags = command.flags; } }
if (change == '') { change = 'User group features changed.'; }
}
if (change != '') {
db.Set(group);
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(['*', group._id, user._id], obj, event);
}
}
break;
}
case 'changemeshnotify':
{
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
// 2 = WebPage device connections
// 4 = WebPage device disconnections
// 8 = WebPage device desktop and serial events
// 16 = Email device connections
// 32 = Email device disconnections
// 64 = Email device help request
// 128 = Messaging device connections
// 256 = Messaging device disconnections
// 512 = Messaging device help request
var err = null;
try {
// Change the current user's notification flags for a meshid
if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
if (parent.IsMeshViewable(user, command.meshid) == false) err = 'Access denied';
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changemeshnotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
// Change the device group notification
if (user.links == null) { user.links = {}; }
if (user.links[command.meshid]) {
// The user has direct rights for this device group
if (command.notify == 0) {
delete user.links[command.meshid].notify;
} else {
user.links[command.meshid].notify = command.notify;
}
}
// Change user notification if needed, this is needed then a user has device rights thru a user group
if ((command.notify == 0) && (user.notify != null) && (user.notify[command.meshid] != null)) { delete user.notify[command.meshid]; }
if ((command.notify != 0) && (user.links[command.meshid] == null)) { if (user.notify == null) { user.notify = {} } user.notify[command.meshid] = command.notify; }
// Save the user
parent.db.SetUser(user);
// Notify change
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
break;
}
case 'changeusernotify':
{
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
// 2 = WebPage device connections
// 4 = WebPage device disconnections
// 8 = WebPage device desktop and serial events
// 16 = Email device connections
// 32 = Email device disconnections
// 64 = Email device help request
// 128 = Messaging device connections
// 256 = Messaging device disconnections
// 512 = Messaging device help request
var err = null;
try {
// Change the current user's notification flags for a meshid
if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid device identifier'; } // Check the meshid
else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
//if (parent.IsMeshViewable(user, command.nodeid) == false) err = 'Access denied';
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeusernotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
// Check if nothing has changed
if ((user.notify == null) && (command.notify == 0)) return;
if ((user.notify != null) && (user.notify[command.nodeid] == command.notify)) return;
// Change the notification
if (user.notify == null) { user.notify = {}; }
if (command.notify == 0) { delete user.notify[command.nodeid]; } else { user.notify[command.nodeid] = command.notify; }
if (Object.keys(user.notify).length == 0) { delete user.notify; }
// Save the user
parent.db.SetUser(user);
// Notify change
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
break;
}
case 'changepassword':
{
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// If this account is settings locked, return here.
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
// Do not allow change password if sspi or ldap
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
// Change our own password
if (common.validateString(command.oldpass, 1, 256) == false) break;
if (common.validateString(command.newpass, 1, 256) == false) break;
if ((command.hint != null) && (common.validateString(command.hint, 0, 256) == false)) break;
if (common.checkPasswordRequirements(command.newpass, domain.passwordrequirements) == false) break; // Password does not meet requirements
// Start by checking the old password
parent.checkUserPassword(domain, user, command.oldpass, function (result) {
if (result == true) {
parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
if (result == 1) {
// Send user notification of error
displayNotificationMessage("Error, unable to change to previously used password.", "Account Settings", 'ServerNotify', 4, 17);
} else if (result == 2) {
// Send user notification of error
displayNotificationMessage("Error, unable to change to commonly used password.", "Account Settings", 'ServerNotify', 4, 18);
} else {
// Update the password
require('./pass').hash(command.newpass, function (err, salt, hash, tag) {
if (err) {
// Send user notification of error
displayNotificationMessage("Error, password not changed.", "Account Settings", 'ServerNotify', 4, 19);
} else {
const nowSeconds = Math.floor(Date.now() / 1000);
// Change the password
if (domain.passwordrequirements != null) {
// Save password hint if this feature is enabled
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; }
// Save previous password if this feature is enabled
if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
if (user.oldpasswords == null) { user.oldpasswords = []; }
user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
}
}
user.salt = salt;
user.hash = hash;
user.passchange = nowSeconds;
delete user.passtype;
db.SetUser(user);
var targets = ['*', 'server-users'];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
// Send user notification of password change
displayNotificationMessage("Password changed.", "Account Settings", 'ServerNotify', 4, 20);
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed this password'); }
}
}, 0);
}
});
} else {
// Send user notification of error
displayNotificationMessage("Current password not correct.", "Account Settings", 'ServerNotify', 4, 21);
}
});
break;
}
case 'changeuserpass':
{
// Change a user's password
if ((user.siteadmin & 2) == 0) break;
if (common.validateString(command.userid, 1, 256) == false) break;
if (common.validateString(command.pass, 0, 256) == false) break;
if ((command.hint != null) && (common.validateString(command.hint, 0, 256) == false)) break;
if (typeof command.removeMultiFactor != 'boolean') break;
if ((command.pass != '') && (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false)) break; // Password does not meet requirements
var chguser = parent.users[command.userid];
if (chguser) {
if ((obj.crossDomain !== true) && (chguser.domain != domain.id)) break; // Operation only valid for current domain
// If we are not full administrator, we can't change anything on a different full administrator
if ((user.siteadmin != SITERIGHT_ADMIN) & (chguser.siteadmin === SITERIGHT_ADMIN)) break;
// Can only perform this operation on other users of our group.
if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
// Compute the password hash & save it
require('./pass').hash(command.pass, function (err, salt, hash, tag) {
if (!err) {
if (command.pass != '') { chguser.salt = salt; chguser.hash = hash; }
if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true) && (command.hint != null)) {
var hint = command.hint;
if (hint.length > 250) { hint = hint.substring(0, 250); }
chguser.passhint = hint;
}
if (command.resetNextLogin === true) { chguser.passchange = -1; } else { chguser.passchange = Math.floor(Date.now() / 1000); }
delete chguser.passtype; // Remove the password type if one was present.
if (command.removeMultiFactor === true) {
delete chguser.otpkeys; // One time backup codes
delete chguser.otpsecret; // OTP Google Authenticator
delete chguser.otphkeys; // FIDO keys
delete chguser.otpekey; // Email 2FA
delete chguser.phone; // SMS 2FA
delete chguser.otpdev; // Push notification 2FA
delete chguser.otpduo; // Duo 2FA
}
db.SetUser(chguser);
var targets = ['*', 'server-users', user._id, chguser._id];
if (chguser.groups) { for (var i in chguser.groups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed account password of user ' + chguser.name); }
} else {
// Report that the password change failed
// TODO
}
}, 0);
}
break;
}
case 'notifyuser':
{
// Send a notification message to a user
if ((user.siteadmin & 2) == 0) break;
if (common.validateString(command.userid, 1, 2048) == false) break;
if (common.validateString(command.msg, 1, 4096) == false) break;
// Can only perform this operation on other users of our group.
var chguser = parent.users[command.userid];
if (chguser == null) break; // This user does not exists
if ((obj.crossDomain !== true) && (chguser.domain != domain.id)) break; // Operation only valid for current domain
if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
// Create the notification message
var notification = { action: 'msg', type: 'notify', id: Math.random(), value: command.msg, title: user.name, icon: 8, userid: user._id, username: user.name };
if (typeof command.url == 'string') { notification.url = command.url; }
if ((typeof command.maxtime == 'number') && (command.maxtime > 0)) { notification.maxtime = command.maxtime; }
if (command.msgid == 11) { notification.value = "Chat Request, Click here to accept."; notification.msgid = 11; } // Chat request
// Get the list of sessions for this user
var sessions = parent.wssessions[command.userid];
if (sessions != null) { for (i in sessions) { try { sessions[i].send(JSON.stringify(notification)); } catch (ex) { } } }
if (parent.parent.multiServer != null) {
// TODO: Add multi-server support
}
// If the user is not connected, use web push if available.
if ((parent.wssessions[chguser._id] == null) && (parent.sessionsCount[chguser._id] == null)) {
// Perform web push notification
var payload = { body: command.msg, icon: 8 }; // Icon 8 is the user icon.
if (command.url) { payload.url = command.url; }
if (domain.title != null) { payload.title = domain.title; } else { payload.title = "MeshCentral"; }
payload.title += ' - ' + user.name;
parent.performWebPush(domain, chguser, payload, { TTL: 60 }); // For now, 1 minute TTL
}
break;
}
case 'meshmessenger':
{
// Setup a user-to-user session
if (common.validateString(command.userid, 1, 2048)) {
// Send a notification message to a user
if ((user.siteadmin & 2) == 0) break;
// Can only perform this operation on other users of our group.
var chguser = parent.users[command.userid];
if (chguser == null) break; // This user does not exists
if ((obj.crossDomain !== true) && (chguser.domain != domain.id)) break; // Operation only valid for current domain
if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
// Create the notification message
var notification = {
'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
};
// Get the list of sessions for this user
var sessions = parent.wssessions[command.userid];
if (sessions != null) { for (i in sessions) { try { sessions[i].send(JSON.stringify(notification)); } catch (ex) { } } }
if (parent.parent.multiServer != null) {
// TODO: Add multi-server support
}
// If the user is not connected, use web push if available.
if ((parent.wssessions[chguser._id] == null) && (parent.sessionsCount[chguser._id] == null)) {
// Create the server url
var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += "/";
var url = "https://" + parent.getWebServerName(domain, req) + ":" + httpsPort + "/" + xdomain + "messenger?id=meshmessenger/" + encodeURIComponent(command.userid) + "/" + encodeURIComponent(user._id);
// Perform web push notification
var payload = { body: "Chat Request, Click here to accept.", icon: 8, url: url }; // Icon 8 is the user icon.
if (domain.title != null) { payload.title = domain.title; } else { payload.title = "MeshCentral"; }
payload.title += ' - ' + user.name;
parent.performWebPush(domain, chguser, payload, { TTL: 60 }); // For now, 1 minute TTL
}
return;
}
// User-to-device chat is not support in LAN-only mode yet. We need the agent to replace the IP address of the server??
if (args.lanonly == true) { return; }
// Setup a user-to-node session
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
// Check if this user has rights to do this
if ((rights & MESHRIGHT_CHATNOTIFY) == 0) return;
// Create the server url
var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += "/";
var url = "https://" + parent.getWebServerName(domain, req) + ":" + httpsPort + "/" + xdomain + "messenger?id=meshmessenger/" + encodeURIComponent(command.nodeid) + "/" + encodeURIComponent(user._id);
// Open a web page on the remote device
routeCommandToNode({ 'action': 'openUrl', 'nodeid': command.nodeid, 'userid': user._id, 'username': user.name, 'url': url });
});
break;
}
case 'createmesh':
{
var err = null;
try {
// Support for old web pages that sent the meshtype as a string.
if (typeof command.meshtype == 'string') { command.meshtype = parseInt(command.meshtype); }
// Check if we have new group restriction
if ((user.siteadmin != SITERIGHT_ADMIN) && ((user.siteadmin & 64) != 0)) { err = 'Permission denied'; }
// In some situations, we need a verified email address to create a device group.
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.
// Create mesh
else if (common.validateString(command.meshname, 1, 128) == false) { err = 'Invalid group name'; } // Meshname is between 1 and 128 characters
else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = 'Invalid group description'; } // Mesh description is between 0 and 1024 characters
else if ((command.meshtype < 1) || (command.meshtype > 4)) { err = 'Invalid group type'; } // Device group types are 1 = AMT, 2 = Agent, 3 = Local
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
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
else if ((domain.ipkvm == null) && (command.meshtype == 4)) { err = 'Invalid group type'; } // IP KVM device group type is not allowed unless enabled
else if ((command.parent != null) && (typeof command.parent !== 'string' || !parent.meshes[command.parent] || parent.meshes[command.parent].domain !== domain.id)) { err = 'Invalid parent group'; }
if ((err == null) && (command.meshtype == 4)) {
if ((command.kvmmodel < 1) || (command.kvmmodel > 2)) { err = 'Invalid KVM model'; }
else if (common.validateString(command.kvmhost, 1, 128) == false) { err = 'Invalid KVM hostname'; }
else if (common.validateString(command.kvmuser, 1, 128) == false) { err = 'Invalid KVM username'; }
else if (common.validateString(command.kvmpass, 1, 128) == false) { err = 'Invalid KVM password'; }
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
parent.crypto.randomBytes(48, function (err, buf) {
// Create new device group identifier
meshid = 'mesh/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
// Create the new device group
var links = {};
links[user._id] = { name: user.name, rights: 4294967295 };
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 };
// Set parent mesh if provided
if (command.parent && parent.meshes[command.parent]) { mesh.parent = command.parent; }
// Add flags and consent if present
if (typeof command.flags == 'number') { mesh.flags = command.flags; }
if (typeof command.consent == 'number') { mesh.consent = command.consent; }
// Add KVM information if needed
if (command.meshtype == 4) { mesh.kvm = { model: command.kvmmodel, host: command.kvmhost, user: command.kvmuser, pass: command.kvmpass }; }
// If this is device group that requires a relay device, store that now
if ((parent.args.lanonly != true) && ((command.meshtype == 3) || (command.meshtype == 4)) && (typeof command.relayid == 'string')) {
// Check the relay id
var relayIdSplit = command.relayid.split('/');
if ((relayIdSplit[0] == 'node') && (relayIdSplit[1] == domain.id)) { mesh.relayid = command.relayid; }
}
// Save the new device group
db.Set(mesh);
parent.meshes[meshid] = mesh;
parent.parent.AddEventDispatch([meshid], ws);
// Change the user to make him administration of the new device group
if (user.links == null) user.links = {};
user.links[meshid] = { rights: 4294967295 };
user.subscriptions = parent.subscribe(user._id, ws);
db.SetUser(user);
// Event the user change
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
// Event the device group creation
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(['*', 'server-createmesh', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created device group ' + mesh.name); }
try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: 'ok', meshid: meshid, links: links })); } catch (ex) { }
// If needed, event that a device is now a device group relay
if (mesh.relayid != null) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
if (node == null) return;
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) };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
});
}
});
break;
}
case 'deletemesh':
{
// Delete a mesh and all computers within it
var err = null;
// Resolve the device group name if needed
if ((typeof command.meshname == 'string') && (command.meshid == null)) {
for (var i in parent.meshes) {
var m = parent.meshes[i];
if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
}
}
}
// Validate input
try {
if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
// Get the device group reference we are going to delete
var mesh = parent.meshes[command.meshid];
if (mesh == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { } } return; }
// Check if this user has rights to do this
var err = null;
if (parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN) { err = 'Access denied'; }
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group'; } // Invalid domain, operation only valid for current domain
// Handle any errors
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } } return; }
// Fire the removal event first, because after this, the event will not route
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
// Remove all user links to this mesh
for (var j in mesh.links) {
if (j.startsWith('user/')) {
var xuser = parent.users[j];
if (xuser && xuser.links) {
delete xuser.links[mesh._id];
db.SetUser(xuser);
parent.parent.DispatchEvent([xuser._id], obj, 'resubscribe');
// Notify user change
var targets = ['*', 'server-users', user._id, xuser._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
}
} else if (j.startsWith('ugrp/')) {
var xgroup = parent.userGroups[j];
if (xgroup && xgroup.links) {
delete xgroup.links[mesh._id];
db.Set(xgroup);
// Notify user group change
var targets = ['*', 'server-ugroups', user._id, xgroup._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
}
}
}
// Delete any invitation codes
delete mesh.invite;
// Delete all files on the server for this mesh
try {
var meshpath = parent.getServerRootFilePath(mesh);
if (meshpath != null) { parent.deleteFolderRec(meshpath); }
} catch (e) { }
parent.parent.RemoveEventDispatchId(command.meshid); // Remove all subscriptions to this mesh
// Notify the devices that they have changed relay roles
if (mesh.relayid != null) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
if (node == null) return;
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) };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
});
}
// Mark the mesh as deleted
mesh.deleted = new Date(); // Mark the time this mesh was deleted, we can expire it at some point.
db.Set(mesh); // We don't really delete meshes because if a device connects to is again, we will un-delete it.
// Delete all devices attached to this mesh in the database
db.RemoveMeshDocuments(command.meshid);
// TODO: We are possibly deleting devices that users will have links to. We need to clean up the broken links from on occasion.
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' deleted device group ' + mesh.name); }
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'editmesh':
{
// Change the name or description of a device group (mesh)
var err = null;
// Resolve the device group name if needed
if ((typeof command.meshidname == 'string') && (command.meshid == null)) {
for (var i in parent.meshes) {
var m = parent.meshes[i];
if ((m.mtype == 2) && (m.name == command.meshidname) && parent.IsMeshViewable(user, m)) {
if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
}
}
}
// Validate input
try {
if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check the meshid
else if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
if (err == null) {
mesh = parent.meshes[command.meshid];
if (mesh == null) { err = 'Invalid group identifier '; }
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
change = '';
// Check if this user has rights to do this
if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return;
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
var changesids = [];
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; }
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; }
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; }
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; }
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; } }
var oldRelayNodeId = null, newRelayNodeId = null;
if ((typeof command.relayid == 'string') && ((mesh.mtype == 3) || (mesh.mtype == 4)) && (mesh.relayid != null) && (command.relayid != mesh.relayid)) {
var relayIdSplit = command.relayid.split('/');
if ((relayIdSplit.length == 3) && (relayIdSplit[0] = 'node') && (relayIdSplit[1] == domain.id)) {
if (change != '') { change += ' and device relay changed'; } else { change = 'Device relay changed'; }
changesids.push(7);
oldRelayNodeId = mesh.relayid;
newRelayNodeId = mesh.relayid = command.relayid;
}
}
// See if we need to change device group invitation codes
if (mesh.mtype == 2) {
if (command.invite === '*') {
// Clear invite codes
if (mesh.invite != null) { delete mesh.invite; }
if (change != '') { change += ' and invite code changed'; } else { change += 'Device group "' + mesh.name + '" invite code changed'; }
changesids.push(6);
} else if ((command.invite != null) && (typeof command.invite == 'object') && (Array.isArray(command.invite.codes)) && (typeof command.invite.flags == 'number')) {
// Set invite codes
if ((mesh.invite == null) || (mesh.invite.codes != command.invite.codes) || (mesh.invite.flags != command.invite.flags)) {
// Check if an invite code is not already in use.
var dup = null;
for (var i in command.invite.codes) {
for (var j in parent.meshes) {
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; }
}
}
if (dup != null) {
// A duplicate was found, don't allow this change.
displayNotificationMessage("Error, invite code \"" + dup + "\" already in use.", "Invite Codes", null, 6, 22, [dup]);
return;
}
mesh.invite = { codes: command.invite.codes, flags: command.invite.flags };
if (typeof command.invite.ag == 'number') { mesh.invite.ag = command.invite.ag; }
if (change != '') { change += ' and invite code changed'; } else { change += 'Device group "' + mesh.name + '" invite code changed'; }
changesids.push(6);
}
}
}
if (change != '') {
db.Set(mesh);
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, 'server-editmesh']), obj, event);
}
// Notify the devices that they have changed relay roles
if (oldRelayNodeId != null) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, oldRelayNodeId, function (node, rights, visible) {
if (node == null) return;
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) };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
});
}
if (newRelayNodeId != null) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, newRelayNodeId, function (node, rights, visible) {
if (node == null) return;
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) };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
});
} else if ((mesh.relayid != null) && (changesids.indexOf(1) >= 0)) {
// Notify of node name change, get the node and the rights for this node, we just want to trigger a device update.
parent.GetNodeWithRights(domain, user, mesh.relayid, function (node, rights, visible) {
if (node == null) return;
var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, node: parent.CloneSafeNode(node), nolog: 1 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
});
}
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'removemeshuser':
{
var xdomain, err = null;
// Resolve the device group name if needed
if ((typeof command.meshname == 'string') && (command.meshid == null)) {
for (var i in parent.meshes) {
var m = parent.meshes[i];
if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
}
}
}
try {
if (common.validateString(command.userid, 1, 1024) == false) { err = "Invalid userid"; } // Check userid
if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid
if (command.userid.indexOf('/') == -1) { command.userid = 'user/' + domain.id + '/' + command.userid; }
if (command.userid == obj.user._id) { err = "Can't remove self"; } // Can't add of modify self
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
else {
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
mesh = parent.meshes[command.meshid];
var meshIdSplit = command.meshid.split('/');
if (mesh == null) { err = "Unknown device group"; }
else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGEUSERS) == 0) { err = "Permission denied"; }
else if (meshIdSplit.length != 3) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
else {
xdomain = domain;
if (obj.crossDomain !== true) { xdomain = parent.parent.config.domains[meshIdSplit[1]]; }
if (xdomain == null) { err = "Invalid domain"; }
}
}
} catch (ex) { err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
console.log(err);
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// 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.
var deluserid = command.userid, deluser = null;
if (deluserid.startsWith('user/')) { deluser = parent.users[deluserid]; }
else if (deluserid.startsWith('ugrp/')) { deluser = parent.userGroups[deluserid]; }
// Search for a user name in that windows domain is the username starts with *\
if ((deluser == null) && (deluserid.startsWith('user/' + xdomain.id + '/*\\')) == true) {
var search = deluserid.split('/')[2].substring(1);
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; } }
}
if (deluser != null) {
// Remove mesh from user
if (deluser.links != null && deluser.links[command.meshid] != null) {
var delmeshrights = deluser.links[command.meshid].rights;
if ((parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN) && ((delmeshrights == MESHRIGHT_ADMIN) || ((delmeshrights & MESHRIGHT_RESTRICTIONS) != 0))) return;
delete deluser.links[command.meshid];
parent.InvalidateNodeCache(deluser, mesh);
if (deluserid.startsWith('user/')) { db.SetUser(deluser); }
else if (deluserid.startsWith('ugrp/')) { db.Set(deluser); }
parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe');
if (deluserid.startsWith('user/')) {
// Notify user change
var targets = ['*', 'server-users', user._id, deluser._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
} else if (deluserid.startsWith('ugrp/')) {
// Notify user group change
var targets = ['*', 'server-ugroups', user._id, deluser._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
}
}
}
// Remove user from the mesh
if (mesh.links[command.userid] != null) {
delete mesh.links[command.userid];
db.Set(mesh);
// Notify mesh change
var event;
if (deluser != null) {
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 };
} else {
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 };
}
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id, command.userid]), obj, event);
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'User not in group' })); } catch (ex) { } }
}
break;
}
case 'meshamtpolicy':
{
// Change a mesh Intel AMT policy
if (common.validateString(command.meshid, 8, 134) == false) break; // Check the meshid
if (common.validateObject(command.amtpolicy) == false) break; // Check the amtpolicy
if (common.validateInt(command.amtpolicy.type, 0, 4) == false) break; // Check the amtpolicy.type
if (command.amtpolicy.type === 2) {
if ((command.amtpolicy.password != null) && (common.validateString(command.amtpolicy.password, 0, 32) == false)) break; // Check the amtpolicy.password
if ((command.amtpolicy.badpass != null) && common.validateInt(command.amtpolicy.badpass, 0, 1) == false) break; // Check the amtpolicy.badpass
if (common.validateInt(command.amtpolicy.cirasetup, 0, 2) == false) break; // Check the amtpolicy.cirasetup
} else if (command.amtpolicy.type === 3) {
if ((command.amtpolicy.password != null) && (common.validateString(command.amtpolicy.password, 0, 32) == false)) break; // Check the amtpolicy.password
if ((command.amtpolicy.badpass != null) && common.validateInt(command.amtpolicy.badpass, 0, 1) == false) break; // Check the amtpolicy.badpass
if ((command.amtpolicy.ccm != null) && common.validateInt(command.amtpolicy.ccm, 0, 2) == false) break; // Check the amtpolicy.ccm
if (common.validateInt(command.amtpolicy.cirasetup, 0, 2) == false) break; // Check the amtpolicy.cirasetup
}
mesh = parent.meshes[command.meshid];
if (mesh) {
// Check if this user has rights to do this
if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return;
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
// TODO: Check if this is a change from the existing policy
// Perform the Intel AMT policy change
var amtpolicy = { type: command.amtpolicy.type };
if ((command.amtpolicy.type === 2) || (command.amtpolicy.type === 3)) {
amtpolicy = { type: command.amtpolicy.type, badpass: command.amtpolicy.badpass, cirasetup: command.amtpolicy.cirasetup };
if (command.amtpolicy.type === 3) { amtpolicy.ccm = command.amtpolicy.ccm; }
if ((command.amtpolicy.password == null) && (mesh.amt != null) && (typeof mesh.amt.password == 'string')) { amtpolicy.password = mesh.amt.password; } // Keep the last password
if ((typeof command.amtpolicy.password == 'string') && (command.amtpolicy.password.length >= 8)) { amtpolicy.password = command.amtpolicy.password; } // Set a new password
}
mesh.amt = amtpolicy;
db.Set(mesh);
var amtpolicy2 = Object.assign({}, amtpolicy); // Shallow clone
if (amtpolicy2.password != null) { amtpolicy2.password = 1; }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(mesh, [user._id]), obj, event);
// If we have peer servers, inform them of the new Intel AMT policy for this device group
if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'newIntelAmtPolicy', meshid: command.meshid, amtpolicy: amtpolicy }); }
// See if any agents for the affected device group is connected, if so, update the Intel AMT policy
for (var nodeid in parent.wsagents) {
const agent = parent.wsagents[nodeid];
if (agent.dbMeshKey == command.meshid) { agent.sendUpdatedIntelAmtPolicy(amtpolicy); }
}
}
break;
}
case 'addlocaldevice':
{
var err = null;
// Perform input validation
try {
if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid
if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name
if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname
if (typeof command.type != 'number') { err = "Invalid type"; } // Type must be a number
if ((command.type != 4) && (command.type != 6) && (command.type != 29)) { err = "Invalid type"; } // Check device type
else {
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
mesh = parent.meshes[command.meshid];
if (mesh == null) { err = "Unknown device group"; }
if (mesh.mtype != 3) { err = "Local device agentless mesh only allowed" } // This operation is only allowed for mesh type 3, local device agentless mesh.
else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
}
} catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Create a new nodeid
parent.crypto.randomBytes(48, function (err, buf) {
// Create the new node
nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
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 } };
db.Set(device);
// Event the new node
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 });
// Send response if required
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addlocaldevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
});
break;
}
case 'addamtdevice':
{
if (args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added
var err = null;
// Perform input validation
try {
if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid device group id"; } // Check meshid
if (common.validateString(command.devicename, 1, 256) == false) { err = "Invalid devicename"; } // Check device name
if (common.validateString(command.hostname, 1, 256) == false) { err = "Invalid hostname"; } // Check hostname
if (common.validateString(command.amtusername, 0, 16) == false) { err = "Invalid amtusername"; } // Check username
if (common.validateString(command.amtpassword, 0, 16) == false) { err = "Invalid amtpassword"; } // Check password
if (command.amttls == '0') { command.amttls = 0; } else if (command.amttls == '1') { command.amttls = 1; } // Check TLS flag
if ((command.amttls != 1) && (command.amttls != 0)) { err = "Invalid amttls"; }
else {
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
// Get the mesh
mesh = parent.meshes[command.meshid];
if (mesh == null) { err = "Unknown device group"; }
if (mesh.mtype != 1) { err = "Intel AMT agentless mesh only allowed"; } // This operation is only allowed for mesh type 1, Intel AMT agentless mesh.
// Check if this user has rights to do this
else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
}
} catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// If we are in WAN-only mode, hostname is not used
if ((args.wanonly == true) && (command.hostname)) { delete command.hostname; }
// Create a new nodeid
parent.crypto.randomBytes(48, function (err, buf) {
// Create the new node
nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
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 } };
// Add optional feilds
if (common.validateInt(command.state, 0, 3)) { device.intelamt.state = command.state; }
if (common.validateString(command.ver, 1, 16)) { device.intelamt.ver = command.ver; }
if (common.validateString(command.hash, 1, 256)) { device.intelamt.hash = command.hash; }
if (common.validateString(command.realm, 1, 256)) { device.intelamt.realm = command.realm; }
// Save the device to the database
db.Set(device);
// Event the new node
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 });
// Send response if required
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addamtdevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
});
break;
}
case 'scanamtdevice':
{
if (args.wanonly == true) return; // This is a WAN-only server, this type of scanning is not allowed.
if (common.validateString(command.range, 1, 256) == false) break; // Check range string
// Ask the RMCP scanning to scan a range of IP addresses
if (parent.parent.amtScanner) {
if (parent.parent.amtScanner.performRangeScan(user._id, command.range) == false) {
parent.parent.DispatchEvent(['*', user._id], obj, { action: 'scanamtdevice', range: command.range, results: null, nolog: 1 });
}
}
break;
}
case 'changeDeviceMesh':
{
var err = null;
// Resolve the device group name if needed
if ((typeof command.meshname == 'string') && (command.meshid == null)) {
for (var i in parent.meshes) {
var m = parent.meshes[i];
if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
}
}
}
// Perform input validation
try {
if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
if (common.validateString(command.meshid, 8, 134) == false) { err = "Invalid groupid"; } // Check meshid
else {
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
mesh = parent.meshes[command.meshid];
if (mesh == null) { err = "Unknown device group"; }
else if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) { err = "Permission denied"; }
else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = "Invalid domain"; } // Invalid domain, operation only valid for current domain
}
} catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// This is to change device guest sharing to the new device group
var changeDeviceShareMeshIdNodeCount = command.nodeids.length;
var changeDeviceShareMeshIdNodeList = [];
// For each nodeid, change the group
for (var i = 0; i < command.nodeids.length; i++) {
var xnodeid = command.nodeids[i];
if (xnodeid.indexOf('/') == -1) { xnodeid = 'node/' + domain.id + '/' + xnodeid; }
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, xnodeid, function (node, rights, visible) {
// Check if we found this device
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; }
// Check if already in the right mesh
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; }
// Make sure both source and target mesh are the same type
try { if (parent.meshes[node.meshid].mtype != parent.meshes[command.meshid].mtype) { changeDeviceShareMeshIdNodeCount--; return; } } catch (e) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Device groups are of different types' })); } catch (ex) { } }
changeDeviceShareMeshIdNodeCount--;
return;
};
// Make sure that we have rights on both source and destination mesh
const targetMeshRights = parent.GetMeshRights(user, command.meshid);
if (((rights & MESHRIGHT_EDITMESH) == 0) || ((targetMeshRights & MESHRIGHT_EDITMESH) == 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'Permission denied' })); } catch (ex) { } }
changeDeviceShareMeshIdNodeCount--;
return;
}
// Perform the switch, start by saving the node with the new meshid.
changeDeviceShareMeshIdNodeList.push(node._id);
changeDeviceShareMeshIdNodeCount--;
if (changeDeviceShareMeshIdNodeCount == 0) { changeDeviceShareMeshId(changeDeviceShareMeshIdNodeList, command.meshid); }
const oldMeshId = node.meshid;
node.meshid = command.meshid;
db.Set(parent.cleanDevice(node));
// If the device is connected on this server, switch it now.
var agentSession = parent.wsagents[node._id];
if (agentSession != null) {
agentSession.dbMeshKey = command.meshid; // Switch the agent mesh
agentSession.meshid = command.meshid.split('/')[2]; // Switch the agent mesh
agentSession.sendUpdatedIntelAmtPolicy(); // Send the new Intel AMT policy
}
// If any MQTT sessions are connected on this server, switch it now.
if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.changeDeviceMesh(node._id, command.meshid); }
// If any CIRA sessions are connected on this server, switch it now.
if (parent.parent.mpsserver != null) { parent.parent.mpsserver.changeDeviceMesh(node._id, command.meshid); }
// Add the connection state
const state = parent.parent.GetConnectivityState(node._id);
if (state) {
node.conn = state.connectivity;
node.pwr = state.powerState;
if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[node._id]; if (agent != null) { node.agct = agent.connectTime; } }
// Uuse the connection time of the CIRA/Relay connection
if ((state.connectivity & 2) != 0) {
var ciraConnection = parent.parent.mpsserver.GetConnectionToNode(node._id, null, true);
if ((ciraConnection != null) && (ciraConnection.tag != null)) { node.cict = ciraConnection.tag.connectTime; }
}
}
// Update lastconnect meshid for this node
db.Get('lc' + node._id, function (err, xnodes) {
if ((xnodes != null) && (xnodes.length == 1) && (xnodes[0].meshid != command.meshid)) { xnodes[0].meshid = command.meshid; db.Set(xnodes[0]); }
});
// Event the node change
var newMesh = parent.meshes[command.meshid];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
// 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.
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid, [oldMeshId, node._id]), obj, event);
// Send response if required
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeDeviceMesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
});
}
break;
}
case 'removedevices':
{
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
// Users and user groups that lose device links are saved once, after all the devices in this
// request are processed. Saving them per device rewrites each user record once per removed
// device, which is very expensive when users have many device links.
var removeDevicesChangedUsers = {}, removeDevicesChangedUserGroups = {}, removeDevicesPending = 1;
function removeDevicesDone() {
if (--removeDevicesPending > 0) return;
for (var uid in removeDevicesChangedUsers) {
var cuser = removeDevicesChangedUsers[uid];
db.SetUser(cuser);
// Notify user change
var targets = ['*', 'server-users', cuser._id];
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) };
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.
parent.parent.DispatchEvent(targets, obj, event);
}
for (var gid in removeDevicesChangedUserGroups) { db.Set(removeDevicesChangedUserGroups[gid]); }
}
for (i in command.nodeids) {
var nodeid = command.nodeids[i], err = null;
// Argument validation
if (common.validateString(nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check nodeid
else {
if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
}
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: err })); } catch (ex) { } }
continue;
}
// Get the node and the rights for this node
removeDevicesPending++;
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
// Check we have the rights to delete this device
if ((rights & MESHRIGHT_UNINSTALL) == 0) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
removeDevicesDone();
return;
}
// Delete this node including network interface information, events and timeline
db.Remove(node._id); // Remove node with that id
db.Remove('if' + node._id); // Remove interface information
db.Remove('nt' + node._id); // Remove notes
db.Remove('lc' + node._id); // Remove last connect time
db.Remove('si' + node._id); // Remove system information
db.Remove('al' + node._id); // Remove error log last time
if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
db.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node
db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
db.Get('ra' + node._id, function (err, nodes) {
if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
});
// Remove any user node links
if (node.links != null) {
for (var i in node.links) {
if (i.startsWith('user/')) {
var cuser = parent.users[i];
if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
// Remove the user link, the user is saved once all devices are processed
delete cuser.links[node._id];
if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
removeDevicesChangedUsers[cuser._id] = cuser;
}
} else if (i.startsWith('ugrp/')) {
var cusergroup = parent.userGroups[i];
if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
// Remove the user group link, the user group is saved once all devices are processed
delete cusergroup.links[node._id];
if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
removeDevicesChangedUserGroups[cusergroup._id] = cusergroup;
// Notify user change
var targets = ['*', 'server-users', cusergroup._id];
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
}
}
}
}
// Event node deletion
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 };
// TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
//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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
// Disconnect all connections if needed
var state = parent.parent.GetConnectivityState(nodeid);
if ((state != null) && (state.connectivity != null)) {
if ((state.connectivity & 1) != 0) { parent.wsagents[nodeid].close(); } // Disconnect mesh agent
if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(nodeid); } // Disconnect CIRA/Relay/LMS connections
}
// Send response if required
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
removeDevicesDone();
});
}
removeDevicesDone();
break;
}
case 'wakedevices':
{
// TODO: We can optimize this a lot.
// - We should get a full list of all MAC's to wake first.
// - We should try to only have one agent per subnet (using Gateway MAC) send a wake-on-lan.
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
// Perform wake-on-lan
for (i in command.nodeids) {
var nodeid = command.nodeids[i];
// Argument validation
if (common.validateString(nodeid, 8, 128) == false) { // Check the nodeid
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
continue;
}
else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { // Invalid domain, operation only valid for current domain
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid domain' })); } catch (ex) { } }
continue;
}
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
// Check we have the rights to wake this device
if ((node == null) || (visible == false) || (rights & MESHRIGHT_WAKEDEVICE) == 0) {
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
return;
}
if ((rights == MESHRIGHT_ADMIN) || ((rights & MESHRIGHT_NOAMT) == 0)) {
parent.parent.DispatchEvent('*', obj, { action: 'wakedevices', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1 });
}
// If this device is connected on MQTT, send a wake action.
if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.publish(node._id, 'powerAction', 'wake'); }
// If this is a IP-KVM or Power Distribution Unit (PDU), dispatch an action event
if (node.mtype == 4) {
// Send out an event to perform turn off command on the port
const targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['devport-operation', 'server-users', user._id]);
var 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." };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(targets, obj, event);
return;
}
// Get the device interface information
db.Get('if' + node._id, function (err, nodeifs) {
if ((nodeifs != null) && (nodeifs.length == 1)) {
var macs = [], nodeif = nodeifs[0];
if (nodeif.netif) {
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); } }
} else if (nodeif.netif2) {
for (var j in nodeif.netif2) { for (var k in nodeif.netif2[j]) { if ((nodeif.netif2[j][k] != null) && 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); } } }
}
if (macs.length == 0) {
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) { } }
return;
}
// Have the server send a wake-on-lan packet (Will not work in WAN-only)
if (parent.parent.meshScanner != null) { parent.parent.meshScanner.wakeOnLan(macs, node.host); }
// Get the list of device groups this user as wake permissions on
var targets = [], targetDeviceGroups = parent.GetAllMeshWithRights(user, MESHRIGHT_WAKEDEVICE);
for (j in targetDeviceGroups) { targets.push(targetDeviceGroups[j]._id); }
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); } }
// Go thru all the connected agents and send wake-on-lan on all the ones in the target mesh list
var wakeCount = 0;
for (j in parent.wsagents) {
var agent = parent.wsagents[j];
if ((agent.authenticated == 2) && ((targets.indexOf(agent.dbMeshKey) >= 0) || (targets.indexOf(agent.dbNodeKey) >= 0))) {
//console.log('Asking agent ' + agent.dbNodeKey + ' to wake ' + macs.join(','));
try { agent.send(JSON.stringify({ action: 'wakeonlan', macs: macs })); wakeCount++; } catch (ex) { }
}
}
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) { } }
} else {
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'No network information for this device' })); } catch (ex) { } }
}
});
});
if (command.nodeids.length > 1) {
// If we are waking multiple devices, confirm we got the command.
try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
}
}
break;
}
case 'webrelay':
{
if (common.validateString(command.nodeid, 8, 128) == false) { err = 'Invalid node id'; } // Check the nodeid
else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
else if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
else if ((command.port != null) && (common.validateInt(command.port, 1, 65535) == false)) { err = 'Invalid port value'; } // Check the port if present
else {
if (command.nodeid.split('/').length == 1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
var snode = command.nodeid.split('/');
if ((snode.length != 3) || (snode[0] != 'node') || (snode[1] != domain.id)) { err = 'Invalid node id'; }
}
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Get the device rights
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
// If node not found or we don't have remote control, reject.
if (node == null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
return;
}
var relayid = null;
var addr = null;
if (node.mtype == 3) { // Setup device relay if needed
var mesh = parent.meshes[node.meshid];
if (mesh && mesh.relayid) { relayid = mesh.relayid; addr = node.host; }
}
var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : parent.getWebServerName(domain, req);
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));
if (webRelayPort == 0) { try { ws.send(JSON.stringify({ action: 'webrelay', responseid: command.responseid, result: 'WebRelay Disabled' })); return; } catch (ex) { } }
const authRelayCookie = parent.parent.encodeCookie({ ruserid: user._id, x: req.session.x }, parent.parent.loginCookieEncryptionKey);
var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + command.nodeid + '&p=' + command.port + '&appid=' + command.appid + '&c=' + authRelayCookie;
if (addr != null) { url += '&addr=' + addr; }
if (relayid != null) { url += '&relayid=' + relayid }
command.url = url;
if (command.responseid != null) { command.result = 'OK'; }
try { ws.send(JSON.stringify(command)); } catch (ex) { }
});
break;
}
case 'runcommands':
{
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
if (typeof command.presetcmd != 'number') {
if (typeof command.type != 'number') break; // Check command type
if (typeof command.runAsUser != 'number') { command.runAsUser = 0; } // Check runAsUser
}
const processRunCommand = function (command) {
for (i in command.nodeids) {
var nodeid = command.nodeids[i], err = null;
// Argument validation
if (common.validateString(nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check nodeid
else {
if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
}
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: err })); } catch (ex) { } }
continue;
}
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
// Check if this node was found
if (node == null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
return;
}
if (command.type == 4) {
// This is an agent console command
// Check we have the rights to run commands on this device, MESHRIGHT_REMOTECONTROL & MESHRIGHT_AGENTCONSOLE are needed
if ((rights & 24) != 24) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
var theCommand = { action: 'msg', type: 'console', value: command.cmds, rights: rights, sessionid: ws.sessionId };
if (parent.parent.multiServer != null) { // peering setup
parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand});
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
} else {
// Send the commands to the agent
var agent = parent.wsagents[node._id];
if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
try { agent.send(JSON.stringify(theCommand)); } catch (ex) { }
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
}
}
} else {
// This is a standard (bash/shell/powershell) command.
// Check we have the rights to run commands on this device
if ((rights & MESHRIGHT_REMOTECOMMAND) == 0) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
if (typeof command.reply != 'boolean') command.reply = false;
if (typeof command.responseid != 'string') command.responseid = null;
var msgid = 24; // "Running commands"
if (command.type == 1) { msgid = 99; } // "Running commands as user"
if (command.type == 2) { msgid = 100; } // "Running commands as user if possible"
// Check if this agent is correct for this command type
// command.type 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux/BSD/macOS
var commandsOk = false;
if ((node.agent.id > 0) && (node.agent.id < 5) || (node.agent.id > 41 && node.agent.id < 44)) {
// Windows Agent
if ((command.type == 1) || (command.type == 2)) { commandsOk = true; }
else if (command.type === 0) { command.type = 1; commandsOk = true; } // Set the default type of this agent
} else {
// Non-Windows Agent
if (command.type == 3) { commandsOk = true; }
else if (command.type === 0) { command.type = 3; commandsOk = true; } // Set the default type of this agent
}
if (commandsOk == true) {
var theCommand = { action: 'runcommands', type: command.type, cmds: command.cmds, runAsUser: command.runAsUser, reply: command.reply, responseid: command.responseid, sessionid: ws.sessionId };
var agent = parent.wsagents[node._id];
if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
// Send the commands to the agent
try { agent.send(JSON.stringify(theCommand)); } catch (ex) { }
if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
// Send out an event that these commands where run on this device
var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(targets, obj, event);
} else if (parent.parent.multiServer != null) { // peering setup
// Send the commands to the agent
parent.parent.multiServer.DispatchMessage({ action: 'agentCommand', nodeid: node._id, command: theCommand});
if (command.responseid != null && command.reply == false) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
// Send out an event that these commands where run on this device
var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.multiServer.DispatchEvent(targets, obj, event);
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
}
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid command type' })); } catch (ex) { } }
}
}
});
}
}
if (typeof command.presetcmd == 'number') {
// If a pre-set command is used, load the command
if (Array.isArray(domain.preconfiguredscripts) == false) return;
const script = domain.preconfiguredscripts[command.presetcmd];
if (script == null) return;
delete command.presetcmd;
// Decode script type
const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent
if (typeof script.type == 'string') { const stype = types.indexOf(script.type.toLowerCase()); if (stype > 0) { command.type = stype; } }
if (command.type == null) return;
// Decode script runas
if (command.type != 4) {
const runAsModes = ['agent', 'userfirst', 'user']; // 0 = AsAgent, 1 = UserFirst, 2 = UserOnly
if (typeof script.runas == 'string') { const srunas = runAsModes.indexOf(script.runas.toLowerCase()); if (srunas >= 0) { command.runAsUser = srunas; } }
}
if (typeof script.file == 'string') {
// The pre-defined script commands are in a file, load it
const scriptPath = parent.common.joinPath(parent.parent.datapath, script.file);
fs.readFile(scriptPath, function (err, data) {
// If loaded correctly, run loaded commands
if ((err != null) || (data == null) || (data.length == 0) || (data.length > 65535)) return;
command.cmds = data.toString();
processRunCommand(command);
});
} else if (typeof script.cmd == 'string') {
// The pre-defined script commands are right in the config.json, use that
command.cmds = script.cmd;
processRunCommand(command);
}
} else if (typeof command.cmdpath == 'string') {
// If a server command path is used, load the script from the path
var file = parent.getServerFilePath(user, domain, command.cmdpath);
if (file != null) {
fs.readFile(file.fullpath, function (err, data) {
// If loaded correctly, run loaded commands
if ((err != null) || (data == null) || (data.length == 0) || (data.length > 65535)) return;
command.cmds = data.toString();
delete command.cmdpath;
processRunCommand(command);
});
}
} else if (typeof command.cmds == 'string') {
// Run provided commands
if (command.cmds.length > 65535) return;
processRunCommand(command);
}
break;
}
case 'uninstallagent':
{
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
for (i in command.nodeids) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
// Check we have the rights to delete this device
if ((rights & MESHRIGHT_UNINSTALL) == 0) return;
// Send uninstall command to connected agent
const agent = parent.wsagents[node._id];
if (agent != null) {
//console.log('Asking agent ' + agent.dbNodeKey + ' to uninstall.');
try { agent.send(JSON.stringify({ action: 'uninstallagent' })); } catch (ex) { }
}
});
}
break;
}
case 'poweraction':
{
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
if ((Number.isInteger(command.actiontype) == false) || (common.validateInt(command.actiontype, 2, 401) == false)) break;
for (i in command.nodeids) {
var nodeid = command.nodeids[i];
// Argument validation
if (common.validateString(nodeid, 8, 128) == false) { continue; } // Check the nodeid
else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { continue; } // Invalid domain, operation only valid for current domain
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
if ((node == null) || (visible == false)) return;
if ((command.actiontype >= 300) && (command.actiontype < 400) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOAMT) != 0)) return;
if ((command.actiontype >= 400) && ((rights & MESHRIGHT_REMOTECONTROL) != 0)) {
// Flash and vibrate
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); }
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); }
} else {
// Check we have the rights to perform this operation
if ((command.actiontype == 302) && ((rights & MESHRIGHT_WAKEDEVICE) == 0)) return; // This is a Intel AMT power on operation, check if we have WAKE rights
if ((command.actiontype != 302) && ((rights & MESHRIGHT_RESETOFF) == 0)) return; // For all other operations, check that we have RESET/OFF rights
// If this device is connected on MQTT, send a power action.
if ((parent.parent.mqttbroker != null) && (command.actiontype >= 0) && (command.actiontype <= 4)) { parent.parent.mqttbroker.publish(node._id, 'powerAction', ['', '', 'poweroff', 'reset', 'sleep'][command.actiontype]); }
// If this is a IP-KVM or Power Distribution Unit (PDU), dispatch an action event
if (node.mtype == 4) {
// Send out an event to perform turn off command on the port
const targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['devport-operation', 'server-users', user._id]);
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." };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(targets, obj, event);
return;
}
if ((command.actiontype >= 300) && (command.actiontype < 400)) {
if ((command.actiontype != 302) && (command.actiontype != 308) && ((command.actiontype < 310) || (command.actiontype > 316))) return; // Invalid action type.
// 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
parent.parent.DispatchEvent('*', obj, { action: 'amtpoweraction', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, actiontype: command.actiontype - 300 });
} else {
if ((command.actiontype < 2) || (command.actiontype > 4)) return; // Invalid action type.
// Mesh Agent power command, get this device and send the power command
const agent = parent.wsagents[node._id];
if (agent != null) {
try { agent.send(JSON.stringify({ action: 'poweraction', actiontype: command.actiontype, userid: user._id, username: user.name, remoteaddr: req.clientIp })); } catch (ex) { }
}
}
}
});
// Confirm we may be doing something (TODO)
if (command.responseid != null) {
try { ws.send(JSON.stringify({ action: 'poweraction', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'poweraction' })); } catch (ex) { }
}
}
break;
}
case 'toast':
{
var err = null;
// Perform input validation
try {
if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
else if (common.validateString(command.msg, 1, 4096) == false) { err = "Invalid message"; } // Check message
else {
var nodeids = [];
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]); } }
command.nodeids = nodeids;
}
} catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Check the title, if needed, use a default one
if (common.validateString(command.title, 1, 512) == false) { delete command.title } // Check title
if ((command.title == null) && (typeof domain.notificationmessages == 'object') && (typeof domain.notificationmessages.title == 'string')) { command.title = domain.notificationmessages.title; }
if ((command.title == null) && (typeof domain.title == 'string')) { command.title = domain.title; }
if (command.title == null) { command.title = "MeshCentral"; }
for (i in command.nodeids) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeids[i], function (node, rights, visible) {
// Check we have the rights to notify this device
if ((rights & MESHRIGHT_CHATNOTIFY) == 0) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } }
return;
}
// Get this device and send toast command
const agent = parent.wsagents[node._id];
if (agent != null) {
try { agent.send(JSON.stringify({ action: 'toast', title: command.title, msg: command.msg, sessionid: ws.sessionId, username: user.name, userid: user._id })); } catch (ex) { }
}
});
}
// Send response if required
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'toast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'changedevice':
{
var err = null;
// Argument validation
try {
if (common.validateString(command.nodeid, 1, 1024) == false) { err = "Invalid nodeid"; } // Check nodeid
else {
if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) { err = "Invalid nodeid"; } // Invalid domain, operation only valid for current domain
else if ((command.userloc) && ((common.validateArray(command.userloc) == false) || ((command.userloc.length != 2) && (command.userloc.length != 0)))) { err = "Invalid user location"; }
else if ((command.tags != null) && (common.validateString(command.tags, 0, 4096) == false) && (common.validateStrArray(command.tags) == false)) { err = "Invalid tags"; }
}
} catch (ex) { console.log(ex); err = "Validation exception: " + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((rights & MESHRIGHT_MANAGECOMPUTERS) == 0) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'Access Denied' })); } catch (ex) { } }
return;
}
node = common.unEscapeLinksFieldName(node); // unEscape node data for rdp/ssh credentials
var mesh = parent.meshes[node.meshid], amtchange = 0;
// Ready the node change event
var changes = [], event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id };
change = 0;
event.msg = ': ';
// If we are in WAN-only mode, host is not used
if ((args.wanonly == true) && (command.host) && (node.mtype != 3) && (node.mtype != 4)) { delete command.host; }
// Look for a change
if ((typeof command.icon == 'number') && (command.icon != node.icon)) { change = 1; node.icon = command.icon; changes.push('icon'); }
if ((typeof command.name == 'string') && (command.name != node.name)) { change = 1; node.name = command.name; changes.push('name'); }
if ((typeof command.host == 'string') && (command.host != node.host)) { change = 1; node.host = command.host; changes.push('host'); }
if (typeof command.consent == 'number') {
var oldConsent = node.consent;
if (command.consent != node.consent) { node.consent = command.consent; }
if (command.consent == 0) { delete node.consent; }
if (oldConsent != node.consent) { change = 1; changes.push('consent'); }
}
if ((typeof command.rdpport == 'number') && (command.rdpport > 0) && (command.rdpport < 65536)) {
if ((command.rdpport == 3389) && (node.rdpport != null)) {
delete node.rdpport; change = 1; changes.push('rdpport'); // Delete the RDP port
} else {
node.rdpport = command.rdpport; change = 1; changes.push('rdpport'); // Set the RDP port
}
}
if ((typeof command.rfbport == 'number') && (command.rfbport > 0) && (command.rfbport < 65536)) {
if ((command.rfbport == 5900) && (node.rfbport != null)) {
delete node.rfbport; change = 1; changes.push('rfbport'); // Delete the RFB port
} else {
node.rfbport = command.rfbport; change = 1; changes.push('rfbport'); // Set the RFB port
}
}
if ((typeof command.sshport == 'number') && (command.sshport > 0) && (command.sshport < 65536)) {
if ((command.sshport == 22) && (node.sshport != null)) {
delete node.sshport; change = 1; changes.push('sshport'); // Delete the SSH port
} else {
node.sshport = command.sshport; change = 1; changes.push('sshport'); // Set the SSH port
}
}
if ((typeof command.httpport == 'number') && (command.httpport > 0) && (command.httpport < 65536)) {
if ((command.httpport == 80) && (node.httpport != null)) {
delete node.httpport; change = 1; changes.push('httpport'); // Delete the HTTP port
} else {
node.httpport = command.httpport; change = 1; changes.push('httpport'); // Set the HTTP port
}
}
if ((typeof command.httpsport == 'number') && (command.httpsport > 0) && (command.httpsport < 65536)) {
if ((command.httpsport == 443) && (node.httpsport != null)) {
delete node.httpsport; change = 1; changes.push('httpsport'); // Delete the HTTPS port
} else {
node.httpsport = command.httpsport; change = 1; changes.push('httpsport'); // Set the HTTPS port
}
}
if ((typeof command.ssh == 'number') && (command.ssh == 0)) {
if ((node.ssh != null) && (node.ssh[user._id] != null)) { delete node.ssh[user._id]; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
}
if ((typeof command.rdp == 'number') && (command.rdp == 0)) {
if ((node.rdp != null) && (node.rdp[user._id] != null)) { delete node.rdp[user._id]; change = 1; changes.push('rdp'); } // Delete the RDP cendentials
}
// Clean up any legacy RDP and SSH credentials
if (node.rdp != null) { delete node.rdp.d; delete node.rdp.u; delete node.rdp.p; }
if (node.ssh != null) { delete node.ssh.u; delete node.ssh.p; delete node.ssh.k; delete node.ssh.kp; }
if (domain.geolocation && command.userloc && ((node.userloc == null) || (command.userloc[0] != node.userloc[0]) || (command.userloc[1] != node.userloc[1]))) {
change = 1;
if ((command.userloc.length == 0) && (node.userloc)) {
delete node.userloc;
changes.push('location removed');
} else {
command.userloc.push((Math.floor((new Date()) / 1000)));
node.userloc = command.userloc.join(',');
changes.push('location');
}
}
if (command.desc != null && (command.desc != node.desc)) { change = 1; node.desc = command.desc; changes.push('description'); }
if ((command.intelamt != null) && (node.intelamt != null)) {
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.
if ((command.intelamt.user != null) && (command.intelamt.pass != null) && ((command.intelamt.user != node.intelamt.user) || (command.intelamt.pass != node.intelamt.pass))) {
change = 1;
node.intelamt.user = command.intelamt.user;
node.intelamt.pass = command.intelamt.pass;
node.intelamt.warn |= 8; // Change warning to "Trying". Bit flags: 1 = Unknown credentials, 2 = Realm Mismatch, 4 = TLS Cert Mismatch, 8 = Trying credentials
changes.push('Intel AMT credentials');
amtchange = 1;
}
}
// Only allow the user to set Intel AMT TLS state if AMT Manager is not active. AMT manager will auto-detect TLS state.
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'); }
}
if (command.tags) { // Node grouping tag, this is a array of strings that can't be empty and can't contain a comma
var ok = true, group2 = [];
if (common.validateString(command.tags, 0, 4096) == true) { command.tags = command.tags.split(','); }
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); } }
group2.sort();
if (node.tags != group2) { node.tags = group2; change = 1; }
} else if ((command.tags === '') && node.tags) { delete node.tags; change = 1; }
if (change == 1) {
// Save the node
db.Set(parent.cleanDevice(node));
// Event the node change. Only do this if the database will not do it.
event.msg = 'Changed device ' + node.name + ' from group ' + mesh.name + ': ' + changes.join(', ');
event.node = parent.CloneSafeNode(node);
event.msgid = 140;
event.msgArgs = [ node.name, mesh.name, changes.join(', ') ];
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
if (amtchange == 1) { event.amtchange = 1; } // This will give a hint to the AMT Manager to reconnect using new AMT credentials
if (command.rdpport == 3389) { event.node.rdpport = 3389; }
if (command.rfbport == 5900) { event.node.rfbport = 5900; }
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.
parent.parent.DispatchEvent(parent.CreateNodeDispatchTargets(node.meshid, node._id, [user._id]), obj, event);
}
// Send response if required
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changedevice', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
});
break;
}
case 'uploadagentcore':
{
if (common.validateString(command.type, 1, 40) == false) break; // Check path
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
// Go thru all node identifiers and run the operation
for (var i in command.nodeids) {
var nodeid = command.nodeids[i];
if (typeof nodeid != 'string') return;
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
if ((node == null) || (((rights & MESHRIGHT_AGENTCONSOLE) == 0) && (user.siteadmin != SITERIGHT_ADMIN))) return;
// TODO: If we have peer servers, inform...
//if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'uploadagentcore', sessionid: ws.sessionId }); }
if (command.type == 'default') {
// Send the default core to the agent
parent.parent.updateMeshCore(function () { parent.sendMeshAgentCore(user, domain, node._id, 'default'); });
} else if (command.type == 'clear') {
// Clear the mesh agent core on the mesh agent
parent.sendMeshAgentCore(user, domain, node._id, 'clear');
} else if (command.type == 'recovery') {
// Send the recovery core to the agent
parent.sendMeshAgentCore(user, domain, node._id, 'recovery');
} else if (command.type == 'tiny') {
// Send the tiny core to the agent
parent.sendMeshAgentCore(user, domain, node._id, 'tiny');
} else if ((command.type == 'custom') && (common.validateString(command.path, 1, 2048) == true)) {
// Send a mesh agent core to the mesh agent
var file = parent.getServerFilePath(user, domain, command.path);
if (file != null) {
fs.readFile(file.fullpath, 'utf8', function (err, data) {
if (err != null) {
data = common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
parent.sendMeshAgentCore(user, domain, node._id, 'custom', data);
}
});
}
}
});
}
break;
}
case 'inviteAgent':
{
var err = null, mesh = null;
// Resolve the device group name if needed
if ((typeof command.meshname == 'string') && (command.meshid == null)) {
for (var i in parent.meshes) {
var m = parent.meshes[i];
if ((m.mtype == 2) && (m.name == command.meshname) && parent.IsMeshViewable(user, m)) {
if (command.meshid == null) { command.meshid = m._id; } else { err = 'Duplicate device groups found'; }
}
}
}
try {
if ((domain.mailserver == null) || (args.lanonly == true)) { err = 'Unsupported feature'; } // This operation requires the email server
else if ((parent.parent.certificates.CommonName == null) || (parent.parent.certificates.CommonName.indexOf('.') == -1)) { err = 'Unsupported feature'; } // Server name must be configured
else if (common.validateString(command.meshid, 8, 134) == false) { err = 'Invalid group identifier'; } // Check meshid
else {
if (command.meshid.split('/').length == 1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group identifier'; } // Invalid domain, operation only valid for current domain
else if (common.validateString(command.email, 4, 1024) == false) { err = 'Invalid email'; } // Check email
else if (command.email.split('@').length != 2) { err = 'Invalid email'; } // Check email
else {
mesh = parent.meshes[command.meshid];
if (mesh == null) { err = 'Unknown device group'; } // Check if the group exists
else if (mesh.mtype != 2) { err = 'Invalid group type'; } // Check if this is the correct group type
else if ((parent.IsMeshViewable(user, mesh) == false) || ((parent.GetMeshRights(user, mesh) & (MESHRIGHT_EDITMESH | MESHRIGHT_MANAGECOMPUTERS)) == 0)) { err = 'Not allowed'; } // Check if this user has rights to do this
}
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Perform email invitation
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);
// Send a response if needed
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'setDeviceEvent':
{
// Argument validation
if (common.validateString(command.msg, 1, 4096) == false) break; // Check event
var decodedMsg;
try { decodedMsg = decodeURIComponent(command.msg); } catch (ex) { break; }
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if (rights == 0) return;
// Add an event for this device
var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
var event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'manual', msg: decodedMsg, domain: domain.id };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
parent.parent.DispatchEvent(targets, obj, event);
});
break;
}
case 'setNotes':
{
// Argument validation
if (common.validateString(command.id, 1, 1024) == false) break; // Check id
var splitid = command.id.split('/');
if ((splitid.length != 3) || (splitid[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
var idtype = splitid[0];
if ((idtype != 'puser') && (idtype != 'user') && (idtype != 'mesh') && (idtype != 'node')) return;
if (idtype == 'node') {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.id, function (node, rights, visible) {
if ((rights & MESHRIGHT_SETNOTES) != 0) {
// Set the id's notes
if (common.validateString(command.notes, 1) == false) {
db.Remove('nt' + node._id); // Delete the note for this node
} else {
db.Set({ _id: 'nt' + node._id, type: 'note', value: command.notes }); // Set the note for this node
}
}
});
} else if (idtype == 'mesh') {
// Get the mesh for this device
mesh = parent.meshes[command.id];
if (mesh) {
// Check if this user has rights to do this
if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_EDITMESH) == 0) return; // Must have rights to edit the mesh
// Set the id's notes
if (common.validateString(command.notes, 1) == false) {
db.Remove('nt' + command.id); // Delete the note for this node
} else {
db.Set({ _id: 'nt' + command.id, type: 'note', value: command.notes }); // Set the note for this mesh
}
}
} else if ((idtype == 'user') && ((user.siteadmin & 2) != 0)) {
// Set the id's notes
if (common.validateString(command.notes, 1) == false) {
db.Remove('nt' + command.id); // Delete the note for this node
} else {
// Can only perform this operation on other users of our group.
var chguser = parent.users[command.id];
if (chguser == null) break; // This user does not exists
if ((obj.crossDomain !== true) && (chguser.domain != domain.id)) break; // Operation only valid for current domain
if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break;
db.Set({ _id: 'nt' + command.id, type: 'note', value: command.notes }); // Set the note for this user
}
} else if (idtype == 'puser') {
// Set the user's personal note, starts with 'ntp' + userid.
if (common.validateString(command.notes, 1) == false) {
db.Remove('ntp' + user._id); // Delete the note for this node
} else {
db.Set({ _id: 'ntp' + user._id, type: 'note', value: command.notes }); // Set the note for this user
}
}
break;
}
case 'otpemail':
{
// Do not allow this command if 2FA's are locked
if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
// Check input
if (typeof command.enabled != 'boolean') return;
// See if we really need to change the state
if ((command.enabled === true) && (user.otpekey != null)) return;
if ((command.enabled === false) && (user.otpekey == null)) return;
// Change the email 2FA of this user
if (command.enabled === true) { user.otpekey = {}; } else { delete user.otpekey; }
parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpemail', success: true, enabled: command.enabled })); // Report success
// Notify change
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
break;
}
case 'otpduo':
{
// Do not allow this command if 2FA's are locked
if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) return;
// Do not allow if Duo is not supported
if ((typeof domain.duo2factor != 'object') || (typeof domain.duo2factor.integrationkey != 'string') || (typeof domain.duo2factor.secretkey != 'string') || (typeof domain.duo2factor.apihostname != 'string')) return;
// Do not allow if Duo is disabled
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) return;
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
// Check input
if ((typeof command.enabled != 'boolean') || (command.enabled != false)) return;
// See if we really need to change the state
if ((command.enabled === false) && (user.otpduo == null)) return;
// Change the duo 2FA of this user
delete user.otpduo;
parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpduo', success: true, enabled: command.enabled })); // Report success
// Notify change
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
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 };
if(user._id === req.session.userid && req.session.tokenUser != null && req.session.tokenName != null) {
event.tokenName = req.session.tokenName; event.tokenUser = req.session.tokenUser;
}
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.
parent.parent.DispatchEvent(targets, obj, event);
break;
}
case 'otpauth-request':
{
// Do not allow this command if 2FA's are locked
if ((domain.passwordrequirements) && (domain.passwordrequirements.lock2factor == true)) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 1 })); return; }
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 3 })); return; }
// Check of OTP 2FA is allowed
if ((domain.passwordrequireme