From 52776b81afab4dd61090b3ca337818a1715b3661 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Mon, 21 Jan 2019 14:05:50 -0800 Subject: [PATCH] Added recovery core support. --- agents/recoverycore.js | 283 ++++++++++++++++++++++++++++ meshagent.js | 95 ++++++---- meshcentral.js | 79 +++++--- meshuser.js | 40 ++-- package.json | 2 +- views/default-min.handlebars | 2 +- views/default-mobile-min.handlebars | 2 +- views/default-mobile.handlebars | 1 + views/default.handlebars | 97 +++++++--- webserver.js | 35 ++-- 10 files changed, 504 insertions(+), 132 deletions(-) create mode 100644 agents/recoverycore.js diff --git a/agents/recoverycore.js b/agents/recoverycore.js new file mode 100644 index 00000000..fd7f447c --- /dev/null +++ b/agents/recoverycore.js @@ -0,0 +1,283 @@ + +var http = require('http'); +var childProcess = require('child_process'); +var meshCoreObj = { "action": "coreinfo", "value": "MeshCore Recovery", "caps": 10 }; // Capability bitmask: 1 = Desktop, 2 = Terminal, 4 = Files, 8 = Console, 16 = JavaScript +var nextTunnelIndex = 1; +var tunnels = {}; + +function sendConsoleText(msg) +{ + require('MeshAgent').SendCommand({ "action": "msg", "type": "console", "value": msg }); +} + +// Split a string taking into account the quoats. Used for command line parsing +function splitArgs(str) +{ + var myArray = [], myRegexp = /[^\s"]+|"([^"]*)"/gi; + do { var match = myRegexp.exec(str); if (match != null) { myArray.push(match[1] ? match[1] : match[0]); } } while (match != null); + return myArray; +} + +// Parse arguments string array into an object +function parseArgs(argv) +{ + var results = { '_': [] }, current = null; + for (var i = 1, len = argv.length; i < len; i++) { + var x = argv[i]; + if (x.length > 2 && x[0] == '-' && x[1] == '-') { + if (current != null) { results[current] = true; } + current = x.substring(2); + } else { + if (current != null) { results[current] = toNumberIfNumber(x); current = null; } else { results['_'].push(toNumberIfNumber(x)); } + } + } + if (current != null) { results[current] = true; } + return results; +} +// Get server target url with a custom path +function getServerTargetUrl(path) +{ + var x = require('MeshAgent').ServerUrl; + //sendConsoleText("mesh.ServerUrl: " + mesh.ServerUrl); + if (x == null) { return null; } + if (path == null) { path = ''; } + x = http.parseUri(x); + if (x == null) return null; + return x.protocol + '//' + x.host + ':' + x.port + '/' + path; +} + +// Get server url. If the url starts with "*/..." change it, it not use the url as is. +function getServerTargetUrlEx(url) +{ + if (url.substring(0, 2) == '*/') { return getServerTargetUrl(url.substring(2)); } + return url; +} + +require('MeshAgent').on('Connected', function () +{ + require('os').name().then(function (v) + { + sendConsoleText("Mesh Agent Receovery Console, OS: " + v); + require('MeshAgent').SendCommand(meshCoreObj); + }); +}); + +// Tunnel callback operations +function onTunnelUpgrade(response, s, head) { + this.s = s; + s.httprequest = this; + s.end = onTunnelClosed; + s.tunnel = this; + + //sendConsoleText('onTunnelUpgrade'); + + if (this.tcpport != null) { + // This is a TCP relay connection, pause now and try to connect to the target. + s.pause(); + s.data = onTcpRelayServerTunnelData; + var connectionOptions = { port: parseInt(this.tcpport) }; + if (this.tcpaddr != null) { connectionOptions.host = this.tcpaddr; } else { connectionOptions.host = '127.0.0.1'; } + s.tcprelay = net.createConnection(connectionOptions, onTcpRelayTargetTunnelConnect); + s.tcprelay.peerindex = this.index; + } else { + // This is a normal connect for KVM/Terminal/Files + s.data = onTunnelData; + } +} + +require('MeshAgent').AddCommandHandler(function (data) +{ + if (typeof data == 'object') + { + // If this is a console command, parse it and call the console handler + switch (data.action) + { + case 'msg': + { + switch (data.type) + { + case 'console': { // Process a console command + if (data.value && data.sessionid) + { + var args = splitArgs(data.value); + processConsoleCommand(args[0].toLowerCase(), parseArgs(args), data.rights, data.sessionid); + } + break; + } + case 'tunnel': + { + if (data.value != null) { // Process a new tunnel connection request + // Create a new tunnel object + var xurl = getServerTargetUrlEx(data.value); + if (xurl != null) { + var woptions = http.parseUri(xurl); + woptions.rejectUnauthorized = 0; + //sendConsoleText(JSON.stringify(woptions)); + var tunnel = http.request(woptions); + tunnel.on('upgrade', function (response, s, head) + { + this.s = s; + s.httprequest = this; + s.tunnel = this; + s.on('end', function () + { + if (tunnels[this.httprequest.index] == null) return; // Stop duplicate calls. + //sendConsoleText("Tunnel #" + this.httprequest.index + " closed.", this.httprequest.sessionid); + delete tunnels[this.httprequest.index]; + + // Clean up WebSocket + this.removeAllListeners('data'); + }); + s.on('data', function (data) + { + if (this.httprequest.state == 0) { + // Check if this is a relay connection + if (data == 'c') { this.httprequest.state = 1; sendConsoleText("Tunnel #" + this.httprequest.index + " now active", this.httprequest.sessionid); } + } else { + // Handle tunnel data + if (this.httprequest.protocol == 0) + { + // Take a look at the protocol + this.httprequest.protocol = parseInt(data); + if (typeof this.httprequest.protocol != 'number') { this.httprequest.protocol = 0; } + if (this.httprequest.protocol == 1) + { + // Remote terminal using native pipes + if (process.platform == "win32") + { + this.httprequest._term = require('win-terminal').Start(80, 25); + this.httprequest._term.pipe(this, { dataTypeSkip: 1 }); + this.pipe(this.httprequest._term, { dataTypeSkip: 1, end: false }); + this.prependListener('end', function () { this.httprequest._term.end(function () { sendConsoleText('Terminal was closed'); }); }); + } + else + { + this.httprequest.process = childProcess.execFile("/bin/sh", ["sh"], { type: childProcess.SpawnTypes.TERM }); + this.httprequest.process.tunnel = this; + this.httprequest.process.on('exit', function (ecode, sig) { this.tunnel.end(); }); + this.httprequest.process.stderr.on('data', function (chunk) { this.parent.tunnel.write(chunk); }); + this.httprequest.process.stdout.pipe(this, { dataTypeSkip: 1 }); // 0 = Binary, 1 = Text. + this.pipe(this.httprequest.process.stdin, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text. + this.prependListener('end', function () { this.httprequest.process.kill(); }); + } + + this.on('end', function () { + if (process.platform == "win32") + { + // Unpipe the web socket + this.unpipe(this.httprequest._term); + this.httprequest._term.unpipe(this); + + // Clean up + this.httprequest._term.end(); + this.httprequest._term = null; + } + }); + } + } + } + }); + }); + tunnel.onerror = function (e) { sendConsoleText('ERROR: ' + JSON.stringify(e)); } + tunnel.sessionid = data.sessionid; + tunnel.rights = data.rights; + tunnel.state = 0; + tunnel.url = xurl; + tunnel.protocol = 0; + tunnel.tcpaddr = data.tcpaddr; + tunnel.tcpport = data.tcpport; + tunnel.end(); + // Put the tunnel in the tunnels list + var index = nextTunnelIndex++; + tunnel.index = index; + tunnels[index] = tunnel; + + //sendConsoleText('New tunnel connection #' + index + ': ' + tunnel.url + ', rights: ' + tunnel.rights, data.sessionid); + } + } + break; + } + + default: + // Unknown action, ignore it. + break; + } + break; + } + default: + // Unknown action, ignore it. + break; + } + } +}); + +function processConsoleCommand(cmd, args, rights, sessionid) +{ + try + { + var response = null; + switch (cmd) + { + case 'help': + response = 'Available commands are: osinfo, dbkeys, dbget, dbset, dbcompact, netinfo.'; + break; + + case 'osinfo': { // Return the operating system information + var i = 1; + if (args['_'].length > 0) { i = parseInt(args['_'][0]); if (i > 8) { i = 8; } response = 'Calling ' + i + ' times.'; } + for (var j = 0; j < i; j++) { + var pr = require('os').name(); + pr.sessionid = sessionid; + pr.then(function (v) { sendConsoleText("OS: " + v, this.sessionid); }); + } + break; + } + case 'dbkeys': { // Return all data store keys + response = JSON.stringify(db.Keys); + break; + } + case 'dbget': { // Return the data store value for a given key + if (db == null) { response = 'Database not accessible.'; break; } + if (args['_'].length != 1) { + response = 'Proper usage: dbget (key)'; // Display the value for a given database key + } else { + response = db.Get(args['_'][0]); + } + break; + } + case 'dbset': { // Set a data store key and value pair + if (db == null) { response = 'Database not accessible.'; break; } + if (args['_'].length != 2) { + response = 'Proper usage: dbset (key) (value)'; // Set a database key + } else { + var r = db.Put(args['_'][0], args['_'][1]); + response = 'Key set: ' + r; + } + break; + } + case 'dbcompact': { // Compact the data store + if (db == null) { response = 'Database not accessible.'; break; } + var r = db.Compact(); + response = 'Database compacted: ' + r; + break; + } + case 'tunnels': { // Show the list of current tunnels + response = ''; + for (var i in tunnels) { response += 'Tunnel #' + i + ', ' + tunnels[i].url + '\r\n'; } + if (response == '') { response = 'No websocket sessions.'; } + break; + } + case 'netinfo': { // Show network interface information + //response = objToString(mesh.NetInfo, 0, ' '); + var interfaces = require('os').networkInterfaces(); + response = objToString(interfaces, 0, ' ', true); + break; + } + default: { // This is an unknown command, return an error message + response = 'Unknown command \"' + cmd + '\", type \"help\" for list of avaialble commands.'; + break; + } + } + } catch (e) { response = 'Command returned an exception error: ' + e; console.log(e); } + if (response != null) { sendConsoleText(response, sessionid); } +} diff --git a/meshagent.js b/meshagent.js index c8bfc8fb..99293502 100644 --- a/meshagent.js +++ b/meshagent.js @@ -106,11 +106,12 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { const agentMeshCoreHash = (msg.length == 52) ? msg.substring(4, 52) : null; // We need to check if the core is current. First, figure out what core we need. - const corename = obj.parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core; + var corename = obj.parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core; + if (obj.agentCoreCheck == 1001) { corename = obj.parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].rcore; } // Use the recovery core. if (corename != null) { const meshcorehash = obj.parent.parent.defaultMeshCoresHash[corename]; if (agentMeshCoreHash != meshcorehash) { - if (obj.agentCoreCheck < 5) { + if ((obj.agentCoreCheck < 5) || (obj.agentCoreCheck == 1001)) { if (meshcorehash == null) { // Clear the core obj.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core @@ -184,6 +185,11 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { obj.send(obj.agentUpdate.buf); // Command 14, mesh agent first data block } }); + } else { + // Check the mesh core, if the agent is capable of running one + if (((obj.agentInfo.capabilities & 16) != 0) && (obj.parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) { + obj.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash. + } } } } @@ -398,51 +404,60 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Command 4, inform mesh agent that it's authenticated. obj.send(obj.common.ShortToStr(4)); - // Check the mesh core, if the agent is capable of running one - if ((obj.agentInfo.capabilities & 16) != 0) { obj.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); } // Command 11, ask for mesh core hash. - // Check if we need to make an native update check obj.agentExeInfo = obj.parent.parent.meshAgentBinaries[obj.agentInfo.agentId]; - if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) { obj.send(obj.common.ShortToStr(12) + obj.common.ShortToStr(0)); } // Ask the agent for it's executable binary hash + const corename = obj.parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core; + if (corename == null) { obj.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0)); } // MeshCommand_CoreModule, ask mesh agent to clear the core - // Check if we already have IP location information for this node - obj.db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) { - if (iplocs.length == 1) { - // We have a location in the database for this remote IP - var iploc = nodes[0], x = {}; - if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) { - x.publicip = iploc.ip; - x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000)); - ChangeAgentLocationInfo(x); - } - } else { - // Check if we need to ask for the IP location - var doIpLocation = 0; - if (device.iploc == null) { - doIpLocation = 1; + if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) { + // Ask the agent for it's executable binary hash + obj.send(obj.common.ShortToStr(12) + obj.common.ShortToStr(0)); + } else { + // Check the mesh core, if the agent is capable of running one + if (((obj.agentInfo.capabilities & 16) != 0) && (corename != null)) { obj.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); } // Command 11, ask for mesh core hash. + } + + // Do this if IP location is enabled on this domain TODO: Set IP location per device group? + if (domain.iplocation == true) { + // Check if we already have IP location information for this node + obj.db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) { + if (iplocs.length == 1) { + // We have a location in the database for this remote IP + var iploc = nodes[0], x = {}; + if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) { + x.publicip = iploc.ip; + x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000)); + ChangeAgentLocationInfo(x); + } } else { - var loc = device.iploc.split(','); - if (loc.length < 3) { - doIpLocation = 2; + // Check if we need to ask for the IP location + var doIpLocation = 0; + if (device.iploc == null) { + doIpLocation = 1; } else { - var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now(); - t.setDate(t.getDate() + 20); - if (t < now) { doIpLocation = 3; } + var loc = device.iploc.split(','); + if (loc.length < 3) { + doIpLocation = 2; + } else { + var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now(); + t.setDate(t.getDate() + 20); + if (t < now) { doIpLocation = 3; } + } + } + + // If we need to ask for IP location, see if we have the quota to do it. + if (doIpLocation > 0) { + obj.db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) { + if (ipLocationLimitor.value > 0) { + ipLocationLimitor.value--; + obj.db.Set(ipLocationLimitor); + obj.send(JSON.stringify({ action: 'iplocation' })); + } + }); } } - - // If we need to ask for IP location, see if we have the quota to do it. - if (doIpLocation > 0) { - obj.db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) { - if (ipLocationLimitor.value > 0) { - ipLocationLimitor.value--; - obj.db.Set(ipLocationLimitor); - obj.send(JSON.stringify({ action: 'iplocation' })); - } - }); - } - } - }); + }); + } }); } diff --git a/meshcentral.js b/meshcentral.js index ae7b6f0c..038135dd 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -894,6 +894,16 @@ function CreateMeshCentralServer(config, args) { 'linux-noamt': 'var addedModules = [];\r\n' }; + // Read the recovery core if present + var meshRecoveryCore = null; + if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) { + try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { } + if (meshRecoveryCore != null) { + modulesAdd['windows-recovery'] = 'var addedModules = [];\r\n'; + modulesAdd['linux-recovery'] = 'var addedModules = [];\r\n'; + } + } + if (obj.args.minifycore !== false) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.min.js')).toString(); } catch (e) { } } // Favor minified meshcore if present. if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (e) { } } // Use non-minified meshcore. if (meshCore != null) { @@ -926,13 +936,24 @@ function CreateMeshCentralServer(config, args) { modulesAdd['linux-amt'] += moduleData; modulesAdd['linux-noamt'] += moduleData; } + + // Merge this module to recovery modules if needed + if (modulesAdd['windows-recovery'] != null) { + if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal')) { + modulesAdd['windows-recovery'] += moduleData; + } + } } } } // Merge the cores and compute the hashes for (var i in modulesAdd) { - obj.defaultMeshCores[i] = obj.common.IntToStr(0) + modulesAdd[i] + meshCore; + if ((i == 'windows-recovery') || (i == 'linux-recovery')) { + obj.defaultMeshCores[i] = obj.common.IntToStr(0) + modulesAdd[i] + meshRecoveryCore; + } else { + obj.defaultMeshCores[i] = obj.common.IntToStr(0) + modulesAdd[i] + meshCore; + } obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest("binary"); obj.debug(1, 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.'); //console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.'); // DEBUG, Print the core size @@ -1027,34 +1048,34 @@ function CreateMeshCentralServer(config, args) { // List of possible mesh agents obj.meshAgentsArchitectureNumbers = { - 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt' }, - 1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt' }, - 2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt' }, - 3: { id: 3, localname: 'MeshService-signed.exe', rname: 'meshagent.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt' }, - 4: { id: 4, localname: 'MeshService64-signed.exe', rname: 'meshagent.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt' }, - 5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt' }, - 6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt' }, - 7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt' }, - 9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple OSX x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 14: { id: 14, localname: 'MeshAgent-Android-APK', rname: 'meshandroid', desc: 'Android Market', update: false, amt: false, platform: 'android', core: 'linux-noamt' }, // Get this one from Google Play - 15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple OSX x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt' }, - 17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt' }, // Get this one from Chrome store - 18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt' }, - 20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt' }, - 21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt' }, - 22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt' }, - 23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'linux-noamt' }, // Get this one from NPM - 24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, - 25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt' }, // "armv6l" and "armv7l" - 10003: { id: 3, localname: 'MeshService.exe', rname: 'meshagent.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt' }, // Unsigned version of the Windows MeshAgent x86 - 10004: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt' } // Unsigned version of the Windows MeshAgent x64 + 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery' }, + 1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery' }, + 2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery' }, + 3: { id: 3, localname: 'MeshService-signed.exe', rname: 'meshagent.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery' }, + 4: { id: 4, localname: 'MeshService64-signed.exe', rname: 'meshagent.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery' }, + 5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery' }, + 6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery' }, + 7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery' }, + 9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple OSX x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 14: { id: 14, localname: 'MeshAgent-Android-APK', rname: 'meshandroid', desc: 'Android Market', update: false, amt: false, platform: 'android', core: 'linux-noamt', rcore: 'linux-recovery' }, // Get this one from Google Play + 15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple OSX x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery' }, + 17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt', rcore: 'linux-recovery' }, // Get this one from Chrome store + 18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery' }, + 20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery' }, + 21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery' }, + 22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery' }, + 23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'linux-noamt', rcore: 'linux-recovery' }, // Get this one from NPM + 24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, + 25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery' }, // "armv6l" and "armv7l" + 10003: { id: 3, localname: 'MeshService.exe', rname: 'meshagent.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'linux-recovery' }, // Unsigned version of the Windows MeshAgent x86 + 10004: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'linux-recovery' } // Unsigned version of the Windows MeshAgent x64 }; // Update the list of available mesh agents diff --git a/meshuser.js b/meshuser.js index 43941dcc..0632e49d 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1215,27 +1215,27 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use { if (user.siteadmin != 0xFFFFFFFF) break; if (obj.common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid - if (command.path) { - if (obj.common.validateString(command.path, 1, 4096) == false) break; // Check path - if (command.path == '*') { - // Update the server default core and send a core hash request - // Load default mesh agent core if present, then perform a core update - obj.parent.parent.updateMeshCore(function () { obj.parent.sendMeshAgentCore(user, domain, command.nodeid, '*'); }); - } else { - // Send a mesh agent core to the mesh agent - var file = obj.parent.getServerFilePath(user, domain, command.path); - if (file != null) { - obj.parent.parent.fs.readFile(file.fullpath, 'utf8', function (err, data) { - if (err != null) { - data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw) - obj.parent.sendMeshAgentCore(user, domain, command.nodeid, data); - } - }); - } - } - } else { + if (obj.common.validateString(command.type, 1, 40) == false) break; // Check path + if (command.type == 'default') { + // Send the default core to the agent + obj.parent.parent.updateMeshCore(function () { obj.parent.sendMeshAgentCore(user, domain, command.nodeid, 'default'); }); + } else if (command.type == 'clear') { // Clear the mesh agent core on the mesh agent - obj.parent.sendMeshAgentCore(user, domain, command.nodeid, null); + obj.parent.sendMeshAgentCore(user, domain, command.nodeid, 'clear'); + } else if (command.type == 'recovery') { + // Send the recovery core to the agent + obj.parent.sendMeshAgentCore(user, domain, command.nodeid, 'recovery'); + } else if ((command.type == 'custom') && (obj.common.validateString(command.path, 1, 2048) == true)) { + // Send a mesh agent core to the mesh agent + var file = obj.parent.getServerFilePath(user, domain, command.path); + if (file != null) { + obj.parent.parent.fs.readFile(file.fullpath, 'utf8', function (err, data) { + if (err != null) { + data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw) + obj.parent.sendMeshAgentCore(user, domain, command.nodeid, 'custom', data); + } + }); + } } break; } diff --git a/package.json b/package.json index 3e837f21..1cad768c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.2.6-n", + "version": "0.2.6-o", "keywords": [ "Remote Management", "Intel AMT", diff --git a/views/default-min.handlebars b/views/default-min.handlebars index 8b17f746..87f4a2af 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file + MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default-mobile-min.handlebars b/views/default-mobile-min.handlebars index 4cc7d8fb..e7511369 100644 --- a/views/default-mobile-min.handlebars +++ b/views/default-mobile-min.handlebars @@ -1 +1 @@ - MeshCentral
{{{title}}}
{{{title2}}}
\ No newline at end of file + MeshCentral
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/default-mobile.handlebars b/views/default-mobile.handlebars index 130acf4b..b65a761c 100644 --- a/views/default-mobile.handlebars +++ b/views/default-mobile.handlebars @@ -1417,6 +1417,7 @@ var deviceHeaders = {}; var deviceHeadersTitles = {}; function updateDevicesEx() { + if (updateDevicesTimer != null) { clearTimeout(updateDevicesTimer); updateDevicesTimer = null; } var r = '', c = 0, current = null, count = 0, displayedMeshes = {}, groups = {}, groupCount = {}; // 3 wide, list view or desktop view diff --git a/views/default.handlebars b/views/default.handlebars index bb71204c..5e3e22bc 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -939,7 +939,7 @@ Q('DeskControl').checked = (getstore('DeskControl', 1) == 1); // Display the page devices - onSortSelectChange(); + onSortSelectChange(true); onSearchInputChanged(); for (var j = 1; j < 5; j++) { Q('devViewButton' + j).classList.remove('viewSelectorSel'); } Q('devViewButton' + Q('viewselect').value).classList.add('viewSelectorSel'); @@ -1106,6 +1106,34 @@ if ((siteRights & 21) != 0) { meshserver.send({ action: 'serverstats', interval: 10000 }); } } + // When the list of events needs to be updated, put a small delay so that the browser + // performs much less work when event storms happen. + var updateEventsNaggleTimer = null; + function updateEventsNaggle() { + if (updateEventsNaggleTimer == null) { + updateEventsNaggleTimer = setTimeout(function () { + events_update(); + updateEventsNaggleTimer = null; + }, 100); + } + } + + // When the list of devices needs to be updated, put a small delay so that the browser + // performs much less work when event storms happen. + var updateDevicesNaggleTimer = null; + function updateDevicesNaggle() { + if (updateDevicesNaggleTimer == null) { + updateDevicesNaggleTimer = setTimeout(function () { + onSortSelectChange(true); + drawNotifications(); + onSearchInputChanged(); + updateDevices(); + updateMapMarkers(); + updateDevicesNaggleTimer = null; + }, 100); + } + } + function onMessage(server, message) { switch (message.action) { case 'serverstats': { @@ -1166,9 +1194,10 @@ nodes.push(message.nodes[m][n]); } } - onSortSelectChange(); - onSearchInputChanged(); - updateDevices(); + //onSortSelectChange(true); + //onSearchInputChanged(); + //updateDevices(); + updateDevicesNaggle(); refreshMap(false, true); if (xxcurrentView == 0) { if ('{{viewmode}}' != '') { go(parseInt('{{viewmode}}')); } else { setDialogMode(0); go(1); } } if ('{{currentNode}}' != '') { gotoDevice('{{currentNode}}',parseInt('{{viewmode}}'));} @@ -1284,7 +1313,8 @@ userEvents_update(); } else { events = message.events; - events_update(); + updateEventsNaggle(); + //events_update(); } break; } @@ -1339,7 +1369,8 @@ events.unshift(message.event); var eventLimit = parseInt(p3limitdropdown.value); while (events.length > eventLimit) { events.pop(); } // Remove element(s) at the end - events_update(); + updateEventsNaggle(); + //events_update(); } switch (message.event.action) { case 'accountcreate': @@ -1442,10 +1473,14 @@ if (!node.icon) node.icon = 1; node.ident = ++nodeShortIdent; nodes.push(node); - onSortSelectChange(); - onSearchInputChanged(); - updateDevices(); - updateMapMarkers(); + + // Web page update + updateDevicesNaggle(); + //onSortSelectChange(true); + //onSearchInputChanged(); + //updateDevices(); + //updateMapMarkers(); + break; } case 'removenode': { @@ -1459,8 +1494,11 @@ // TODO: Correctly disconnect from this node (Desktop/Terminal/Files...) } nodes.splice(index, 1); - updateDevices(); - updateMapMarkers(); + + // Web page update + updateDevicesNaggle(); + //updateDevices(); + //updateMapMarkers(); } break; } @@ -1503,10 +1541,13 @@ if (node.rname) { node.rnamel = node.rname.toLowerCase(); } else { node.rnamel = node.namel; } if (message.event.node.icon) { node.icon = message.event.node.icon; } - onSortSelectChange(true); - drawNotifications(); + // Web page update + updateDevicesNaggle(); + //onSortSelectChange(true); + //drawNotifications(); + //updateMapMarkers(); + refreshDevice(node._id); - updateMapMarkers(); if ((currentNode == node) && (xxdialogMode != null) && (xxdialogTag == '@xxmap')) { p10showNodeLocationDialog(); } } @@ -1522,8 +1563,12 @@ // Change the node connection state node.conn = message.event.conn; node.pwr = message.event.pwr; - updateDevices(); - updateMapMarkers(); + + // Web page update + updateDevicesNaggle(); + //updateDevices(); + //updateMapMarkers(); + refreshDevice(node._id); } break; @@ -1542,7 +1587,8 @@ } case 'clearevents': { events = []; - events_update(); + updateEventsNaggle(); + //events_update(); break; } case 'login': { @@ -1601,7 +1647,7 @@ function onRealNameCheckBox() { showRealNames = Q('RealNameCheckBox').checked; putstore("showRealNames", showRealNames ? 1 : 0); - onSortSelectChange(); + onSortSelectChange(true); return; } @@ -1635,7 +1681,7 @@ showRealNames = !showRealNames; Q('RealNameCheckBox').value = showRealNames; putstore("showRealNames", showRealNames ? 1 : 0); - onSortSelectChange(); + onSortSelectChange(true); return; } if (e.ctrlKey == true || e.altKey == true || e.metaKey == true) return; @@ -5008,16 +5054,16 @@ if (e.shiftKey == true) { meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, path:'*' }); } // Upload default core else if (e.altKey == true) { meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id }); } // Clear the core else if (e.ctrlKey == true) { p15uploadCore2(); } // Upload the core from a file - else { setDialogMode(2, "Change Mesh Agent Core", 3, p15uploadCoreEx, '
Change Core
'); } + else { setDialogMode(2, "Change Mesh Agent Core", 3, p15uploadCoreEx, '
Change Core
'); } } function p15uploadCoreEx() { if (Q('d3coreMode').value == 1) { // Upload default core - meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, path:'*' }); + meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, type: 'default' }); } else if (Q('d3coreMode').value == 2) { // Clear the core - meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id }); + meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, type: 'clear' }); } else if (Q('d3coreMode').value == 3) { // Upload file as core p15uploadCore2(); @@ -5027,6 +5073,9 @@ } else if (Q('d3coreMode').value == 5) { // Hard disconnect the mesh agent meshserver.send({ action: 'agentdisconnect', nodeid: consoleNode._id, disconnectMode: 2 }); + } else if (Q('d3coreMode').value == 6) { + // Upload a recovery core + meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, type:'recovery' }); } } @@ -5047,7 +5096,7 @@ } else { // Upload server mesh agent code var files = d3getFileSel(); - if (files.length == 1) { meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, path: d3filetreelocation.join('/') + '/' + files[0] }); } + if (files.length == 1) { meshserver.send({ action: 'uploadagentcore', nodeid: consoleNode._id, type: 'custom', path: d3filetreelocation.join('/') + '/' + files[0] }); } } } diff --git a/webserver.js b/webserver.js index 482ce881..005d9104 100644 --- a/webserver.js +++ b/webserver.js @@ -754,7 +754,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { res.sendStatus(404); return; } else { nologout = true; - req.session.userid = 'user/' + domain.id + '/' + req.connection.user; + req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase(); req.session.usersid = req.connection.userSid; req.session.usersGroups = req.connection.userGroups; req.session.domainid = domain.id; @@ -1108,7 +1108,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.fs.readFile(file.path, 'utf8', function (err, data) { if (err != null) return; data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw) - obj.sendMeshAgentCore(user, domain, fields.attrib[0], data); // Upload the core + obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core try { obj.fs.unlinkSync(file.path); } catch (e) { } }); } @@ -2105,31 +2105,34 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }; // Send the core module to the mesh agent - obj.sendMeshAgentCore = function (user, domain, nodeid, core) { + obj.sendMeshAgentCore = function (user, domain, nodeid, coretype, coredata) { if (nodeid == null) return; - var splitnode = nodeid.split('/'); + const splitnode = nodeid.split('/'); if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain - var agent = obj.wsagents[nodeid]; + + // TODO: This command only works if the agent is connected on the same server. Will not work with multi server peering. + const agent = obj.wsagents[nodeid]; if (agent == null) return; // Check we have agent rights var rights = user.links[agent.dbMeshKey].rights; if ((rights != null) && ((rights & MESHRIGHT_AGENTCONSOLE) != 0) && (user.siteadmin == 0xFFFFFFFF)) { - if (core == null) { + if (coretype == 'clear') { // Clear the mesh agent core - agent.agentCoreCheck = 1000; // Tell the agent object we are not using a custom core. + agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core. agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0)); - } else if (core == '*') { + } else if (coretype == 'default') { + // Reset to default code agent.agentCoreCheck = 0; // Tell the agent object we are using a default code - // Reset the core to the server default agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash. - } else { - agent.agentCoreCheck = 1000; // Tell the agent object we are not using a custom core. - // Perform a SHA384 hash on the core module - var hash = obj.crypto.createHash('sha384').update(Buffer.from(core, 'binary')).digest().toString('binary'); - - // Send the code module to the agent - agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash + core); + } else if (coretype == 'recovery') { + // Reset to recovery core + agent.agentCoreCheck = 1001; // Tell the agent object we are using the recovery core. + agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash. + } else if (coretype == 'custom') { + agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core. + const hash = obj.crypto.createHash('sha384').update(Buffer.from(core, 'binary')).digest().toString('binary'); // Perform a SHA384 hash on the core module + agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash + core); // Send the code module to the agent } } };