meshcentral
Version:
Web based remote computer management server
733 lines (647 loc) • 487 kB
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.se