UNPKG

meshcentral

Version:

Web based remote computer management server

716 lines (634 loc) • 591 kB
/** * @description MeshCentral MeshAgent * @author Ylian Saint-Hilaire & Bryan Roe * @copyright Intel Corporation 2018-2022 * @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_RELAY = 0x00200000; // 2097152 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; // MeshCentral Satellite const SATELLITE_PRESENT = 1; // This session is a MeshCentral Satellite session const SATELLITE_802_1x = 2; // This session supports 802.1x profile checking and creation // 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; // Information related to the current page the user is looking at obj.deviceSkip = 0; // How many devices to skip obj.deviceLimit = 0; // How many devices to view obj.visibleDevices = null; // An object of visible nodeid's if the user is in paging mode if (domain.maxdeviceview != null) { obj.deviceLimit = domain.maxdeviceview; } // 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 data through the websocket obj.send = function (object) { try { ws.send(JSON.stringify(object)); } catch(ex) {} } // 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 (ex) { console.log(ex); } } // Soft close, close the websocket if (arg == 2) { try { obj.ws._socket._parent.end(); parent.parent.debug('user', 'Hard disconnect'); } catch (ex) { console.log(ex); } } // 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 // Update user last access time if (obj.user != null) { const timeNow = Math.floor(Date.now() / 1000); if (obj.user.access < (timeNow - 300)) { // Only update user access time if longer than 5 minutes obj.user.access = timeNow; parent.db.SetUser(user); // Event the change var message = { etype: 'user', userid: obj.user._id, username: obj.user.name, account: parent.CloneSafeUser(obj.user), action: 'accountchange', domain: domain.id, nolog: 1 }; if (parent.db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. var targets = ['*', 'server-users', obj.user._id]; if (obj.user.groups) { for (var i in obj.user.groups) { targets.push('server-users:' + i); } } parent.parent.DispatchEvent(targets, obj, message); } } // 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; } // If this user is not viewing all devices and paging, check if this event is in the current page if (isEventWithinPage(ids) == false) 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.action == 'changenode') && (event.node != null) && ((event.node.rdp != null) || (event.node.ssh != null)))) { event = common.Clone(event); if ((event.action == 'deviceShareUpdate') && (Array.isArray(event.deviceShares))) { for (var i in event.deviceShares) { if (event.deviceShares[i].userid != user._id) { delete event.deviceShares[i].url; } } } if ((event.action == 'changenode') && (event.node != null) && ((event.node.rdp != null) || (event.node.ssh != null))) { // Clean up RDP & SSH credentials if ((event.node.rdp != null) && (typeof event.node.rdp[user._id] == 'number')) { event.node.rdp = event.node.rdp[user._id]; } else { delete event.node.rdp; } if ((event.node.ssh != null) && (typeof event.node.ssh[user._id] == 'number')) { event.node.ssh = event.node.ssh[user._id]; } else { delete event.node.ssh; } } } // This is a MeshCentral Satellite message if (event.action == 'satellite') { if ((obj.ws.satelliteFlags & event.satelliteFlags) != 0) { try { ws.send(JSON.stringify(event)); } catch (ex) { } return; } } // 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, ConnectedIntelAMT: 0 }; if (parent.relaySessionErrorCount != 0) { serverStats.RelayErrors = parent.relaySessionErrorCount; } if (parent.parent.mpsserver != null) { serverStats.ConnectedIntelAMTCira = 0; for (var i in parent.parent.mpsserver.ciraConnections) { serverStats.ConnectedIntelAMTCira += parent.parent.mpsserver.ciraConnections[i].length; } } for (var i in parent.parent.connectivityByNode) { const node = parent.parent.connectivityByNode[i]; if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { serverStats.ConnectedIntelAMT++; } } // 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.devicemeshrouterlinks == 'object') { serverinfo.devicemeshrouterlinks = domain.devicemeshrouterlinks; } 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, localurl: domain.altmessenging.localurl, type: domain.altmessenging.type }]; } 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, type: domain.altmessenging[i].type }); } } } 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 if (domain.passwordrequirements) { if (domain.passwordrequirements.lock2factor == true) { serverinfo.lock2factor = true; } // Indicate 2FA change are not allowed if (typeof domain.passwordrequirements.maxfidokeys == 'number') { serverinfo.maxfidokeys = domain.passwordrequirements.maxfidokeys; } } if (parent.parent.msgserver != null) { // Setup messaging providers information serverinfo.userMsgProviders = parent.parent.msgserver.providers; if (parent.parent.msgserver.discordUrl != null) { serverinfo.discordUrl = parent.parent.msgserver.discordUrl; } } if ((typeof parent.parent.config.messaging == 'object') && (typeof parent.parent.config.messaging.ntfy == 'object') && (typeof parent.parent.config.messaging.ntfy.userurl == 'string')) { // nfty user url serverinfo.userMsgNftyUrl = parent.parent.config.messaging.ntfy.userurl; } // Build the mobile agent URL, this is used to connect mobile devices var agentServerName = parent.getWebServerName(domain, req); 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; } else { if (typeof domain.guestdevicesharing == 'object') { if (typeof domain.guestdevicesharing.maxsessiontime == 'number') { serverinfo.guestdevicesharingmaxtime = domain.guestdevicesharing.maxsessiontime; } } } if (typeof domain.userconsentflags == 'number') { serverinfo.consent = domain.userconsentflags; } if ((typeof domain.usersessionidletimeout == 'number') && (domain.usersessionidletimeout > 0)) {serverinfo.timeout = (domain.usersessionidletimeout * 60 * 1000); } if (typeof domain.logoutonidlesessiontimeout == 'boolean') { serverinfo.logoutonidlesessiontimeout = domain.logoutonidlesessiontimeout; } else { // Default serverinfo.logoutonidlesessiontimeout = true; } 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; } if (Array.isArray(domain.preconfiguredscripts)) { const r = []; for (var i in domain.preconfiguredscripts) { const types = ['', 'bat', 'ps1', 'sh', 'agent']; // 1 = Windows Command, 2 = Windows PowerShell, 3 = Linux, 4 = Agent const script = domain.preconfiguredscripts[i]; if ((typeof script.name == 'string') && (script.name.length <= 32) && (typeof script.type == 'string') && ((typeof script.file == 'string') || (typeof script.cmd == 'string'))) { const s = { name: script.name, type: types.indexOf(script.type.toLowerCase()) }; if (s.type > 0) { r.push(s); } } } serverinfo.preConfiguredScripts = r; } if (domain.maxdeviceview != null) { serverinfo.maxdeviceview = domain.maxdeviceview; } // Maximum number of devices a user can view at any given time // 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._id, 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 (ex) { console.log(ex); } // 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 'nodes': { // If in paging mode, look to set the skip and limit values if (domain.maxdeviceview != null) { if ((typeof command.skip == 'number') && (command.skip >= 0)) { obj.deviceSkip = command.skip; } if ((typeof command.limit == 'number') && (command.limit > 0)) { obj.deviceLimit = command.limit; } if (obj.deviceLimit > domain.maxdeviceview) { obj.deviceLimit = domain.maxdeviceview; } } 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 ==