diff --git a/agents/meshcore.min.js b/agents/meshcore.min.js index 6de857a6..21b632ca 100644 --- a/agents/meshcore.min.js +++ b/agents/meshcore.min.js @@ -831,6 +831,19 @@ function createMeshCore(agent) } case 'ping': { mesh.SendCommand('{"action":"pong"}'); break; } case 'pong': { break; } + case 'plugin': { + if (typeof data.pluginaction == 'string') { + try { + + MeshServerLog('Plugin called', data); + /* Not yet implmented + require(data.plugin.name).serveraction(data);*/ + } catch(e) { + MeshServerLog('Error calling plugin', data); + } + } + break; + } default: // Unknown action, ignore it. break; @@ -1479,6 +1492,26 @@ function createMeshCore(agent) // Unknown action, ignore it. break; } + } else if (this.httprequest.protocol == 7) { // plugin data exchange + var cmd = null; + try { cmd = JSON.parse(data); } catch (e) { }; + if (cmd == null) { return; } + if ((cmd.ctrlChannel == '102938') || ((cmd.type == 'offer') && (cmd.sdp != null))) { onTunnelControlData(cmd, this); return; } // If this is control data, handle it now. + if (cmd.action == undefined) { return; } + + switch (cmd.action) { + case 'plugin': { + try { + require(cmd.plugin).consoleaction(cmd, null, null, this); + } catch (e) { throw e; } + + break; + } + default: { + // probably shouldn't happen, but just in case this feature is expanded + } + } + } //sendConsoleText("Got tunnel #" + this.httprequest.index + " data: " + data, this.httprequest.sessionid); } @@ -1684,7 +1717,7 @@ function createMeshCore(agent) var response = null; switch (cmd) { case 'help': { // Displays available commands - var fin = '', f = '', availcommands = 'help,info,osinfo,args,print,type,dbget,dbset,dbcompact,eval,parseuri,httpget,nwslist,wsconnect,wssend,wsclose,notify,ls,ps,kill,amt,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,sendcaps,openurl,amtreset,amtccm,amtacm,amtdeactivate,amtpolicy,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,scanamt'; + var fin = '', f = '', availcommands = 'help,info,osinfo,args,print,type,dbkeys,dbget,dbset,dbcompact,eval,parseuri,httpget,nwslist,plugin,wsconnect,wssend,wsclose,notify,ls,ps,kill,amt,netinfo,location,power,wakeonlan,setdebug,smbios,rawsmbios,toast,lock,users,sendcaps,openurl,amtreset,amtccm,amtacm,amtdeactivate,amtpolicy,getscript,getclip,setclip,log,av,cpuinfo,sysinfo,apf,scanwifi,scanamt'; availcommands = availcommands.split(',').sort(); while (availcommands.length > 0) { if (f.length > 100) { fin += (f + ',\r\n'); f = ''; } @@ -2339,6 +2372,22 @@ function createMeshCore(agent) } break; } + case 'plugin': { + if (typeof args['_'][0] == 'string') { + try { + // pass off the action to the plugin + // for plugin creators, you'll want to have a plugindir/modules_meshcore/plugin.js + // to control the output / actions here. + response = require(args['_'][0]).consoleaction(args, rights, sessionid, mesh); + } catch(e) { + response = 'There was an error in the plugin (' + e + ')'; + } + } else { + response = 'Proper usage: plugin [pluginName] [args].'; + } + + break; + } default: { // This is an unknown command, return an error message response = 'Unknown command \"' + cmd + '\", type \"help\" for list of avaialble commands.'; break; @@ -2451,6 +2500,8 @@ function createMeshCore(agent) //if (process.platform == 'win32') { try { pr = require('win-info').pendingReboot(); } catch (ex) { pr = null; } } // Pending reboot if ((meshCoreObj.av == null) || (JSON.stringify(meshCoreObj.av) != JSON.stringify(av))) { meshCoreObj.av = av; mesh.SendCommand(meshCoreObj); } } + + // TODO: add plugin hook here } diff --git a/meshagent.js b/meshagent.js index 2ef67cb1..9224db2d 100644 --- a/meshagent.js +++ b/meshagent.js @@ -1277,6 +1277,18 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { }); break; } + case 'plugin': { + if (typeof command.plugin == 'string') { + try { + var pluginHandler = require('./pluginHandler.js').pluginHandler(parent.parent); + pluginHandler.plugins[command.plugin].serveraction(command, obj, parent); + } catch (e) { + + console.log('Error loading plugin handler ('+ e + ')'); + } + } + break; + } default: { parent.agentStats.unknownAgentActionCount++; console.log('Unknown agent action (' + obj.remoteaddrport + '): ' + command.action + '.'); diff --git a/meshcentral.js b/meshcentral.js index 919ca9b0..a1583df1 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -865,6 +865,11 @@ function CreateMeshCentralServer(config, args) { // Dispatch an event that the server is now running obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' }); + obj.pluginHandler = require("./pluginHandler.js").pluginHandler(obj); + + // Plugin hook. Need to run something at server startup? This is the place. + obj.pluginHandler.callHook("server_startup"); + // Load the login cookie encryption key from the database if allowed if ((obj.config) && (obj.config.settings) && (obj.config.settings.allowlogintoken == true)) { obj.db.Get('LoginCookieEncryptionKey', function (err, docs) { @@ -1347,7 +1352,8 @@ function CreateMeshCentralServer(config, args) { } } } - + obj.pluginHandler = require("./pluginHandler.js").pluginHandler(obj); + obj.pluginHandler.addMeshCoreModules(modulesAdd); // Merge the cores and compute the hashes for (var i in modulesAdd) { if ((i == 'windows-recovery') || (i == 'linux-recovery')) { diff --git a/meshuser.js b/meshuser.js index afc4a237..df015534 100644 --- a/meshuser.js +++ b/meshuser.js @@ -3004,6 +3004,17 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'plugin': { + command.userid = user._id; + + if (command.routeToNode === true) { + routeCommandToNode(command); + } else { + // TODO + } + + break; + } default: { // Unknown user action console.log('Unknown action from user ' + user.name + ': ' + command.action + '.'); diff --git a/pluginHandler.js b/pluginHandler.js new file mode 100644 index 00000000..2eb93294 --- /dev/null +++ b/pluginHandler.js @@ -0,0 +1,166 @@ +/** +* @description MeshCentral plugin module +* @author Ryan Blenis +* @copyright +* @license Apache-2.0 +* @version v0.0.1 +*/ + +/*xjslint node: true */ +/*xjslint plusplus: true */ +/*xjslint maxlen: 256 */ +/*jshint node: true */ +/*jshint strict: false */ +/*jshint esversion: 6 */ +"use strict"; + +module.exports.pluginHandler = function (parent) { + var obj = {}; + + obj.fs = require('fs'); + obj.path = require('path'); + obj.parent = parent; + obj.pluginPath = obj.parent.path.join(obj.parent.datapath, 'plugins'); + obj.plugins = {}; + obj.exports = {}; + + try { + obj.enabled = obj.parent.config.settings.plugins.enabled; + obj.loadList = obj.parent.config.settings.plugins.list; + } catch (e) { // Config file options not present, disable self + obj.enabled = false; + obj.loadList = {}; + console.log('Plugin options not added to the config file. Plugins disabled. Please see the documentation.'); + } + + if (obj.enabled) { + obj.loadList.forEach(function(plugin, index) { + if (obj.fs.existsSync(obj.pluginPath + '/' + plugin)) { + try { + obj.plugins[plugin] = require(obj.pluginPath + '/' + plugin + '/' + plugin + '.js')[plugin](obj); + obj.exports[plugin] = obj.plugins[plugin].exports; + } catch (e) { + console.log("Error loading plugin: " + plugin + " (" + e + "). It has been disabled.", e.stack); + } + } + }); + } + + obj.prepExports = function() { + var str = 'function() {\r\n'; + str += ' var obj = {};\r\n'; + + for (const p of Object.keys(obj.plugins)) { + str += ' obj.'+ p +' = {};\r\n'; + for (const l of Object.values(obj.exports[p])) { + str += ' obj.'+ p +'.'+ l + ' = '+ obj.plugins[p][l].toString()+'\r\n'; + } + } + + str += 'obj.enabled = '+ obj.enabled +';\r\n'; + str += `obj.onDeviceRefeshEnd = function(nodeid, panel, refresh, event) { + for (const p of Object.keys(obj)) { + if (typeof obj[p].onDeviceRefreshEnd == 'function') { + obj[p].onDeviceRefreshEnd(nodeid, panel, refresh, event); + } + } + }; + obj.registerPluginTab = function(pluginRegInfo) { + var d = pluginRegInfo(); + if (!Q(d.tabId)) { + QA('p19headers', ''+d.tabTitle+''); + } + }; + obj.callPluginPage = function(id) { + var pages = Q('p19pages').querySelectorAll("#p19pages>div"); + for (const i of pages) { + i.style.display = 'none'; + } + QV(id, true); + }; + return obj; };`; + return str; + } + + obj.callHook = function(hookName, ...args) { + for (var p in obj.plugins) { + if (typeof obj.plugins[p][hookName] == 'function') { + try { + obj.plugins[p][hookName](args); + } catch (e) { + console.log('Error ocurred while running plugin hook' + p + ':' + hookName + ' (' + e + ')'); + } + } + } + }; + + obj.addMeshCoreModules = function(modulesAdd) { + if (obj.enabled !== true) return; + for (var plugin in obj.plugins) { + var moduleDirPath = null; + var modulesDir = null; + //if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(obj.pluginPath, 'modules_meshcore_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (e) { } } // Favor minified modules if present. + if (modulesDir == null) { try { moduleDirPath = obj.path.join(obj.pluginPath, plugin + '/modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (e) { } } // Use non-minified mofules. + if (modulesDir != null) { + for (var i in modulesDir) { + if (modulesDir[i].toLowerCase().endsWith('.js')) { + var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3); + if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files. + var moduleData = [ 'try { addModule("', moduleName, '", "', obj.parent.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (e) { }\r\n' ]; + + // Merge this module + // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms. + if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) { + // Add to IA / Intel AMT cores only + modulesAdd['windows-amt'].push(...moduleData); + modulesAdd['linux-amt'].push(...moduleData); + } else if (moduleName.startsWith('win-')) { + // Add to Windows cores only + modulesAdd['windows-amt'].push(...moduleData); + } else if (moduleName.startsWith('linux-')) { + // Add to Linux cores only + modulesAdd['linux-amt'].push(...moduleData); + modulesAdd['linux-noamt'].push(...moduleData); + } else { + // Add to all cores + modulesAdd['windows-amt'].push(...moduleData); + modulesAdd['linux-amt'].push(...moduleData); + modulesAdd['linux-noamt'].push(...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'].push(...moduleData); + } + } + + // Merge this module to agent recovery modules if needed + if (modulesAdd['windows-agentrecovery'] != null) { + if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal')) { + modulesAdd['windows-agentrecovery'].push(...moduleData); + } + } + } + } + } + } + }; + + obj.deviceViewPanel = function() { + var panel = {}; + for (var p in obj.plugins) { + if (typeof obj.plugins[p][hookName] == 'function') { + try { + panel[p].header = obj.plugins[p].on_device_header(); + panel[p].content = obj.plugins[p].on_device_page(); + } catch (e) { + console.log('Error ocurred while getting plugin views ' + p + ':' + ' (' + e + ')'); + } + } + } + return panel; + } + + return obj; +}; \ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 7ffa3a1d..23878299 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -122,6 +122,7 @@ Details Intel® AMT Console + Plugins   @@ -847,6 +848,26 @@
+