MeshCentral/meshuser.js
2021-08-27 14:42:28 -07:00

6562 lines
443 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @description MeshCentral MeshAgent
* @author Ylian Saint-Hilaire & Bryan Roe
* @copyright Intel Corporation 2018-2021
* @license Apache-2.0
* @version v0.0.1
*/
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict";
// 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_ADMIN = 0xFFFFFFFF;
// 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_ADMIN = 0xFFFFFFFF;
// 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;
// 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 a message to the user
//obj.send = function (data) { try { if (typeof data == 'string') { ws.send(Buffer.from(data, 'binary')); } else { ws.send(data); } } catch (e) { } }
// 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 (e) { console.log(e); } } // Soft close, close the websocket
if (arg == 2) { try { obj.ws._socket._parent.end(); parent.parent.debug('user', 'Hard disconnect'); } catch (e) { console.log(e); } } // 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
// 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
delete command.nodeid; // Remove the nodeid since it's implied
try { agent.send(JSON.stringify(command)); } catch (ex) { }
} 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
parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid);
} else { if (func) { func(false); } }
});
} else { if (func) { func(false); } return false; }
}
} else { if (func) { func(false); } return false; }
if (func) { func(true); }
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))) {
ws.send(JSON.stringify({ action: 'stopped', msg: 'Session count exceed' }));
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; }
// 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 {
// 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
ws.send(JSON.stringify({ action: 'event', event: event }));
} 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.
ws.send(JSON.stringify({ action: 'event', event: event }));
} 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]; } } }
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 } }));
}
} else {
// This is not a device group event, we can get this event.
ws.send(JSON.stringify({ action: 'event', event: event }));
}
}
} catch (e) { }
}
};
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
};
if (parent.relaySessionErrorCount != 0) { serverStats.RelayErrors = parent.relaySessionErrorCount; }
if (parent.parent.mpsserver != null) {
serverStats.ConnectedIntelAMT = 0;
for (var i in parent.parent.mpsserver.ciraConnections) { serverStats.ConnectedIntelAMT += parent.parent.mpsserver.ciraConnections[i].length; }
}
// 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 };
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 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.altmessenging == 'object') && (typeof domain.altmessenging.name == 'string') && (typeof domain.altmessenging.url == 'string')) { serverinfo.altmessenging = [{ name: domain.altmessenging.name, url: domain.altmessenging.url }]; }
if (typeof domain.devicemeshrouterlinks == 'object') { serverinfo.devicemeshrouterlinks = domain.devicemeshrouterlinks; }
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 }); } } }
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
// Build the mobile agent URL, this is used to connect mobile devices
var agentServerName = parent.getWebServerName(domain);
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; }
if (typeof domain.userconsentflags == 'number') { serverinfo.consent = domain.userconsentflags; }
if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) { serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); }
if (user.siteadmin === SITERIGHT_ADMIN) {
if (parent.parent.config.settings.managealldevicegroups.indexOf(user._id) >= 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; }
}
// 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.name, 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 (e) { console.log(e); }
// 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 (common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
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 'intersession':
{
// Sends data between sessions of the same user
var sessions = parent.wssessions[obj.user._id];
if (sessions == null) break;
// Create the notification message and send on all sessions except our own (no echo back).
var notification = JSON.stringify(command);
for (var i in sessions) { if (sessions[i] != obj.ws) { try { sessions[i].send(notification); } catch (ex) { } } }
// TODO: Send the message of user sessions connected to other servers.
break;
}
case 'authcookie':
{
// Renew the authentication cookie
try {
ws.send(JSON.stringify({
action: 'authcookie',
cookie: parent.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, parent.parent.loginCookieEncryptionKey),
rcookie: parent.parent.encodeCookie({ ruserid: user._id }, parent.parent.loginCookieEncryptionKey)
}));
} catch (ex) { }
break;
}
case 'logincookie':
{
// If allowed, return a login cookie
if (parent.parent.config.settings.allowlogintoken === true) {
try { ws.send(JSON.stringify({ action: 'logincookie', cookie: parent.parent.encodeCookie({ u: user._id, a: 3 }, parent.parent.loginCookieEncryptionKey) })); } catch (ex) { }
}
break;
}
case 'servertimelinestats':
{
// Only accept if the "My Server" tab is allowed for this domain
if (domain.myserver === false) break;
if ((user.siteadmin & 21) == 0) return; // Only site administrators with "site backup" or "site restore" or "site update" permissions can use this.
if (common.validateInt(command.hours, 0, 24 * 30) == false) return;
db.GetServerStats(command.hours, function (err, docs) {
if (err == null) { try { ws.send(JSON.stringify({ action: 'servertimelinestats', events: docs })); } catch (ex) { } }
});
break;
}
case 'nodes':
{
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'; }
}
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, function (err, docs) {
if (docs == null) { docs = []; }
parent.common.unEscapeAllLinksFieldName(docs);
var r = {};
for (i in docs) {
// Check device links, if a link points to an unknown user, remove it.
parent.cleanDevice(docs[i]);
// 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) { docs[i].ssh = (docs[i].ssh.k) ? 2 : 1; }
// Remove RDP credentials if present
if (docs[i].rdp != null) { docs[i].rdp = 1; }
// 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; }
r[meshid].push(docs[i]);
}
try { ws.send(JSON.stringify({ action: 'nodes', responseid: command.responseid, nodes: r, tag: command.tag })); } 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, 4, 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(path + '/' + command.newfolder); } catch (ex) {
try { fs.mkdirSync(path); } catch (ex) { }
try { fs.mkdirSync(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(path + '/' + command.oldname, 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;
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));
}
}
}
if (sendUpdate == true) { parent.parent.DispatchEvent([user._id], obj, 'updatefiles'); } // Fire an event causing this user to update this files
}
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; }
// 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 = require('url').parse(command.value, true); } catch (ex) { }
if (url == null) break; // Bad URL
if (url.query && url.query.nodeid && (url.query.nodeid != command.nodeid)) break; // Bad NodeID in URL query string
// Check rights
if (url.query.p == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; }
else if ((url.query.p == '4') || (url.query.p == '5')) { requiredNonRights = MESHRIGHT_NOFILES; }
// 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.query.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.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; }
}
// 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.user != null) && ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0)) {
// TODO: Add the meshes command.user has access to (???)
var filter = ['user/' + domain.id + '/' + command.user];
const userSplit = command.user.split('/');
if (userSplit.length == 3) { filter = []; if ((userSplit[0] == 'user') && (userSplit[1] == domain.id)) { filter = [command.user]; } }
if ((command.limit == null) || (typeof command.limit != 'number')) {
// Send the list of all events for this session
db.GetUserEvents(filter, domain.id, command.user, 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.GetUserEventsWithLimit(filter, domain.id, command.user, command.limit, 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 if (command.nodeid != null) { // Device filtered events
// Check that the user has access to this nodeid
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; }
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, 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, 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]); } }
if ((command.limit == null) || (typeof command.limit != 'number')) {
// Send the list of all events for this session
db.GetEvents(filter, domain.id, 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, 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) return;
if ((command.limit == null) || (typeof command.limit != 'number')) {
// Send the list of all recordings
db.GetEvents(['recording'], domain.id, function (err, docs) {
if (err != null) 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, function (err, docs) {
if (err != null) 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 'changelang':
{
// 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;
if (common.validateString(command.lang, 1, 6) == false) return;
// Always lowercase the language
command.lang = command.lang.toLowerCase();
// Update the user's language
var oldlang = user.lang;
if (command.lang == '*') { delete user.lang; } else { user.lang = command.lang; }
parent.db.SetUser(user);
// Event the change
var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, msgid: 2, msgArgs: [(oldlang ? oldlang : 'default'), (user.lang ? user.lang : 'default')] };
if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
message.msg = 'Changed language from ' + (oldlang ? oldlang : 'default') + ' to ' + (user.lang ? user.lang : 'default');
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
parent.parent.DispatchEvent(targets, obj, message);
break;
}
case 'changeemail':
{
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// If the email is the username, this command is not allowed.
if (domain.usernameisemail) return;
// If this account is settings locked, return here.
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return;
// Change our own email address
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
if (common.validateEmail(command.email, 1, 1024) == false) return;
// Always lowercase the email address
command.email = command.email.toLowerCase();
if (obj.user.email != command.email) {
// Check if this email is already validated on a different account
db.GetUserWithVerifiedEmail(domain.id, command.email, function (err, docs) {
if ((docs != null) && (docs.length > 0)) {
// Notify the duplicate email error
try { ws.send(JSON.stringify({ action: 'msg', type: 'notify', title: 'Account Settings', id: Math.random(), tag: 'ServerNotify', value: 'Failed to change email address, another account already using: ' + command.email + '.', titleid: 4, msgid: 13, args: [command.email] })); } catch (ex) { }
} else {
// Update the user's email
var oldemail = user.email;
user.email = command.email;
user.emailVerified = false;
parent.db.SetUser(user);
// Event the change
var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id };
if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
if (oldemail != null) {
message.msg = 'Changed email of user ' + user.name + ' from ' + oldemail + ' to ' + user.email;
} else {
message.msg = 'Set email of user ' + user.name + ' to ' + user.email;
}
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
parent.parent.DispatchEvent(targets, obj, message);
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed email from ' + oldemail + ' to ' + user.email); }
// Send the verification email
if (domain.mailserver != null) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req)); }
}
});
}
break;
}
case 'verifyemail':
{
// 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;
// Send a account email verification email
if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) return;
if (common.validateString(command.email, 3, 1024) == false) return;
// Always lowercase the email address
command.email = command.email.toLowerCase();
if ((domain.mailserver != null) && (obj.user.email.toLowerCase() == command.email)) {
// Send the verification email
domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req));
}
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 (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 (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 (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 'adduserbatch':
{
var err = null;
// Add many new user accounts
if ((user.siteadmin & 2) == 0) { err = 'Access denied'; }
else if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { err = 'Unable to create users when in SSPI or LDAP mode'; }
else if (!Array.isArray(command.users)) { err = 'Invalid users'; }
else {
var userCount = 0;
for (var i in command.users) {
if (domain.usernameisemail) { if (command.users[i].email) { command.users[i].user = command.users[i].email; } else { command.users[i].email = command.users[i].user; } } // If the email is the username, set this here.
if (common.validateUsername(command.users[i].user, 1, 256) == false) { err = 'Invalid username'; break; } // Username is between 1 and 64 characters, no spaces
if ((command.users[i].user[0] == '~') || (command.users[i].user.indexOf('/') >= 0)) { err = 'Invalid username'; break; } // This is a reserved user name or invalid name
if (common.validateString(command.users[i].pass, 1, 256) == false) { err = 'Invalid password'; break; } // Password is between 1 and 256 characters
if (common.checkPasswordRequirements(command.users[i].pass, domain.passwordrequirements) == false) { err = 'Invalid password'; break; } // Password does not meet requirements
if ((command.users[i].email != null) && (common.validateEmail(command.users[i].email, 1, 1024) == false)) { err = 'Invalid email'; break; } // Check if this is a valid email address
userCount++;
}
}
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adduserbatch', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Check if we exceed the maximum number of user accounts
db.isMaxType(domain.limits.maxuseraccounts + userCount, 'user', domain.id, function (maxExceed) {
if (maxExceed) {
// Account count exceed, do notification
// Create the notification message
var notification = { action: 'msg', type: 'notify', id: Math.random(), value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: domain.id, titleid: 2, msgid: 10 };
// Get the list of sessions for this user
var sessions = parent.wssessions[user._id];
if (sessions != null) { for (i in sessions) { try { if (sessions[i].domainid == domain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } }
// TODO: Notify all sessions on other peers.
} else {
for (var i in command.users) {
// Check if this is an existing user
var newuserid = 'user/' + domain.id + '/' + command.users[i].user.toLowerCase();
var newuser = { type: 'user', _id: newuserid, name: command.users[i].user, creation: Math.floor(Date.now() / 1000), domain: domain.id };
if (domain.newaccountsrights) { newuser.siteadmin = domain.newaccountsrights; }
if (common.validateString(command.users[i].realname, 1, 256)) { newuser.realname = command.users[i].realname; }
if (command.users[i].email != null) { newuser.email = command.users[i].email.toLowerCase(); if (command.users[i].emailVerified === true) { newuser.emailVerified = true; } } // Email, always lowercase
if (command.users[i].resetNextLogin === true) { newuser.passchange = -1; } else { newuser.passchange = Math.floor(Date.now() / 1000); }
if (user.groups) { newuser.groups = user.groups; } // New accounts are automatically part of our groups (Realms).
if (parent.users[newuserid] == null) {
parent.users[newuserid] = newuser;
// Create a user, generate a salt and hash the password
require('./pass').hash(command.users[i].pass, function (err, salt, hash, newuser) {
if (err) throw err;
newuser.salt = salt;
newuser.hash = hash;
db.SetUser(newuser);
var event, targets = ['*', 'server-users'];
if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
if (newuser.email == null) {
event = { etype: 'user', userid: newuser._id, username: newuser.name, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 64, msgArgs: [newuser.name], msg: 'Account created, username is ' + newuser.name, domain: domain.id };
} else {
event = { etype: 'user', userid: newuser._id, username: newuser.name, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 65, msgArgs: [newuser.email], msg: 'Account created, email is ' + newuser.email, domain: domain.id };
}
if (parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
parent.parent.DispatchEvent(targets, obj, event);
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created user account ' + newuser.name); }
}, newuser);
}
}
}
});
break;
}
case 'adduser':
{
// If the email is the username, set this here.
if (domain.usernameisemail) { if (command.email) { command.username = command.email; } else { command.email = command.username; } }
// Randomize the password if needed
if (command.randomPassword === true) { command.pass = getRandomPassword(); }
// Add a new user account
var err = null, errid = 0, newusername, newuserid, newuserdomain;
try {
if ((user.siteadmin & 2) == 0) { err = "Permission denied"; errid = 1; }
else if (common.validateUsername(command.username, 1, 256) == false) { err = "Invalid username"; errid = 2; } // Username is between 1 and 64 characters, no spaces
else if ((command.username[0] == '~') || (command.username.indexOf('/') >= 0)) { err = "Invalid username"; errid = 2; } // Usernames cant' start with ~ and can't have '/'
else if (common.validateString(command.pass, 1, 256) == false) { err = "Invalid password"; errid = 3; } // Password is between 1 and 256 characters
else if ((command.randomPassword !== true) && (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false)) { err = "Invalid password"; errid = 3; } // Password does not meet requirements
else if ((command.email != null) && (common.validateEmail(command.email, 1, 1024) == false)) { err = "Invalid email"; errid = 4; } // Check if this is a valid email address
else if ((obj.crossDomain === true) && (command.domain != null) && ((typeof command.domain != 'string') || (parent.parent.config.domains[command.domain] == null))) { err = "Invalid domain"; errid = 5; } // Check if this is a valid domain
else {
newuserdomain = domain;
if ((obj.crossDomain === true) && (command.domain != null)) { newuserdomain = parent.parent.config.domains[command.domain]; }
newusername = command.username;
newuserid = 'user/' + newuserdomain.id + '/' + command.username.toLowerCase();
if (command.siteadmin != null) {
if ((typeof command.siteadmin != 'number') || (Number.isInteger(command.siteadmin) == false)) { err = "Invalid site permissions"; errid = 6; } // Check permissions
else if ((user.siteadmin != SITERIGHT_ADMIN) && ((command.siteadmin & (SITERIGHT_ADMIN - 224)) != 0)) { err = "Invalid site permissions"; errid = 6; }
}
if (parent.users[newuserid]) { err = "User already exists"; errid = 7; } // Account already exists
else if ((newuserdomain.auth == 'sspi') || (newuserdomain.auth == 'ldap')) { err = "Unable to add user in this mode"; errid = 8; }
}
} catch (ex) { err = "Validation exception"; errid = 9; }
// Handle any errors
if (err != null) {
if (command.responseid != null) {
try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: err, msgid: errid })); } catch (ex) { }
} else {
// Send error back, user not found.
displayNotificationMessage(err, "New Account", 'ServerNotify', null, 1, errid);
}
break;
}
// Check if we exceed the maximum number of user accounts
db.isMaxType(newuserdomain.limits.maxuseraccounts, 'user', newuserdomain.id, function (maxExceed) {
if (maxExceed) {
// Account count exceed, do notification
if (command.responseid != null) {
// Respond privately if requested
try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: 'maxUsersExceed' })); } catch (ex) { }
} else {
// Create the notification message
var notification = { action: 'msg', type: 'notify', id: Math.random(), value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: newuserdomain.id, titleid: 2, msgid: 10 };
// Get the list of sessions for this user
var sessions = parent.wssessions[user._id];
if (sessions != null) { for (i in sessions) { try { if (sessions[i].domainid == newuserdomain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } }
// TODO: Notify all sessions on other peers.
}
} else {
// Remove any events for this userid
if (command.removeEvents === true) { db.RemoveAllUserEvents(newuserdomain.id, newuserid); }
// Create a new user
var newuser = { type: 'user', _id: newuserid, name: newusername, creation: Math.floor(Date.now() / 1000), domain: newuserdomain.id };
if (command.siteadmin != null) { newuser.siteadmin = command.siteadmin; }
else if (newuserdomain.newaccountsrights) { newuser.siteadmin = newuserdomain.newaccountsrights; }
if (command.email != null) { newuser.email = command.email.toLowerCase(); if (command.emailVerified === true) { newuser.emailVerified = true; } } // Email
if (command.resetNextLogin === true) { newuser.passchange = -1; } else { newuser.passchange = Math.floor(Date.now() / 1000); }
if (user.groups) { newuser.groups = user.groups; } // New accounts are automatically part of our groups (Realms).
if (common.validateString(command.realname, 1, 256)) { newuser.realname = command.realname; }
if ((command.consent != null) && (typeof command.consent == 'number')) { if (command.consent == 0) { delete chguser.consent; } else { newuser.consent = command.consent; } change = 1; }
if ((command.phone != null) && (typeof command.phone == 'string') && ((command.phone == '') || isPhoneNumber(command.phone))) { if (command.phone == '') { delete newuser.phone; } else { newuser.phone = command.phone; } change = 1; }
// Auto-join any user groups
if (typeof newuserdomain.newaccountsusergroups == 'object') {
for (var i in newuserdomain.newaccountsusergroups) {
var ugrpid = newuserdomain.newaccountsusergroups[i];
if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + newuserdomain.id + '/' + ugrpid; }
var ugroup = parent.userGroups[ugrpid];
if (ugroup != null) {
// Add group to the user
if (newuser.links == null) { newuser.links = {}; }
newuser.links[ugroup._id] = { rights: 1 };
// Add user to the group
ugroup.links[newuser._id] = { userid: newuser._id, name: newuser.name, rights: 1 };
db.Set(ugroup);
// Notify user group change
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 80, msgArgs: [newuser.name, ugroup.name], msg: 'Added user ' + newuser.name + ' to user group ' + ugroup.name, addUserDomain: newuserdomain.id };
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, newuser._id], obj, event);
}
}
}
parent.users[newuserid] = newuser;
// Create a user, generate a salt and hash the password
require('./pass').hash(command.pass, function (err, salt, hash, tag) {
if (err == null) {
newuser.salt = salt;
newuser.hash = hash;
db.SetUser(newuser);
var event, targets = ['*', 'server-users'];
if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
if (command.email == null) {
event = { etype: 'user', userid: newuser._id, username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 64, msgArgs: [command.username], msg: 'Account created, username is ' + command.username, domain: newuserdomain.id };
} else {
event = { etype: 'user', userid: newuser._id, username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msgid: 65, msgArgs: [command.email.toLowerCase()], msg: 'Account created, email is ' + command.email.toLowerCase(), domain: newuserdomain.id };
}
if (parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
parent.parent.DispatchEvent(targets, obj, event);
// Perform email invitation
if ((command.emailInvitation == true) && (command.emailVerified == true) && command.email && domain.mailserver) {
domain.mailserver.sendAccountInviteMail(newuserdomain, (user.realname ? user.realname : user.name), newusername, command.email.toLowerCase(), command.pass, parent.getLanguageCodes(req));
}
// Log in the auth log
if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' created a user account ' + newuser.name); }
// OK Response
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: 'passwordHashError' })); } catch (ex) { } }
}
}, 0);
}
});
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.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) && (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 (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 'updateUserImage':
{
if (req.session.loginToken != null) break; // Do not allow this command when logged in using a login token
var uid = user._id;
if ((typeof command.userid == 'string') && ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0)) { uid = command.userid; }
var chguser = parent.users[uid], flags = 0, change = 0;
if (chguser == null) break;
if (typeof chguser.flags == 'number') { flags = chguser.flags; }
if (command.image == 0) {
// Delete the image
db.Remove('im' + uid);
if ((flags & 1) != 0) { flags -= 1; change = 1; }
} else if ((typeof command.image == 'string') && (command.image.length < 600000) && ((command.image.startsWith('data:image/png;base64,') || (command.image.startsWith('data:image/jpeg;base64,'))))) {
// Save the new image
db.Set({ _id: 'im' + uid, image: command.image });
if ((flags & 1) == 0) { flags += 1; }
change = 1;
}
// Update the user if needed
if (change == 1) {
chguser.flags = flags;
db.SetUser(chguser);
// Event the change
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: uid, username: chguser.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 66, msgArgs: [chguser.name], msg: 'Account changed: ' + chguser.name, domain: domain.id, accountImageChange: 1 };
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 '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 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 (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 (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 device 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 };
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 displatched.
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 = 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];
// 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 (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 (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 (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
var group = parent.userGroups[command.ugrpid];
if (group != null) {
if ((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 (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, msg: change, domain: domain.id };
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 'addusertousergroup':
{
var err = null;
try {
if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; }
else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid
else if (common.validateStrArray(command.usernames, 1, 64) == false) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters
else {
var ugroupidsplit = command.ugrpid.split('/');
if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = 'Invalid groupid'; }
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Fetch the domain
var addUserDomain = domain;
if (obj.crossDomain === true) { addUserDomain = parent.parent.config.domains[ugroupidsplit[1]]; }
if (addUserDomain == null) { err = 'Invalid domain'; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addusertousergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Get the user group
var group = parent.userGroups[command.ugrpid];
if (group != null) {
if (group.links == null) { group.links = {}; }
var unknownUsers = [], addedCount = 0, failCount = 0;
for (var i in command.usernames) {
// Check if the user exists
var chguserid = 'user/' + addUserDomain.id + '/' + command.usernames[i].toLowerCase();
var chguser = parent.users[chguserid];
if (chguser == null) { chguserid = 'user/' + addUserDomain.id + '/' + command.usernames[i]; chguser = parent.users[chguserid]; }
if (chguser != null) {
// Add mesh to user
if (chguser.links == null) { chguser.links = {}; }
chguser.links[group._id] = { rights: 1 };
db.SetUser(chguser);
parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
// Notify user change
var targets = ['*', 'server-users', user._id, chguser._id];
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 67, msgArgs: [chguser.name], msg: 'User group membership changed: ' + chguser.name, domain: addUserDomain.id };
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);
// Add a user to the user group
group.links[chguserid] = { userid: chguser._id, name: chguser.name, rights: 1 };
addedCount++;
} else {
unknownUsers.push(command.usernames[i]);
failCount++;
}
}
if (addedCount > 0) {
// Save the new group to the database
db.Set(group);
// Notify user group change
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, action: 'usergroupchange', links: group.links, msgid: 71, msgArgs: [chguser.name, group.name], msg: 'Added user ' + chguser.name + ' to user group ' + group.name, addUserDomain: domain.id };
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(['*', group._id, user._id, chguserid], obj, event);
}
if (unknownUsers.length > 0) {
// Send error back, user not found.
displayNotificationMessage('User' + ((unknownUsers.length > 1) ? 's' : '') + ' ' + EscapeHtml(unknownUsers.join(', ')) + ' not found.', "Device Group", 'ServerNotify', 5, (unknownUsers.length > 1) ? 16 : 15, [EscapeHtml(unknownUsers.join(', '))]);
}
}
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addusertousergroup', responseid: command.responseid, result: 'ok', added: addedCount, failed: failCount })); } catch (ex) { } }
break;
}
case 'removeuserfromusergroup':
{
var err = null;
try {
if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; }
else if (common.validateString(command.ugrpid, 1, 1024) == false) { err = 'Invalid groupid'; }
else if (common.validateString(command.userid, 1, 256) == false) { err = 'Invalid userid'; }
else {
var ugroupidsplit = command.ugrpid.split('/');
if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || ((obj.crossDomain !== true) && (ugroupidsplit[1] != domain.id))) { err = 'Invalid groupid'; }
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Fetch the domain
var removeUserDomain = domain;
if (obj.crossDomain !== true) { removeUserDomain = parent.parent.config.domains[ugroupidsplit[1]]; }
if (removeUserDomain == null) { err = 'Invalid domain'; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeuserfromusergroup', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Check if the user exists
if (command.userid.startsWith('user/') == false) {
if (parent.users['user/' + removeUserDomain.id + '/' + command.userid.toLowerCase()] != null) { command.userid = 'user/' + removeUserDomain.id + '/' + command.userid.toLowerCase(); }
else if (parent.users['user/' + removeUserDomain.id + '/' + command.userid] != null) { command.userid = 'user/' + removeUserDomain.id + '/' + command.userid; }
}
var chguser = parent.users[command.userid];
if (chguser != null) {
var change = false;
if ((chguser.links != null) && (chguser.links[command.ugrpid] != null)) {
change = true;
delete chguser.links[command.ugrpid];
// Notify user change
var targets = ['*', 'server-users', user._id, chguser._id];
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(chguser), action: 'accountchange', msgid: 67, msgArgs: [chguser.name], msg: 'User group membership changed: ' + chguser.name, domain: removeUserDomain.id };
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);
db.SetUser(chguser);
parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe');
}
// Get the user group
var group = parent.userGroups[command.ugrpid];
if (group != null) {
// Remove the user from the group
if ((group.links != null) && (group.links[command.userid] != null)) {
change = true;
delete group.links[command.userid];
db.Set(group);
// Notify user group change
if (change) {
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, action: 'usergroupchange', links: group.links, msgid: 72, msgArgs: [chguser.name, group.name], msg: 'Removed user ' + chguser.name + ' from user group ' + group.name, domain: removeUserDomain.id };
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(['*', group._id, user._id, chguser._id], obj, event);
}
}
}
}
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeuserfromusergroup', responseid: command.responseid, result: 'ok', added: addedCount, failed: failCount })); } catch (ex) { } }
break;
}
case 'changemeshnotify':
{
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
var err = null;
try {
// Change the current user's notification flags for a meshid
if (common.validateString(command.meshid, 1, 1024) == 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 notification (TODO: Add user group support, not sure how to do this here)
// TODO (UserGroups)
if (user.links[command.meshid]) {
if (command.notify == 0) {
delete user.links[command.meshid].notify;
} else {
user.links[command.meshid].notify = 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 (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;
// 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 (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 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
}
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 (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 ((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 ((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) + ":" + 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) + ":" + 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 64 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 > 3)) { err = 'Invalid group type'; } // Device group types are 1 = AMT, 2 = Agent, 3 = Local
else if ((parent.args.wanonly == true) && (command.meshtype == 3)) { err = 'Invalid group type'; } // Local device group type is not allowed in WAN mode
} 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 };
// Add flags and consent if present
if (typeof command.flags == 'number') { mesh.flags = command.flags; }
if (typeof command.consent == 'number') { mesh.consent = command.consent; }
// 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 (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, name: command.meshname, mtype: command.meshtype, desc: command.desc, action: 'createmesh', links: links, msgid: 76, msgArgs: [command.meshname], msg: 'Device group created: ' + command.meshname, domain: domain.id, creation: mesh.creation, creatorid: mesh.creatorid, creatorname: mesh.creatorname, flags: mesh.flags, consent: mesh.consent };
parent.parent.DispatchEvent(['*', 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) { }
});
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, 1, 1024) == 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, meshid: command.meshid, name: command.meshname, action: 'deletemesh', msgid: 77, msgArgs: [command.meshname], msg: 'Device group deleted: ' + command.meshname, domain: domain.id };
parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(command.meshid), 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 (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 (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
// 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, 1, 1024) == 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
if ((common.validateString(command.meshname, 1, 128) == true) && (command.meshname != mesh.name)) { change = 'Group name changed from "' + mesh.name + '" to "' + command.meshname + '"'; mesh.name = command.meshname; }
if ((common.validateString(command.desc, 0, 1024) == true) && (command.desc != mesh.desc)) { if (change != '') change += ' and description changed'; else change += 'Group "' + mesh.name + '" description changed'; mesh.desc = command.desc; }
if ((common.validateInt(command.flags) == true) && (command.flags != mesh.flags)) { if (change != '') change += ' and flags changed'; else change += 'Group "' + mesh.name + '" flags changed'; mesh.flags = command.flags; }
if ((common.validateInt(command.consent) == true) && (command.consent != mesh.consent)) { if (change != '') change += ' and consent changed'; else change += 'Group "' + mesh.name + '" consent changed'; 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 += 'Group "' + mesh.name + '" auto-remove changed'; if (command.expireDevs == 0) { delete mesh.expireDevs; } else { mesh.expireDevs = command.expireDevs; } }
// 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 += 'Group "' + mesh.name + '" invite code changed'; }
} else if ((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 (change != '') { change += ' and invite code changed'; } else { change += 'Group "' + mesh.name + '" invite code changed'; }
}
}
}
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, msg: change, domain: domain.id, invite: mesh.invite, expireDevs: command.expireDevs };
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 (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'editmesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
break;
}
case 'addmeshuser':
{
var err = null, mesh, meshIdSplit;
if (typeof command.userid == 'string') { command.userids = [command.userid]; }
// 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'; }
}
}
}
var selfMeshRights = 0;
try {
if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid
else if (common.validateInt(command.meshadmin) == false) { err = 'Invalid group rights'; } // Mesh rights must be an integer
else if ((common.validateStrArray(command.usernames, 1, 64) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters
else {
if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
mesh = parent.meshes[command.meshid];
meshIdSplit = command.meshid.split('/');
if (mesh == null) { err = 'Unknown group'; }
else if (((selfMeshRights = parent.GetMeshRights(user, mesh)) & MESHRIGHT_MANAGEUSERS) == 0) { err = 'Permission denied'; }
else if ((meshIdSplit.length != 3) || (meshIdSplit[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addmeshuser', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Convert user names to userid's
if (command.userids == null) {
command.userids = [];
for (var i in command.usernames) {
if (parent.users['user/' + domain.id + '/' + command.usernames[i].toLowerCase()] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i].toLowerCase()); }
else if (parent.users['user/' + domain.id + '/' + command.usernames[i]] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i]); }
}
}
var unknownUsers = [], successCount = 0, failCount = 0, msgs = [];
for (var i in command.userids) {
// Check if the user exists
var newuserid = command.userids[i], newuser = null;
if (newuserid.startsWith('user/')) { newuser = parent.users[newuserid]; }
else if (newuserid.startsWith('ugrp/')) { newuser = parent.userGroups[newuserid]; }
// Search for a user name in that windows domain is the username starts with *\
if ((newuser == null) && (newuserid.startsWith('user/' + domain.id + '/*\\')) == true) {
var search = newuserid.split('/')[2].substring(1);
for (var i in parent.users) { if (i.endsWith(search) && (parent.users[i].domain == domain.id)) { newuser = parent.users[i]; command.userids[i] = newuserid = parent.users[i]._id; break; } }
}
// Make sure this user is in the same domain as the device group
if (meshIdSplit[1] != newuserid.split('/')[1]) { msgs.push("Mismatch domains"); continue; }
if (newuser != null) {
// Can't add or modify self
if (newuserid == obj.user._id) { msgs.push("Can't change self"); continue; }
var targetMeshRights = 0;
if ((newuser.links != null) && (newuser.links[command.meshid] != null) && (newuser.links[command.meshid].rights != null)) { targetMeshRights = newuser.links[command.meshid].rights; }
if ((targetMeshRights === MESHRIGHT_ADMIN) && (selfMeshRights != MESHRIGHT_ADMIN)) { msgs.push("Can't change rights of device group administrator"); continue; } // A non-admin can't kick out an admin
if (command.remove === true) {
// Remove mesh from user or user group
delete newuser.links[command.meshid];
} else {
// Adjust rights since we can't add more rights that we have outself for MESHRIGHT_MANAGEUSERS
if ((selfMeshRights != MESHRIGHT_ADMIN) && (command.meshadmin == MESHRIGHT_ADMIN)) { msgs.push("Can't set device group administrator, if not administrator"); continue; }
if (((selfMeshRights & 2) == 0) && ((command.meshadmin & 2) != 0) && ((targetMeshRights & 2) == 0)) { command.meshadmin -= 2; }
// Add mesh to user or user group
if (newuser.links == null) { newuser.links = {}; }
if (newuser.links[command.meshid]) { newuser.links[command.meshid].rights = command.meshadmin; } else { newuser.links[command.meshid] = { rights: command.meshadmin }; }
}
if (newuserid.startsWith('user/')) { db.SetUser(newuser); }
else if (newuserid.startsWith('ugrp/')) { db.Set(newuser); }
parent.parent.DispatchEvent([newuser._id], obj, 'resubscribe');
if (newuserid.startsWith('user/')) {
// Notify user change
var targets = ['*', 'server-users', user._id, newuser._id];
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(newuser), action: 'accountchange', msgid: 78, msgArgs: [newuser.name], msg: 'Device group membership changed: ' + newuser.name, domain: domain.id };
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 (newuserid.startsWith('ugrp/')) {
// Notify user group change
var targets = ['*', 'server-ugroups', user._id, newuser._id];
var event = { etype: 'ugrp', username: user.name, ugrpid: newuser._id, name: newuser.name, desc: newuser.desc, action: 'usergroupchange', links: newuser.links, msgid: 79, msgArgs: [newuser.name], msg: 'User group changed: ' + newuser.name, domain: domain.id };
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);
}
var event;
if (command.remove === true) {
// Remove userid from the mesh
delete mesh.links[newuserid];
db.Set(mesh);
event = { etype: 'mesh', username: newuser.name, userid: user._id, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Removed user ' + newuser.name + ' from device group ' + mesh.name, domain: domain.id, invite: mesh.invite };
} else {
// Add userid to the mesh
mesh.links[newuserid] = { name: newuser.name, rights: command.meshadmin };
db.Set(mesh);
event = { etype: 'mesh', username: newuser.name, userid: user._id, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Added user ' + newuser.name + ' to device group ' + mesh.name, domain: domain.id, invite: mesh.invite };
}
// Notify mesh change
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, newuserid]), obj, event);
if (command.remove === true) { msgs.push("Removed user " + newuserid.split('/')[2]); } else { msgs.push("Added user " + newuserid.split('/')[2]); }
successCount++;
} else {
msgs.push("Unknown user " + newuserid.split('/')[2]);
unknownUsers.push(newuserid.split('/')[2]);
failCount++;
}
}
if ((successCount == 0) && (failCount == 0)) { msgs.push("Nothing done"); }
if (unknownUsers.length > 0) {
// Send error back, user not found.
displayNotificationMessage('User' + ((unknownUsers.length > 1) ? 's' : '') + ' ' + EscapeHtml(unknownUsers.join(', ')) + ' not found.', "Device Group", 'ServerNotify', 5, (unknownUsers.length > 1) ? 16 : 15, [EscapeHtml(unknownUsers.join(', '))]);
}
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addmeshuser', responseid: command.responseid, result: msgs.join(', '), success: successCount, failed: failCount })); } catch (ex) { } }
break;
}
case 'adddeviceuser': {
if (typeof command.userid == 'string') { command.userids = [command.userid]; }
var err = null, nodeIdSplit;
try {
if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid
else if (common.validateInt(command.rights) == false) { err = 'Invalid rights'; } // Device rights must be an integer
else if ((command.rights & 7) != 0) { err = 'Invalid rights'; } // EDITMESH, MANAGEUSERS or MANAGECOMPUTERS rights can't be assigned to a user to device link
else if ((common.validateStrArray(command.usernames, 1, 64) == false) && (common.validateStrArray(command.userids, 1, 128) == false)) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters
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
}
} catch (ex) { err = 'Validation exception: ' + ex; }
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adddeviceuser', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Convert user names to userid's
if (command.userids == null) {
command.userids = [];
for (var i in command.usernames) {
if (command.usernames[i] != null) {
if (parent.users['user/' + domain.id + '/' + command.usernames[i].toLowerCase()] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i].toLowerCase()); }
else if (parent.users['user/' + domain.id + '/' + command.usernames[i]] != null) { command.userids.push('user/' + domain.id + '/' + command.usernames[i]); }
}
}
}
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
// Check if already in the right mesh
if ((node == null) || (node.meshid == command.meshid)) return;
var dispatchTargets = ['*', node.meshid, node._id];
// Check that we have rights to manage users on this device
if ((rights & MESHRIGHT_MANAGEUSERS) == 0) return;
// Add the new link to the users
var nodeChanged = false;
for (var i in command.userids) {
var newuserid = command.userids[i];
// Add a user
var newuser = null;
if (newuserid.startsWith('ugrp/')) { newuser = parent.userGroups[newuserid]; }
if (newuserid.startsWith('user/')) {
newuser = parent.users[newuserid];
// Search for a user name in that windows domain is the username starts with *\
if ((newuser == null) && (newuserid.startsWith('user/' + domain.id + '/*\\')) == true) {
var search = newuserid.split('/')[2].substring(1);
for (var i in parent.users) { if (i.endsWith(search) && (parent.users[i].domain == domain.id)) { newuser = parent.users[i]; command.userids[i] = newuserid = newuser._id; break; } }
}
}
// Check the the user and device are in the same domain
if (command.nodeid.split('/')[1] != newuserid.split('/')[1]) return; // Domain mismatch
if (newuser != null) {
// Add this user to the dispatch target list
dispatchTargets.push(newuser._id);
if (command.remove === true) {
// Remove link to this user
if (newuser.links != null) {
delete newuser.links[command.nodeid];
if (Object.keys(newuser.links).length == 0) { delete newuser.links; }
}
// Remove link to this device
if (node.links != null) {
delete node.links[newuserid];
nodeChanged = true;
if (Object.keys(node.links).length == 0) { delete node.links; }
}
} else {
// Add the new link to this user
if (newuser.links == null) { newuser.links = {}; }
newuser.links[command.nodeid] = { rights: command.rights };
// Add the new link to the device
if (node.links == null) { node.links = {}; }
node.links[newuserid] = { rights: command.rights }
nodeChanged = true;
}
// Save the user to the database
if (newuserid.startsWith('user/')) {
db.SetUser(newuser);
parent.parent.DispatchEvent([newuser], obj, 'resubscribe');
// Notify user change
var targets = ['*', 'server-users', newuserid];
var event;
if (command.rights == 0) {
event = { etype: 'user', userid: user._id, username: user.name, action: 'accountchange', msgid: 81, msgArgs: [newuser.name], msg: 'Removed user device rights for ' + newuser.name, domain: domain.id, account: parent.CloneSafeUser(newuser), nodeListChange: newuserid };
} else {
event = { etype: 'user', userid: user._id, username: user.name, action: 'accountchange', msgid: 82, msgArgs: [newuser.name], msg: 'Changed user device rights for ' + newuser.name, domain: domain.id, account: parent.CloneSafeUser(newuser), nodeListChange: newuserid };
}
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 (newuserid.startsWith('ugrp/')) {
db.Set(newuser);
// Notify user group change
var targets = ['*', 'server-ugroups', newuser._id];
var event = { etype: 'ugrp', username: user.name, ugrpid: newuser._id, name: newuser.name, action: 'usergroupchange', links: newuser.links, msgid: 79, msgArgs: [newuser.name], msg: 'User group changed: ' + newuser.name, domain: domain.id };
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);
}
}
}
// Save the device
if (nodeChanged == true) {
// Save the node to the database
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: domain.id, msgid: 81, 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: domain.id, msgid: 82, msgArgs: [node.name], msg: 'Changed user device rights for ' + node.name, node: parent.CloneSafeNode(node) }
}
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(dispatchTargets, obj, event);
}
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adddeviceuser', 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, 1, 1024) == 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 ((delmeshrights == MESHRIGHT_ADMIN) && (parent.GetMeshRights(user, mesh) != MESHRIGHT_ADMIN)) return; // A non-admin can't kick out an admin
delete deluser.links[command.meshid];
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 (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 (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 };
}
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, 1, 1024) == 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];
change = '';
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
change = '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, msg: change, domain: domain.id, invite: mesh.invite };
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':
{
if (args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added
if (common.validateString(command.meshid, 1, 1024) == false) break; // Check meshid
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
if (common.validateString(command.devicename, 1, 256) == false) break; // Check device name
if (common.validateString(command.hostname, 1, 256) == false) break; // Check hostname
if (typeof command.type != 'number') break; // Type must be a number
if ((command.type != 4) && (command.type != 6) && (command.type != 29)) break; // Check device type
// Get the mesh
mesh = parent.meshes[command.meshid];
if (mesh) {
if (mesh.mtype != 3) return; // This operation is only allowed for mesh type 3, local device agentless mesh.
// Check if this user has rights to do this
if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) return;
// 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 });
});
}
break;
}
case 'addamtdevice':
{
if (args.wanonly == true) return; // This is a WAN-only server, local Intel AMT computers can't be added
if (common.validateString(command.meshid, 1, 1024) == false) break; // Check meshid
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
if (common.validateString(command.devicename, 1, 256) == false) break; // Check device name
if (common.validateString(command.hostname, 1, 256) == false) break; // Check hostname
if (common.validateString(command.amtusername, 0, 16) == false) break; // Check username
if (common.validateString(command.amtpassword, 0, 16) == false) break; // 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)) break;
// If we are in WAN-only mode, hostname is not used
if ((args.wanonly == true) && (command.hostname)) { delete command.hostname; }
// Get the mesh
mesh = parent.meshes[command.meshid];
if (mesh) {
if (mesh.mtype != 1) return; // This operation is only allowed for mesh type 1, Intel AMT agentless mesh.
// Check if this user has rights to do this
if ((parent.GetMeshRights(user, mesh) & MESHRIGHT_MANAGECOMPUTERS) == 0) return;
// 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 });
});
}
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, 1, 1024) == 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;
}
// 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) { } } 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) { } } return; }
// Make sure both source and target mesh are the same type
try { if (parent.meshes[node.meshid].mtype != parent.meshes[command.meshid].mtype) 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) { } }
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) { } }
return;
}
// Perform the switch, start by saving the node with the new 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 };
// 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
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;
// 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._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 & save the user
delete cuser.links[node._id];
if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
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);
}
}
}
}
// 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
}
});
}
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
// Event wakeup, this will cause Intel AMT wake operations on this and other servers.
parent.parent.DispatchEvent('*', obj, { action: 'wakedevices', userid: user._id, username: user.name, nodeids: command.nodeids, domain: domain.id, nolog: 1 });
// Perform wake-on-lan
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) {
// Check we have the rights to delete this device
if ((rights & MESHRIGHT_WAKEDEVICE) == 0) return;
// If this device is connected on MQTT, send a wake action.
if (parent.parent.mqttbroker != null) { parent.parent.mqttbroker.publish(node._id, 'powerAction', 'wake'); }
// 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].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) 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
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 })); } catch (ex) { }
}
}
}
});
});
// Confirm we may be doing something (TODO)
if (command.responseid != null) {
try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'wakedevices' })); } catch (ex) { }
}
}
break;
}
case 'runcommands':
{
if (common.validateArray(command.nodeids, 1) == false) break; // Check nodeid's
if (typeof command.type != 'number') break; // Check command type
if (typeof command.cmds != 'string') break; // Check commands
if (typeof command.runAsUser != 'number') { command.runAsUser = 0; } // Check runAsUser
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;
}
// 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;
}
// Get the agent and run the commands
var agent = parent.wsagents[node._id];
if ((agent != null) && (agent.authenticated == 2) && (agent.agentInfo != null)) {
// 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 ((agent.agentInfo.agentId > 0) && (agent.agentInfo.agentId < 5)) {
// 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) {
// Send the commands to the agent
try { agent.send(JSON.stringify({ action: 'runcommands', type: command.type, cmds: command.cmds, runAsUser: command.runAsUser })); } catch (ex) { }
if (command.responseid != null) { 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 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"
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 };
parent.parent.DispatchEvent(targets, obj, event);
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Invalid command type' })); } catch (ex) { } }
}
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'runcommands', responseid: command.responseid, result: 'Agent not connected' })); } catch (ex) { } }
}
});
}
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 (common.validateInt(command.actiontype, 2, 401) == false) break; // Check actiontype
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 ((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 delete this device
if ((rights & MESHRIGHT_RESETOFF) == 0) return;
// 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 ((command.actiontype >= 300) && (command.actiontype < 400)) {
if ((command.actiontype != 302) && (command.actiontype != 308) && (command.actiontype != 310)) return; // Invalid action type.
// Intel AMT power command, actiontype: 2 = Power on, 8 = Power down, 10 = reset
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 })); } 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.title, 1, 512) == false) { err = "Invalid title"; } // Check title
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;
}
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) && (command.userloc.length != 2) && (command.userloc.length != 0)) { err = "Invalid user location"; }
}
} 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;
}
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)) { 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.ssh == 'number') && (command.ssh == 0)) {
if (node.ssh != null) { delete node.ssh; change = 1; changes.push('ssh'); } // Delete the SSH cendentials
}
if ((typeof command.rdp == 'number') && (command.rdp == 0)) {
if (node.rdp != null) { delete node.rdp; change = 1; changes.push('rdp'); } // Delete the RDP cendentials
}
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) {
if ((parent.parent.amtManager == null) || (node.intelamt.user == null) || (node.intelamt.user == '') || ((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);
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 'agentdisconnect':
{
if (common.validateInt(command.disconnectMode) == false) return; // Check disconnect mode
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((node == null) || (((rights & MESHRIGHT_AGENTCONSOLE) == 0) && (user.siteadmin != SITERIGHT_ADMIN))) return;
// Force mesh agent disconnection
parent.forceMeshAgentDisconnect(user, domain, node._id, command.disconnectMode);
});
break;
}
case 'close':
{
// Close the web socket session
if (obj.req.session && obj.req.session.ws && obj.req.session.ws == ws) { delete obj.req.session.ws; }
try { ws.close(); } catch (e) { }
break;
}
case 'getcookie':
{
// Check if this user has rights on this nodeid
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) return; // Access denied.
// Add a user authentication cookie to a url
var cookieContent = { userid: user._id, domainid: user.domain };
if (command.nodeid) { cookieContent.nodeid = command.nodeid; }
if (command.tcpaddr) { cookieContent.tcpaddr = command.tcpaddr; } // Indicates the browser want the agent to TCP connect to a remote address
if (command.tcpport) { cookieContent.tcpport = command.tcpport; } // Indicates the browser want the agent to TCP connect to a remote port
if (command.ip) { cookieContent.ip = command.ip; } // Indicates the browser want to agent to relay a TCP connection to a IP:port
if (node.mtype == 3) { cookieContent.lc = 1; command.localRelay = true; } // Indicate this is for a local connection
command.cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
command.trustedCert = parent.isTrustedCert(domain);
try { ws.send(JSON.stringify(command)); } catch (ex) { }
});
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, 1, 1024) == 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) { 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
// 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: decodeURIComponent(command.msg), domain: domain.id };
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 ((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 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 (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 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 if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if (twoStepLoginSupported) {
// Request a one time password to be setup
var otplib = null;
try { otplib = require('otplib'); } catch (ex) { }
if (otplib == null) { break; }
const secret = otplib.authenticator.generateSecret(); // TODO: Check the random source of this value.
var domainName = parent.certificates.CommonName;
if (domain.dns != null) { domainName = domain.dns; }
ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, domainName, secret) }));
}
break;
}
case 'otpauth-setup':
{
// 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 if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if (twoStepLoginSupported) {
// Perform the one time password setup
var otplib = null;
try { otplib = require('otplib'); } catch (ex) { }
if (otplib == null) { break; }
otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
if (otplib.authenticator.check(command.token, command.secret) === true) {
// Token is valid, activate 2-step login on this account.
user.otpsecret = command.secret;
parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpauth-setup', success: true })); // 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: 90, msg: 'Added authentication application', domain: domain.id };
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 {
ws.send(JSON.stringify({ action: 'otpauth-setup', success: false })); // Report fail
}
}
break;
}
case 'otpauth-clear':
{
// 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 if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if (twoStepLoginSupported) {
// Clear the one time password secret
if (user.otpsecret) {
delete user.otpsecret;
parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otpauth-clear', success: true })); // 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: 91, msg: 'Removed authentication application', domain: domain.id };
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 {
ws.send(JSON.stringify({ action: 'otpauth-clear', success: false })); // Report fail
}
}
break;
}
case 'otpauth-getpasswords':
{
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// Check if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if (twoStepLoginSupported == false) break;
var actionTaken = false, actionText = null, actionId = 0;
if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 1024) == 0)) { // Don't allow generation of tokens if the account is settings locked
// Perform a sub-action
if (command.subaction == 1) { // Generate a new set of tokens
var randomNumbers = [], v;
for (var i = 0; i < 10; i++) { do { v = getRandomEightDigitInteger(); } while (randomNumbers.indexOf(v) >= 0); randomNumbers.push(v); }
user.otpkeys = { keys: [] };
for (var i = 0; i < 10; i++) { user.otpkeys.keys[i] = { p: randomNumbers[i], u: true } }
actionTaken = true;
actionId = 92;
actionText = "New 2FA backup codes generated";
} else if (command.subaction == 2) { // Clear all tokens
actionTaken = (user.otpkeys != null);
delete user.otpkeys;
if (actionTaken) {
actionId = 93;
actionText = "2FA backup codes cleared";
}
}
// Save the changed user
if (actionTaken) { parent.db.SetUser(user); }
}
// Return one time passwords for this user
if (count2factoraAuths() > 0) {
ws.send(JSON.stringify({ action: 'otpauth-getpasswords', passwords: user.otpkeys ? user.otpkeys.keys : null }));
}
// Notify change
if (actionText != null) {
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: actionId, msg: actionText, domain: domain.id };
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 'otp-hkey-get':
{
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// Check if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if (twoStepLoginSupported == false) break;
// Send back the list of keys we have, just send the list of names and index
var hkeys = [];
if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { hkeys.push({ i: user.otphkeys[i].keyIndex, name: user.otphkeys[i].name, type: user.otphkeys[i].type }); } }
ws.send(JSON.stringify({ action: 'otp-hkey-get', keys: hkeys }));
break;
}
case 'otp-hkey-remove':
{
// 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 if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if (twoStepLoginSupported == false || command.index == null) break;
// Remove a key
var foundAtIndex = -1;
if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyIndex == command.index) { foundAtIndex = i; } } }
if (foundAtIndex != -1) {
user.otphkeys.splice(foundAtIndex, 1);
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: 94, msg: 'Removed security key', domain: domain.id };
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 'otp-hkey-yubikey-add':
{
// 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.
// Yubico API id and signature key can be requested from https://upgrade.yubico.com/getapikey/
var yubikeyotp = null;
try { yubikeyotp = require('yubikeyotp'); } catch (ex) { }
// Check if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if ((yubikeyotp == null) || (twoStepLoginSupported == false) || (typeof command.otp != 'string')) {
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
break;
}
// Check if Yubikey support is present or OTP no exactly 44 in length
if ((typeof domain.yubikey != 'object') || (typeof domain.yubikey.id != 'string') || (typeof domain.yubikey.secret != 'string') || (command.otp.length != 44)) {
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
break;
}
// TODO: Check if command.otp is modhex encoded, reject if not.
// Query the YubiKey server to validate the OTP
var request = { otp: command.otp, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
yubikeyotp.verifyOTP(request, function (err, results) {
if ((results != null) && (results.status == 'OK')) {
var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
var keyId = command.otp.substring(0, 12);
if (user.otphkeys == null) { user.otphkeys = []; }
// Check if this key was already registered, if so, remove it.
var foundAtIndex = -1;
for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyid == keyId) { foundAtIndex = i; } }
if (foundAtIndex != -1) { user.otphkeys.splice(foundAtIndex, 1); }
// Add the new key and notify
user.otphkeys.push({ name: command.name, type: 2, keyid: keyId, keyIndex: keyIndex });
parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: true, name: command.name, index: keyIndex }));
// Notify change TODO: Should be done on all sessions/servers for this user.
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: 95, msg: 'Added security key', domain: domain.id };
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 {
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
}
});
break;
}
case 'otpdev-clear':
{
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// Remove the authentication push notification device
if (user.otpdev != null) {
// Change the user
user.otpdev = obj.dbNodeKey;
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: 114, msg: "Removed push notification authentication device", domain: domain.id };
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 'otpdev-set':
{
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// Attempt to add a authentication push notification device
// This will only send a push notification to the device, the device needs to confirm for the auth device to be added.
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
// Only allow use of devices with full rights
if ((node == null) || (visible == false) || (rights != 0xFFFFFFFF) || (node.agent == null) || (node.agent.id != 14) || (node.pmt == null)) return;
// Encode the cookie
const code = Buffer.from(user.name).toString('base64');
const authCookie = parent.parent.encodeCookie({ a: 'addAuth', c: code, u: user._id, n: node._id });
// Send out a push message to the device
var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
if (err == null) {
parent.parent.debug('email', 'Successfully auth addition send push message to device ' + node.name);
} else {
parent.parent.debug('email', 'Failed auth addition push message to device ' + node.name + ', error: ' + errdesc);
}
});
});
break;
}
case 'webauthn-startregister':
{
// 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 if 2-step login is supported
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if ((twoStepLoginSupported == false) || (command.name == null)) break;
// Send the registration request
var registrationOptions = parent.webauthn.generateRegistrationChallenge("Anonymous Service", { id: Buffer.from(user._id, 'binary').toString('base64'), name: user._id, displayName: user._id.split('/')[2] });
obj.webAuthnReqistrationRequest = { action: 'webauthn-startregister', keyname: command.name, request: registrationOptions };
ws.send(JSON.stringify(obj.webAuthnReqistrationRequest));
break;
}
case 'webauthn-endregister':
{
// 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.
const twoStepLoginSupported = ((parent.parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.nousers !== true));
if ((twoStepLoginSupported == false) || (obj.webAuthnReqistrationRequest == null)) return;
// Figure out the origin
var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
var origin = "https://" + (domain.dns ? domain.dns : parent.certificates.CommonName);
if (httpport != 443) { origin += ':' + httpport; }
// Use internal WebAuthn module to check the response
var regResult = null;
try { regResult = parent.webauthn.verifyAuthenticatorAttestationResponse(command.response.response); } catch (ex) { regResult = { verified: false, error: ex }; }
if (regResult.verified === true) {
// Since we are registering a WebAuthn/FIDO2 key, remove all U2F keys (Type 1).
var otphkeys2 = [];
if (user.otphkeys && Array.isArray(user.otphkeys)) { for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type != 1) { otphkeys2.push(user.otphkeys[i]); } } }
user.otphkeys = otphkeys2;
// Add the new WebAuthn/FIDO2 keys
var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
if (user.otphkeys == null) { user.otphkeys = []; }
user.otphkeys.push({ name: obj.webAuthnReqistrationRequest.keyname, type: 3, publicKey: regResult.authrInfo.publicKey, counter: regResult.authrInfo.counter, keyIndex: keyIndex, keyId: regResult.authrInfo.keyId });
parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: true, name: command.name, index: keyIndex }));
// 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: 95, msg: 'Added security key', domain: domain.id };
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 {
//console.log('webauthn-endregister-error', regResult.error);
ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: false, error: regResult.error, name: command.name, index: keyIndex }));
}
delete obj.hardwareKeyRegistrationRequest;
break;
}
case 'verifyPhone': {
// 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.
if (parent.parent.smsserver == null) return;
if (common.validateString(command.phone, 1, 18) == false) break; // Check phone length
if (isPhoneNumber(command.phone) == false) break; // Check phone
const code = common.zeroPad(getRandomSixDigitInteger(), 6)
const phoneCookie = parent.parent.encodeCookie({ a: 'verifyPhone', c: code, p: command.phone, s: ws.sessionId });
parent.parent.smsserver.sendPhoneCheck(domain, command.phone, code, parent.getLanguageCodes(req), function (success) {
ws.send(JSON.stringify({ action: 'verifyPhone', cookie: phoneCookie, success: success }));
});
break;
}
case 'confirmPhone': {
// 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.
if ((parent.parent.smsserver == null) || (typeof command.cookie != 'string') || (typeof command.code != 'string') || (obj.failedSmsCookieCheck == 1)) break; // Input checks
var cookie = parent.parent.decodeCookie(command.cookie);
if (cookie == null) break; // Invalid cookie
if (cookie.s != ws.sessionId) break; // Invalid session
if (cookie.c != command.code) {
obj.failedSmsCookieCheck = 1;
// Code does not match, delay the response to limit how many guesses we can make and don't allow more than 1 guess at any given time.
setTimeout(function () {
ws.send(JSON.stringify({ action: 'verifyPhone', cookie: command.cookie, success: true }));
delete obj.failedSmsCookieCheck;
}, 2000 + (parent.crypto.randomBytes(2).readUInt16BE(0) % 4095));
break;
}
// Set the user's phone
user.phone = cookie.p;
db.SetUser(user);
// Event the change
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 96, msgArgs: [user.name], msg: 'Verified phone number of user ' + EscapeHtml(user.name), domain: domain.id };
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(['*', 'server-users', user._id], obj, event);
break;
}
case 'removePhone': {
// 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.
if (user.phone == null) break;
// Clear the user's phone
delete user.phone;
db.SetUser(user);
// Event the change
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 97, msgArgs: [user.name], msg: 'Removed phone number of user ' + EscapeHtml(user.name), domain: domain.id };
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(['*', 'server-users', user._id], obj, event);
break;
}
case 'smsuser': { // Send a SMS message to a user
var errMsg = null, errId = 0, smsuser = null;
if (parent.parent.smsserver == null) { errMsg = "SMS gateway not enabled"; errId = 23; }
else if ((user.siteadmin & 2) == 0) { errMsg = "No user management rights"; errId = 24; }
else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = "Invalid username"; errId = 2; }
else if (common.validateString(command.msg, 1, 160) == false) { errMsg = "Invalid SMS message"; errId = 25; }
else {
smsuser = parent.users[command.userid];
if (smsuser == null) { errMsg = "Invalid username"; errId = 2; }
else if (smsuser.phone == null) { errMsg = "No phone number for this user"; errId = 26; }
}
if (errMsg != null) { displayNotificationMessage(errMsg); break; }
parent.parent.smsserver.sendSMS(smsuser.phone, command.msg, function (success, msg) {
if (success) {
displayNotificationMessage("SMS succesfuly sent.", null, null, null, 27);
} else {
if (typeof msg == 'string') { displayNotificationMessage("SMS error: " + msg, null, null, null, 29, [msg]); } else { displayNotificationMessage("SMS error", null, null, null, 28); }
}
});
break;
}
case 'emailuser': { // Send a email message to a user
var errMsg = null, emailuser = null;
if (domain.mailserver == null) { errMsg = 'Email server not enabled'; }
else if ((user.siteadmin & 2) == 0) { errMsg = 'No user management rights'; }
else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = 'Invalid userid'; }
else if (common.validateString(command.subject, 1, 1000) == false) { errMsg = 'Invalid subject message'; }
else if (common.validateString(command.msg, 1, 10000) == false) { errMsg = 'Invalid message'; }
else {
emailuser = parent.users[command.userid];
if (emailuser == null) { errMsg = 'Invalid userid'; }
else if (emailuser.email == null) { errMsg = 'No validated email address for this user'; }
else if (emailuser.emailVerified !== true) { errMsg = 'No validated email address for this user'; }
}
if (errMsg != null) { displayNotificationMessage(errMsg); break; }
domain.mailserver.sendMail(emailuser.email, command.subject, command.msg);
displayNotificationMessage("Email sent.", null, null, null, 14);
break;
}
case 'getClip': {
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
// Ask for clipboard data from agent
var agent = parent.wsagents[node._id];
if (agent != null) { try { agent.send(JSON.stringify({ action: 'getClip' })); } catch (ex) { } }
});
break;
}
case 'setClip': {
if (common.validateString(command.data, 1, 65535) == false) break; // Check
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
// Send clipboard data to the agent
var agent = parent.wsagents[node._id];
if (agent != null) { try { agent.send(JSON.stringify({ action: 'setClip', data: command.data })); } catch (ex) { } }
});
break;
}
case 'userWebState': {
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
if (common.validateString(command.state, 1, 10000) == false) break; // Check state size, no more than 10k
command.state = parent.filterUserWebState(command.state); // Filter the state to remove anything bad
if ((command.state == null) || (typeof command.state !== 'string')) break; // If state did not validate correctly, quit here.
db.Set({ _id: 'ws' + user._id, state: command.state });
parent.parent.DispatchEvent([user._id], obj, { action: 'userWebState', nolog: 1, domain: domain.id, state: command.state });
break;
}
case 'getNotes':
{
// 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 (visible == false) return;
// Get the notes about this node
db.Get('nt' + command.id, function (err, notes) {
try {
if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
} catch (ex) { }
});
});
} 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
// Get the notes about this node
db.Get('nt' + command.id, function (err, notes) {
try {
if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
} catch (ex) { }
});
}
} else if ((idtype == 'user') && ((user.siteadmin & 2) != 0)) {
// Get the notes about this node
db.Get('nt' + command.id, function (err, notes) {
try {
if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
} catch (ex) { }
});
} else if (idtype == 'puser') {
// Get personal note, starts with 'ntp' + userid
db.Get('ntp' + user._id, function (err, notes) {
try {
if ((notes == null) || (notes.length != 1)) { ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: null })); return; }
ws.send(JSON.stringify({ action: 'getNotes', id: command.id, notes: notes[0].value }));
} catch (ex) { }
});
}
break;
}
case 'createInviteLink': {
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'; }
}
}
}
if (common.validateString(command.meshid, 8, 128) == false) { err = 'Invalid group id'; } // Check the meshid
else if (common.validateInt(command.expire, 0, 99999) == false) { err = 'Invalid expire time'; } // Check the expire time in hours
else if (common.validateInt(command.flags, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
else if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid group identifier'; } // Check meshid
else {
if (command.meshid.split('/').length == 1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; }
var smesh = command.meshid.split('/');
if ((smesh.length != 3) || (smesh[0] != 'mesh') || (smesh[1] != domain.id)) { err = 'Invalid group id'; }
mesh = parent.meshes[command.meshid];
if ((mesh == null) || (parent.IsMeshViewable(user, mesh) == false)) { err = 'Invalid group id'; }
}
var serverName = parent.getWebServerName(domain);
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createInviteLink', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
const inviteCookie = parent.parent.encodeCookie({ a: 4, mid: command.meshid, f: command.flags, expire: command.expire * 60 }, parent.parent.invitationLinkEncryptionKey);
if (inviteCookie == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createInviteLink', responseid: command.responseid, result: 'Unable to generate invitation cookie' })); } catch (ex) { } } break; }
// 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://' + serverName + ':' + httpsPort + '/' + xdomain + 'agentinvite?c=' + inviteCookie;
if (serverName.split('.') == 1) { url = '/' + xdomain + 'agentinvite?c=' + inviteCookie; }
ws.send(JSON.stringify({ action: 'createInviteLink', meshid: command.meshid, url: url, expire: command.expire, cookie: inviteCookie, responseid: command.responseid, tag: command.tag }));
break;
}
case 'deviceShares': {
if (domain.guestdevicesharing === false) return; // This feature is not allowed.
var err = null;
// Argument validation
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
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', 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: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
return;
}
// If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
if (rights != MESHRIGHT_ADMIN) {
// If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
}
parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
if (err != null) return;
var now = Date.now(), removed = false, okDocs = [];
for (var i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc.expireTime < now) {
// This share is expired.
parent.db.Remove(doc._id, function () { }); removed = true;
} else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
okDocs.push(doc);
}
}
try { ws.send(JSON.stringify({ action: 'deviceShares', nodeid: command.nodeid, deviceShares: okDocs })); } catch (ex) { }
// If we removed any shares, send device share update
if (removed == true) {
var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
}
});
});
break;
}
case 'removeDeviceShare': {
if (domain.guestdevicesharing === false) return; // This feature is not allowed.
var err = null;
// Argument validation
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
if (common.validateString(command.publicid, 1, 128) == false) { err = 'Invalid public id'; } // Check the public identifier
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', 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: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
return;
}
// If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
if (rights != MESHRIGHT_ADMIN) {
// If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
}
parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
if (err != null) return;
// Remove device sharing
var now = Date.now(), removedExact = null, removed = false, okDocs = [];
for (var i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc.publicid == command.publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
okDocs.push(doc);
}
}
// Confirm removal if requested
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, nodeid: command.nodeid, publicid: doc.publicid, removed: removedExact })); } catch (ex) { } }
// Event device share removal
if (removedExact != null) {
// Send out an event that we removed a device share
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: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: [removedExact.guestName], domain: domain.id, publicid: command.publicid };
parent.parent.DispatchEvent(targets, obj, event);
}
// If we removed any shares, send device share update
if (removed == true) {
var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'OK' })); } catch (ex) { } }
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removeDeviceShare', responseid: command.responseid, result: 'Invalid device share identifier.' })); } catch (ex) { } }
}
});
});
break;
}
case 'createDeviceShareLink': {
if (domain.guestdevicesharing === false) return; // This feature is not allowed.
var err = null;
// Argument validation
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
if (common.validateString(command.guestname, 1, 128) == false) { err = 'Invalid guest name'; } // Check the guest name
else if ((command.expire != null) && (typeof command.expire != 'number')) { err = 'Invalid expire time'; } // Check the expire time in hours
else if ((command.start != null) && (typeof command.start != 'number')) { err = 'Invalid start time'; } // Check the start time in seconds
else if ((command.end != null) && (typeof command.end != 'number')) { err = 'Invalid end time'; } // Check the end time in seconds
else if (common.validateInt(command.consent, 0, 256) == false) { err = 'Invalid flags'; } // Check the flags
else if (common.validateInt(command.p, 1, 7) == false) { err = 'Invalid protocol'; } // Check the protocol, 1 = Terminal, 2 = Desktop, 4 = Files
else if ((command.expire == null) && ((command.start == null) || (command.end == null) || (command.start > command.end))) { err = 'No time specified'; } // Check that a time range is present
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: 'createDeviceShareLink', 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: 'deviceShares', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } }
return;
}
// If there is MESHRIGHT_DESKLIMITEDINPUT or we don't have MESHRIGHT_GUESTSHARING on this account, reject this request.
if (rights != MESHRIGHT_ADMIN) {
// If we don't have remote control, or have limited input, or don't have guest sharing permission, fail here.
if (((rights & MESHRIGHT_REMOTECONTROL) == 0) || ((rights & MESHRIGHT_DESKLIMITEDINPUT) != 0) || ((rights & MESHRIGHT_GUESTSHARING) == 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
}
// If we are limited to no terminal, don't allow terminal sharing
if (((command.p & 1) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOTERMINAL) != 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
// If we are limited to no desktop, don't allow desktop sharing
if (((command.p & 2) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NODESKTOP) != 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
// If we are limited to no files, don't allow file sharing
if (((command.p & 4) != 0) && (rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOFILES) != 0)) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deviceShares', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } }
return;
}
// If we have view only remote desktop rights, force view-only on the guest share.
if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_REMOTEVIEWONLY) != 0)) { command.viewOnly = true; command.p = (command.p & 1); }
// Create cookie
var publicid = getRandomPassword(), startTime, expireTime;
if (command.expire != null) {
// Now until expire in hours
startTime = Date.now();
expireTime = Date.now() + (60000 * command.expire);
} else {
// Time range in seconds
startTime = command.start * 1000;
expireTime = command.end * 1000;
}
var cookie = { a: 5, p: command.p, uid: user._id, gn: command.guestname, nid: node._id, cf: command.consent, start: startTime, expire: expireTime, pid: publicid };
if (command.viewOnly === true) { cookie.vo = 1; }
const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
if (inviteCookie == null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createDeviceShareLink', responseid: command.responseid, result: 'Unable to generate shareing cookie' })); } catch (ex) { } } return; }
command.start = startTime;
command.expire = expireTime;
// Create the server url
var serverName = parent.getWebServerName(domain);
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://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
command.url = url;
if (command.responseid != null) { command.result = 'OK'; }
try { ws.send(JSON.stringify(command)); } catch (ex) { }
// Create a device sharing database entry
var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: node._id, p: command.p, domain: node.domain, publicid: publicid, startTime: startTime, expireTime: expireTime, userid: user._id, guestName: command.guestname, consent: command.consent, url: url };
if (command.viewOnly === true) { shareEntry.viewOnly = true; }
parent.db.Set(shareEntry);
// Send out an event that we added a device share
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: 'addedDeviceShare', msg: 'Added Device Share', msgid: 101, msgArgs: [command.guestname, 'DATETIME:' + startTime, 'DATETIME:' + expireTime], domain: domain.id };
parent.parent.DispatchEvent(targets, obj, event);
// Send device share update
parent.db.GetAllTypeNodeFiltered([command.nodeid], domain.id, 'deviceshare', null, function (err, docs) {
if (err != null) return;
// Check device sharing
var now = Date.now();
for (var i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
}
}
// Send device share update
var targets = parent.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', user._id]);
parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: node._id, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
});
});
break;
}
case 'traceinfo': {
// Only accept if the tracing tab is allowed for this domain
if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.trace !== true))) break;
if ((user.siteadmin === SITERIGHT_ADMIN) && (typeof command.traceSources == 'object')) {
parent.parent.debugRemoteSources = command.traceSources;
parent.parent.DispatchEvent(['*'], obj, { action: 'traceinfo', userid: user._id, username: user.name, traceSources: command.traceSources, nolog: 1, domain: domain.id });
}
break;
}
case 'sendmqttmsg': {
if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }; // MQTT not available
if (common.validateArray(command.nodeids, 1) == false) { err = 'Invalid nodeids'; }; // Check nodeid's
if (common.validateString(command.topic, 1, 64) == false) { err = 'Invalid topic'; } // Check the topic
if (common.validateString(command.msg, 1, 4096) == false) { err = 'Invalid msg'; } // Check the message
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'sendmqttmsg', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Send the MQTT message
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) {
// If this device is connected on MQTT, send a wake action.
if (rights != 0) {
parent.parent.mqttbroker.publish(node._id, command.topic, command.msg);
}
});
}
break;
}
case 'getmqttlogin': {
var err = null;
if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; }
if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid
// Handle any errors
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', 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) {
// Check if this user has rights to do this
if (rights == MESHRIGHT_ADMIN) {
var token = parent.parent.mqttbroker.generateLogin(node.meshid, node._id);
var r = { action: 'getmqttlogin', responseid: command.responseid, nodeid: node._id, user: token.user, pass: token.pass };
const serverName = parent.getWebServerName(domain);
// Add MPS URL
if (parent.parent.mpsserver != null) {
r.mpsCertHashSha384 = parent.parent.certificateOperations.getCertHash(parent.parent.mpsserver.certificates.mps.cert);
r.mpsCertHashSha1 = parent.parent.certificateOperations.getCertHashSha1(parent.parent.mpsserver.certificates.mps.cert);
r.mpsUrl = 'mqtts://' + serverName + ':' + ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport) + '/';
}
// Add WS URL
var xdomain = (domain.dns == null) ? domain.id : '';
if (xdomain != '') xdomain += '/';
var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
r.wsUrl = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'mqtt.ashx';
r.wsTrustedCert = parent.isTrustedCert(domain);
try { ws.send(JSON.stringify(r)); } catch (ex) { }
} else {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Unable to perform this operation' })); } catch (ex) { } }
}
});
break;
}
case 'amt': {
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
if (common.validateInt(command.mode, 0, 3) == false) break; // Check connection mode
// Validate if communication mode is possible
if (command.mode == null || command.mode == 0) {
break; //unsupported
} else if (command.mode == 1) {
var state = parent.parent.GetConnectivityState(command.nodeid);
if ((state == null) || (state.connectivity & 4) == 0) break;
} else if (command.mode == 2) {
if (parent.parent.mpsserver.ciraConnections[command.nodeid] == null) break;
}
/*
else if (command.mode == 3) {
if (parent.parent.apfserver.apfConnections[command.nodeid] == null) break;
}
*/
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((rights & MESHRIGHT_REMOTECONTROL) == 0) return;
handleAmtCommand(command, node);
});
break;
}
case 'distributeCore': {
// This is only available when plugins are enabled since it could cause stress on the server
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
for (var i in command.nodes) {
parent.sendMeshAgentCore(user, domain, command.nodes[i]._id, 'default');
}
break;
}
case 'plugins': {
// Since plugin actions generally require a server restart, use the Full admin permission
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
parent.db.getPlugins(function(err, docs) {
try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
});
break;
}
case 'pluginLatestCheck': {
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
parent.parent.pluginHandler.getPluginLatest()
.then(function(latest) {
try { ws.send(JSON.stringify({ action: 'pluginVersionsAvailable', list: latest })); } catch (ex) { }
});
break;
}
case 'addplugin': {
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
try {
parent.parent.pluginHandler.getPluginConfig(command.url)
.then(parent.parent.pluginHandler.addPlugin)
.then(function(docs){
var targets = ['*', 'server-users'];
parent.parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs });
})
.catch(function(err) {
if (typeof err == 'object') err = err.message;
try { ws.send(JSON.stringify({ action: 'pluginError', msg: err })); } catch (er) { }
});
} catch(e) { console.log('Cannot add plugin: ' + e); }
break;
}
case 'installplugin': {
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
parent.parent.pluginHandler.installPlugin(command.id, command.version_only, null, function(){
parent.db.getPlugins(function(err, docs) {
try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
});
var targets = ['*', 'server-users'];
parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
});
break;
}
case 'disableplugin': {
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
parent.parent.pluginHandler.disablePlugin(command.id, function(){
parent.db.getPlugins(function(err, docs) {
try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
var targets = ['*', 'server-users'];
parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
});
});
break;
}
case 'removeplugin': {
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
parent.parent.pluginHandler.removePlugin(command.id, function(){
parent.db.getPlugins(function(err, docs) {
try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
});
});
break;
}
case 'getpluginversions': {
if ((user.siteadmin != SITERIGHT_ADMIN) || (parent.parent.pluginHandler == null)) break; // Must be full admin with plugins enabled
parent.parent.pluginHandler.getPluginVersions(command.id)
.then(function (versionInfo) {
try { ws.send(JSON.stringify({ action: 'downgradePluginVersions', info: versionInfo, error: null })); } catch (ex) { }
})
.catch(function (e) {
try { ws.send(JSON.stringify({ action: 'pluginError', msg: e })); } catch (ex) { }
});
break;
}
case 'plugin': {
if (parent.parent.pluginHandler == null) break; // If the plugin's are not supported, reject this command.
command.userid = user._id;
if (command.routeToNode === true) {
routeCommandToNode(command);
} else {
try {
parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
} catch (e) { console.log('Error loading plugin handler (' + e + ')'); }
}
break;
}
case 'uicustomevent': {
// Event the change
var message = { etype: 'user', userid: user._id, username: user.name, action: 'uicustomevent', domain: domain.id, uisection: command.section, element: command.element };
if (command.selectedDevices != null) { message.selectedDevices = command.selectedDevices; }
if (command.src != null) { message.src = command.src; }
if (command.values != null) { message.values = command.values; }
if (typeof command.logmsg == 'string') { message.msg = command.logmsg; } else { message.nolog = 1; }
parent.parent.DispatchEvent(['*', user._id], obj, message);
break;
}
case 'serverBackup': {
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
if ((user.siteadmin != SITERIGHT_ADMIN) || (typeof parent.parent.config.settings.autobackup.googledrive != 'object')) return;
if (command.service == 'googleDrive') {
if (command.state == 0) {
parent.db.Remove('GoogleDriveBackup', function () { try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 1 })); } catch (ex) { } });
} else if (command.state == 1) {
const {google} = require('googleapis');
obj.oAuth2Client = new google.auth.OAuth2(command.clientid, command.clientsecret, "urn:ietf:wg:oauth:2.0:oob");
obj.oAuth2Client.xxclientid = command.clientid;
obj.oAuth2Client.xxclientsecret = command.clientsecret;
const authUrl = obj.oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/drive.file'] });
try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 2, url: authUrl })); } catch (ex) { }
} else if ((command.state == 2) && (obj.oAuth2Client != null)) {
obj.oAuth2Client.getToken(command.code, function (err, token) {
if (err != null) { console.log('GoogleDrive (getToken) error: ', err); return; }
parent.db.Set({ _id: 'GoogleDriveBackup', state: 3, clientid: obj.oAuth2Client.xxclientid, clientsecret: obj.oAuth2Client.xxclientsecret, token: token });
try { ws.send(JSON.stringify({ action: 'serverBackup', service: 'googleDrive', state: 3 })); } catch (ex) { }
});
}
}
break;
}
case 'twoFactorCookie': {
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) break;
// Generate a two-factor cookie
if (((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
var maxCookieAge = domain.twofactorcookiedurationdays;
if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
const twoFactorCookie = parent.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, parent.parent.loginCookieEncryptionKey);
try { ws.send(JSON.stringify({ action: 'twoFactorCookie', cookie: twoFactorCookie })); } catch (ex) { }
}
break;
}
case 'amtsetupbin': {
if ((command.oldmebxpass != 'admin') && (common.validateString(command.oldmebxpass, 8, 16) == false)) break; // Check password
if (common.validateString(command.newmebxpass, 8, 16) == false) break; // Check password
if ((command.baremetal) && (parent.parent.amtProvisioningServer != null)) {
// Create bare metal setup.bin
var bin = parent.parent.certificateOperations.GetBareMetalSetupBinFile(domain.amtacmactivation, command.oldmebxpass, command.newmebxpass, domain, user);
try { ws.send(JSON.stringify({ action: 'amtsetupbin', file: Buffer.from(bin, 'binary').toString('base64') })); } catch (ex) { }
} else {
// Create standard setup.bin
var bin = parent.parent.certificateOperations.GetSetupBinFile(domain.amtacmactivation, command.oldmebxpass, command.newmebxpass, domain, user);
try { ws.send(JSON.stringify({ action: 'amtsetupbin', file: Buffer.from(bin, 'binary').toString('base64') })); } catch (ex) { }
}
break;
}
case 'meshToolInfo': {
if (typeof command.name != 'string') break;
var info = parent.parent.meshToolsBinaries[command.name];
var responseCmd = { action: 'meshToolInfo', name: command.name, hash: info.hash, size: info.size, url: info.url };
if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
break;
}
case 'pushmessage': {
// Check if this user has rights on this nodeid
if (parent.parent.firebase == null) return;
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
if (common.validateString(command.title, 1, 1024) == false) break; // Check title
if (common.validateString(command.msg, 1, 1024) == false) break; // Check message
db.Get(command.nodeid, function (err, nodes) { // TODO: Make a NodeRights(user) method that also does not do a db call if agent is connected (???)
if ((err == null) && (nodes.length == 1)) {
const node = nodes[0];
if (((parent.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_CHATNOTIFY) != 0) && (typeof node.pmt == 'string')) {
// Send out a push message to the device
var payload = { notification: { title: command.title, body: command.msg } };
var options = { priority: "Normal", timeToLive: 5 * 60 }; // TTL: 5 minutes
parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
if (err == null) {
parent.parent.debug('email', 'Successfully send push message to device ' + node.name + ', title: ' + command.title + ', msg: ' + command.msg);
} else {
parent.parent.debug('email', 'Failed to send push message to device ' + node.name + ', title: ' + command.title + ', msg: ' + command.msg + ', error: ' + errdesc);
}
});
}
}
});
break;
}
case 'pushconsole': {
// Check if this user has rights on this nodeid
if (parent.parent.firebase == null) return;
if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
if (common.validateString(command.console, 1, 3000) == false) break; // Check console command
db.Get(command.nodeid, function (err, nodes) { // TODO: Make a NodeRights(user) method that also does not do a db call if agent is connected (???)
if ((err == null) && (nodes.length == 1)) {
const node = nodes[0];
if ((parent.GetNodeRights(user, node.meshid, node._id) == MESHRIGHT_ADMIN) && (typeof node.pmt == 'string')) {
// Send out a push message to the device
var payload = { data: { con: command.console, s: ws.sessionId } };
var options = { priority: "Normal", timeToLive: 60 }; // TTL: 1 minutes, priority 'Normal' or 'High'
parent.parent.firebase.sendToDevice(node, payload, options, function (id, err, errdesc) {
if (err != null) {
try { ws.send(JSON.stringify({ action: 'msg', type: 'console', nodeid: node._id, value: 'Failed: ' + errdesc })); } catch (ex) { }
parent.parent.debug('email', 'Failed to send push console message to device ' + node.name + ', command: ' + command.console + ', error: ' + errdesc);
}
});
}
}
});
break;
}
case 'webpush': {
// Check if web push is enabled
if (parent.parent.webpush == null) break;
// Adds a web push session to the user. Start by sanitizing the input.
if ((typeof command.sub != 'object') && (typeof command.sub.keys != 'object') && (typeof command.sub.endpoint != 'string')) break;
if (common.validateString(command.sub.endpoint, 1, 1024) == false) break; // Check endpoint
if (common.validateString(command.sub.keys.auth, 1, 64) == false) break; // Check key auth
if (common.validateString(command.sub.keys.p256dh, 1, 256) == false) break; // Check key dh
var newWebPush = { endpoint: command.sub.endpoint, keys: { auth: command.sub.keys.auth, p256dh: command.sub.keys.p256dh } }
// See if we need to add this session
var changed = false;
if (user.webpush == null) {
changed = true;
user.webpush = [newWebPush];
} else {
var found = false;
for (var i in user.webpush) {
if ((user.webpush[i].endpoint == newWebPush.endpoint) && (user.webpush[i].keys.auth == newWebPush.keys.auth) && (user.webpush[i].keys.p256dh == newWebPush.keys.p256dh)) { found = true; }
}
if (found == true) break;
changed = true;
user.webpush.push(newWebPush);
while (user.webpush.length > 5) { user.webpush.shift(); }
}
// If we added the session, update the user
if (changed == true) {
// Update the database
parent.db.SetUser(user);
// Event the change
var message = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
var targets = ['*', 'server-users', user._id];
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
parent.parent.DispatchEvent(targets, obj, message);
}
break;
}
case 'updateAgents': {
// Update agents for selected devices
if (common.validateStrArray(command.nodeids, 1) == false) break; // Check nodeids
for (var i in command.nodeids) { routeCommandToNode({ action: 'msg', type: 'console', nodeid: command.nodeids[i], value: 'agentupdate' }, MESHRIGHT_ADMIN, 0); }
break;
}
case 'previousLogins': {
// TODO: Make a better database call to get filtered data.
if (command.userid == null) {
// Get previous logins for self
if (db.GetUserLoginEvents) {
// New way
db.GetUserLoginEvents(domain.id, user._id, function (err, docs) {
if (err != null) return;
var e = [];
for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName }); }
try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { }
});
} else {
// Old way
db.GetUserEvents([user._id], domain.id, user._id.split('/')[2], function (err, docs) {
if (err != null) return;
var e = [];
for (var i in docs) {
if ((docs[i].msgArgs) && (docs[i].userid == user._id) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) {
e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs, tn: docs[i].tokenName });
}
}
try { ws.send(JSON.stringify({ action: 'previousLogins', events: e })); } catch (ex) { }
});
}
} else {
// Get previous logins for specific userid
if (user.siteadmin === SITERIGHT_ADMIN) {
var splitUser = command.userid.split('/');
if ((obj.crossDomain === true) || (splitUser[1] === domain.id)) {
if (db.GetUserLoginEvents) {
// New way
db.GetUserLoginEvents(splitUser[1], command.userid, function (err, docs) {
if (err != null) return;
var e = [];
for (var i in docs) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); }
try { ws.send(JSON.stringify({ action: 'previousLogins', userid: command.userid, events: e })); } catch (ex) { }
});
} else {
// Old way
db.GetUserEvents([command.userid], splitUser[1], splitUser[2], function (err, docs) {
if (err != null) return;
var e = [];
for (var i in docs) { if ((docs[i].msgArgs) && (docs[i].userid == command.userid) && ((docs[i].action == 'authfail') || (docs[i].action == 'login'))) { e.push({ t: docs[i].time, m: docs[i].msgid, a: docs[i].msgArgs }); } }
try { ws.send(JSON.stringify({ action: 'previousLogins', userid: command.userid, events: e })); } catch (ex) { }
});
}
}
}
}
break;
}
case 'oneclickrecovery': { // Intel(R) AMT One Click Recovery (OCR)
if (common.validateStrArray(command.nodeids, 1) == false) break; // Check nodeids
if (common.validateString(command.path, 1, 2048) == false) break; // Check file path
if (command.type != 'diskimage') break; // Make sure type is correct
var file = parent.getServerFilePath(user, domain, command.path);
if (file == null) return;
// 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 and if we have full rights
if ((node == null) || (rights != 0xFFFFFFFF)) return;
// Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
parent.parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.fullpath });
});
}
break;
}
case 'loginTokens': { // Respond with the list of currently valid login tokens
if (req.session.loginToken != null) break; // Do not allow this command when logged in using a login token
if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.logintokens == false)) break; // Login tokens are not supported on this server
// If remove is an array or strings, we are going to be removing these and returning the results.
if (common.validateStrArray(command.remove, 1) == false) { delete command.remove; }
parent.db.GetAllTypeNodeFiltered(['logintoken-' + user._id], domain.id, 'logintoken', null, function (err, docs) {
if (err != null) return;
var now = Date.now(), removed = [], okDocs = [];
for (var i = 0; i < docs.length; i++) {
const doc = docs[i];
if (((doc.expire != 0) && (doc.expire < now)) || (doc.tokenUser == null) || ((command.remove != null) && (command.remove.indexOf(doc.tokenUser) >= 0))) {
// This share is expired.
parent.db.Remove(doc._id, function () { }); removed.push(doc.tokenUser);
} else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.userid; delete doc.salt; delete doc.hash;
okDocs.push(doc);
}
}
try { ws.send(JSON.stringify({ action: 'loginTokens', loginTokens: okDocs })); } catch (ex) { }
// If any login tokens where removed, event the change.
if (removed.length > 0) {
// Dispatch the new event
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, action: 'loginTokenChanged', domain: domain.id, loginTokens: okDocs, removed: removed, nolog: 1 };
parent.parent.DispatchEvent(targets, obj, event);
}
});
break;
}
case 'createLoginToken': { // Create a new login token
var err = null;
if (req.session.loginToken != null) { err = "Access denied"; } // Do not allow this command when logged in using a login token
else if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.logintokens == false)) { err = "Not supported"; } // Login tokens are not supported on this server
else if (common.validateString(command.name, 1, 100) == false) { err = "Invalid name"; } // Check name
else if ((typeof command.expire != 'number') || (command.expire < 0)) { err = "Invalid expire value"; } // Check expire
// Handle any errors
if (err != null) {
if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createLoginToken', responseid: command.responseid, result: err })); } catch (ex) { } }
break;
}
// Generate a token username. Don't have any + or / in the username or password
var tokenUser = '~t:' + Buffer.from(parent.parent.crypto.randomBytes(12), 'binary').toString('base64');
while ((tokenUser.indexOf('+') >= 0) || (tokenUser.indexOf('/') >= 0)) { tokenUser = '~t:' + Buffer.from(parent.parent.crypto.randomBytes(12), 'binary').toString('base64'); };
var tokenPass = Buffer.from(parent.parent.crypto.randomBytes(15), 'binary').toString('base64');
while ((tokenPass.indexOf('+') >= 0) || (tokenPass.indexOf('/') >= 0)) { tokenPass = Buffer.from(parent.parent.crypto.randomBytes(15), 'binary').toString('base64'); };
// Create a user, generate a salt and hash the password
require('./pass').hash(tokenPass, function (err, salt, hash, tag) {
if (err) throw err;
// Compute expire time
const created = Date.now();
var expire = 0;
if (command.expire > 0) { expire = created + (command.expire * 60000); }
// Generate the token password
const dbentry = { _id: 'logintoken-' + tokenUser, type: 'logintoken', nodeid: 'logintoken-' + user._id, userid: user._id, name: command.name, tokenUser: tokenUser, salt: salt, hash: hash, domain: domain.id, created: created, expire: expire };
parent.db.Set(dbentry);
// Send the token information back
try { ws.send(JSON.stringify({ action: 'createLoginToken', name: command.name, tokenUser: tokenUser, tokenPass: tokenPass, created: created, expire: expire })); } catch (ex) { }
// Dispatch the new event
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, action: 'loginTokenAdded', msgid: 115, msg: "Added login token", domain: domain.id, newToken: { name: command.name, tokenUser: tokenUser, created: created, expire: expire } };
parent.parent.DispatchEvent(targets, obj, event);
});
break;
}
case 'trafficstats': {
try { ws.send(JSON.stringify({ action: 'trafficstats', stats: parent.getTrafficStats() })); } catch (ex) { }
break;
}
case 'trafficdelta': {
const stats = parent.getTrafficDelta(obj.trafficStats);
obj.trafficStats = stats.current;
try { ws.send(JSON.stringify({ action: 'trafficdelta', delta: stats.delta })); } catch (ex) { }
break;
}
case 'getDeviceDetails': {
if ((common.validateStrArray(command.nodeids, 1) == false) && (command.nodeids != null)) break; // Check nodeids
if (common.validateString(command.type, 3, 4) == false) break; // Check type
// Create a list of node ids and query them for last device connection time
const ids = []
for (var i in command.nodeids) { ids.push('lc' + command.nodeids[i]); }
db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) {
const lastConnects = {};
if (docs != null) { for (var i in docs) { lastConnects[docs[i]._id] = docs[i]; } }
getDeviceDetailedInfo(command.nodeids, command.type, function (results, type) {
for (var i = 0; i < results.length; i++) {
// Remove any device system and network information is we do not have details rights to this device
if ((parent.GetNodeRights(user, results[i].node.meshid, results[i].node._id) & MESHRIGHT_DEVICEDETAILS) == 0) {
delete results[i].sys; delete results[i].net;
}
// Merge any last connection information
const lc = lastConnects['lc' + results[i].node._id];
if (lc != null) { delete lc._id; delete lc.type;; delete lc.meshid; delete lc.domain; results[i].lastConnect = lc; }
}
var output = null;
if (type == 'csv') {
try {
// Create the CSV file
output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,avdetails,cpu,osbuild,biosDate,biosVendor,biosVersion,boardName,boardVendor,boardVersion,productUuid,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n';
for (var i = 0; i < results.length; i++) {
const nodeinfo = results[i];
// Node information
if (nodeinfo.node != null) {
const n = nodeinfo.node;
output += csvClean(n._id) + ',' + csvClean(n.name) + ',' + csvClean(n.rname ? n.rname : '') + ',' + csvClean(n.host ? n.host : '') + ',' + (n.icon ? n.icon : 1) + ',' + (n.ip ? n.ip : '') + ',' + (n.osdesc ? csvClean(n.osdesc) : '') + ',' + csvClean(parent.meshes[n.meshid].name);
if (typeof n.wsc == 'object') {
output += ',' + csvClean(n.wsc.antiVirus ? n.wsc.antiVirus : '') + ',' + csvClean(n.wsc.autoUpdate ? n.wsc.autoUpdate : '') + ',' + csvClean(n.wsc.firewall ? n.wsc.firewall : '')
} else { output += ',,,'; }
if (typeof n.av == 'object') {
var avdetails = '', firstav = true;
for (var a in n.av) { if (typeof n.av[a].product == 'string') { if (firstav) { firstav = false; } else { avdetails += '|'; } avdetails += (n.av[a].product + '/' + ((n.av[a].enabled) ? 'enabled' : 'disabled') + '/' + ((n.av[a].updated) ? 'updated' : 'notupdated')); } }
output += ',' + csvClean(avdetails);
}
else { output += ','; }
} else {
output += ',,,,,,,,,,,';
}
// System infomation
if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.windows) && (nodeinfo.sys.hardware.windows)) {
// Windows
output += ',';
if (nodeinfo.sys.hardware.windows.cpu && (nodeinfo.sys.hardware.windows.cpu.length > 0) && (typeof nodeinfo.sys.hardware.windows.cpu[0].Name == 'string')) { output += csvClean(nodeinfo.sys.hardware.windows.cpu[0].Name); }
output += ',';
if (nodeinfo.sys.hardware.windows.osinfo && (nodeinfo.sys.hardware.windows.osinfo.BuildNumber)) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.BuildNumber); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_date)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_date); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_vendor); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.bios_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.bios_version); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_name)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_name); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_vendor); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.board_version)) { output += csvClean(nodeinfo.sys.hardware.identifiers.board_version); }
output += ',';
if (nodeinfo.sys.hardware.identifiers && (nodeinfo.sys.hardware.identifiers.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.identifiers.product_uuid); }
} else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.mobile)) {
// Mobile
output += ',';
output += ',';
output += ',';
output += ',';
output += ',';
if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.bootloader)) { output += csvClean(nodeinfo.sys.hardware.mobile.bootloader); }
output += ',';
if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.model)) { output += csvClean(nodeinfo.sys.hardware.mobile.model); }
output += ',';
if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.brand)) { output += csvClean(nodeinfo.sys.hardware.mobile.brand); }
output += ',';
output += ',';
if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.id)) { output += csvClean(nodeinfo.sys.hardware.mobile.id); }
} else if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.windows) && (nodeinfo.sys.hardware.linux)) {
// Linux
output += ',';
output += ',';
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_date)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_date); }
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_vendor); }
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.bios_version)) { output += csvClean(nodeinfo.sys.hardware.linux.bios_version); }
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_name)) { output += csvClean(nodeinfo.sys.hardware.linux.board_name); }
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_vendor)) { output += csvClean(nodeinfo.sys.hardware.linux.board_vendor); }
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.board_version)) { output += csvClean(nodeinfo.sys.hardware.linux.board_version); }
output += ',';
if (nodeinfo.sys.hardware.linux && (nodeinfo.sys.hardware.linux.product_uuid)) { output += csvClean(nodeinfo.sys.hardware.linux.product_uuid); }
} else {
output += ',,,,,,,,,';
}
// Agent information
if ((nodeinfo.sys) && (nodeinfo.sys.hardware) && (nodeinfo.sys.hardware.agentvers)) {
output += ',';
if (nodeinfo.sys.hardware.agentvers.openssl) { output += csvClean(nodeinfo.sys.hardware.agentvers.openssl); }
output += ',';
if (nodeinfo.sys.hardware.agentvers.commitDate) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitDate); }
output += ',';
if (nodeinfo.sys.hardware.agentvers.commitHash) { output += csvClean(nodeinfo.sys.hardware.agentvers.commitHash); }
output += ',';
if (nodeinfo.sys.hardware.agentvers.compileTime) { output += csvClean(nodeinfo.sys.hardware.agentvers.compileTime); }
} else {
output += ',,,,';
}
// Network interfaces
if ((nodeinfo.net) && (nodeinfo.net.netif2)) {
output += ',';
output += Object.keys(nodeinfo.net.netif2).length; // Interface count
var macs = [], addresses = [];
for (var j in nodeinfo.net.netif2) {
if (Array.isArray(nodeinfo.net.netif2[j])) {
for (var k = 0; k < nodeinfo.net.netif2[j].length; k++) {
if (typeof nodeinfo.net.netif2[j][k].mac == 'string') { macs.push(nodeinfo.net.netif2[j][k].mac); }
if (typeof nodeinfo.net.netif2[j][k].address == 'string') { addresses.push(nodeinfo.net.netif2[j][k].address); }
}
}
}
output += ',';
output += csvClean(macs.join(' ')); // MACS
output += ',';
output += csvClean(addresses.join(' ')); // Addresses
} else {
output += ',,,';
}
// Last connection information
if (nodeinfo.lastConnect) {
output += ',';
if (nodeinfo.lastConnect.time) {
// Last connection time
if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
output += csvClean(new Date(nodeinfo.lastConnect.time).toLocaleString(command.l, { timeZone: command.tz }))
} else {
output += nodeinfo.lastConnect.time;
}
}
output += ',';
if (typeof nodeinfo.lastConnect.addr == 'string') { output += csvClean(nodeinfo.lastConnect.addr); } // Last connection address and port
} else {
output += ',,';
}
output += '\r\n';
}
} catch (ex) { console.log(ex); }
} else {
// Create the JSON file
// Add the device group name to each device
for (var i = 0; i < results.length; i++) {
const nodeinfo = results[i];
if (nodeinfo.node) {
const mesh = parent.meshes[nodeinfo.node.meshid];
if (mesh) { results[i].node.groupname = mesh.name; }
}
}
output = JSON.stringify(results, null, 2);
}
try { ws.send(JSON.stringify({ action: 'getDeviceDetails', data: output, type: type })); } catch (ex) { }
});
});
break;
}
default: {
// Unknown user action
console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
break;
}
}
}
const serverCommands = {
'files': serverCommandFiles,
'getnetworkinfo': serverCommandGetNetworkInfo,
'getsysinfo': serverCommandGetSysInfo,
'lastconnect': serverCommandLastConnect,
'lastconnects': serverCommandLastConnects,
'meshes': serverCommandMeshes,
'ping': serverCommandPing,
'pong': serverCommandPong,
'powertimeline': serverCommandPowerTimeline,
'print': serverCommandPrint,
'serverclearerrorlog': serverCommandServerClearErrorLog,
'serverconsole': serverCommandServerConsole,
'servererrors': serverCommandServerErrors,
'serverstats': serverCommandServerStats,
'serverupdate': serverCommandServerUpdate,
'serverversion': serverCommandServerVersion,
'users': serverCommandUsers
};
const serverUserCommands = {
'acceleratorsstats': [serverUserCommandAcceleratorsStats, "Show data on work being offloaded to other CPU's"],
'agentissues': [serverUserCommandAgentIssues, ""],
'agentstats': [serverUserCommandAgentStats, ""],
'amtacm': [serverUserCommandAmtAcm, ""],
'amtmanager': [serverUserCommandAmtManager, ""],
'amtpasswords': [serverUserCommandAmtPasswords, ""],
'args': [serverUserCommandArgs, ""],
'autobackup': [serverUserCommandAutoBackup, ""],
'backupconfig': [serverUserCommandBackupConfig, ""],
'badlogins': [serverUserCommandBadLogins, ""],
'certexpire': [serverUserCommandCertExpire, ""],
'certhashes': [serverUserCommandCertHashes, ""],
'closeusersessions': [serverUserCommandCloseUserSessions, "Disconnects all sessions for a specified user."],
'cores': [serverUserCommandCores, ""],
'dbcounters': [serverUserCommandDbCounters, ""],
'dbstats': [serverUserCommandDbStats, ""],
'dispatchtable': [serverUserCommandDispatchTable, ""],
'dropallcira': [serverUserCommandDropAllCira, ""],
'dupagents': [serverUserCommandDupAgents, ""],
'email': [serverUserCommandEmail, ""],
'firebase': [serverUserCommandFirebase, ""],
'heapdump': [serverUserCommandHeapDump, ""],
'heapdump2': [serverUserCommandHeapDump2, ""],
'help': [serverUserCommandHelp, ""],
'info': [serverUserCommandInfo, "Returns the most immidiatly useful information about this server, including MeshCentral and NodeJS versions. This is often information required to file a bug."],
'le': [serverUserCommandLe, ""],
'lecheck': [serverUserCommandLeCheck, ""],
'leevents': [serverUserCommandLeEvents, ""],
'maintenance': [serverUserCommandMaintenance, ""],
'migrationagents': [serverUserCommandMigrationAgents, ""],
'mps': [serverUserCommandMps, ""],
'mpsstats': [serverUserCommandMpsStats, ""],
'nodeconfig': [serverUserCommandNodeConfig, ""],
'print': [serverUserCommandPrint, ""],
'relays': [serverUserCommandRelays, ""],
'removeinactivedevices': [serverUserCommandRemoveInactiveDevices, ""],
'resetserver': [serverUserCommandResetServer, "Causes the server to reset, this is sometimes useful is the config.json file was changed."],
'serverupdate': [serverUserCommandServerUpdate, "Updates server to latest version. Optional version argument to install specific version. Example: serverupdate 0.8.49"],
'setmaxtasks': [serverUserCommandSetMaxTasks, ""],
'showpaths': [serverUserCommandShowPaths, ""],
'sms': [serverUserCommandSMS, ""],
'swarmstats': [serverUserCommandSwarmStats, ""],
'tasklimiter': [serverUserCommandTaskLimiter, "Returns the internal status of the tasklimiter. This is a system used to smooth out work done by the server. It's used by, for example, agent updates so that not all agents are updated at the same time."],
'trafficdelta': [serverUserCommandTrafficDelta, ""],
'trafficstats': [serverUserCommandTrafficStats, ""],
'updatecheck': [serverUserCommandUpdateCheck, ""],
'usersessions': [serverUserCommandUserSessions, "Returns a list of active sessions grouped by user."],
'versions': [serverUserCommandVersions, "Returns all internal versions for NodeJS running this server."],
'watchdog': [serverUserCommandWatchdog, ""],
'webpush': [serverUserCommandWebPush, ""],
'webstats': [serverUserCommandWebStats, ""]
};
function serverCommandFiles(command) {
// Send the full list of server files to the browser app
updateUserFiles(user, ws, domain);
}
function serverCommandGetNetworkInfo(command) {
if (!validNodeIdAndDomain(command)) return;
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((visible == false) || ((rights & MESHRIGHT_DEVICEDETAILS) == 0)) { try { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { } return; }
// Get network information about this node
db.Get('if' + node._id, function (err, netinfos) {
if ((netinfos == null) || (netinfos.length != 1)) { try { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: node._id, netif: null, netif2: null })); } catch (ex) { } return; }
var netinfo = netinfos[0];
// Unescape any field names that have special characters if needed
if (netinfo.netif2 != null) {
for (var i in netinfo.netif2) {
var esc = common.unEscapeFieldName(i);
if (esc !== i) { netinfo.netif2[esc] = netinfo.netif2[i]; delete netinfo.netif2[i]; }
}
}
try { ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: node._id, updateTime: netinfo.updateTime, netif: netinfo.netif, netif2: netinfo.netif2 })); } catch (ex) { }
});
});
}
function serverCommandGetSysInfo(command) {
if (!validNodeIdAndDomain(command)) return;
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if ((visible == false) || ((rights & MESHRIGHT_DEVICEDETAILS) == 0)) { try { ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { } return; }
// Query the database system information
db.Get('si' + command.nodeid, function (err, docs) {
if ((docs != null) && (docs.length > 0)) {
var doc = docs[0];
doc.action = 'getsysinfo';
doc.nodeid = node._id;
doc.tag = command.tag;
delete doc.type;
delete doc.domain;
delete doc._id;
if (command.nodeinfo === true) { doc.node = node; doc.rights = rights; }
try { ws.send(JSON.stringify(doc)); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: node._id, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { }
}
});
});
}
function serverCommandLastConnect(command) {
if (!validNodeIdAndDomain(command)) return;
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if (visible == false) { try { ws.send(JSON.stringify({ action: 'lastconnect', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'Invalid device id' })); } catch (ex) { } return; }
// Query the database for the last time this node connected
db.Get('lc' + command.nodeid, function (err, docs) {
if ((docs != null) && (docs.length > 0)) {
try { ws.send(JSON.stringify({ action: 'lastconnect', nodeid: command.nodeid, time: docs[0].time, addr: docs[0].addr })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'lastconnect', nodeid: command.nodeid, tag: command.tag, noinfo: true, result: 'No data' })); } catch (ex) { }
}
});
});
}
function serverCommandLastConnects(command) {
const links = parent.GetAllMeshIdWithRights(user);
const extraids = getUserExtraIds();
db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, function (err, docs) {
if (docs == null) return;
// Create a list of node ids for this user and query them for last device connection time
const ids = []
for (var i in docs) { ids.push('lc' + docs[i]._id); }
// Pull list of last connections only for device owned by this user
db.GetAllIdsOfType(ids, domain.id, 'lastconnect', function (err, docs) {
if (docs == null) return;
const response = {};
for (var j in docs) { response[docs[j]._id.substring(2)] = docs[j].time; }
try { ws.send(JSON.stringify({ action: 'lastconnects', lastconnects: response, tag: command.tag })); } catch (ex) { }
});
});
}
function serverCommandMeshes(command) {
// Request a list of all meshes this user as rights to
try { ws.send(JSON.stringify({ action: 'meshes', meshes: parent.GetAllMeshWithRights(user).map(parent.CloneSafeMesh), tag: command.tag })); } catch (ex) { }
}
function serverCommandPing(command) { try { ws.send('{action:"pong"}'); } catch (ex) { } }
function serverCommandPong(command) { } // NOP
function serverCommandPowerTimeline(command) {
// Get the node and the rights for this node
parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
if (visible == false) return;
// Query the database for the power timeline for a given node
// The result is a compacted array: [ startPowerState, startTimeUTC, powerState ] + many[ deltaTime, powerState ]
db.getPowerTimeline(node._id, function (err, docs) {
if ((err == null) && (docs != null) && (docs.length > 0)) {
var timeline = [], time = null, previousPower;
for (i in docs) {
var doc = docs[i], j = parseInt(i);
doc.time = Date.parse(doc.time);
if (time == null) { // First element
// Skip all starting power 0 events.
if ((doc.power == 0) && ((doc.oldPower == null) || (doc.oldPower == 0))) continue;
time = doc.time;
if (doc.oldPower) { timeline.push(doc.oldPower, time / 1000, doc.power); } else { timeline.push(0, time / 1000, doc.power); }
} else if (previousPower != doc.power) { // Delta element
// If this event is of a short duration (2 minutes or less), skip it.
if ((docs.length > (j + 1)) && ((Date.parse(docs[j + 1].time) - doc.time) < 120000)) continue;
timeline.push((doc.time - time) / 1000, doc.power);
time = doc.time;
}
previousPower = doc.power;
}
try { ws.send(JSON.stringify({ action: 'powertimeline', nodeid: node._id, timeline: timeline, tag: command.tag })); } catch (ex) { }
} else {
// No records found, send current state if we have it
var state = parent.parent.GetConnectivityState(command.nodeid);
if (state != null) { try { ws.send(JSON.stringify({ action: 'powertimeline', nodeid: node._id, timeline: [state.powerState, Date.now(), state.powerState], tag: command.tag })); } catch (ex) { } }
}
});
});
}
function serverCommandPrint(command) { console.log(command.value); }
function serverCommandServerClearErrorLog(command) {
// Clear the server error log if user has site update permissions
if (userHasSiteUpdate()) { fs.unlink(parent.parent.getConfigFilePath('mesherrors.txt'), function (err) { }); }
}
function serverCommandServerConsole(command) {
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) return;
// This is a server console message, only process this if full administrator
if (user.siteadmin != SITERIGHT_ADMIN) return;
// Only accept if the console is allowed for this domain
if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.console !== true))) return;
var cmdargs = splitArgs(command.value);
if (cmdargs.length == 0) return;
const cmd = cmdargs[0].toLowerCase();
cmdargs = parseArgs(cmdargs);
var cmdData = { result: '', command: command, cmdargs: cmdargs };
// Find the command in the lookup table and run it.
var cmdTableEntry = serverUserCommands[cmd];
if (cmdTableEntry != null) { try { cmdTableEntry[0](cmdData); } catch (ex) { cmdData.result = '' + ex; }
} else { cmdData.result = 'Unknown command \"' + cmd + '\", type \"help\" for list of available commands.'; }
// Send back the command result
if (cmdData.result != '') { try { ws.send(JSON.stringify({ action: 'serverconsole', value: cmdData.result, tag: command.tag })); } catch (ex) { } }
}
function serverCommandServerErrors(command) {
// Load the server error log
if (userHasSiteUpdate() && domainHasMyServerErrorLog())
fs.readFile(parent.parent.getConfigFilePath('mesherrors.txt'), 'utf8', function (err, data) { try { ws.send(JSON.stringify({ action: 'servererrors', data: data })); } catch (ex) { } });
}
function serverCommandServerStats(command) {
// Only accept if the "My Server" tab is allowed for this domain
if (domain.myserver === false) return;
if ((user.siteadmin & 21) == 0) return; // Only site administrators with "site backup" or "site restore" or "site update" permissions can use this.
if (common.validateInt(command.interval, 1000, 1000000) == false) {
// Clear the timer
if (obj.serverStatsTimer != null) { clearInterval(obj.serverStatsTimer); delete obj.serverStatsTimer; }
} else {
// Set the timer
obj.SendServerStats();
obj.serverStatsTimer = setInterval(obj.SendServerStats, command.interval);
}
}
function serverCommandServerUpdate(command) {
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) return;
// Perform server update
if (userHasSiteUpdate() && domainHasMyServerUpgrade() && !((command.version != null) && (typeof command.version != 'string')))
parent.parent.performServerUpdate(command.version);
}
function serverCommandServerVersion(command) {
// Do not allow this command when logged in using a login token
if (req.session.loginToken != null) return;
// Check the server version
if (userHasSiteUpdate() && domainHasMyServerUpgrade())
parent.parent.getServerTags(function (tags, err) { try { ws.send(JSON.stringify({ action: 'serverversion', tags: tags })); } catch (ex) { } });
}
function serverCommandUsers(command) {
// Request a list of all users
if ((user.siteadmin & 2) == 0) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'users', responseid: command.responseid, result: 'Access denied' })); } catch (ex) { } } return; }
var docs = [];
for (i in parent.users) {
if (((obj.crossDomain === true) || (parent.users[i].domain == domain.id)) && (parent.users[i].name != '~')) {
// If we are part of a user group, we can only see other members of our own group
if ((obj.crossDomain === true) || (user.groups == null) || (user.groups.length == 0) || ((parent.users[i].groups != null) && (findOne(parent.users[i].groups, user.groups)))) {
docs.push(parent.CloneSafeUser(parent.users[i]));
}
}
}
try { ws.send(JSON.stringify({ action: 'users', users: docs, tag: command.tag })); } catch (ex) { }
}
function serverUserCommandHelp(cmdData) {
var fin = '', f = '', availcommands = [];
for (var i in serverUserCommands) { availcommands.push(i); }
availcommands = availcommands.sort();
while (availcommands.length > 0) { if (f.length > 80) { fin += (f + ',\r\n'); f = ''; } f += (((f != '') ? ', ' : ' ') + availcommands.shift()); }
if (f != '') { fin += f; }
if (cmdData.cmdargs['_'].length == 0) {
cmdData.result = 'Available commands: \r\n' + fin + '\r\nType help <command> for details.';
} else {
var cmd2 = cmdData.cmdargs['_'][0].toLowerCase();
var cmdTableEntry = serverUserCommands[cmd2];
if (cmdTableEntry) {
if (cmdTableEntry[1] == '') {
cmdData.result = 'No help available for this command.'
} else {
cmdData.result = cmdTableEntry[1]; }
} else {
cmdData.result = "This command does not exist.";
}
}
}
function serverUserCommandCertExpire(cmdData) {
const now = Date.now();
for (var i in parent.webCertificateExpire) {
const domainName = (i == '') ? '[Default]' : i;
cmdData.result += domainName + ', expires in ' + Math.floor((parent.webCertificateExpire[i] - now) / 86400000) + ' day(s)\r\n';
}
}
function serverUserCommandWebPush(cmdData) {
if (parent.parent.webpush == null) {
cmdData.result = "Web push not supported.";
} else {
if (cmdData.cmdargs['_'].length != 1) {
cmdData.result = "Usage: WebPush \"Message\"";
} else {
const pushSubscription = { "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABgIkO9hjXHWhMPiuk-ppNRw7r_pUZitddwCEK4ykdzeIxOIjFnYhIt_nr-qUca2mpZziwQsSEhYTUCiuYrhWnVDRweMtiUj16yJJq8V5jneaEaUYjEIe5jp3DOMNpoTm1aHgX74gCR8uTXSITcM97bNi-hRxcQ4f6Ie4WSAmoXpd89B_g", "keys": { "auth": "UB2sbLVK7ALnSHw5P1dahg", "p256dh": "BIoRbcNSxBuTjN39CCCUCHo1f4NxBJ1YDdu_k4MbPW_q3NK1_RufnydUzLPDp8ibBVItSI72-s48QJvOjQ_S8Ok" } }
parent.parent.webpush.sendNotification(pushSubscription, cmdData.cmdargs['_'][0]).then(
function (value) { try { ws.send(JSON.stringify({ action: 'OK', value: cmdData.result, tag: cmdData.command.tag })); } catch (ex) { } },
function (error) { try { ws.send(JSON.stringify({ action: 'Error', value: cmdData.result, tag: cmdData.command.tag })); } catch (ex) { } }
);
}
}
}
function serverUserCommandAmtManager(cmdData) {
if (parent.parent.amtManager == null) {
cmdData.result = 'Intel AMT Manager not active.';
} else {
cmdData.result = parent.parent.amtManager.getStatusString();
}
}
function serverUserCommandCertHashes(cmdData) {
cmdData.result += 'AgentCertHash: ' + parent.agentCertificateHashHex;
for (var i in parent.webCertificateHashs) { cmdData.result += '\r\nwebCertificateHash (' + i + '): ' + common.rstr2hex(parent.webCertificateHashs[i]); }
for (var i in parent.webCertificateFullHashs) { cmdData.result += '\r\nwebCertificateFullHash (' + i + '): ' + common.rstr2hex(parent.webCertificateFullHashs[i]); }
cmdData.result += '\r\ndefaultWebCertificateHash: ' + common.rstr2hex(parent.defaultWebCertificateHash);
cmdData.result += '\r\ndefaultWebCertificateFullHash: ' + common.rstr2hex(parent.defaultWebCertificateFullHash);
}
function serverUserCommandAmtAcm(cmdData) {
if ((domain.amtacmactivation == null) || (domain.amtacmactivation.acmmatch == null) || (domain.amtacmactivation.acmmatch.length == 0)) {
cmdData.result = 'No Intel AMT activation certificates.';
} else {
if (domain.amtacmactivation.log != null) { cmdData.result += '--- Activation Log ---\r\nFile : ' + domain.amtacmactivation.log + '\r\n'; }
for (var i in domain.amtacmactivation.acmmatch) {
var acmcert = domain.amtacmactivation.acmmatch[i];
cmdData.result += '--- Activation Certificate ' + (parseInt(i) + 1) + ' ---\r\nName : ' + acmcert.cn + '\r\nSHA1 : ' + acmcert.sha1 + '\r\nSHA256: ' + acmcert.sha256 + '\r\n';
}
}
}
function serverUserCommandHeapDump(cmdData) {
// Heapdump support, see example at:
// https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
if (parent.parent.config.settings.heapdump === true) {
var dumpFileName = parent.path.join(parent.parent.datapath, `heapDump-${Date.now()}.heapsnapshot`);
try { ws.send(JSON.stringify({ action: 'serverconsole', value: "Generating dump file at: " + dumpFileName, tag: cmdData.command.tag })); } catch (ex) { }
require('heapdump').writeSnapshot(dumpFileName, (err, filename) => {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: "Done.", tag: cmdData.command.tag })); } catch (ex) { }
});
} else {
cmdData.result = "Heapdump not supported, add \"heapdump\":true to settings section of config.json.";
}
}
function serverUserCommandHeapDump2(cmdData) {
var heapdump = null;
try { heapdump = require('heapdump'); } catch (ex) { }
if (heapdump == null) {
cmdData.result = 'Heapdump module not installed, run "npm install heapdump".';
} else {
heapdump.writeSnapshot(function (err, filename) {
if (err != null) {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: 'Unable to write heapdump: ' + err })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: 'Wrote heapdump at ' + filename })); } catch (ex) { }
}
});
}
}
function serverUserCommandSMS(cmdData) {
if (parent.parent.smsserver == null) {
cmdData.result = "No SMS gateway in use.";
} else {
if (cmdData.cmdargs['_'].length != 2) {
cmdData.result = "Usage: SMS \"PhoneNumber\" \"Message\".";
} else {
parent.parent.smsserver.sendSMS(cmdData.cmdargs['_'][0], cmdData.cmdargs['_'][1], function (status, msg) {
if (typeof msg == 'string') {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? ('Success: ' + msg) : ('Failed: ' + msg), tag: cmdData.command.tag })); } catch (ex) { }
} else {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: status ? 'Success' : 'Failed', tag: cmdData.command.tag })); } catch (ex) { }
}
});
}
}
}
function serverUserCommandEmail(cmdData) {
if (domain.mailserver == null) {
cmdData.result = "No email service enabled.";
} else {
if (cmdData.cmdargs['_'].length != 3) {
cmdData.result = "Usage: email \"user@sample.com\" \"Subject\" \"Message\".";
} else {
domain.mailserver.sendMail(cmdData.cmdargs['_'][0], cmdData.cmdargs['_'][1], cmdData.cmdargs['_'][2]);
cmdData.result = "Done.";
}
}
}
function serverUserCommandLe(cmdData) {
if (parent.parent.letsencrypt == null) {
cmdData.result = "Let's Encrypt not in use.";
} else {
cmdData.result = JSON.stringify(parent.parent.letsencrypt.getStats(), null, 4);
}
}
function serverUserCommandLeCheck(cmdData) {
if (parent.parent.letsencrypt == null) {
cmdData.result = "Let's Encrypt not in use.";
} else {
cmdData.result = ["CertOK", "Request:NoCert", "Request:Expire", "Request:MissingNames"][parent.parent.letsencrypt.checkRenewCertificate()];
}
}
function serverUserCommandLeEvents(cmdData) {
if (parent.parent.letsencrypt == null) {
cmdData.result = "Let's Encrypt not in use.";
} else {
cmdData.result = parent.parent.letsencrypt.events.join('\r\n');
}
}
function serverUserCommandBadLogins(cmdData) {
if (parent.parent.config.settings.maxinvalidlogin == false) {
cmdData.result = 'Bad login filter is disabled.';
} else {
if (cmdData.cmdargs['_'] == 'reset') {
// Reset bad login table
parent.badLoginTable = {};
parent.badLoginTableLastClean = 0;
cmdData.result = 'Done.'
} else if (cmdData.cmdargs['_'] == '') {
// Show current bad login table
if (typeof parent.parent.config.settings.maxinvalidlogin.coolofftime == 'number') {
cmdData.result = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s), " + parent.parent.config.settings.maxinvalidlogin.coolofftime + " minute(s) cooloff.\r\n";
} else {
cmdData.result = "Max is " + parent.parent.config.settings.maxinvalidlogin.count + " bad login(s) in " + parent.parent.config.settings.maxinvalidlogin.time + " minute(s).\r\n";
}
var badLoginCount = 0;
parent.cleanBadLoginTable();
for (var i in parent.badLoginTable) {
badLoginCount++;
if (typeof parent.badLoginTable[i] == 'number') {
cmdData.result += "Cooloff for " + Math.floor((parent.badLoginTable[i] - Date.now()) / 60000) + " minute(s)\r\n";
} else {
if (parent.badLoginTable[i].length > 1) {
cmdData.result += (i + ' - ' + parent.badLoginTable[i].length + " records\r\n");
} else {
cmdData.result += (i + ' - ' + parent.badLoginTable[i].length + " record\r\n");
}
}
}
if (badLoginCount == 0) { cmdData.result += 'No bad logins.'; }
} else {
cmdData.result = 'Usage: badlogin [reset]';
}
}
}
function serverUserCommandDispatchTable(cmdData) {
for (var i in parent.parent.eventsDispatch) {
cmdData.result += (i + ', ' + parent.parent.eventsDispatch[i].length + '\r\n');
}
}
function serverUserCommandDropAllCira(cmdData) {
if (parent.parent.mpsserver == null) { cmdData.result = 'MPS not setup.'; return; }
const dropCount = parent.parent.mpsserver.dropAllConnections();
cmdData.result = 'Dropped ' + dropCount + ' connection(s).';
}
function serverUserCommandDupAgents(cmdData) {
for (var i in parent.duplicateAgentsLog) {
cmdData.result += JSON.stringify(parent.duplicateAgentsLog[i]) + '\r\n';
}
if (cmdData.result == '') { cmdData.result = 'No duplicate agents in log.'; }
}
function serverUserCommandAgentStats(cmdData) {
var stats = parent.getAgentStats();
for (var i in stats) {
if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
}
}
function serverUserCommandAgentIssues(cmdData) {
var stats = parent.getAgentIssues();
if (stats.length == 0) {
cmdData.result = "No agent issues.";
} else {
for (var i in stats) { cmdData.result += stats[i].join(', ') + '\r\n'; }
}
}
function serverUserCommandWebStats(cmdData) {
var stats = parent.getStats();
for (var i in stats) {
if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
}
}
function serverUserCommandTrafficStats(cmdData) {
var stats = parent.getTrafficStats();
for (var i in stats) {
if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
}
}
function serverUserCommandTrafficDelta(cmdData) {
const stats = parent.getTrafficDelta(obj.trafficStats);
obj.trafficStats = stats.current;
for (var i in stats.delta) {
if (typeof stats.delta[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats.delta[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats.delta[i] + '\r\n'); }
}
}
function serverUserCommandWatchdog(cmdData) {
if (parent.parent.watchdog == null) {
cmdData.result = 'Server watchdog not active.';
} else {
cmdData.result = 'Server watchdog active.\r\n';
if (parent.parent.watchdogmaxtime != null) { cmdData.result += 'Largest timeout was ' + parent.parent.watchdogmax + 'ms on ' + parent.parent.watchdogmaxtime + '\r\n'; }
for (var i in parent.parent.watchdogtable) { cmdData.result += parent.parent.watchdogtable[i] + '\r\n'; }
}
}
function serverUserCommandAcceleratorsStats(cmdData) {
var stats = parent.parent.certificateOperations.getAcceleratorStats();
for (var i in stats) {
if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
}
}
function serverUserCommandMpsStats(cmdData) {
if (parent.parent.mpsserver == null) {
cmdData.result = 'MPS not enabled.';
} else {
var stats = parent.parent.mpsserver.getStats();
for (var i in stats) {
if (typeof stats[i] == 'object') { cmdData.result += (i + ': ' + JSON.stringify(stats[i]) + '\r\n'); } else { cmdData.result += (i + ': ' + stats[i] + '\r\n'); }
}
}
}
function serverUserCommandMps(cmdData) {
if (parent.parent.mpsserver == null) {
cmdData.result = 'MPS not enabled.';
} else {
const connectionTypes = ['CIRA', 'Relay', 'LMS'];
for (var nodeid in parent.parent.mpsserver.ciraConnections) {
cmdData.result += nodeid;
var connections = parent.parent.mpsserver.ciraConnections[nodeid];
for (var i in connections) { cmdData.result += ', ' + connectionTypes[connections[i].tag.connType]; }
cmdData.result += '\r\n';
}
if (cmdData.result == '') { cmdData.result = 'MPS has not connections.'; }
}
}
function serverUserCommandDbStats(cmdData) {
parent.parent.db.getStats(function (stats) {
var r2 = '';
for (var i in stats) { r2 += (i + ': ' + stats[i] + '\r\n'); }
try { ws.send(JSON.stringify({ action: 'serverconsole', value: r2, tag: cmdData.command.tag })); } catch (ex) { }
});
}
function serverUserCommandDbCounters(cmdData) {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: JSON.stringify(parent.parent.db.dbCounters, null, 2), tag: cmdData.command.tag })); } catch (ex) { }
}
function serverUserCommandServerUpdate(cmdData) {
cmdData.result = 'Performing server update...';
var version = null;
if (cmdData.cmdargs['_'].length > 0) {
version = cmdData.cmdargs['_'][0];
// This call is SLOW. We only want to validate version if we have to
if (version != 'stable' && version != 'latest') {
parent.parent.getServerVersions((data) => {
var versions = JSON.parse(data);
if (versions.includes(version)) {
if (parent.parent.performServerUpdate(version) == false) {
try {
ws.send(JSON.stringify({ action: 'serverconsole',
value: 'Server self-update not possible.'}));
} catch (ex) { }
}
} else {
try {
ws.send(JSON.stringify({ action: 'serverconsole',
value: 'Invalid version. Aborting update'}));
} catch (ex) { }
}
});
} else {
if (parent.parent.performServerUpdate(version) == false) {
cmdData.result = 'Server self-update not possible.';
}
}
} else {
if (parent.parent.performServerUpdate(version) == false) {
cmdData.result = 'Server self-update not possible.';
}
}
}
function serverUserCommandPrint(cmdData) {
console.log(cmdData.cmdargs['_'][0]);
}
function serverUserCommandAmtPasswords(cmdData) {
if (parent.parent.amtPasswords == null) {
cmdData.result = "No Intel AMT password table."
} else {
for (var i in parent.parent.amtPasswords) { cmdData.result += (i + ' - ' + parent.parent.amtPasswords[i].join(', ') + '\r\n'); }
}
}
function serverUserCommandUpdateCheck(cmdData) {
parent.parent.getServerTags(function (tags, error) {
var r2 = '';
if (error != null) { r2 += 'Exception: ' + error + '\r\n'; }
else { for (var i in tags) { r2 += i + ': ' + tags[i] + '\r\n'; } }
try { ws.send(JSON.stringify({ action: 'serverconsole', value: r2, tag: cmdData.command.tag })); } catch (ex) { }
});
cmdData.result = "Checking server update...";
}
function serverUserCommandMaintenance(cmdData) {
var arg = null, changed = false;
if ((cmdData.cmdargs['_'] != null) && (cmdData.cmdargs['_'][0] != null)) { arg = cmdData.cmdargs['_'][0].toLowerCase(); }
if (arg == 'enabled') { parent.parent.config.settings.maintenancemode = 1; changed = true; }
else if (arg == 'disabled') { delete parent.parent.config.settings.maintenancemode; changed = true; }
cmdData.result = 'Maintenance mode: ' + ((parent.parent.config.settings.maintenancemode == null) ? 'Disabled' : 'Enabled');
if (changed == false) { cmdData.result += '\r\nTo change type: maintenance [enabled|disabled]'; }
}
function serverUserCommandInfo(cmdData) {
var info = process.memoryUsage();
info.dbType = ['None', 'NeDB', 'MongoJS', 'MongoDB'][parent.db.databaseType];
try { if (parent.parent.multiServer != null) { info.serverId = parent.parent.multiServer.serverid; } } catch (ex) { }
if (parent.db.databaseType == 3) { info.dbChangeStream = parent.db.changeStream; }
if (parent.parent.pluginHandler != null) { info.plugins = []; for (var i in parent.parent.pluginHandler.plugins) { info.plugins.push(i); } }
try { info.nodeVersion = process.version; } catch (ex) { }
try { info.meshVersion = parent.parent.currentVer; } catch (ex) { }
try { info.platform = process.platform; } catch (ex) { }
try { info.arch = process.arch; } catch (ex) { }
try { info.pid = process.pid; } catch (ex) { }
try { info.uptime = process.uptime(); } catch (ex) { }
try { info.cpuUsage = process.cpuUsage(); } catch (ex) { }
try { info.warnings = parent.parent.getServerWarnings(); } catch (ex) { }
try { info.database = ["Unknown", "NeDB", "MongoJS", "MongoDB", "MariaDB", "MySQL"][parent.parent.db.databaseType]; } catch (ex) { }
try { info.productionMode = ((process.env.NODE_ENV != null) && (process.env.NODE_ENV == 'production')); } catch (ex) { }
try { info.allDevGroupManagers = parent.parent.config.settings.managealldevicegroups; } catch (ex) { }
cmdData.result = JSON.stringify(info, null, 4);
}
function serverUserCommandNodeConfig(cmdData) {
cmdData.result = JSON.stringify(process.config, null, 4);
}
function serverUserCommandVersions(cmdData) {
cmdData.result = JSON.stringify(process.versions, null, 4);
}
function serverUserCommandArgs(cmdData) {
cmdData.result = 'args: ' + JSON.stringify(cmdData.cmdargs);
}
function serverUserCommandUserSessions(cmdData) {
var userSessionCount = 0;
var filter = null;
var arg = cmdData.cmdargs['_'][0];
if (typeof arg == 'string') { if (arg.indexOf('/') >= 0) { filter = arg; } else { filter = ('user/' + domain.id + '/' + arg); } }
for (var i in parent.wssessions) {
if ((filter == null) || (filter == i)) {
userSessionCount++;
cmdData.result += (i + ', ' + parent.wssessions[i].length + ' session' + ((parent.wssessions[i].length > 1) ? 's' : '') + '.\r\n');
for (var j in parent.wssessions[i]) {
cmdData.result += ' ' + parent.wssessions[i][j].clientIp + ' --> ' + parent.wssessions[i][j].sessionId + ((parent.wssessions[i][j].xclosed) ? (', CLOSED-' + parent.wssessions[i][j].xclosed):'') + '\r\n';
}
}
}
if (userSessionCount == 0) { cmdData.result = 'None.'; }
}
function serverUserCommandCloseUserSessions(cmdData) {
var userSessionCount = 0;
var filter = null;
var arg = cmdData.cmdargs['_'][0];
if (typeof arg == 'string') { if (arg.indexOf('/') >= 0) { filter = arg; } else { filter = ('user/' + domain.id + '/' + arg); } }
if (filter == null) {
cmdData.result += "Usage: closeusersessions <username>";
} else {
cmdData.result += "Closing user sessions for: " + filter + '\r\n';
for (var i in parent.wssessions) {
if (filter == i) {
userSessionCount++;
for (var j in parent.wssessions[i]) {
parent.wssessions[i][j].send(JSON.stringify({ action: 'stopped', msg: "Administrator forced disconnection" }));
parent.wssessions[i][j].close();
}
}
}
if (userSessionCount < 2) { cmdData.result += 'Disconnected ' + userSessionCount + ' session.'; } else { cmdData.result += 'Disconnected ' + userSessionCount + ' sessions.'; };
}
}
function serverUserCommandResetServer(cmdData) {
console.log("Server restart...");
process.exit(0);
}
function serverUserCommandTaskLimiter(cmdData) {
if (parent.parent.taskLimiter != null) {
//var obj = { maxTasks: maxTasks, maxTaskTime: (maxTaskTime * 1000), nextTaskId: 0, currentCount: 0, current: {}, pending: [[], [], []], timer: null };
const tl = parent.parent.taskLimiter;
cmdData.result += 'MaxTasks: ' + tl.maxTasks + ', NextTaskId: ' + tl.nextTaskId + '\r\n';
cmdData.result += 'MaxTaskTime: ' + (tl.maxTaskTime / 1000) + ' seconds, Timer: ' + (tl.timer != null) + '\r\n';
var c = [];
for (var i in tl.current) { c.push(i); }
cmdData.result += 'Current (' + tl.currentCount + '): [' + c.join(', ') + ']\r\n';
cmdData.result += 'Pending (High/Med/Low): ' + tl.pending[0].length + ', ' + tl.pending[1].length + ', ' + tl.pending[2].length + '\r\n';
}
}
function serverUserCommandSetMaxTasks(cmdData) {
if ((cmdData.cmdargs["_"].length != 1) || (parseInt(cmdData.cmdargs["_"][0]) < 1) || (parseInt(cmdData.cmdargs["_"][0]) > 1000)) {
cmdData.result = 'Usage: setmaxtasks [1 to 1000]';
} else {
parent.parent.taskLimiter.maxTasks = parseInt(cmdData.cmdargs["_"][0]);
cmdData.result = 'MaxTasks set to ' + parent.parent.taskLimiter.maxTasks + '.';
}
}
function serverUserCommandCores(cmdData) {
if (parent.parent.defaultMeshCores != null) {
for (var i in parent.parent.defaultMeshCores) {
cmdData.result += i + ': ' + parent.parent.defaultMeshCores[i].length + ' bytes\r\n';
}
}
}
function serverUserCommandShowPaths(cmdData) {
cmdData.result = 'Parent: ' + parent.parent.parentpath + '\r\n';
cmdData.result += 'Data: ' + parent.parent.datapath + '\r\n';
cmdData.result += 'Files: ' + parent.parent.filespath + '\r\n';
cmdData.result += 'Backup: ' + parent.parent.backuppath + '\r\n';
cmdData.result += 'Record: ' + parent.parent.recordpath + '\r\n';
cmdData.result += 'WebPublic: ' + parent.parent.webPublicPath + '\r\n';
cmdData.result += 'WebViews: ' + parent.parent.webViewsPath + '\r\n';
if (parent.parent.webViewsOverridePath) { cmdData.result += 'XWebPublic: ' + parent.parent.webViewsOverridePath + '\r\n'; }
if (parent.parent.webViewsOverridePath) { cmdData.result += 'XWebViews: ' + parent.parent.webPublicOverridePath + '\r\n'; }
}
function serverUserCommandMigrationAgents(cmdData) {
if (parent.parent.swarmserver == null) {
cmdData.result = 'Swarm server not running.';
} else {
for (var i in parent.parent.swarmserver.migrationAgents) {
var arch = parent.parent.swarmserver.migrationAgents[i];
for (var j in arch) { var agent = arch[j]; cmdData.result += 'Arch ' + agent.arch + ', Ver ' + agent.ver + ', Size ' + ((agent.binary == null) ? 0 : agent.binary.length) + '<br />'; }
}
}
}
function serverUserCommandSwarmStats(cmdData) {
if (parent.parent.swarmserver == null) {
cmdData.result = 'Swarm server not running.';
} else {
for (var i in parent.parent.swarmserver.stats) {
if (typeof parent.parent.swarmserver.stats[i] == 'object') {
cmdData.result += i + ': ' + JSON.stringify(parent.parent.swarmserver.stats[i]) + '\r\n';
} else {
cmdData.result += i + ': ' + parent.parent.swarmserver.stats[i] + '\r\n';
}
}
}
}
function serverUserCommandRelays(cmdData) {
for (var i in parent.wsrelays) {
cmdData.result += 'id: ' + i + ', ' + ((parent.wsrelays[i].state == 2) ? 'connected' : 'pending');
if (parent.wsrelays[i].peer1 != null) {
cmdData.result += ', ' + cleanRemoteAddr(parent.wsrelays[i].peer1.req.clientIp);
if (parent.wsrelays[i].peer1.user) { cmdData.result += ' (User:' + parent.wsrelays[i].peer1.user.name + ')' }
}
if (parent.wsrelays[i].peer2 != null) {
cmdData.result += ' to ' + cleanRemoteAddr(parent.wsrelays[i].peer2.req.clientIp);
if (parent.wsrelays[i].peer2.user) { cmdData.result += ' (User:' + parent.wsrelays[i].peer2.user.name + ')' }
}
cmdData.result += '\r\n';
}
if (cmdData.result == '') { cmdData.result = 'No relays.'; }
}
function serverUserCommandRemoveInactiveDevices(cmdData) {
parent.db.removeInactiveDevices();
cmdData.result = 'Ok';
}
function serverUserCommandAutoBackup(cmdData) {
var backupResult = parent.db.performBackup(function (msg) {
try { ws.send(JSON.stringify({ action: 'serverconsole', value: msg, tag: cmdData.command.tag })); } catch (ex) { }
});
if (backupResult == 0) { cmdData.result = 'Starting auto-backup...'; } else { cmdData.result = 'Backup alreay in progress.'; }
}
function serverUserCommandBackupConfig(cmdData) {
cmdData.result = parent.db.getBackupConfig();
}
function serverUserCommandFirebase(cmdData) {
if (parent.parent.firebase == null) {
cmdData.result = "Firebase push messaging not supported";
} else {
cmdData.result = JSON.stringify(parent.parent.firebase.stats, null, 2);
}
}
function validNodeIdAndDomain(command) {
if (common.validateString(command.nodeid, 1, 1024) == false) return false; // Check nodeid
if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
if ((command.nodeid.split('/').length != 3) || (command.nodeid.split('/')[1] != domain.id)) return false; // Invalid domain, operation only valid for current domain
return true;
}
function getUserExtraIds() {
var extraids = null;
if (obj.user.links != null) {
for (var i in obj.user.links) {
if (i.startsWith('node/')) { if (extraids == null) { extraids = []; } extraids.push(i); }
else if (i.startsWith('ugrp/')) {
const g = parent.userGroups[i];
if ((g != null) && (g.links != null)) {
for (var j in g.links) { if (j.startsWith('node/')) { if (extraids == null) { extraids = []; } extraids.push(j); } }
}
}
}
}
return extraids;
}
function userHasSiteUpdate() { return ((user.siteadmin & SITERIGHT_SERVERUPDATE) > 0); }
function domainHasMyServerErrorLog() { return !((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.errorlog !== true))); }
function domainHasMyServerUpgrade() { return !((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver !== true) && (domain.myserver.upgrade !== true))); }
function csvClean(s) { return '\"' + s.split('\"').join('').split(',').join('').split('\r').join('').split('\n').join('') + '\"'; }
// Return detailed information about an array of nodeid's
function getDeviceDetailedInfo(nodeids, type, func) {
if (nodeids == null) { getAllDeviceDetailedInfo(type, func); return; }
var results = [], resultPendingCount = 0;
for (var i in nodeids) {
// Fetch the node from the database
resultPendingCount++;
const getNodeFunc = function (node, rights, visible) {
if ((node != null) && (visible == true)) {
const getNodeSysInfoFunc = function (err, docs) {
const getNodeNetInfoFunc = function (err, docs) {
var netinfo = null;
if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; }
resultPendingCount--;
getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo });
if (resultPendingCount == 0) { func(getNodeFunc.results, type); }
}
getNodeNetInfoFunc.results = getNodeSysInfoFunc.results;
getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid;
getNodeNetInfoFunc.node = getNodeSysInfoFunc.node;
if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; }
// Query the database for network information
db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc);
}
getNodeSysInfoFunc.results = getNodeFunc.results;
getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid;
getNodeSysInfoFunc.node = node;
// Query the database for system information
db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc);
} else { resultPendingCount--; }
if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); }
}
getNodeFunc.results = results;
getNodeFunc.nodeid = nodeids[i];
parent.GetNodeWithRights(domain, user, nodeids[i], getNodeFunc);
}
}
// Return detailed information about all nodes this user has access to
function getAllDeviceDetailedInfo(type, func) {
// Get all device groups this user has access to
var links = parent.GetAllMeshIdWithRights(user);
// Add any nodes with direct rights or any nodes with user group direct rights
var extraids = getUserExtraIds();
// Request a list of all nodes
db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', null, function (err, docs) {
if (docs == null) { docs = []; }
parent.common.unEscapeAllLinksFieldName(docs);
var results = [], resultPendingCount = 0;
for (i in docs) {
// Check device links, if a link points to an unknown user, remove it.
parent.cleanDevice(docs[i]);
// Fetch the node from the database
resultPendingCount++;
const getNodeFunc = function (node, rights, visible) {
if ((node != null) && (visible == true)) {
const getNodeSysInfoFunc = function (err, docs) {
const getNodeNetInfoFunc = function (err, docs) {
var netinfo = null;
if ((err == null) && (docs != null) && (docs.length == 1)) { netinfo = docs[0]; }
resultPendingCount--;
getNodeNetInfoFunc.results.push({ node: parent.CloneSafeNode(getNodeNetInfoFunc.node), sys: getNodeNetInfoFunc.sysinfo, net: netinfo });
if (resultPendingCount == 0) { func(getNodeFunc.results, type); }
}
getNodeNetInfoFunc.results = getNodeSysInfoFunc.results;
getNodeNetInfoFunc.nodeid = getNodeSysInfoFunc.nodeid;
getNodeNetInfoFunc.node = getNodeSysInfoFunc.node;
if ((err == null) && (docs != null) && (docs.length == 1)) { getNodeNetInfoFunc.sysinfo = docs[0]; }
// Query the database for network information
db.Get('if' + getNodeSysInfoFunc.nodeid, getNodeNetInfoFunc);
}
getNodeSysInfoFunc.results = getNodeFunc.results;
getNodeSysInfoFunc.nodeid = getNodeFunc.nodeid;
getNodeSysInfoFunc.node = node;
// Query the database for system information
db.Get('si' + getNodeFunc.nodeid, getNodeSysInfoFunc);
} else { resultPendingCount--; }
if (resultPendingCount == 0) { func(getNodeFunc.results.join('\r\n'), type); }
}
getNodeFunc.results = results;
getNodeFunc.nodeid = docs[i]._id;
parent.GetNodeWithRights(domain, user, docs[i]._id, getNodeFunc);
}
});
}
// Display a notification message for this session only.
function displayNotificationMessage(msg, title, tag, titleid, msgid, args) {
ws.send(JSON.stringify({ 'action': 'msg', 'type': 'notify', id: Math.random(), 'value': msg, 'title': title, 'userid': user._id, 'username': user.name, 'tag': tag, 'titleid': titleid, 'msgid': msgid, 'args': args }));
}
// Read the folder and all sub-folders and serialize that into json.
function readFilesRec(path) {
var r = {}, dir = fs.readdirSync(path);
for (var i in dir) {
var f = { t: 3, d: 111 };
var stat = fs.statSync(path + '/' + dir[i]);
if ((stat.mode & 0x004000) == 0) { f.s = stat.size; f.d = stat.mtime.getTime(); } else { f.t = 2; f.f = readFilesRec(path + '/' + dir[i]); }
r[dir[i]] = f;
}
return r;
}
// Delete a directory with a files and directories within it
// TODO, make this an async function
function deleteFolderRecursive(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(function (file, index) {
var curPath = parent.path.join(path, file);;
if (fs.lstatSync(curPath).isDirectory()) { // recurse
deleteFolderRecursive(curPath);
} else { // delete file
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(path);
}
};
function updateUserFiles(user, ws, domain) {
if ((user == null) || (user.siteadmin == null) || ((user.siteadmin & 8) == 0)) return;
// Request the list of server files
var files = { action: 'files', filetree: { n: 'Root', f: {} } };
// Add user files
files.filetree.f[user._id] = { t: 1, n: 'My Files', f: {} };
files.filetree.f[user._id].maxbytes = parent.getQuota(user._id, domain);
var usersplit = user._id.split('/'), domainx = 'domain';
if (usersplit[1].length > 0) domainx = 'domain-' + usersplit[1];
// Read all files recursively
try {
files.filetree.f[user._id].f = readFilesRec(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2]));
} catch (e) {
// TODO: We may want to fake this file structure until it's needed.
// Got an error, try to create all the folders and try again...
try { fs.mkdirSync(parent.filespath); } catch (e) { }
try { fs.mkdirSync(parent.path.join(parent.filespath, domainx)); } catch (e) { }
try { fs.mkdirSync(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2])); } catch (e) { }
try { fs.mkdirSync(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2] + '/Public')); } catch (e) { }
try { files.filetree.f[user._id].f = readFilesRec(parent.path.join(parent.filespath, domainx + '/user-' + usersplit[2])); } catch (e) { }
}
// Add files for each mesh // TODO: Get all meshes including groups!!
for (var i in user.links) {
if ((user.links[i].rights & 32) != 0) { // Check that we have file permissions
var mesh = parent.meshes[i];
if (mesh) {
var meshsplit = mesh._id.split('/');
files.filetree.f[mesh._id] = { t: 4, n: mesh.name, f: {} };
files.filetree.f[mesh._id].maxbytes = parent.getQuota(mesh._id, domain);
// Read all files recursively
try {
files.filetree.f[mesh._id].f = readFilesRec(parent.path.join(parent.filespath, domainx + '/mesh-' + meshsplit[2]));
} catch (e) {
files.filetree.f[mesh._id].f = {}; // Got an error, return empty folder. We will create the folder only when needed.
}
}
}
}
// Respond
try { ws.send(JSON.stringify(files)); } catch (ex) { }
}
function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
//function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
// Split a string taking into account the quoats. Used for command line parsing
function splitArgs(str) { var myArray = [], myRegexp = /[^\s"]+|"([^"]*)"/gi; do { var match = myRegexp.exec(str); if (match != null) { myArray.push(match[1] ? match[1] : match[0]); } } while (match != null); return myArray; }
function toNumberIfNumber(x) { if ((typeof x == 'string') && (+parseInt(x) === x)) { x = parseInt(x); } return x; }
function isPhoneNumber(x) {
var ok = true;
if (x.startsWith('+')) { x = x.substring(1); }
for (var i = 0; i < x.length; i++) { var c = x.charCodeAt(i); if (((c < 48) || (c > 57)) && (c != 32) && (c != 45) && (c != 46)) { ok = false; } }
return ok && (x.length >= 10);
}
function removeAllUnderScore(obj) {
if (typeof obj != 'object') return obj;
for (var i in obj) { if (i.startsWith('_')) { delete obj[i]; } else if (typeof obj[i] == 'object') { removeAllUnderScore(obj[i]); } }
return obj;
}
// Generate a 8 digit integer with even random probability for each value.
function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
// Parse arguments string array into an object
function parseArgs(argv) {
var results = { '_': [] }, current = null;
for (var i = 1, len = argv.length; i < len; i++) {
var x = argv[i];
if (x.length > 2 && x[0] == '-' && x[1] == '-') {
if (current != null) { results[current] = true; }
current = x.substring(2);
} else {
if (current != null) { results[current] = toNumberIfNumber(x); current = null; } else { results['_'].push(toNumberIfNumber(x)); }
}
}
if (current != null) { results[current] = true; }
return results;
}
// Return true if at least one element of arr2 is in arr1
function findOne(arr1, arr2) { if ((arr1 == null) || (arr2 == null)) return false; return arr2.some(function (v) { return arr1.indexOf(v) >= 0; }); };
function getRandomPassword() { return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); }
function handleAmtCommand(cmd, node) {
if (cmd == null) return;
var host = cmd.nodeid;
if (cmd.mode == 1) { host = node.host; }
var tlsoptions = null;
var wsman = new Wsman(WsmanComm, host, node.intelamt.tls ? 16993 : 16992, node.intelamt.user, node.intelamt.pass,
node.intelamt.tls, tlsoptions, parent.parent, cmd.mode);
var amt = new Amt(wsman);
switch (cmd.command) {
case 'Get-GeneralSettings': {
amt.Get('AMT_GeneralSettings', function (obj, name, response, status) {
if (status == 200) {
var resp = { action: 'amt', nodeid: cmd.nodeid, command: 'Get-GeneralSettings', value: response.Body }
ws.send(JSON.stringify(resp));
} else {
ws.send(JSON.stringify({ 'error': error }));
}
});
break;
}
default: {
// Do nothing
}
}
}
// Return the number of 2nd factor for this account
function count2factoraAuths() {
var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null));
var sms2fa = ((parent.parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)));
var authFactorCount = 0;
if (typeof user.otpsecret == 'string') { authFactorCount++; } // Authenticator time factor
if (email2fa && (user.otpekey != null)) { authFactorCount++; } // EMail factor
if (sms2fa && (user.phone != null)) { authFactorCount++; } // SMS factor
if (user.otphkeys != null) { authFactorCount += user.otphkeys.length; } // FIDO hardware factor
if ((authFactorCount > 0) && (user.otpkeys != null)) { authFactorCount++; } // Backup keys
return authFactorCount;
}
return obj;
};