mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2025-11-21 02:09:05 -05:00
* Improve plugin file handling and cleanup logic Adds checks and fallbacks for temporary directory write access when creating plugin zip files, and enhances error handling when creating write streams. Updates plugin removal to use fs.rmSync with force and recursive options, and adds error handling for directory removal. * Update pluginHandler.js
606 lines
30 KiB
JavaScript
606 lines
30 KiB
JavaScript
/**
|
|
* @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";
|
|
|
|
/*
|
|
Existing plugins:
|
|
https://raw.githubusercontent.com/ryanblenis/MeshCentral-Sample/master/config.json
|
|
https://raw.githubusercontent.com/ryanblenis/MeshCentral-DevTools/master/config.json
|
|
*/
|
|
|
|
|
|
module.exports.pluginHandler = function (parent) {
|
|
var obj = {};
|
|
|
|
obj.fs = require('fs');
|
|
obj.path = require('path');
|
|
obj.common = require('./common.js');
|
|
obj.parent = parent;
|
|
obj.pluginPath = obj.parent.path.join(obj.parent.datapath, 'plugins');
|
|
obj.plugins = {};
|
|
obj.exports = {};
|
|
obj.loadList = obj.parent.config.settings.plugins.list; // For local development / manual install, not from DB
|
|
|
|
if (typeof obj.loadList != 'object') {
|
|
obj.loadList = {};
|
|
parent.db.getPlugins(function (err, plugins) {
|
|
plugins.forEach(function (plugin) {
|
|
if (plugin.status != 1) return;
|
|
if (obj.fs.existsSync(obj.pluginPath + '/' + plugin.shortName)) {
|
|
try {
|
|
obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj);
|
|
obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports;
|
|
} catch (e) {
|
|
console.log("Error loading plugin: " + plugin.shortName + " (" + e + "). It has been disabled.", e.stack);
|
|
}
|
|
try { // try loading local info about plugin to database (if it changed locally)
|
|
var plugin_config = obj.fs.readFileSync(obj.pluginPath + '/' + plugin.shortName + '/config.json');
|
|
plugin_config = JSON.parse(plugin_config);
|
|
parent.db.updatePlugin(plugin._id, plugin_config);
|
|
} catch (e) { console.log("Plugin config file for " + plugin.name + " could not be parsed."); }
|
|
}
|
|
});
|
|
obj.parent.updateMeshCore(); // db calls are async, lets inject here once we're ready
|
|
});
|
|
} else {
|
|
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 (var p of Object.keys(obj.plugins)) {
|
|
str += ' obj.' + p + ' = {};\r\n';
|
|
if (Array.isArray(obj.exports[p])) {
|
|
for (var l of Object.values(obj.exports[p])) {
|
|
str += ' obj.' + p + '.' + l + ' = ' + obj.plugins[p][l].toString() + '\r\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
str += `
|
|
obj.callHook = function(hookName, ...args) {
|
|
for (const p of Object.keys(obj)) {
|
|
if (typeof obj[p][hookName] == 'function') {
|
|
obj[p][hookName].apply(this, args);
|
|
}
|
|
}
|
|
};
|
|
// accepts a function returning an object or an object with { tabId: "yourTabIdValue", tabTitle: "Your Tab Title" }
|
|
obj.registerPluginTab = function(pluginRegInfo) {
|
|
var d = null;
|
|
if (typeof pluginRegInfo == 'function') d = pluginRegInfo();
|
|
else d = pluginRegInfo;
|
|
if (d.tabId == null || d.tabTitle == null) { return false; }
|
|
if (!Q(d.tabId)) {
|
|
var defaultOn = 'class="on"';
|
|
if (Q('p19headers').querySelectorAll("span.on").length) defaultOn = '';
|
|
QA('p19headers', '<span ' + defaultOn + ' id="p19ph-' + d.tabId + '" onclick="return pluginHandler.callPluginPage(\\''+d.tabId+'\\', this);">'+d.tabTitle+'</span>');
|
|
QA('p19pages', '<div id="' + d.tabId + '"></div>');
|
|
}
|
|
QV('MainDevPlugins', true);
|
|
};
|
|
obj.callPluginPage = function(id, el) {
|
|
var pages = Q('p19pages').querySelectorAll("#p19pages>div");
|
|
for (const i of pages) { i.style.display = 'none'; }
|
|
QV(id, true);
|
|
var tabs = Q('p19headers').querySelectorAll("span");
|
|
for (const i of tabs) { i.classList.remove('on'); }
|
|
el.classList.add('on');
|
|
putstore('_curPluginPage', id);
|
|
};
|
|
obj.addPluginEx = function() {
|
|
meshserver.send({ action: 'addplugin', url: Q('pluginurlinput').value});
|
|
};
|
|
obj.addPluginDlg = function() {
|
|
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://" />');
|
|
focusTextBox('pluginurlinput');
|
|
};
|
|
obj.refreshPluginHandler = function() {
|
|
let st = document.createElement('script');
|
|
st.src = '/pluginHandler.js';
|
|
document.body.appendChild(st);
|
|
};
|
|
return obj; }`;
|
|
return str;
|
|
}
|
|
|
|
obj.refreshJS = function (req, res) {
|
|
// to minimize server reboots when installing new plugins, we call the new data and overwrite the old pluginHandler on the front end
|
|
res.set('Content-Type', 'text/javascript');
|
|
res.send('pluginHandlerBuilder = ' + obj.prepExports() + '\r\n' + ' pluginHandler = new pluginHandlerBuilder(); pluginHandler.callHook("onWebUIStartupEnd");');
|
|
}
|
|
|
|
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 occurred while running plugin hook " + p + ':' + hookName, e);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
obj.addMeshCoreModules = function (modulesAdd) {
|
|
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 occurred while getting plugin views " + p + ':' + ' (' + e + ')');
|
|
}
|
|
}
|
|
}
|
|
return panel;
|
|
};
|
|
|
|
obj.isValidConfig = function (conf, url) { // check for the required attributes
|
|
var isValid = true;
|
|
if (!(
|
|
typeof conf.name == 'string'
|
|
&& typeof conf.shortName == 'string'
|
|
&& typeof conf.version == 'string'
|
|
// && typeof conf.author == 'string'
|
|
&& typeof conf.description == 'string'
|
|
&& typeof conf.hasAdminPanel == 'boolean'
|
|
&& typeof conf.homepage == 'string'
|
|
&& typeof conf.changelogUrl == 'string'
|
|
&& typeof conf.configUrl == 'string'
|
|
&& typeof conf.repository == 'object'
|
|
&& typeof conf.repository.type == 'string'
|
|
&& typeof conf.repository.url == 'string'
|
|
&& typeof conf.meshCentralCompat == 'string'
|
|
// && conf.configUrl == url // make sure we're loading a plugin from its desired config
|
|
)) isValid = false;
|
|
// more checks here?
|
|
if (conf.repository.type == 'git') {
|
|
if (typeof conf.downloadUrl != 'string') isValid = false;
|
|
}
|
|
return isValid;
|
|
};
|
|
|
|
// https://raw.githubusercontent.com/ryanblenis/MeshCentral-Sample/master/config.json
|
|
obj.getPluginConfig = function (configUrl) {
|
|
return new Promise(function (resolve, reject) {
|
|
var http = (configUrl.indexOf('https://') >= 0) ? require('https') : require('http');
|
|
if (configUrl.indexOf('://') === -1) reject("Unable to fetch the config: Bad URL (" + configUrl + ")");
|
|
var options = require('url').parse(configUrl);
|
|
if (typeof parent.config.settings.plugins.proxy == 'string') { // Proxy support
|
|
const HttpsProxyAgent = require('https-proxy-agent');
|
|
options.agent = new HttpsProxyAgent(require('url').parse(parent.config.settings.plugins.proxy));
|
|
}
|
|
http.get(options, function (res) {
|
|
var configStr = '';
|
|
res.on('data', function (chunk) {
|
|
configStr += chunk;
|
|
});
|
|
res.on('end', function () {
|
|
if (configStr[0] == '{') { // Let's be sure we're JSON
|
|
try {
|
|
var pluginConfig = JSON.parse(configStr);
|
|
if (Array.isArray(pluginConfig) && pluginConfig.length == 1) pluginConfig = pluginConfig[0];
|
|
if (obj.isValidConfig(pluginConfig, configUrl)) {
|
|
resolve(pluginConfig);
|
|
} else {
|
|
reject("This does not appear to be a valid plugin configuration.");
|
|
}
|
|
} catch (e) { reject("Error getting plugin config. Check that you have valid JSON."); }
|
|
} else {
|
|
reject("Error getting plugin config. Check that you have valid JSON.");
|
|
}
|
|
});
|
|
|
|
}).on('error', function (e) {
|
|
reject("Error getting plugin config: " + e.message);
|
|
});
|
|
})
|
|
};
|
|
|
|
// MeshCentral now adheres to semver, drop the -<alpha> off the version number for later versions for comparing plugins prior to this change
|
|
obj.versionToNumber = function(ver) { var x = ver.split('-'); if (x.length != 2) return ver; return x[0]; }
|
|
|
|
// Check if the current version of MeshCentral is at least the minimal required.
|
|
obj.versionCompare = function(current, minimal) {
|
|
if (minimal.startsWith('>=')) { minimal = minimal.substring(2); }
|
|
var c = obj.versionToNumber(current).split('.'), m = obj.versionToNumber(minimal).split('.');
|
|
if (c.length != m.length) return false;
|
|
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; } }
|
|
return true;
|
|
}
|
|
|
|
obj.versionGreater = function(a, b) {
|
|
a = obj.versionToNumber(String(a).replace(/^v/, ''));
|
|
b = obj.versionToNumber(String(b).replace(/^v/, ''));
|
|
const partsA = a.split('.').map(Number);
|
|
const partsB = b.split('.').map(Number);
|
|
|
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
const numA = partsA[i] || 0;
|
|
const numB = partsB[i] || 0;
|
|
if (numA > numB) return true;
|
|
if (numA < numB) return false;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
obj.versionLower = function(a, b) {
|
|
a = obj.versionToNumber(String(a).replace(/^v/, ''));
|
|
b = obj.versionToNumber(String(b).replace(/^v/, ''));
|
|
const partsA = a.split('.').map(Number);
|
|
const partsB = b.split('.').map(Number);
|
|
|
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
const numA = partsA[i] || 0;
|
|
const numB = partsB[i] || 0;
|
|
if (numA < numB) return true;
|
|
if (numA > numB) return false;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
obj.getPluginLatest = function () {
|
|
return new Promise(function (resolve, reject) {
|
|
parent.db.getPlugins(function (err, plugins) {
|
|
var proms = [];
|
|
plugins.forEach(function (curconf) {
|
|
proms.push(obj.getPluginConfig(curconf.configUrl).catch(e => { return null; }));
|
|
});
|
|
var latestRet = [];
|
|
Promise.all(proms).then(function (newconfs) {
|
|
var nconfs = [];
|
|
// Filter out config download issues
|
|
newconfs.forEach(function (nc) { if (nc !== null) nconfs.push(nc); });
|
|
if (nconfs.length == 0) { resolve([]); } else {
|
|
nconfs.forEach(function (newconf) {
|
|
var curconf = null;
|
|
plugins.forEach(function (conf) {
|
|
if (conf.configUrl == newconf.configUrl) curconf = conf;
|
|
});
|
|
if (curconf == null) reject("Some plugin configs could not be parsed");
|
|
latestRet.push({
|
|
'id': curconf._id,
|
|
'installedVersion': curconf.version,
|
|
'version': newconf.version,
|
|
'hasUpdate': obj.versionGreater(newconf.version, curconf.version),
|
|
'meshCentralCompat': obj.versionCompare(parent.currentVer, newconf.meshCentralCompat),
|
|
'changelogUrl': curconf.changelogUrl,
|
|
'status': curconf.status
|
|
});
|
|
resolve(latestRet);
|
|
});
|
|
}
|
|
}).catch((e) => { console.log("Error reaching plugins, update call aborted.", e) });
|
|
});
|
|
});
|
|
};
|
|
|
|
obj.addPlugin = function (pluginConfig) {
|
|
return new Promise(function (resolve, reject) {
|
|
parent.db.addPlugin({
|
|
'name': pluginConfig.name,
|
|
'shortName': pluginConfig.shortName,
|
|
'version': pluginConfig.version,
|
|
'description': pluginConfig.description,
|
|
'hasAdminPanel': pluginConfig.hasAdminPanel,
|
|
'homepage': pluginConfig.homepage,
|
|
'changelogUrl': pluginConfig.changelogUrl,
|
|
'configUrl': pluginConfig.configUrl,
|
|
'downloadUrl': pluginConfig.downloadUrl,
|
|
'repository': {
|
|
'type': pluginConfig.repository.type,
|
|
'url': pluginConfig.repository.url
|
|
},
|
|
'meshCentralCompat': pluginConfig.meshCentralCompat,
|
|
'versionHistoryUrl': pluginConfig.versionHistoryUrl,
|
|
'status': 0 // 0: disabled, 1: enabled
|
|
}, function () {
|
|
parent.db.getPlugins(function (err, docs) {
|
|
if (err) reject(err);
|
|
else resolve(docs);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
obj.installPlugin = function (id, version_only, force_url, func) {
|
|
parent.db.getPlugin(id, function (err, docs) {
|
|
// the "id" would probably suffice, but is probably an sanitary issue, generate a random instead
|
|
var randId = Math.random().toString(32).replace('0.', '');
|
|
var tmpDir = require('os').tmpdir();
|
|
var fileName = obj.parent.path.join(tmpDir, 'Plugin_' + randId + '.zip');
|
|
try {
|
|
obj.fs.accessSync(tmpDir, obj.fs.constants.W_OK);
|
|
} catch (e) {
|
|
var pluginTmpPath = obj.parent.path.join(obj.pluginPath, '_tmp');
|
|
if (!obj.fs.existsSync(pluginTmpPath)) {
|
|
obj.fs.mkdirSync(pluginTmpPath, { recursive: true });
|
|
}
|
|
fileName = obj.parent.path.join(pluginTmpPath, 'Plugin_' + randId + '.zip');
|
|
}
|
|
var plugin = docs[0];
|
|
if (plugin.repository.type == 'git') {
|
|
var file;
|
|
try {
|
|
file = obj.fs.createWriteStream(fileName);
|
|
} catch (e) {
|
|
if (fileName.indexOf(tmpDir) >= 0) {
|
|
var pluginTmpPath = obj.parent.path.join(obj.pluginPath, '_tmp');
|
|
if (!obj.fs.existsSync(pluginTmpPath)) {
|
|
obj.fs.mkdirSync(pluginTmpPath, { recursive: true });
|
|
}
|
|
fileName = obj.parent.path.join(pluginTmpPath, 'Plugin_' + randId + '.zip');
|
|
file = obj.fs.createWriteStream(fileName);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
var dl_url = plugin.downloadUrl;
|
|
if (version_only != null && version_only != false) dl_url = version_only.url;
|
|
if (force_url != null) dl_url = force_url;
|
|
var url = require('url');
|
|
var q = url.parse(dl_url, true);
|
|
var http = (q.protocol == "http:") ? require('http') : require('https');
|
|
var opts = {
|
|
path: q.pathname,
|
|
host: q.hostname,
|
|
port: q.port,
|
|
headers: {
|
|
'User-Agent': 'MeshCentral'
|
|
},
|
|
followRedirects: true,
|
|
method: 'GET'
|
|
};
|
|
if (typeof parent.config.settings.plugins.proxy == 'string') { // Proxy support
|
|
const HttpsProxyAgent = require('https-proxy-agent');
|
|
opts.agent = new HttpsProxyAgent(require('url').parse(parent.config.settings.plugins.proxy));
|
|
}
|
|
var request = http.get(opts, function (response) {
|
|
// handle redirections with grace
|
|
if (response.headers.location) {
|
|
file.close(() => obj.fs.unlink(fileName, () => {}));
|
|
return obj.installPlugin(id, version_only, response.headers.location, func);
|
|
}
|
|
response.pipe(file);
|
|
file.on('finish', function () {
|
|
file.close(function () {
|
|
var yauzl = require('yauzl');
|
|
if (!obj.fs.existsSync(obj.pluginPath)) {
|
|
obj.fs.mkdirSync(obj.pluginPath);
|
|
}
|
|
if (!obj.fs.existsSync(obj.parent.path.join(obj.pluginPath, plugin.shortName))) {
|
|
obj.fs.mkdirSync(obj.parent.path.join(obj.pluginPath, plugin.shortName));
|
|
}
|
|
yauzl.open(fileName, { lazyEntries: true }, function (err, zipfile) {
|
|
if (err) throw err;
|
|
zipfile.readEntry();
|
|
zipfile.on('entry', function (entry) {
|
|
let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
|
|
let pathReg = new RegExp(/(.*?\/)/);
|
|
//if (process.platform == 'win32') { pathReg = new RegExp(/(.*?\\/); }
|
|
let filePath = obj.parent.path.join(pluginPath, entry.fileName.replace(pathReg, '')); // remove top level dir
|
|
|
|
if (/\/$/.test(entry.fileName)) { // dir
|
|
if (!obj.fs.existsSync(filePath))
|
|
obj.fs.mkdirSync(filePath);
|
|
zipfile.readEntry();
|
|
} else { // file
|
|
zipfile.openReadStream(entry, function (err, readStream) {
|
|
if (err) throw err;
|
|
readStream.on('end', function () { zipfile.readEntry(); });
|
|
if (process.platform == 'win32') {
|
|
readStream.pipe(obj.fs.createWriteStream(filePath));
|
|
} else {
|
|
var fileMode = (entry.externalFileAttributes >> 16) & 0x0fff;
|
|
if( fileMode <= 0 ) fileMode = 0o644;
|
|
readStream.pipe(obj.fs.createWriteStream(filePath, { mode: fileMode }));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
zipfile.on('end', function () {
|
|
setTimeout(function () {
|
|
obj.fs.unlinkSync(fileName);
|
|
if (version_only == null || version_only === false) {
|
|
parent.db.setPluginStatus(id, 1, func);
|
|
} else {
|
|
parent.db.updatePlugin(id, { status: 1, version: version_only.name }, func);
|
|
}
|
|
try {
|
|
obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj);
|
|
obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports;
|
|
if (typeof obj.plugins[plugin.shortName].server_startup == 'function') obj.plugins[plugin.shortName].server_startup();
|
|
} catch (e) { console.log('Error instantiating new plugin: ', e); }
|
|
try {
|
|
var plugin_config = obj.fs.readFileSync(obj.pluginPath + '/' + plugin.shortName + '/config.json');
|
|
plugin_config = JSON.parse(plugin_config);
|
|
parent.db.updatePlugin(plugin._id, plugin_config);
|
|
} catch (e) { console.log('Error reading plugin config upon install'); }
|
|
parent.updateMeshCore();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
} else if (plugin.repository.type == 'npm') {
|
|
// @TODO npm support? (need a test plugin)
|
|
}
|
|
});
|
|
};
|
|
|
|
obj.getPluginVersions = function (id) {
|
|
return new Promise(function (resolve, reject) {
|
|
parent.db.getPlugin(id, function (err, docs) {
|
|
var plugin = docs[0];
|
|
if (plugin.versionHistoryUrl == null) reject("No version history available for this plugin.");
|
|
var url = require('url');
|
|
var q = url.parse(plugin.versionHistoryUrl, true);
|
|
var http = (q.protocol == 'http:') ? require('http') : require('https');
|
|
var opts = {
|
|
path: q.pathname,
|
|
host: q.hostname,
|
|
port: q.port,
|
|
headers: {
|
|
'User-Agent': 'MeshCentral',
|
|
'Accept': 'application/vnd.github.v3+json'
|
|
}
|
|
};
|
|
if (typeof parent.config.settings.plugins.proxy == 'string') { // Proxy support
|
|
const HttpsProxyAgent = require('https-proxy-agent');
|
|
options.agent = new HttpsProxyAgent(require('url').parse(parent.config.settings.plugins.proxy));
|
|
}
|
|
http.get(opts, function (res) {
|
|
var versStr = '';
|
|
res.on('data', function (chunk) {
|
|
versStr += chunk;
|
|
});
|
|
res.on('end', function () {
|
|
if ((versStr[0] == '{') || (versStr[0] == '[')) { // let's be sure we're JSON
|
|
try {
|
|
var vers = JSON.parse(versStr);
|
|
var vList = [];
|
|
vers.forEach((v) => {
|
|
if (obj.versionLower(v.name, plugin.version)) vList.push(v);
|
|
});
|
|
if (vers.length == 0) reject("No previous versions available.");
|
|
resolve({ 'id': plugin._id, 'name': plugin.name, versionList: vList });
|
|
} catch (e) { reject("Version history problem."); }
|
|
} else {
|
|
reject("Version history appears to be malformed." + versStr);
|
|
}
|
|
});
|
|
}).on('error', function (e) {
|
|
reject("Error getting plugin versions: " + e.message);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
obj.disablePlugin = function (id, func) {
|
|
parent.db.getPlugin(id, function (err, docs) {
|
|
var plugin = docs[0];
|
|
parent.db.setPluginStatus(id, 0, func);
|
|
delete obj.plugins[plugin.shortName];
|
|
delete obj.exports[plugin.shortName];
|
|
parent.updateMeshCore();
|
|
});
|
|
};
|
|
|
|
obj.removePlugin = function (id, func) {
|
|
parent.db.getPlugin(id, function (err, docs) {
|
|
var plugin = docs[0];
|
|
let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
|
|
if (obj.fs.existsSync(pluginPath)) {
|
|
try {
|
|
obj.fs.rmSync(pluginPath, { recursive: true, force: true });
|
|
} catch (e) {
|
|
console.log("Error removing plugin directory:", e);
|
|
}
|
|
}
|
|
parent.db.deletePlugin(id, func);
|
|
delete obj.plugins[plugin.shortName];
|
|
});
|
|
};
|
|
|
|
obj.handleAdminReq = function (req, res, user, serv) {
|
|
if ((req.query.pin == null) || (obj.common.isAlphaNumeric(req.query.pin) !== true)) { res.sendStatus(401); return; }
|
|
var path = obj.path.join(obj.pluginPath, req.query.pin, 'views');
|
|
serv.app.set('views', path);
|
|
if ((obj.plugins[req.query.pin] != null) && (typeof obj.plugins[req.query.pin].handleAdminReq == 'function')) {
|
|
obj.plugins[req.query.pin].handleAdminReq(req, res, user);
|
|
} else {
|
|
res.sendStatus(401);
|
|
}
|
|
}
|
|
|
|
obj.handleAdminPostReq = function (req, res, user, serv) {
|
|
if ((req.query.pin == null) || (obj.common.isAlphaNumeric(req.query.pin) !== true)) { res.sendStatus(401); return; }
|
|
var path = obj.path.join(obj.pluginPath, req.query.pin, 'views');
|
|
serv.app.set('views', path);
|
|
if ((obj.plugins[req.query.pin] != null) && (typeof obj.plugins[req.query.pin].handleAdminPostReq == 'function')) {
|
|
obj.plugins[req.query.pin].handleAdminPostReq(req, res, user);
|
|
} else {
|
|
res.sendStatus(401);
|
|
}
|
|
}
|
|
return obj;
|
|
};
|