pluginHandler.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /**
  2. * @description MeshCentral plugin module
  3. * @author Ryan Blenis
  4. * @copyright
  5. * @license Apache-2.0
  6. * @version v0.0.1
  7. */
  8. /*xjslint node: true */
  9. /*xjslint plusplus: true */
  10. /*xjslint maxlen: 256 */
  11. /*jshint node: true */
  12. /*jshint strict: false */
  13. /*jshint esversion: 6 */
  14. "use strict";
  15. /*
  16. Existing plugins:
  17. https://raw.githubusercontent.com/ryanblenis/MeshCentral-Sample/master/config.json
  18. https://raw.githubusercontent.com/ryanblenis/MeshCentral-DevTools/master/config.json
  19. */
  20. module.exports.pluginHandler = function (parent) {
  21. var obj = {};
  22. obj.fs = require('fs');
  23. obj.path = require('path');
  24. obj.common = require('./common.js');
  25. obj.parent = parent;
  26. obj.pluginPath = obj.parent.path.join(obj.parent.datapath, 'plugins');
  27. obj.plugins = {};
  28. obj.exports = {};
  29. obj.loadList = obj.parent.config.settings.plugins.list; // For local development / manual install, not from DB
  30. if (typeof obj.loadList != 'object') {
  31. obj.loadList = {};
  32. parent.db.getPlugins(function (err, plugins) {
  33. plugins.forEach(function (plugin) {
  34. if (plugin.status != 1) return;
  35. if (obj.fs.existsSync(obj.pluginPath + '/' + plugin.shortName)) {
  36. try {
  37. obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj);
  38. obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports;
  39. } catch (e) {
  40. console.log("Error loading plugin: " + plugin.shortName + " (" + e + "). It has been disabled.", e.stack);
  41. }
  42. try { // try loading local info about plugin to database (if it changed locally)
  43. var plugin_config = obj.fs.readFileSync(obj.pluginPath + '/' + plugin.shortName + '/config.json');
  44. plugin_config = JSON.parse(plugin_config);
  45. parent.db.updatePlugin(plugin._id, plugin_config);
  46. } catch (e) { console.log("Plugin config file for " + plugin.name + " could not be parsed."); }
  47. }
  48. });
  49. obj.parent.updateMeshCore(); // db calls are async, lets inject here once we're ready
  50. });
  51. } else {
  52. obj.loadList.forEach(function (plugin, index) {
  53. if (obj.fs.existsSync(obj.pluginPath + '/' + plugin)) {
  54. try {
  55. obj.plugins[plugin] = require(obj.pluginPath + '/' + plugin + '/' + plugin + '.js')[plugin](obj);
  56. obj.exports[plugin] = obj.plugins[plugin].exports;
  57. } catch (e) {
  58. console.log("Error loading plugin: " + plugin + " (" + e + "). It has been disabled.", e.stack);
  59. }
  60. }
  61. });
  62. }
  63. obj.prepExports = function () {
  64. var str = 'function() {\r\n';
  65. str += ' var obj = {};\r\n';
  66. for (var p of Object.keys(obj.plugins)) {
  67. str += ' obj.' + p + ' = {};\r\n';
  68. if (Array.isArray(obj.exports[p])) {
  69. for (var l of Object.values(obj.exports[p])) {
  70. str += ' obj.' + p + '.' + l + ' = ' + obj.plugins[p][l].toString() + '\r\n';
  71. }
  72. }
  73. }
  74. str += `
  75. obj.callHook = function(hookName, ...args) {
  76. for (const p of Object.keys(obj)) {
  77. if (typeof obj[p][hookName] == 'function') {
  78. obj[p][hookName].apply(this, args);
  79. }
  80. }
  81. };
  82. // accepts a function returning an object or an object with { tabId: "yourTabIdValue", tabTitle: "Your Tab Title" }
  83. obj.registerPluginTab = function(pluginRegInfo) {
  84. var d = null;
  85. if (typeof pluginRegInfo == 'function') d = pluginRegInfo();
  86. else d = pluginRegInfo;
  87. if (d.tabId == null || d.tabTitle == null) { return false; }
  88. if (!Q(d.tabId)) {
  89. var defaultOn = 'class="on"';
  90. if (Q('p19headers').querySelectorAll("span.on").length) defaultOn = '';
  91. QA('p19headers', '<span ' + defaultOn + ' id="p19ph-' + d.tabId + '" onclick="return pluginHandler.callPluginPage(\\''+d.tabId+'\\', this);">'+d.tabTitle+'</span>');
  92. QA('p19pages', '<div id="' + d.tabId + '"></div>');
  93. }
  94. QV('MainDevPlugins', true);
  95. };
  96. obj.callPluginPage = function(id, el) {
  97. var pages = Q('p19pages').querySelectorAll("#p19pages>div");
  98. for (const i of pages) { i.style.display = 'none'; }
  99. QV(id, true);
  100. var tabs = Q('p19headers').querySelectorAll("span");
  101. for (const i of tabs) { i.classList.remove('on'); }
  102. el.classList.add('on');
  103. putstore('_curPluginPage', id);
  104. };
  105. obj.addPluginEx = function() {
  106. meshserver.send({ action: 'addplugin', url: Q('pluginurlinput').value});
  107. };
  108. obj.addPluginDlg = function() {
  109. if (typeof showModal === 'function') {
  110. setDialogMode(2, "Plugin Download URL", 3, obj.addPluginEx, '<p><b>WARNING:</b> Downloading plugins may compromise server security. Only download from trusted sources.</p><input type=text id=pluginurlinput style=width:100% placeholder="https://" />');
  111. showModal('xxAddAgentModal', 'idx_dlgOkButton', obj.addPluginEx);
  112. focusTextBox('pluginurlinput');
  113. } else {
  114. // Fallback to setDialogMode for default.handlebars
  115. setDialogMode(2, "Plugin Download URL", 3, obj.addPluginEx, '<p><b>WARNING:</b> Downloading plugins may compromise server security. Only download from trusted sources.</p><input type=text id=pluginurlinput style=width:100% placeholder="https://" />');
  116. focusTextBox('pluginurlinput');
  117. }
  118. };
  119. obj.refreshPluginHandler = function() {
  120. let st = document.createElement('script');
  121. st.src = '/pluginHandler.js';
  122. document.body.appendChild(st);
  123. };
  124. return obj; }`;
  125. return str;
  126. }
  127. obj.refreshJS = function (req, res) {
  128. // to minimize server reboots when installing new plugins, we call the new data and overwrite the old pluginHandler on the front end
  129. res.set('Content-Type', 'text/javascript');
  130. res.send('pluginHandlerBuilder = ' + obj.prepExports() + '\r\n' + ' pluginHandler = new pluginHandlerBuilder(); pluginHandler.callHook("onWebUIStartupEnd");');
  131. }
  132. obj.callHook = function (hookName, ...args) {
  133. for (var p in obj.plugins) {
  134. if (typeof obj.plugins[p][hookName] == 'function') {
  135. try {
  136. obj.plugins[p][hookName](...args);
  137. } catch (e) {
  138. console.log("Error occurred while running plugin hook " + p + ':' + hookName, e);
  139. }
  140. }
  141. }
  142. };
  143. obj.addMeshCoreModules = function (modulesAdd) {
  144. for (var plugin in obj.plugins) {
  145. var moduleDirPath = null;
  146. var modulesDir = null;
  147. //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.
  148. if (modulesDir == null) { try { moduleDirPath = obj.path.join(obj.pluginPath, plugin + '/modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (e) { } } // Use non-minified mofules.
  149. if (modulesDir != null) {
  150. for (var i in modulesDir) {
  151. if (modulesDir[i].toLowerCase().endsWith('.js')) {
  152. var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
  153. if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
  154. 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'];
  155. // Merge this module
  156. // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
  157. if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
  158. // Add to IA / Intel AMT cores only
  159. modulesAdd['windows-amt'].push(...moduleData);
  160. modulesAdd['linux-amt'].push(...moduleData);
  161. } else if (moduleName.startsWith('win-')) {
  162. // Add to Windows cores only
  163. modulesAdd['windows-amt'].push(...moduleData);
  164. } else if (moduleName.startsWith('linux-')) {
  165. // Add to Linux cores only
  166. modulesAdd['linux-amt'].push(...moduleData);
  167. modulesAdd['linux-noamt'].push(...moduleData);
  168. } else {
  169. // Add to all cores
  170. modulesAdd['windows-amt'].push(...moduleData);
  171. modulesAdd['linux-amt'].push(...moduleData);
  172. modulesAdd['linux-noamt'].push(...moduleData);
  173. }
  174. // Merge this module to recovery modules if needed
  175. if (modulesAdd['windows-recovery'] != null) {
  176. if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal')) {
  177. modulesAdd['windows-recovery'].push(...moduleData);
  178. }
  179. }
  180. // Merge this module to agent recovery modules if needed
  181. if (modulesAdd['windows-agentrecovery'] != null) {
  182. if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal')) {
  183. modulesAdd['windows-agentrecovery'].push(...moduleData);
  184. }
  185. }
  186. }
  187. }
  188. }
  189. }
  190. };
  191. obj.deviceViewPanel = function () {
  192. var panel = {};
  193. for (var p in obj.plugins) {
  194. if (typeof obj.plugins[p].on_device_header === "function" && typeof obj.plugins[p].on_device_page === "function") {
  195. try {
  196. panel[p] = {
  197. header: obj.plugins[p].on_device_header(),
  198. content: obj.plugins[p].on_device_page()
  199. };
  200. } catch (e) {
  201. console.log("Error occurred while getting plugin views " + p + ':' + ' (' + e + ')');
  202. }
  203. }
  204. }
  205. return panel;
  206. };
  207. obj.isValidConfig = function (conf, url) { // check for the required attributes
  208. var isValid = true;
  209. if (!(
  210. typeof conf.name == 'string'
  211. && typeof conf.shortName == 'string'
  212. && typeof conf.version == 'string'
  213. // && typeof conf.author == 'string'
  214. && typeof conf.description == 'string'
  215. && typeof conf.hasAdminPanel == 'boolean'
  216. && typeof conf.homepage == 'string'
  217. && typeof conf.changelogUrl == 'string'
  218. && typeof conf.configUrl == 'string'
  219. && typeof conf.repository == 'object'
  220. && typeof conf.repository.type == 'string'
  221. && typeof conf.repository.url == 'string'
  222. && typeof conf.meshCentralCompat == 'string'
  223. // && conf.configUrl == url // make sure we're loading a plugin from its desired config
  224. )) isValid = false;
  225. // more checks here?
  226. if (conf.repository.type == 'git') {
  227. if (typeof conf.downloadUrl != 'string') isValid = false;
  228. }
  229. return isValid;
  230. };
  231. // https://raw.githubusercontent.com/ryanblenis/MeshCentral-Sample/master/config.json
  232. obj.getPluginConfig = function (configUrl) {
  233. return new Promise(function (resolve, reject) {
  234. var http = (configUrl.indexOf('https://') >= 0) ? require('https') : require('http');
  235. if (configUrl.indexOf('://') === -1) reject("Unable to fetch the config: Bad URL (" + configUrl + ")");
  236. var options = require('url').parse(configUrl);
  237. if (typeof parent.config.settings.plugins.proxy == 'string') { // Proxy support
  238. const HttpsProxyAgent = require('https-proxy-agent');
  239. options.agent = new HttpsProxyAgent(require('url').parse(parent.config.settings.plugins.proxy));
  240. }
  241. http.get(options, function (res) {
  242. var configStr = '';
  243. res.on('data', function (chunk) {
  244. configStr += chunk;
  245. });
  246. res.on('end', function () {
  247. if (configStr[0] == '{') { // Let's be sure we're JSON
  248. try {
  249. var pluginConfig = JSON.parse(configStr);
  250. if (Array.isArray(pluginConfig) && pluginConfig.length == 1) pluginConfig = pluginConfig[0];
  251. if (obj.isValidConfig(pluginConfig, configUrl)) {
  252. resolve(pluginConfig);
  253. } else {
  254. reject("This does not appear to be a valid plugin configuration.");
  255. }
  256. } catch (e) { reject("Error getting plugin config. Check that you have valid JSON."); }
  257. } else {
  258. reject("Error getting plugin config. Check that you have valid JSON.");
  259. }
  260. });
  261. }).on('error', function (e) {
  262. reject("Error getting plugin config: " + e.message);
  263. });
  264. })
  265. };
  266. // MeshCentral now adheres to semver, drop the -<alpha> off the version number for later versions for comparing plugins prior to this change
  267. obj.versionToNumber = function(ver) { var x = ver.split('-'); if (x.length != 2) return ver; return x[0]; }
  268. // Check if the current version of MeshCentral is at least the minimal required.
  269. obj.versionCompare = function(current, minimal) {
  270. if (minimal.startsWith('>=')) { minimal = minimal.substring(2); }
  271. var c = obj.versionToNumber(current).split('.'), m = obj.versionToNumber(minimal).split('.');
  272. if (c.length != m.length) return false;
  273. for (var i = 0; i < c.length; i++) { var cx = parseInt(c[i]), cm = parseInt(m[i]); if (cx > cm) { return true; } if (cx < cm) { return false; } }
  274. return true;
  275. }
  276. obj.versionGreater = function(a, b) {
  277. a = obj.versionToNumber(String(a).replace(/^v/, ''));
  278. b = obj.versionToNumber(String(b).replace(/^v/, ''));
  279. const partsA = a.split('.').map(Number);
  280. const partsB = b.split('.').map(Number);
  281. for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
  282. const numA = partsA[i] || 0;
  283. const numB = partsB[i] || 0;
  284. if (numA > numB) return true;
  285. if (numA < numB) return false;
  286. }
  287. return false;
  288. };
  289. obj.versionLower = function(a, b) {
  290. a = obj.versionToNumber(String(a).replace(/^v/, ''));
  291. b = obj.versionToNumber(String(b).replace(/^v/, ''));
  292. const partsA = a.split('.').map(Number);
  293. const partsB = b.split('.').map(Number);
  294. for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
  295. const numA = partsA[i] || 0;
  296. const numB = partsB[i] || 0;
  297. if (numA < numB) return true;
  298. if (numA > numB) return false;
  299. }
  300. return false;
  301. };
  302. obj.getPluginLatest = function () {
  303. return new Promise(function (resolve, reject) {
  304. parent.db.getPlugins(function (err, plugins) {
  305. var proms = [];
  306. plugins.forEach(function (curconf) {
  307. proms.push(obj.getPluginConfig(curconf.configUrl).catch(e => { return null; }));
  308. });
  309. var latestRet = [];
  310. Promise.all(proms).then(function (newconfs) {
  311. var nconfs = [];
  312. // Filter out config download issues
  313. newconfs.forEach(function (nc) { if (nc !== null) nconfs.push(nc); });
  314. if (nconfs.length == 0) { resolve([]); } else {
  315. nconfs.forEach(function (newconf) {
  316. var curconf = null;
  317. plugins.forEach(function (conf) {
  318. if (conf.configUrl == newconf.configUrl) curconf = conf;
  319. });
  320. if (curconf == null) reject("Some plugin configs could not be parsed");
  321. latestRet.push({
  322. 'id': curconf._id,
  323. 'installedVersion': curconf.version,
  324. 'version': newconf.version,
  325. 'hasUpdate': obj.versionGreater(newconf.version, curconf.version),
  326. 'meshCentralCompat': obj.versionCompare(parent.currentVer, newconf.meshCentralCompat),
  327. 'changelogUrl': curconf.changelogUrl,
  328. 'status': curconf.status
  329. });
  330. resolve(latestRet);
  331. });
  332. }
  333. }).catch((e) => { console.log("Error reaching plugins, update call aborted.", e) });
  334. });
  335. });
  336. };
  337. obj.addPlugin = function (pluginConfig) {
  338. return new Promise(function (resolve, reject) {
  339. parent.db.addPlugin({
  340. 'name': pluginConfig.name,
  341. 'shortName': pluginConfig.shortName,
  342. 'version': pluginConfig.version,
  343. 'description': pluginConfig.description,
  344. 'hasAdminPanel': pluginConfig.hasAdminPanel,
  345. 'homepage': pluginConfig.homepage,
  346. 'changelogUrl': pluginConfig.changelogUrl,
  347. 'configUrl': pluginConfig.configUrl,
  348. 'downloadUrl': pluginConfig.downloadUrl,
  349. 'repository': {
  350. 'type': pluginConfig.repository.type,
  351. 'url': pluginConfig.repository.url
  352. },
  353. 'meshCentralCompat': pluginConfig.meshCentralCompat,
  354. 'versionHistoryUrl': pluginConfig.versionHistoryUrl,
  355. 'status': 0 // 0: disabled, 1: enabled
  356. }, function () {
  357. parent.db.getPlugins(function (err, docs) {
  358. if (err) reject(err);
  359. else resolve(docs);
  360. });
  361. });
  362. });
  363. };
  364. obj.installPlugin = function (id, version_only, force_url, func) {
  365. parent.db.getPlugin(id, function (err, docs) {
  366. // the "id" would probably suffice, but is probably an sanitary issue, generate a random instead
  367. var randId = Math.random().toString(32).replace('0.', '');
  368. var tmpDir = require('os').tmpdir();
  369. var fileName = obj.parent.path.join(tmpDir, 'Plugin_' + randId + '.zip');
  370. try {
  371. obj.fs.accessSync(tmpDir, obj.fs.constants.W_OK);
  372. } catch (e) {
  373. var pluginTmpPath = obj.parent.path.join(obj.pluginPath, '_tmp');
  374. if (!obj.fs.existsSync(pluginTmpPath)) {
  375. obj.fs.mkdirSync(pluginTmpPath, { recursive: true });
  376. }
  377. fileName = obj.parent.path.join(pluginTmpPath, 'Plugin_' + randId + '.zip');
  378. }
  379. var plugin = docs[0];
  380. if (plugin.repository.type == 'git') {
  381. var file;
  382. try {
  383. file = obj.fs.createWriteStream(fileName);
  384. } catch (e) {
  385. if (fileName.indexOf(tmpDir) >= 0) {
  386. var pluginTmpPath = obj.parent.path.join(obj.pluginPath, '_tmp');
  387. if (!obj.fs.existsSync(pluginTmpPath)) {
  388. obj.fs.mkdirSync(pluginTmpPath, { recursive: true });
  389. }
  390. fileName = obj.parent.path.join(pluginTmpPath, 'Plugin_' + randId + '.zip');
  391. file = obj.fs.createWriteStream(fileName);
  392. } else {
  393. throw e;
  394. }
  395. }
  396. var dl_url = plugin.downloadUrl;
  397. if (version_only != null && version_only != false) dl_url = version_only.url;
  398. if (force_url != null) dl_url = force_url;
  399. var url = require('url');
  400. var q = url.parse(dl_url, true);
  401. var http = (q.protocol == "http:") ? require('http') : require('https');
  402. var opts = {
  403. path: q.pathname,
  404. host: q.hostname,
  405. port: q.port,
  406. headers: {
  407. 'User-Agent': 'MeshCentral'
  408. },
  409. followRedirects: true,
  410. method: 'GET'
  411. };
  412. if (typeof parent.config.settings.plugins.proxy == 'string') { // Proxy support
  413. const HttpsProxyAgent = require('https-proxy-agent');
  414. opts.agent = new HttpsProxyAgent(require('url').parse(parent.config.settings.plugins.proxy));
  415. }
  416. var request = http.get(opts, function (response) {
  417. // handle redirections with grace
  418. if (response.headers.location) {
  419. file.close(() => obj.fs.unlink(fileName, () => {}));
  420. return obj.installPlugin(id, version_only, response.headers.location, func);
  421. }
  422. response.pipe(file);
  423. file.on('finish', function () {
  424. file.close(function () {
  425. var yauzl = require('yauzl');
  426. if (!obj.fs.existsSync(obj.pluginPath)) {
  427. obj.fs.mkdirSync(obj.pluginPath);
  428. }
  429. if (!obj.fs.existsSync(obj.parent.path.join(obj.pluginPath, plugin.shortName))) {
  430. obj.fs.mkdirSync(obj.parent.path.join(obj.pluginPath, plugin.shortName));
  431. }
  432. yauzl.open(fileName, { lazyEntries: true }, function (err, zipfile) {
  433. if (err) throw err;
  434. zipfile.readEntry();
  435. zipfile.on('entry', function (entry) {
  436. let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
  437. let pathReg = new RegExp(/(.*?\/)/);
  438. //if (process.platform == 'win32') { pathReg = new RegExp(/(.*?\\/); }
  439. let filePath = obj.parent.path.join(pluginPath, entry.fileName.replace(pathReg, '')); // remove top level dir
  440. if (/\/$/.test(entry.fileName)) { // dir
  441. if (!obj.fs.existsSync(filePath))
  442. obj.fs.mkdirSync(filePath);
  443. zipfile.readEntry();
  444. } else { // file
  445. zipfile.openReadStream(entry, function (err, readStream) {
  446. if (err) throw err;
  447. readStream.on('end', function () { zipfile.readEntry(); });
  448. if (process.platform == 'win32') {
  449. readStream.pipe(obj.fs.createWriteStream(filePath));
  450. } else {
  451. var fileMode = (entry.externalFileAttributes >> 16) & 0x0fff;
  452. if( fileMode <= 0 ) fileMode = 0o644;
  453. readStream.pipe(obj.fs.createWriteStream(filePath, { mode: fileMode }));
  454. }
  455. });
  456. }
  457. });
  458. zipfile.on('end', function () {
  459. setTimeout(function () {
  460. obj.fs.unlinkSync(fileName);
  461. if (version_only == null || version_only === false) {
  462. parent.db.setPluginStatus(id, 1, func);
  463. } else {
  464. parent.db.updatePlugin(id, { status: 1, version: version_only.name }, func);
  465. }
  466. try {
  467. obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj);
  468. obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports;
  469. if (typeof obj.plugins[plugin.shortName].server_startup == 'function') obj.plugins[plugin.shortName].server_startup();
  470. } catch (e) { console.log('Error instantiating new plugin: ', e); }
  471. try {
  472. var plugin_config = obj.fs.readFileSync(obj.pluginPath + '/' + plugin.shortName + '/config.json');
  473. plugin_config = JSON.parse(plugin_config);
  474. parent.db.updatePlugin(plugin._id, plugin_config);
  475. } catch (e) { console.log('Error reading plugin config upon install'); }
  476. parent.updateMeshCore();
  477. });
  478. });
  479. });
  480. });
  481. });
  482. });
  483. } else if (plugin.repository.type == 'npm') {
  484. // @TODO npm support? (need a test plugin)
  485. }
  486. });
  487. };
  488. obj.getPluginVersions = function (id) {
  489. return new Promise(function (resolve, reject) {
  490. parent.db.getPlugin(id, function (err, docs) {
  491. var plugin = docs[0];
  492. if (plugin.versionHistoryUrl == null) reject("No version history available for this plugin.");
  493. var url = require('url');
  494. var q = url.parse(plugin.versionHistoryUrl, true);
  495. var http = (q.protocol == 'http:') ? require('http') : require('https');
  496. var opts = {
  497. path: q.pathname,
  498. host: q.hostname,
  499. port: q.port,
  500. headers: {
  501. 'User-Agent': 'MeshCentral',
  502. 'Accept': 'application/vnd.github.v3+json'
  503. }
  504. };
  505. if (typeof parent.config.settings.plugins.proxy == 'string') { // Proxy support
  506. const HttpsProxyAgent = require('https-proxy-agent');
  507. options.agent = new HttpsProxyAgent(require('url').parse(parent.config.settings.plugins.proxy));
  508. }
  509. http.get(opts, function (res) {
  510. var versStr = '';
  511. res.on('data', function (chunk) {
  512. versStr += chunk;
  513. });
  514. res.on('end', function () {
  515. if ((versStr[0] == '{') || (versStr[0] == '[')) { // let's be sure we're JSON
  516. try {
  517. var vers = JSON.parse(versStr);
  518. var vList = [];
  519. vers.forEach((v) => {
  520. if (obj.versionLower(v.name, plugin.version)) vList.push(v);
  521. });
  522. if (vers.length == 0) reject("No previous versions available.");
  523. resolve({ 'id': plugin._id, 'name': plugin.name, versionList: vList });
  524. } catch (e) { reject("Version history problem."); }
  525. } else {
  526. reject("Version history appears to be malformed." + versStr);
  527. }
  528. });
  529. }).on('error', function (e) {
  530. reject("Error getting plugin versions: " + e.message);
  531. });
  532. });
  533. });
  534. };
  535. obj.disablePlugin = function (id, func) {
  536. parent.db.getPlugin(id, function (err, docs) {
  537. var plugin = docs[0];
  538. parent.db.setPluginStatus(id, 0, func);
  539. delete obj.plugins[plugin.shortName];
  540. delete obj.exports[plugin.shortName];
  541. parent.updateMeshCore();
  542. });
  543. };
  544. obj.removePlugin = function (id, func) {
  545. parent.db.getPlugin(id, function (err, docs) {
  546. var plugin = docs[0];
  547. let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
  548. if (obj.fs.existsSync(pluginPath)) {
  549. try {
  550. obj.fs.rmSync(pluginPath, { recursive: true, force: true });
  551. } catch (e) {
  552. console.log("Error removing plugin directory:", e);
  553. }
  554. }
  555. parent.db.deletePlugin(id, func);
  556. delete obj.plugins[plugin.shortName];
  557. });
  558. };
  559. obj.handleAdminReq = function (req, res, user, serv) {
  560. if ((req.query.pin == null) || (obj.common.isAlphaNumeric(req.query.pin) !== true)) { res.sendStatus(401); return; }
  561. var path = obj.path.join(obj.pluginPath, req.query.pin, 'views');
  562. serv.app.set('views', path);
  563. if ((obj.plugins[req.query.pin] != null) && (typeof obj.plugins[req.query.pin].handleAdminReq == 'function')) {
  564. obj.plugins[req.query.pin].handleAdminReq(req, res, user);
  565. } else {
  566. res.sendStatus(401);
  567. }
  568. }
  569. obj.handleAdminPostReq = function (req, res, user, serv) {
  570. if ((req.query.pin == null) || (obj.common.isAlphaNumeric(req.query.pin) !== true)) { res.sendStatus(401); return; }
  571. var path = obj.path.join(obj.pluginPath, req.query.pin, 'views');
  572. serv.app.set('views', path);
  573. if ((obj.plugins[req.query.pin] != null) && (typeof obj.plugins[req.query.pin].handleAdminPostReq == 'function')) {
  574. obj.plugins[req.query.pin].handleAdminPostReq(req, res, user);
  575. } else {
  576. res.sendStatus(401);
  577. }
  578. }
  579. return obj;
  580. };