From e2566d2d1c9c45c86c352de8f570e4fdae243bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Vest=C3=B8l?= Date: Thu, 27 Sep 2018 14:35:41 +0200 Subject: [PATCH] [big change] Added serverManager plugin --- .gitignore | 1 + sharedPlugins/serverManager/config.js | 13 ++ sharedPlugins/serverManager/index.js | 43 +++++ .../serverManager/js/api-endpoints.js | 127 ++++++++++++++ sharedPlugins/serverManager/js/ui.js | 19 +++ sharedPlugins/serverManager/masterPlugin.js | 158 ++++++++++++++++++ sharedPlugins/serverManager/static/index.html | 54 ++++++ sharedPlugins/serverManager/static/index.js | 60 +++++++ .../serverManager/static/serverManager.css | 23 +++ 9 files changed, 498 insertions(+) create mode 100644 sharedPlugins/serverManager/config.js create mode 100644 sharedPlugins/serverManager/index.js create mode 100644 sharedPlugins/serverManager/js/api-endpoints.js create mode 100644 sharedPlugins/serverManager/js/ui.js create mode 100644 sharedPlugins/serverManager/masterPlugin.js create mode 100644 sharedPlugins/serverManager/static/index.html create mode 100644 sharedPlugins/serverManager/static/index.js create mode 100644 sharedPlugins/serverManager/static/serverManager.css diff --git a/.gitignore b/.gitignore index 3b838688..b5f29114 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ sharedPlugins/* !sharedPlugins/researchSync/ !sharedPlugins/globalChat/ !sharedPlugins/clusterioMod/ +!sharedPlugins/serverManager/ # File used for marking plugins as disabled sharedPlugins/*/DISABLED diff --git a/sharedPlugins/serverManager/config.js b/sharedPlugins/serverManager/config.js new file mode 100644 index 00000000..9b1f8277 --- /dev/null +++ b/sharedPlugins/serverManager/config.js @@ -0,0 +1,13 @@ +/* + Clusterio plugin for remote server management +*/ +module.exports = { + name: "serverManager", + version: "1.0.0", + binary: "nodePackage", + description: "Server management tools for remote config edits and web based server management", + masterPlugin: "masterPlugin.js", + dependencies: [ + "playerManager", + ] +} diff --git a/sharedPlugins/serverManager/index.js b/sharedPlugins/serverManager/index.js new file mode 100644 index 00000000..7b519b3d --- /dev/null +++ b/sharedPlugins/serverManager/index.js @@ -0,0 +1,43 @@ +const needle = require("needle"); +const fs = require("fs-extra"); + +const pluginConfig = require("./config"); +const COMPRESS_LUA = false; + +module.exports = class remoteCommands { + constructor(mergedConfig, messageInterface, extras){ + this.messageInterface = messageInterface; + this.config = mergedConfig; + this.socket = extras.socket; + const pluginManager = require("./../../lib/manager/pluginManager.js")(this.config); + const configManager = require("./../../lib/manager/configManager.js"); + // const modManager = require("./../../lib/manager/modManager.js")(this.config); + this.socket.on("hello", () => { + this.socket.emit("registerServerManager"); + }); + this.socket.on("serverManagerGetStatus", async data => { + + }); + this.socket.on("serverManagerGetMods", async (data, callback) => { + // TODO: Implement this function separately from listMods (which console.logs it) + // let mods = modManager.getMods() + // callback(mods); + }); + this.socket.on("serverManagerGetConfig", async (data, callback) => { + let config = await configManager.getConfig(data.instance); + callback(config); + }); + this.socket.on("serverManagerEditConfig", async (data, callback) => { + callback(configManager.editConfig(data.instance, data.entry, data.value)); + }); + this.socket.on("serverManagerGetPlugins", async (data, callback) => { + callback(await pluginManager.getPlugins()); + }); + this.socket.on("serverManagerEnablePlugin", async (data, callback) => { + callback(await pluginManager.enablePlugin(data.name, data.instanceID /* Currently ignored */)); + }); + this.socket.on("serverManagerDisablePlugin", async (data, callback) => { + callback(await pluginManager.disablePlugin(data.name, data.instanceID /* Currently ignored */)); + }); + } +} diff --git a/sharedPlugins/serverManager/js/api-endpoints.js b/sharedPlugins/serverManager/js/api-endpoints.js new file mode 100644 index 00000000..6c3301ef --- /dev/null +++ b/sharedPlugins/serverManager/js/api-endpoints.js @@ -0,0 +1,127 @@ +module.exports = that => { + /** + - Enable a plugin by deleting the DISABLED file + - Use socket.io to update masters list of plugins for this server + + Give standard JSON object response + */ + that.app.post("/api/serverManager/enablePlugin", async (req, res) => { + if((req.body.token || req.cookies.token) + && req.body.instanceID + && typeof req.body.instanceID == "string" + && req.body.pluginName + && typeof req.body.pluginName == "string" + && that.playerManager){ + let perms = await that.playerManager.main.getPermissions((req.body.token || req.cookies.token)); + if(perms.cluster.includes("enablePlugin") + || (perms.instance[req.body.instanceID] + && perms.instance[req.body.instanceID].includes("enablePlugin"))){ + that.serverSockets[req.body.instanceID].socket.emit("serverManagerEnablePlugin", { + name: req.body.pluginName, + instanceID: req.body.instanceID, + }, async resp => { + if(resp.ok){ + await updateData("plugins", req.body.instanceID); + } + res.send(resp); + }); + } else { + res.send({ + ok:false, + msg:"Insufficient permissions", + }); + } + } else { + res.send({ + ok:false, + msg:"Invalid request body", + }); + } + }); + /** + - Disable a plugin by adding an empty file named DISABLED to the plugins install pathname + - Use socket.io to update masters lost of plugins for this server + + Give standard JSON object response + */ + that.app.post("/api/serverManager/disablePlugin", async (req, res) => { + if((req.body.token || req.cookies.token) + && req.body.instanceID + && typeof req.body.instanceID == "string" + && req.body.pluginName + && typeof req.body.pluginName == "string" + && that.playerManager){ + let perms = await that.playerManager.main.getPermissions((req.body.token || req.cookies.token)); + if(perms.cluster.includes("disablePlugin") + || (perms.instance[req.body.instanceID] + && perms.instance[req.body.instanceID].includes("disablePlugin"))){ + that.serverSockets[req.body.instanceID].socket.emit("serverManagerDisablePlugin", { + name: req.body.pluginName, + instanceID: req.body.instanceID, + }, async resp => { + if(resp.ok){ + await updateData("plugins", req.body.instanceID); + } + res.send(resp); + }); + } else { + res.send({ + ok:false, + msg:"Insufficient permissions", + }); + } + } else { + res.send({ + ok:false, + msg:"Invalid request body", + }); + } + }); + that.app.post("/api/serverManager/editConfig", async (req,res) => { + if((req.body.token || req.cookies.token) + && req.body.instanceID + && typeof req.body.instanceID == "string" + && req.body.entry + && typeof req.body.entry == "string" + && req.body.value !== undefined + && that.playerManager){ + let perms = await that.playerManager.main.getPermissions((req.body.token || req.cookies.token)); + if(perms.cluster.includes("writeConfig") + || (perms.instance[req.body.instanceID] + && perms.instance[req.body.instanceID].includes("writeConfig"))){ + that.serverSockets[req.body.instanceID].socket.emit("serverManagerEditConfig", { + entry: req.body.entry, + value: req.body.value, + }, async resp => { + if(resp.ok){ + // Retrieve the latest config from the server and store it + await updateData("config", req.body.instanceID); + } + res.send(resp); + }); + } else { + res.send({ + ok:false, + msg:"Insufficient permissions", + }); + } + } else { + res.send({ + ok:false, + msg:"Invalid request body", + }); + } + }); + async function updateData(type, instanceID){ + // Retrieve the latest ${type} from the server and store it + if(type == "config"){ + that.serverSockets[instanceID].socket.emit("serverManagerGetConfig", {}, data => { + that.serverSockets[instanceID].config = data; + }); + } else if(type == "plugins"){ + that.serverSockets[instanceID].socket.emit("serverManagerGetPlugins", {}, data => { + that.serverSockets[instanceID].plugins = data; + }); + } + } +} diff --git a/sharedPlugins/serverManager/js/ui.js b/sharedPlugins/serverManager/js/ui.js new file mode 100644 index 00000000..a7d23289 --- /dev/null +++ b/sharedPlugins/serverManager/js/ui.js @@ -0,0 +1,19 @@ +let masterPlugin; + +module.exports = { + injectThis: function(x){masterPlugin = x; return this}, + ui: { + slaveListing: [ + { + name:"ServerMangerButton", + getHtml: ({slave}) => { + let html = ""; + if(masterPlugin.serverSockets[slave.unique]){ + let socket = masterPlugin.serverSockets[slave.unique]; + return `Server manager` + } + }, + } + ], + } +} diff --git a/sharedPlugins/serverManager/masterPlugin.js b/sharedPlugins/serverManager/masterPlugin.js new file mode 100644 index 00000000..cfe7e2a3 --- /dev/null +++ b/sharedPlugins/serverManager/masterPlugin.js @@ -0,0 +1,158 @@ +const fs = require("fs-extra"); +const path = require("path"); +const Express = require("express"); +const ejs = require("ejs"); +const bcrypt = require("bcrypt-promise"); +const crypto = require('crypto'); +const base64url = require('base64url'); +const sanitizer = require('sanitizer'); + + +class masterPlugin { + constructor({config, pluginConfig, pluginPath, socketio, express}){ + this.config = config; + this.pluginConfig = pluginConfig; + this.pluginPath = pluginPath; + this.io = socketio; + this.app = express; + + // load databases + // const database = getDatabaseSync("database/playerManager.json"); + // this.whitelist = getDatabaseSync("database/whitelist.json").whitelist || []; + // this.banlist = getDatabaseSync("database/banlist.json").banlist || []; + + // initialize web API + require("./js/api-endpoints.js")(this); + + // expose UI elements embedded in the master + this.ui = require("./js/ui.js").injectThis(this).ui; + + this.serverSockets = {}; + + this.io.on("connection", socket => { + let instanceID = "unknown"; + let isServerManager = false; + socket.on("registerSlave", data => { + if(data.instanceID && !isNaN(Number(data.instanceID))){ + instanceID = data.instanceID; + if(isServerManager){ + this.serverSockets[instanceID] = {socket}; + validServerManager({server: this.serverSockets[instanceID]}); + } + } + }); + socket.on("registerServerManager", () => { + isServerManager = true; + if(instanceID !== "unknown"){ + this.serverSockets[instanceID] = {socket}; + validServerManager({server: this.serverSockets[instanceID]}); + } + console.log("Registered serverManager socket") + + socket.on("disconnect", () => { + if(this.serverSockets[instanceID]) delete this.serverSockets[instanceID]; + console.log(`serverManager ${instanceID} disconnected, ${(Object.keys(this.serverSockets).length)} left`); + }); + }); + socket.on("gameChat", async data => { + // let chatLine = data.data.replace(/(\r\n\t|\n|\r\t)/gm, "").replace("\r", ""); + // if(typeof chatLine == "string") this.handleChatLine(chatLine, instanceID); + }); + }); + let validServerManager = ({server}) => { + server.socket.emit("serverManagerGetPlugins", {}, data => { + server.plugins = data; + }); + server.socket.emit("serverManagerGetConfig", {}, data => { + server.config = data; + }); + } + + // I can't seem to get express static pages + ejs rendering to work properly, so I write my own thing. + let pages = [ + { + addr: "/serverManager", + path: path.join(__dirname,"static/index.html"), + render: ejs + }, + ] + pages.forEach(page => { + this.app.get(page.addr, async (req,res) => { + if(page.render){ + if((req.query.token || req.cookies.token) && this.playerManager){ + var permissions = await this.playerManager.main.getPermissions((req.query.token || req.cookies.token)); + } else { + var permissions = {}; + } + page.render.renderFile(page.path, { + permissions, + serverManager: this, + req: req, + }, async (err, prom) => { + if(err) console.log(err); + let str = await prom; + res.send(str); + }); + } else { + res.send(await fs.readFile(page.path)); + } + }); + }); + this.app.use('/serverManager', Express.static(path.join(__dirname, 'static'))); + } + findInArray(key, value, array){ + let indexes = []; + for(let i in array){ + if(array[i][key] && array[i][key] === value) indexes.push(i); + } + return indexes; + } + async onExit(){ + // await saveDatabase("database/banlist.json", {banlist: this.banlist}); + return; + } + async onLoadFinish({plugins}){ + this.masterPlugins = plugins; + plugins.forEach(plugin => { + if(plugin.pluginConfig.name == "playerManager"){ + this.playerManager = plugin; + } + }); + } + async onPlayerManagerGetPermissions({permissions, user}){ + if(user + && user.admin){ + permissions.cluster.push("addPlugin"); + permissions.cluster.push("removePlugin"); + permissions.cluster.push("enablePlugin"); + permissions.cluster.push("disablePlugin"); + permissions.cluster.push("readConfig"); + permissions.cluster.push("writeConfig"); + } + return permissions; + } +} +module.exports = masterPlugin; + +function getDatabaseSync(path){ + let db; + try { + db = JSON.parse(fs.readFileSync(path, "utf8")); + } catch(e){ + db = {}; + } + return db; +} +async function saveDatabase(path, database){ + if(!path){ + throw new Error("No path provided!"); + } else if(!database){ + throw new Error("No database provided!"); + } else { + try { + await fs.writeFile(path, JSON.stringify(database, null, 4)); + } catch(e){ + throw new Error("Unable to write to database! "+path); + } + } +} diff --git a/sharedPlugins/serverManager/static/index.html b/sharedPlugins/serverManager/static/index.html new file mode 100644 index 00000000..d3c6e04f --- /dev/null +++ b/sharedPlugins/serverManager/static/index.html @@ -0,0 +1,54 @@ + + + <%- include("./../../../static/head.html") %> + + + + + + <%- include("./../../../static/sidebar.html") %> +
+
+ <% + html = ""; + if(req.query + && req.query.instanceID + && slaves[req.query.instanceID] + && slaves[req.query.instanceID].mods + && serverManager.serverSockets[req.query.instanceID]){ + let slave = slaves[req.query.instanceID]; + html += ` +
+ ${slave.mods.map(mod => mod.name).reduce((acc, modname) => acc + "
  • " + modname + "
  • ")} +
    +
      ` + let server = serverManager.serverSockets[req.query.instanceID]; + if(server.plugins) server.plugins.forEach(plugin => { + html += `
    • ${plugin.name}

      ${plugin.description}

    • ` + }); + html += `
    ` + if(permissions && permissions.cluster.includes("readConfig")){ + if(server.config){ + html += `
      `; + server.config.forEach(setting => { + if(setting.entry[1] !== "_" && setting.entry !== "masterAuthSecret"){ + html += `
    • +

      ${setting.entry}

      + +
    • ` + } + }); + } + } + html += `
    + +
    ` + } + %> + <%- html %> +
    +
    + + diff --git a/sharedPlugins/serverManager/static/index.js b/sharedPlugins/serverManager/static/index.js new file mode 100644 index 00000000..f17308ff --- /dev/null +++ b/sharedPlugins/serverManager/static/index.js @@ -0,0 +1,60 @@ +// Handle config editing +originalSettings = {}; +let settings = document.querySelectorAll(".settingInput"); +settings.forEach(setting => { + let entry = setting.children[0].innerText + originalSettings[setting.children[0].innerText] = { + entry: setting.children[0].innerText, + value: setting.children[1].value, + } + setting.children[1].addEventListener("input", ()=>{ + let newValue = setting.children[1].value; + if(newValue != originalSettings[entry].value){ + setting.children[1].style.backgroundColor = "lightgreen"; + } else { + setting.children[1].style.backgroundColor = ""; + } + }); +}); + +submitSettings.addEventListener("click", async ()=>{ + let settings = document.querySelectorAll(".settingInput"); + for(let i = 0; i < settings.length; i++){ + let setting = settings[i]; + let entry = setting.children[0].innerText + if(setting.children[1].value != originalSettings[entry].value){ + // upload new changes, display results somehow + let status = await postJSON("/api/serverManager/editConfig", { + entry, + value: setting.children[1].value, + instanceID: getParameterByName("instanceID"), + }); + if(status.ok){ + setting.children[1].style.backgroundColor = ""; + } + } + } +}); +// Handle plugin management +let pluginStatuses = document.querySelectorAll(".pluginStatus"); +pluginStatuses.forEach(pluginStatus => { + pluginStatus.addEventListener("click", async ()=>{ + if(pluginStatus.style.backgroundColor == "green"){ + let status = await postJSON("/api/serverManager/disablePlugin", { + instanceID: getParameterByName("instanceID"), + pluginName: pluginStatus.parentElement.children[1].children[0].innerText, + }); + if(status.ok){ + pluginStatus.style.backgroundColor = "red"; + } + } else { + let status = await postJSON("/api/serverManager/enablePlugin", { + instanceID: getParameterByName("instanceID"), + pluginName: pluginStatus.parentElement.children[1].children[0].innerText, + }); + if(status.ok){ + pluginStatus.style.backgroundColor = "green"; + } + } + }); +}); \ No newline at end of file diff --git a/sharedPlugins/serverManager/static/serverManager.css b/sharedPlugins/serverManager/static/serverManager.css new file mode 100644 index 00000000..7a610d99 --- /dev/null +++ b/sharedPlugins/serverManager/static/serverManager.css @@ -0,0 +1,23 @@ +#mods { + width: calc(( 100% ) / 4); + float:left; +} +#plugins { + width:calc((( 100% ) - (( 100% ) / 4))/2); + float:left; +} +#config { + width:calc((( 100% ) - (( 100% ) / 4))/2); + float:left; +} +ul { + list-style: none; + padding:0px; +} +#plugins > ul > li > div { + float:left; +} +#plugins .pluginStatus { + width:40px; + height: 40px; +} \ No newline at end of file