mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2024-12-25 06:35:54 -05:00
c50b3c0d26
Fix obscure bug when the last user added in list of addusertousergroup was an unknown user causing the server to crash. Also updated the group update message of same to display all users added, rather than only the last one in the list.
6911 lines
469 KiB
JavaScript
6911 lines
469 KiB
JavaScript
/**
|
||
* @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_NONEWDEVICES = 0x00001000; // 4096
|
||
const SITERIGHT_ADMIN = 0xFFFFFFFF;
|
||
|
||
// Protocol Numbers
|
||
const PROTOCOL_TERMINAL = 1;
|
||
const PROTOCOL_DESKTOP = 2;
|
||
const PROTOCOL_FILES = 5;
|
||
const PROTOCOL_AMTWSMAN = 100;
|
||
const PROTOCOL_AMTREDIR = 101;
|
||
const PROTOCOL_MESSENGER = 200;
|
||
const PROTOCOL_WEBRDP = 201;
|
||
const PROTOCOL_WEBSSH = 202;
|
||
const PROTOCOL_WEBSFTP = 203;
|
||
const PROTOCOL_WEBVNC = 204;
|
||
|
||
// 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 {
|
||
// If updating guest device shares, if we are updating a user that is not creator of the share, remove the URL.
|
||
if ((event.action == 'deviceShareUpdate') && (Array.isArray(event.deviceShares))) {
|
||
event = common.Clone(event);
|
||
for (var i in event.deviceShares) { if (event.deviceShares[i].userid != user._id) { delete event.deviceShares[i].url; } }
|
||
}
|
||
|
||
// 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 (ex) { console.log(ex); }
|
||
}
|
||
};
|
||
|
||
user.subscriptions = parent.subscribe(user._id, ws); // Subscribe to events
|
||
try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
|
||
|
||
// Send current server statistics
|
||
obj.SendServerStats = function () {
|
||
// Take a look at server stats
|
||
var os = require('os');
|
||
var stats = { action: 'serverstats', totalmem: os.totalmem(), freemem: os.freemem() };
|
||
try { stats.cpuavg = os.loadavg(); } catch (ex) { }
|
||
if (parent.parent.platform != 'win32') {
|
||
try { stats.availablemem = 1024 * Number(/MemAvailable:[ ]+(\d+)/.exec(fs.readFileSync('/proc/meminfo', 'utf8'))[1]); } catch (ex) { }
|
||
}
|
||
|
||
// Count the number of device groups that are not deleted
|
||
var activeDeviceGroups = 0;
|
||
for (var i in parent.meshes) { if (parent.meshes[i].deleted == null) { activeDeviceGroups++; } } // This is not ideal for performance, we want to dome something better.
|
||
var serverStats = {
|
||
UserAccounts: Object.keys(parent.users).length,
|
||
DeviceGroups: activeDeviceGroups,
|
||
AgentSessions: Object.keys(parent.wsagents).length,
|
||
ConnectedUsers: Object.keys(parent.wssessions).length,
|
||
UsersSessions: Object.keys(parent.wssessions2).length,
|
||
RelaySessions: parent.relaySessionCount,
|
||
RelayCount: Object.keys(parent.wsrelays).length
|
||
};
|
||
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 users
|
||
if (domain.sessionrecording.onlyselectedusergroups === true) { serverinfo.userGroupsSessionRecording = 1; } // Allow enabling of session recording for user groups
|
||
if (domain.sessionrecording.onlyselecteddevicegroups === true) { serverinfo.devGroupSessionRecording = 1; } // Allow enabling of session recording for device groups
|
||
}
|
||
if ((parent.parent.config.domains[domain.id].amtacmactivation != null) && (parent.parent.config.domains[domain.id].amtacmactivation.acmmatch != null)) {
|
||
var matchingDomains = [];
|
||
for (var i in parent.parent.config.domains[domain.id].amtacmactivation.acmmatch) {
|
||
var cn = parent.parent.config.domains[domain.id].amtacmactivation.acmmatch[i].cn;
|
||
if ((cn != '*') && (matchingDomains.indexOf(cn) == -1)) { matchingDomains.push(cn); }
|
||
}
|
||
if (matchingDomains.length > 0) { serverinfo.amtAcmFqdn = matchingDomains; }
|
||
}
|
||
if ((typeof domain.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; }
|
||
}
|
||
if (Array.isArray(domain.preconfiguredremoteinput)) { serverinfo.preConfiguredRemoteInput = domain.preconfiguredremoteinput; }
|
||
|
||
// 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 'urlargs':
|
||
{
|
||
console.log(req.query);
|
||
console.log(command.args);
|
||
break;
|
||
}
|
||
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 'interuser':
|
||
{
|
||
// Sends data between users only if allowed. Only a user in the "interUserMessaging": [] list, in the settings section of the config.json can receive and send inter-user messages from and to all users.
|
||
if ((parent.parent.config.settings.interusermessaging == null) || (parent.parent.config.settings.interusermessaging == false) || (command.data == null)) return;
|
||
if (typeof command.sessionid == 'string') { var userSessionId = command.sessionid.split('/'); if (userSessionId.length != 4) return; command.userid = userSessionId[0] + '/' + userSessionId[1] + '/' + userSessionId[2]; }
|
||
if (common.validateString(command.userid, 0, 2014) == false) return;
|
||
var userSplit = command.userid.split('/');
|
||
if (userSplit.length == 1) { command.userid = 'user/' + domain.id + '/' + command.userid; userSplit = command.userid.split('/'); }
|
||
if ((userSplit.length != 3) || (userSplit[0] != 'user') || (userSplit[1] != domain.id) || (parent.users[command.userid] == null)) return; // Make sure the target userid is valid and within the domain
|
||
const allowed = ((parent.parent.config.settings.interusermessaging === true) || (parent.parent.config.settings.interusermessaging.indexOf(obj.user._id) >= 0) || (parent.parent.config.settings.interusermessaging.indexOf(command.userid) >= 0));
|
||
if (allowed == false) return;
|
||
|
||
// Get sessions
|
||
var sessions = parent.wssessions[command.userid];
|
||
if (sessions == null) break;
|
||
|
||
// Create the notification message and send on all sessions except our own (no echo back).
|
||
var notification = JSON.stringify({ action: 'interuser', sessionid: ws.sessionId, data: command.data, scope: (command.sessionid != null)?'session':'user' });
|
||
for (var i in sessions) {
|
||
if ((command.sessionid != null) && (sessions[i].sessionId != command.sessionid)) continue; // Send to a specific session
|
||
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) {
|
||
|
||
//console.log(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
|
||
change = '';
|
||
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 ((command.flags != null) && (typeof command.flags == 'number')) {
|
||
// Flags: 2 = Session Recording
|
||
if ((command.flags == 0) && (group.flags != null)) { delete group.flags; } else { if (command.flags !== group.flags) { group.flags = command.flags; } }
|
||
if (change == '') { change = 'User group features changed.'; }
|
||
}
|
||
|
||
if (change != '') {
|
||
db.Set(group);
|
||
var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, consent: ((group.consent == null) ? 0 : group.consent), action: 'usergroupchange', links: group.links, flags: group.flags, msg: change, domain: domain.id };
|
||
if (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, knownUsers;
|
||
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');
|
||
|
||
knownUsers.push(chguser)
|
||
// 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: [knownUsers.map((u)=>u.name), group.name], msg: 'Added user(s) ' + knownUsers.map((u)=>u.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.
|
||
|
||
// 2 = WebPage device connections
|
||
// 4 = WebPage device disconnections
|
||
// 8 = WebPage device desktop and serial events
|
||
// 16 = Email device connections
|
||
// 32 = Email device disconnections
|
||
|
||
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 device group notification
|
||
if (user.links[command.meshid]) {
|
||
if (command.notify == 0) {
|
||
delete user.links[command.meshid].notify;
|
||
} else {
|
||
user.links[command.meshid].notify = command.notify;
|
||
}
|
||
}
|
||
|
||
// Change user notification if needed, this is needed then a user has device rights thru a user group
|
||
if ((command.notify == 0) && (user.notify != null) && (user.notify[command.meshid] != null)) { delete user.notify[command.meshid]; }
|
||
if ((command.notify != 0) && (user.links[command.meshid] == null)) { if (user.notify == null) { user.notify = {} } user.notify[command.meshid] = command.notify; }
|
||
|
||
// Save the user
|
||
parent.db.SetUser(user);
|
||
|
||
// Notify change
|
||
var targets = ['*', 'server-users', user._id];
|
||
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
|
||
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 73, msg: 'Device group notification changed', domain: domain.id };
|
||
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
|
||
parent.parent.DispatchEvent(targets, obj, event);
|
||
|
||
break;
|
||
}
|
||
case 'changeusernotify':
|
||
{
|
||
if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) return; // If this account is settings locked, return here.
|
||
|
||
// 2 = WebPage device connections
|
||
// 4 = WebPage device disconnections
|
||
// 8 = WebPage device desktop and serial events
|
||
// 16 = Email device connections
|
||
// 32 = Email device disconnections
|
||
|
||
var err = null;
|
||
try {
|
||
// Change the current user's notification flags for a meshid
|
||
if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid device identifier'; } // Check the meshid
|
||
else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
|
||
if (common.validateInt(command.notify) == false) { err = 'Invalid notification flags'; }
|
||
//if (parent.IsMeshViewable(user, command.nodeid) == false) err = 'Access denied';
|
||
} catch (ex) { err = 'Validation exception: ' + ex; }
|
||
|
||
// Handle any errors
|
||
if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'changeusernotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
|
||
|
||
// Check if nothing has changed
|
||
if ((user.notify == null) && (command.notify == 0)) return;
|
||
if ((user.notify != null) && (user.notify[command.nodeid] == command.notify)) return;
|
||
|
||
// Change the notification
|
||
if (user.notify == null) { user.notify = {}; }
|
||
if (command.notify == 0) { delete user.notify[command.nodeid]; } else { user.notify[command.nodeid] = command.notify; }
|
||
if (Object.keys(user.notify).length == 0) { delete user.notify; }
|
||
|
||
// Save the user
|
||
parent.db.SetUser(user);
|
||
|
||
// Notify change
|
||
var targets = ['*', 'server-users', user._id];
|
||
if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
|
||
var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 130, msg: 'User notifications changed', domain: domain.id };
|
||
if (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) { // Check the nodeid
|
||
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
|
||
continue;
|
||
}
|
||
else if (nodeid.indexOf('/') == -1) { nodeid = 'node/' + domain.id + '/' + nodeid; }
|
||
else if ((nodeid.split('/').length != 3) || (nodeid.split('/')[1] != domain.id)) { // Invalid domain, operation only valid for current domain
|
||
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid domain' })); } catch (ex) { } }
|
||
continue;
|
||
}
|
||
|
||
// Get the node and the rights for this node
|
||
parent.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
|
||
// Check we have the rights to delete this device
|
||
if ((node == null) || (visible == false) || (rights & MESHRIGHT_WAKEDEVICE) == 0) {
|
||
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Invalid nodeid' })); } catch (ex) { } }
|
||
return;
|
||
}
|
||
|
||
// If 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) {
|
||
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'No known MAC addresses for this device' })); } catch (ex) { } }
|
||
return;
|
||
}
|
||
|
||
// Have the server send a wake-on-lan packet (Will not work in WAN-only)
|
||
if (parent.parent.meshScanner != null) { parent.parent.meshScanner.wakeOnLan(macs, node.host); }
|
||
|
||
// Get the list of device groups this user as wake permissions on
|
||
var targets = [], targetDeviceGroups = parent.GetAllMeshWithRights(user, MESHRIGHT_WAKEDEVICE);
|
||
for (j in targetDeviceGroups) { targets.push(targetDeviceGroups[j]._id); }
|
||
for (j in user.links) { if ((j.startsWith('node/')) && (typeof user.links[j].rights == 'number') && ((user.links[j].rights & MESHRIGHT_WAKEDEVICE) != 0)) { targets.push(j); } }
|
||
|
||
// Go thru all the connected agents and send wake-on-lan on all the ones in the target mesh list
|
||
var wakeCount = 0;
|
||
for (j in parent.wsagents) {
|
||
var agent = parent.wsagents[j];
|
||
if ((agent.authenticated == 2) && ((targets.indexOf(agent.dbMeshKey) >= 0) || (targets.indexOf(agent.dbNodeKey) >= 0))) {
|
||
//console.log('Asking agent ' + agent.dbNodeKey + ' to wake ' + macs.join(','));
|
||
try { agent.send(JSON.stringify({ action: 'wakeonlan', macs: macs })); wakeCount++; } catch (ex) { }
|
||
}
|
||
}
|
||
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'Used ' + wakeCount + ' device(s) to send wake packets' })); } catch (ex) { } }
|
||
} else {
|
||
if (command.nodeids.length == 1) { try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'No network information for this device' })); } catch (ex) { } }
|
||
}
|
||
});
|
||
});
|
||
|
||
if (command.nodeids.length > 1) {
|
||
// If we are waking multiple devices, confirm we got the command.
|
||
try { ws.send(JSON.stringify({ action: 'wakedevices', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case '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, userid: user._id, username: user.name, remoteaddr: req.clientIp })); } catch (ex) { }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Confirm we may be doing something (TODO)
|
||
if (command.responseid != null) {
|
||
try { ws.send(JSON.stringify({ action: 'poweraction', responseid: command.responseid, result: 'ok' })); } catch (ex) { }
|
||
} else {
|
||
try { ws.send(JSON.stringify({ action: 'poweraction' })); } catch (ex) { }
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case 'toast':
|
||
{
|
||
var err = null;
|
||
|
||
// Perform input validation
|
||
try {
|
||
if (common.validateStrArray(command.nodeids, 1, 256) == false) { err = "Invalid nodeids"; } // Check nodeids
|
||
else if (common.validateString(command.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 != null) && (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;
|
||
if (doc.userid != user._id) { delete doc.url; } // If this is not the user who created this link, don't give the link.
|
||
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: command.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 this is an agent self-sharing link, notify the agent
|
||
if (command.publicid.startsWith('AS:node/')) { routeCommandToNode({ action: 'msg', type: 'guestShare', nodeid: command.publicid.substring(3), flags: 0, url: null, viewOnly: false }); }
|
||
}
|
||
|
||
// 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 = null, expireTime = null;
|
||
if (command.expire != null) {
|
||
if (command.expire !== 0) {
|
||
// Now until expire in hours
|
||
startTime = Date.now();
|
||
expireTime = Date.now() + (60000 * command.expire);
|
||
} else {
|
||
delete 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, pid: publicid }; // Old style sharing cookie
|
||
var cookie = { a: 6, pid: publicid }; // New style sharing cookie
|
||
if ((startTime != null) && (expireTime != null)) { command.start = startTime; command.expire = cookie.e = expireTime; }
|
||
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; }
|
||
|
||
// 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;
|
||
command.publicid = publicid;
|
||
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, userid: user._id, guestName: command.guestname, consent: command.consent, url: url };
|
||
if ((startTime != null) && (expireTime != null)) { shareEntry.startTime = startTime; shareEntry.expireTime = expireTime; }
|
||
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;
|
||
if ((startTime != null) && (expireTime != null)) {
|
||
event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'addedDeviceShare', msg: 'Added device share: ' + command.guestname + '.', msgid: 101, msgArgs: [command.guestname, 'DATETIME:' + startTime, 'DATETIME:' + expireTime], domain: domain.id };
|
||
} else {
|
||
event = { etype: 'node', userid: user._id, username: user.name, nodeid: node._id, action: 'addedDeviceShare', msg: 'Added device share ' + command.guestname + ' with unlimited time.', msgid: 131, msgArgs: [command.guestname], 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;
|
||
}
|
||
case 'report': {
|
||
// Report request. Validate the input
|
||
if (common.validateInt(command.type, 1, 2) == false) break; // Validate type
|
||
if (common.validateInt(command.groupBy, 1, 3) == false) break; // Validate groupBy: 1 = User, 2 = Device, 3 = Day
|
||
if ((typeof command.start != 'number') || (typeof command.end != 'number') || (command.start >= command.end)) break; // Validate start and end time
|
||
const manageAllDeviceGroups = ((user.siteadmin == 0xFFFFFFFF) && (parent.parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0));
|
||
if ((command.devGroup != null) && (manageAllDeviceGroups == false) && ((user.links == null) || (user.links[command.devGroup] == null))) break; // Asking for a device group that is not allowed
|
||
|
||
if (command.type == 1) { // This is the remote session report. Shows desktop, terminal, files...
|
||
// If we are not user administrator on this site, only search for events with our own user id.
|
||
var ids = [user._id];
|
||
if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) {
|
||
if (command.devGroup != null) {
|
||
ids = [ user._id, command.devGroup ];
|
||
} else {
|
||
if (manageAllDeviceGroups) { ids = ['*']; } else if (user.links) { for (var i in user.links) { ids.push(i); } }
|
||
}
|
||
}
|
||
|
||
// Get the events in the time range
|
||
// MySQL or MariaDB query will ignore the MsgID filter.
|
||
db.GetEventsTimeRange(ids, domain.id, [5, 10, 11, 12, 122, 123, 124, 125, 126], new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
|
||
if (err != null) return;
|
||
var data = { groups: {} };
|
||
var guestNamePresent = false;
|
||
|
||
// Columns
|
||
if (command.groupBy == 1) {
|
||
data.groupFormat = 'user';
|
||
data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: 'nodeid', title: "device", format: 'node' }, { id: 'meshid', title: "devgroup", format: 'mesh' }, { id: 'guestname', title: "guest", align: 'center' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
|
||
} else if (command.groupBy == 2) {
|
||
data.groupFormat = 'nodemesh';
|
||
data.columns = [{ id: 'time', title: "time", format: 'datetime' }, { id: 'userid', title: "user", format: 'user' }, { id: 'guestname', title: "guest", align: 'center' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
|
||
} else if (command.groupBy == 3) {
|
||
data.columns = [{ id: 'time', title: "time", format: 'time' }, { id: 'nodeid', title: "device", format: 'node' }, { id: 'meshid', title: "devgroup", format: 'mesh' }, { id: 'guestname', title: "guest", align: 'center' }, { id: 'userid', title: "user", format: 'user' }, { id: 'protocol', title: "session", format: 'protocol', align: 'center' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: 'protocol' } ];
|
||
}
|
||
|
||
// Add traffic columns
|
||
if (command.showTraffic) {
|
||
data.columns.push({ id: 'bytesin', title: "bytesin", format: 'bytes', align: 'center', sumBy: 'protocol' });
|
||
data.columns.push({ id: 'bytesout', title: "bytesout", format: 'bytes', align: 'center', sumBy: 'protocol' });
|
||
}
|
||
|
||
// Rows
|
||
for (var i in docs) {
|
||
// If MySQL or MariaDB query, we can't filter on MsgID, so we have to do it here.
|
||
if ((docs[i].msgid != 5) && (docs[i].msgid != 10) && (docs[i].msgid != 11) && (docs[i].msgid != 12) && ((docs[i].msgid < 122) && (docs[i].msgid > 126))) continue;
|
||
if ((command.devGroup != null) && (docs[i].ids != null) && (docs[i].ids.indexOf(command.devGroup) == -1)) continue;
|
||
|
||
var entry = { time: docs[i].time.valueOf() };
|
||
|
||
// UserID
|
||
if (command.groupBy != 1) { entry.userid = docs[i].userid; }
|
||
if (command.groupBy != 2) { entry.nodeid = docs[i].nodeid; }
|
||
entry.protocol = docs[i].protocol;
|
||
|
||
// Device Group
|
||
if (docs[i].ids != null) { for (var j in docs[i].ids) { if (docs[i].ids[j].startsWith('mesh/')) { entry.meshid = docs[i].ids[j]; } } }
|
||
|
||
// Add traffic data
|
||
if (command.showTraffic) { entry.bytesin = docs[i].bytesin; entry.bytesout = docs[i].bytesout; }
|
||
|
||
// Add guest name if present
|
||
if (docs[i].guestname != null) { entry.guestname = docs[i].guestname; guestNamePresent = true; }
|
||
|
||
// Session length
|
||
if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { entry.length = docs[i].msgArgs[3]; }
|
||
else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { entry.length = docs[i].msgArgs[0]; }
|
||
|
||
if (command.groupBy == 1) { // Add entry to per user
|
||
if (data.groups[docs[i].userid] == null) { data.groups[docs[i].userid] = { entries: [] }; }
|
||
data.groups[docs[i].userid].entries.push(entry);
|
||
} else if (command.groupBy == 2) { // Add entry to per mesh+device
|
||
if (entry.meshid != null) {
|
||
var k = docs[i].nodeid + '/' + entry.meshid;
|
||
if (data.groups[k] == null) { data.groups[k] = { entries: [] }; }
|
||
data.groups[k].entries.push(entry);
|
||
} else {
|
||
if (data.groups[docs[i].nodeid] == null) { data.groups[docs[i].nodeid] = { entries: [] }; }
|
||
data.groups[docs[i].nodeid].entries.push(entry);
|
||
}
|
||
} else if (command.groupBy == 3) { // Add entry to per day
|
||
var day;
|
||
if ((typeof command.l == 'string') && (typeof command.tz == 'string')) {
|
||
day = new Date(docs[i].time).toLocaleDateString(command.l, { timeZone: command.tz });
|
||
} else {
|
||
day = docs[i].time; // TODO
|
||
}
|
||
if (data.groups[day] == null) { data.groups[day] = { entries: [] }; }
|
||
data.groups[day].entries.push(entry);
|
||
}
|
||
|
||
}
|
||
|
||
// Remove guest column if not needed
|
||
if (guestNamePresent == false) {
|
||
if ((command.groupBy == 1) || (command.groupBy == 3)) {
|
||
data.columns.splice(3, 1);
|
||
} else if (command.groupBy == 2) {
|
||
data.columns.splice(2, 1);
|
||
}
|
||
}
|
||
|
||
try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
|
||
});
|
||
}
|
||
|
||
if (command.type == 2) { // This is the user traffic usage report.
|
||
// If we are not user administrator on this site, only search for events with our own user id.
|
||
var ids = [user._id]; // If we are nto user administrator, only count our own traffic.
|
||
if ((user.siteadmin & SITERIGHT_MANAGEUSERS) != 0) { ids = ['*']; } // If user administrator, count traffic of all users.
|
||
|
||
// Get the events in the time range
|
||
// MySQL or MariaDB query will ignore the MsgID filter.
|
||
db.GetEventsTimeRange(ids, domain.id, [5, 10, 11, 12, 122, 123, 124, 125, 126], new Date(command.start * 1000), new Date(command.end * 1000), function (err, docs) {
|
||
if (err != null) return;
|
||
var data = { groups: { 0: { entries: [] } } };
|
||
data.columns = [{ id: 'userid', title: "user", format: 'user' }, { id: 'length', title: "length", format: 'seconds', align: 'center', sumBy: true }, { id: 'bytesin', title: "bytesin", format: 'bytes', align: 'center', sumBy: true }, { id: 'bytesout', title: "bytesout", format: 'bytes', align: 'center', sumBy: true }];
|
||
var userEntries = {};
|
||
|
||
// Sum all entry logs for each user
|
||
for (var i in docs) {
|
||
// If MySQL or MariaDB query, we can't filter on MsgID, so we have to do it here.
|
||
if ((docs[i].msgid != 5) && (docs[i].msgid != 10) && (docs[i].msgid != 11) && (docs[i].msgid != 12) && ((docs[i].msgid < 122) && (docs[i].msgid > 126))) continue;
|
||
if ((command.devGroup != null) && (docs[i].ids != null) && (docs[i].ids.indexOf(command.devGroup) == -1)) continue;
|
||
|
||
// Fetch or create the user entry
|
||
var userEntry = userEntries[docs[i].userid];
|
||
if (userEntry == null) { userEntry = { userid: docs[i].userid, length: 0, bytesin: 0, bytesout: 0}; }
|
||
if (docs[i].bytesin) { userEntry.bytesin += docs[i].bytesin; }
|
||
if (docs[i].bytesout) { userEntry.bytesout += docs[i].bytesout; }
|
||
|
||
// Session length
|
||
if (((docs[i].msgid >= 10) && (docs[i].msgid <= 12)) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[3] == 'number')) { userEntry.length += docs[i].msgArgs[3]; }
|
||
else if ((docs[i].msgid >= 122) && (docs[i].msgid <= 126) && (docs[i].msgArgs != null) && (typeof docs[i].msgArgs == 'object') && (typeof docs[i].msgArgs[0] == 'number')) { userEntry.length += docs[i].msgArgs[0]; }
|
||
|
||
// Set the user entry
|
||
userEntries[docs[i].userid] = userEntry;
|
||
}
|
||
|
||
var userEntries2 = [];
|
||
for (var i in userEntries) { userEntries2.push(userEntries[i]); }
|
||
data.groups[0].entries = userEntries2;
|
||
|
||
try { ws.send(JSON.stringify({ action: 'report', data: data })); } catch (ex) { }
|
||
});
|
||
}
|
||
|
||
break;
|
||
}
|
||
case 'endDesktopMultiplex': {
|
||
var err = null, xuser = null;
|
||
try {
|
||
if (command.xuserid.indexOf('/') < 0) { command.xuserid = 'user/' + domain.id + '/' + command.xuserid; }
|
||
if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid device identifier'; } // Check the meshid
|
||
else if (command.nodeid.indexOf('/') == -1) { command.nodeid = 'node/' + domain.id + '/' + command.nodeid; }
|
||
const xusersplit = command.xuserid.split('/');
|
||
xuser = parent.users[command.xuserid];
|
||
if (xuser == null) { err = 'User does not exists'; }
|
||
else if ((obj.crossDomain !== true) && ((xusersplit.length != 3) || (xusersplit[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: 'changeusernotify', responseid: command.responseid, result: err })); } catch (ex) { } } break; }
|
||
|
||
// Get the node and the rights for this node
|
||
parent.GetNodeWithRights(domain, xuser, command.nodeid, function (node, rights, visible) {
|
||
if ((rights != 0xFFFFFFFF) && (xuser._id != command.xuserid)) return;
|
||
const desktopRelay = parent.desktoprelays[command.nodeid];
|
||
if ((desktopRelay == null) || (desktopRelay === 1)) return; // If the desktopRelay is equal to 1, the relay is being constructed.
|
||
var viewersToClose = []; // Create a list of viewers to close. We don't want to close directly because it will change "desktopRelay.viewers" and we will not enumerate correctly.
|
||
for (var i = 0; i < desktopRelay.viewers.length; i++) {
|
||
const viewer = desktopRelay.viewers[i];
|
||
if ((viewer.user._id == command.xuserid) && (viewer.guestName == command.guestname)) { viewersToClose.push(viewer); } // Only close viewers that match the userid and guestname if present.
|
||
}
|
||
for (var i = 0; i < viewersToClose.length; i++) { viewersToClose[i].close(); } // Close any viewers we need closed.
|
||
});
|
||
|
||
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, ""],
|
||
'emailnotifications': [serverUserCommandEmailNotifications, ""],
|
||
'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 serverUserCommandEmailNotifications(cmdData) {
|
||
if (domain.mailserver == null) {
|
||
cmdData.result = "No email service enabled.";
|
||
} else {
|
||
var x = '';
|
||
for (var userid in domain.mailserver.deviceNotifications) {
|
||
x += userid + '\r\n';
|
||
for (var nodeid in domain.mailserver.deviceNotifications[userid].nodes) {
|
||
const info = domain.mailserver.deviceNotifications[userid].nodes[nodeid];
|
||
x += ' ' + info.mn + ', ' + info.nn + ', c:' + (info.c ? info.c : 0) + ', d:' + (info.d ? info.d : 0) + '\r\n';
|
||
}
|
||
}
|
||
cmdData.result = ((x == '')?'None':x);
|
||
}
|
||
}
|
||
|
||
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 = {};
|
||
try { info.meshVersion = 'v' + parent.parent.currentVer; } catch (ex) { }
|
||
try { info.nodeVersion = process.version; } catch (ex) { }
|
||
try { info.runMode = (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][(args.lanonly ? 2 : (args.wanonly ? 1 : 0))]); } catch (ex) { }
|
||
try { info.productionMode = ((process.env.NODE_ENV != null) && (process.env.NODE_ENV == 'production')); } catch (ex) { }
|
||
try { info.database = ["Unknown", "NeDB", "MongoJS", "MongoDB", "MariaDB", "MySQL"][parent.parent.db.databaseType]; } catch (ex) { }
|
||
try { if (parent.db.databaseType == 3) { info.dbChangeStream = parent.db.changeStream; } } catch (ex) { }
|
||
try { if (parent.parent.multiServer != null) { info.serverId = parent.parent.multiServer.serverid; } } catch (ex) { }
|
||
try { if (parent.parent.pluginHandler != null) { info.plugins = []; for (var i in parent.parent.pluginHandler.plugins) { info.plugins.push(i); } } } 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.memoryUsage = process.memoryUsage(); } catch (ex) { }
|
||
try { info.warnings = parent.parent.getServerWarnings(); } 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.'; }
|
||
}
|
||
|
||
// removeinactivedevices showall|showremoved
|
||
function serverUserCommandRemoveInactiveDevices(cmdData) {
|
||
var arg = cmdData.cmdargs['_'][0];
|
||
if ((arg == null) && (arg != 'showremoved') && (arg != 'showall')) {
|
||
cmdData.result = 'Usage: removeinactivedevices [showremoved|showall]';
|
||
} else {
|
||
parent.db.removeInactiveDevices((arg == 'showall'), function (msg) { try { ws.send(JSON.stringify({ action: 'serverconsole', value: msg, tag: cmdData.command.tag })); } catch (ex) { } });
|
||
}
|
||
}
|
||
|
||
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, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
||
//function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); 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;
|
||
};
|