UNPKG

meshcentral

Version:

Web based remote computer management and file server

751 lines (680 loc) • 346 kB
/** * @description MeshCentral MeshAgent * @author Ylian Saint-Hilaire & Bryan Roe * @copyright Intel Corporation 2018-2020 * @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; const MESHRIGHT_MANAGEUSERS = 0x00000002; const MESHRIGHT_MANAGECOMPUTERS = 0x00000004; const MESHRIGHT_REMOTECONTROL = 0x00000008; const MESHRIGHT_AGENTCONSOLE = 0x00000010; const MESHRIGHT_SERVERFILES = 0x00000020; const MESHRIGHT_WAKEDEVICE = 0x00000040; const MESHRIGHT_SETNOTES = 0x00000080; const MESHRIGHT_REMOTEVIEWONLY = 0x00000100; const MESHRIGHT_NOTERMINAL = 0x00000200; const MESHRIGHT_NOFILES = 0x00000400; const MESHRIGHT_NOAMT = 0x00000800; const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000; const MESHRIGHT_LIMITEVENTS = 0x00002000; const MESHRIGHT_CHATNOTIFY = 0x00004000; const MESHRIGHT_UNINSTALL = 0x00008000; const MESHRIGHT_NODESKTOP = 0x00010000; const MESHRIGHT_ADMIN = 0xFFFFFFFF; // Site rights const SITERIGHT_SERVERBACKUP = 0x00000001; const SITERIGHT_MANAGEUSERS = 0x00000002; const SITERIGHT_SERVERRESTORE = 0x00000004; const SITERIGHT_FILEACCESS = 0x00000008; const SITERIGHT_SERVERUPDATE = 0x00000010; const SITERIGHT_LOCKED = 0x00000020; const SITERIGHT_NONEWGROUPS = 0x00000040; const SITERIGHT_NOMESHCMD = 0x00000080; const SITERIGHT_USERGROUPS = 0x00000100; const SITERIGHT_RECORDINGS = 0x00000200; const SITERIGHT_LOCKSETTINGS = 0x00000400; const SITERIGHT_ALLEVENTS = 0x00000800; const SITERIGHT_ADMIN = 0xFFFFFFFF; 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'); // 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() { obj.ws.send('{"action":"ping"}'); } function sendPong() { obj.ws.send('{"action":"pong"}'); } // 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) { if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('user', 'Soft disconnect'); } catch (e) { console.log(e); } } // Soft close, close the websocket if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('user', 'Hard disconnect'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket // Perform timer cleanup if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; } if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; } // Perform cleanup parent.parent.RemoveAllEventDispatch(ws); if (obj.serverStatsTimer != null) { clearInterval(obj.serverStatsTimer); delete obj.serverStatsTimer; } if (req.session && req.session.ws && req.session.ws == ws) { delete req.session.ws; } if (parent.wssessions2[ws.sessionId]) { delete parent.wssessions2[ws.sessionId]; } if ((obj.user != null) && (parent.wssessions[obj.user._id])) { var i = parent.wssessions[obj.user._id].indexOf(ws); if (i >= 0) { parent.wssessions[obj.user._id].splice(i, 1); var user = parent.users[obj.user._id]; if (user) { 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]; } } } // 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 }); } // Aggressive cleanup if (obj.user) { delete obj.user; } if (obj.domain) { delete obj.domain; } if (ws.userid) { delete ws.userid; } if (ws.domainid) { delete ws.domainid; } if (ws.sessionId) { delete ws.sessionId; } if (ws.HandleEvent) { delete ws.HandleEvent; } ws.removeAllListeners(["message", "close", "error"]); }; // 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) { 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 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 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.GetRoutingServerId(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, agent.dbNodeKey, 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 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 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) { // Send the request to all peer servers // TODO !!!! // 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; // 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) { // 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(); } else if (event == 'resubscribe') { user.subscriptions = parent.subscribe(user._id, ws); } else if (event == 'updatefiles') { updateUserFiles(user, ws, domain); } else { // Because of the device group "Show Self Events Only", we need to do more checks here. if (id.startsWith('mesh/')) { // Check if we have rights to get this message. If we have limited events on this mesh, don't send the event to the user. var meshrights = parent.GetMeshRights(user, id); if ((meshrights === MESHRIGHT_ADMIN) || ((meshrights & MESHRIGHT_LIMITEVENTS) == 0) || (ids.indexOf(user._id) >= 0)) { // We have the device group rights to see this event or we are directly targetted by the event ws.send(JSON.stringify({ action: 'event', event: event })); } else { // Check if no other users are targeted by the event, if not, we can get this event. var userTarget = false; for (var i in ids) { if (ids[i].startsWith('user/')) { userTarget = true; } } if (userTarget == false) { ws.send(JSON.stringify({ action: 'event', event: event })); } } } else if (event.ugrpid != null) { if ((user.siteadmin & SITERIGHT_USERGROUPS) != 0) { // If we have the rights to see users in a group, send the group as is. ws.send(JSON.stringify({ action: 'event', event: event })); } else { // We don't have the rights to see otehr users in the user group, remove the links that are not for ourselves. var links = {}; if (event.links) { for (var i in event.links) { if ((i == user._id) || i.startsWith('mesh/') || i.startsWith('node/')) { links[i] = event.links[i]; } } } ws.send(JSON.stringify({ action: 'event', event: { ugrpid: event.ugrpid, domain: event.domain, time: event.time, name: event.name, action: event.action, username: event.username, links: links, h: event.h } })); } } else { // This is not a device group event, we can get this event. ws.send(JSON.stringify({ action: 'event', event: event })); } } } catch (e) { } } }; user.subscriptions = parent.subscribe(user._id, ws); // Subscribe to events try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive // Send current server statistics obj.SendServerStats = function () { // Take a look at server stats var os = require('os'); var stats = { action: 'serverstats', totalmem: os.totalmem(), freemem: os.freemem() }; if (parent.parent.platform != 'win32') { stats.cpuavg = os.loadavg(); 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 = Object.keys(parent.parent.mpsserver.ciraConnections).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.InvalidRSAsiguature = 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 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: ((parent.parent.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() }; serverinfo.languages = parent.renderLanguages; serverinfo.tlshash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(); // SHA384 of server HTTPS certificate 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 (args.notls == true) { serverinfo.https = false; } else { serverinfo.https = true; serverinfo.redirport = args.redirport; } 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); } } } // 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) { // 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." })); } 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 switch (command.action) { case 'pong': { break; } // NOP case 'ping': { try { ws.send('{action:"pong"}'); } catch (ex) { } 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 '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': { 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 'serverstats': { 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); } break; } case 'meshes': { // 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) { } 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 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); } } } } } } } else { // Request list of all nodes for one specific meshid meshid = command.meshid; if (common.validateString(meshid, 0, 128) == false) { err = 'Invalid group id'; } else { if (meshid.split('/').length == 1) { meshid = 'mesh/' + domain.id + '/' + command.meshid; } if (parent.IsMeshViewable(user, meshid)) { links.push(meshid); } else { err = 'Invalid group id'; } } } } catch (ex) { err = 'Validation exception: ' + ex; } } // Handle any errors if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'nodes', responseid: command.responseid, result: err })); } catch (ex) { } } break; } // Request a list of all nodes db.GetAllTypeNoTypeFieldMeshFiltered(links, extraids, domain.id, 'node', command.id, function (err, docs) { if (docs == null) { docs = []; } 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; } } if ((state.connectivity & 2) != 0) { var cira = parent.parent.mpsserver.ciraConnections[docs[i]._id]; if (cira != null) { docs[i].cict = cira.tag.connectTime; } } } // Compress the meshid's meshid = docs[i].meshid; if (!r[meshid]) { r[meshid] = []; } delete docs[i].meshid; // Remove Intel AMT credential if present if (docs[i].intelamt != null && docs[i].intelamt.pass != null) { delete docs[i].intelamt.pass; } // 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 'powertimeline': { // 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) { } } } }); }); break; } case 'getsysinfo': { if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check the 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; // Invalid domain, operation only valid for current domain // 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: '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) { } } }); }); break; } case 'lastconnect': { if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check the 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; // Invalid domain, operation only valid for current domain // 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) { } } }); }); break; } case 'files': { // Send the full list of server files to the browser app updateUserFiles(user, ws, domain); 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 } e