UNPKG

meshcentral

Version:

Web based remote computer management server

731 lines (645 loc) 493 kB
/** * @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))) { try { ws.send(JSON.stringify({ action: 'stopped', msg: 'Session count exceed' })); } catch (ex) { } try { ws.close(); } catch (e) { } return; } } // Associate this websocket session with the web session ws.userid = user._id; ws.domainid = domain.id; ws.clientIp = req.clientIp; // Create a new session id for this user. parent.crypto.randomBytes(20, function (err, randombuf) { ws.sessionId = user._id + '/' + randombuf.toString('hex'); // Add this web socket session to session list parent.wssessions2[ws.sessionId] = ws; if (!parent.wssessions[user._id]) { parent.wssessions[user._id] = [ws]; } else { parent.wssessions[user._id].push(ws); } if (parent.parent.multiServer == null) { var targets = ['*', 'server-users']; if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } } parent.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: user._id, username: user.name, count: parent.wssessions[user._id].length, nolog: 1, domain: domain.id }); } else { parent.recountSessions(ws.sessionId); // Recount sessions } // If we have peer servers, inform them of the new session if (parent.parent.multiServer != null) { parent.parent.multiServer.DispatchMessage({ action: 'sessionStart', sessionid: ws.sessionId }); } // Handle events ws.HandleEvent = function (source, event, ids, id) { // If this session is logged in using a loginToken and the token is removed, disconnect. if ((req.session.loginToken != null) && (typeof event == 'object') && (event.action == 'loginTokenChanged') && (event.removed != null) && (event.removed.indexOf(req.session.loginToken) >= 0)) { delete req.session; obj.close(); return; } // 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 try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { } } else { // Check if no other users are targeted by the event, if not, we can get this event. var userTarget = false; for (var i in ids) { if (ids[i].startsWith('user/')) { userTarget = true; } } if (userTarget == false) { ws.send(JSON.stringify({ action: 'event', event: event })); } } } else if (event.ugrpid != null) { if ((user.siteadmin & SITERIGHT_USERGROUPS) != 0) { // If we have the rights to see users in a group, send the group as is. try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { } } else { // We don't have the rights to see otehr users in the user group, remove the links that are not for ourselves. var links = {}; if (event.links) { for (var i in event.links) { if ((i == user._id) || i.startsWith('mesh/') || i.startsWith('node/')) { links[i] = event.links[i]; } } } try { ws.send(JSON.stringify({ action: 'event', event: { ugrpid: event.ugrpid, domain: event.domain, time: event.time, name: event.name, action: event.action, username: event.username, links: links, h: event.h } })); } catch (ex) { } } } else { // This is not a device group event, we can get this event. try { ws.send(JSON.stringify({ action: 'event', event: event })); } catch (ex) { } } } } catch (ex) { console.log(ex); } } }; user.subscriptions = parent.subscribe(user._id, ws); // Subscribe to events try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive // Send current server statistics obj.SendServerStats = function () { // Take a look at server stats var os = require('os'); var stats = { action: 'serverstats', totalmem: os.totalmem(), freemem: os.freemem() }; try { stats.cpuavg = os.loadavg(); } catch (ex) { } if (parent.parent.platform != 'win32') { try { stats.availablemem = 1024 * Number(/MemAvailable:[ ]+(\d+)/.exec(fs.readFileSync('/proc/meminfo', 'utf8'))[1]); } catch (ex) { } } // Count the number of device groups that are not deleted var activeDeviceGroups = 0; for (var i in parent.meshes) { if (parent.meshes[i].deleted == null) { activeDeviceGroups++; } } // This is not ideal for performance, we want to dome something better. var serverStats = { UserAccounts: Object.keys(parent.users).length, DeviceGroups: activeDeviceGroups, AgentSessions: Object.keys(parent.wsagents).length, ConnectedUsers: Object.keys(parent.wssessions).length, UsersSessions: Object.keys(parent.wssessions2).length, RelaySessions: parent.relaySessionCount, RelayCount: Object.keys(parent.wsrelays).length }; 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 er