From 43c1f2ae932f51fc91fc51d0690b5f9355d37b77 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Mon, 9 Apr 2018 11:25:57 +0200 Subject: [PATCH] remoteCommands added plugin and master API endpoint for executing things --- .gitignore | 4 +- client.js | 4 +- config.json | 7 ++- master.js | 62 +++++++++++++++++++++++--- package.json | 2 + sharedPlugins/remoteCommands/config.js | 14 ++++++ sharedPlugins/remoteCommands/index.js | 23 ++++++++++ 7 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 sharedPlugins/remoteCommands/config.js create mode 100644 sharedPlugins/remoteCommands/index.js diff --git a/.gitignore b/.gitignore index a62cbcce..1670d0c3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ Factorio* database/items.db config.json package-lock.json +secret-api-token.txt mod_cache.json static/pictures/spritesheet.js static/pictures/spritesheet.png @@ -44,4 +45,5 @@ static/external sharedPlugins/* !sharedPlugins/inventoryImports/ !sharedPlugins/UPSdisplay/ -!sharedPlugins/remoteMap/ \ No newline at end of file +!sharedPlugins/remoteMap/ +!sharedPlugins/remoteCommands/ diff --git a/client.js b/client.js index 2301672e..4f44fd70 100644 --- a/client.js +++ b/client.js @@ -445,7 +445,7 @@ function instanceManagement() { global.subscribedFiles = {}; } if(pluginConfig.binary == "nodePackage"){ - // require index.js.main() of plugin and execute it as a function + // require index.js.main() of plugin and execute it as a class let pluginClass = require("./sharedPlugins/" + pluginDirectories[I] + "/index.js"); plugins[I] = new pluginClass(combinedConfig, function(data){ if(data.toString('utf8')[0] != "/") { @@ -453,6 +453,8 @@ function instanceManagement() { } else { messageInterface(data.toString('utf8')); } + }, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat. + socket, // socket.io connection to master (and ES6 destructuring, yay) }); if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){ if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) { diff --git a/config.json b/config.json index 779bcc1a..aba81cbc 100644 --- a/config.json +++ b/config.json @@ -8,6 +8,10 @@ "__comment2": "IP the master web server runs at, can be ignored on the master system", "masterIP": "localhost", "masterPort": 8080, + "masterAuthToken": "", + + "__commentAuthSecret": "This is a long random string generated by keyboard mashing. Please mash you own string for security.", + "masterAuthSecret": "anfeåpvunuennuusnvzunsrevunåzeufnewnufasiojfåqwoiuajknwkjfdabewfkjadfnewåfundjkcvnewmfqewrmn123", "__comment3": "Your factorio.com username for factorio MP services, also username will be admin", "username": "", @@ -30,5 +34,6 @@ "entriesPerSecond": 0.01666666667 }, "logItemTransfers": false, - "uploadModsToMaster": true + "uploadModsToMaster": true, + "allowRemoteCommandExecution": false } diff --git a/master.js b/master.js index d3892caf..318221c1 100644 --- a/master.js +++ b/master.js @@ -36,13 +36,15 @@ const averagedTimeSeries = require("averaged-timeseries"); const deepmerge = require("deepmerge"); const path = require("path"); const fs = require("fs"); -var nedb = require("nedb"); +const nedb = require("nedb"); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); -var express = require("express"); -var ejs = require("ejs"); +const express = require("express"); +const ejs = require("ejs"); // Required for express post requests -var bodyParser = require("body-parser"); -var fileUpload = require('express-fileupload'); +const bodyParser = require("body-parser"); +const fileUpload = require('express-fileupload'); var app = express(); app.use(bodyParser.json()); @@ -685,6 +687,43 @@ app.post("/api/getTimelineStats", function(req,res) { } }); /** +POST endpoint for running commands on slaves. +Requires x-access-token header to be set. Find you api token in secret-api-token.txt on the master (after running it once) + +@memberof clusterioMaster +@instance +@alias api/runCommand +@param {object} JSON {instanceID:19412312, command:"/c game.print('hello')"} +@returns {object} Status {auth: bool, message: "Informative error"} +*/ +// write an auth token to file +fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, { + expiresIn: 86400*365 // expires in 1 year +})); +app.post("/api/runCommand", (req,res) => { + var token = req.headers['x-access-token']; + if(!token) return res.status(401).send({ auth: false, message: 'No token provided.' }); + + jwt.verify(token, config.masterAuthSecret, function(err, decoded) { + if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' }); + + // validate request.body + let body = req.body; + if(body.instanceID + && Object.keys(wsSlaves).includes(body.instanceID) + && body.command + && typeof body.command == "string" + && body.command[0] == "/"){ + // execute command + wsSlaves[body.instanceID].runCommand(body.command, data => { + res.status(200).send({auth: true, message: "success", data}); + }); + } else { + res.status(400).send({auth: true, message: "Error: invalid request.body"}); + } + }); +}); +/** GET endpoint. Returns factorio's base locale as a JSON object. @memberof clusterioMaster @@ -792,6 +831,19 @@ class wsSlave { }); } }); + // handle command return values + this.commandsWaitingForReturn = {}; + this.socket.on("runCommandReturnValue", resp => { + if(resp.commandID && resp.body && this.commandsWaitingForReturn[resp.commandID] && this.commandsWaitingForReturn[resp.commandID].callback && typeof this.commandsWaitingForReturn[resp.commandID].callback == "function"){ + this.commandsWaitingForReturn[resp.commandID].callback(resp.body); + delete this.commandsWaitingForReturn[resp.commandID]; + } + }); + } + runCommand(command, callback){ + let commandID = Math.random().toString(); + this.socket.emit("runCommand", {command, commandID}); + this.commandsWaitingForReturn[commandID] = {callback, timestamp: Date.now()}; } } io.on('connection', function (socket) { diff --git a/package.json b/package.json index c7168eac..76666614 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "app-root-path": "^2.0.1", "as-table": "^1.0.31", "averaged-timeseries": "^1.4.0", + "bcryptjs": "^2.4.3", "body-parser": "^1.18.2", "codecov": "^2.3.1", "deepmerge": "^1.5.2", @@ -33,6 +34,7 @@ "hash-files": "^1.1.1", "html5-validator": "^1.0.7", "ini": "^1.3.5", + "jsonwebtoken": "^8.2.1", "leveldown": "^1.9.0", "linvodb3": "^3.25.1", "luamin": "^1.0.4", diff --git a/sharedPlugins/remoteCommands/config.js b/sharedPlugins/remoteCommands/config.js new file mode 100644 index 00000000..6d3d451d --- /dev/null +++ b/sharedPlugins/remoteCommands/config.js @@ -0,0 +1,14 @@ +/* + Config for clusterio remote command plugin. + Allows you to execute commands on slaves with a web API. + + Careful, may allow for RCE vulnerabilities. +*/ +var modInfo = require("./info.json"); +module.exports = { + // Name of package. For display somewhere I guess. + name: "remoteCommands", + version: "1.0.0", + binary: "nodePackage", + description: "Config for clusterio remote command plugin. Allows you to execute commands on slaves with a web API.", +} \ No newline at end of file diff --git a/sharedPlugins/remoteCommands/index.js b/sharedPlugins/remoteCommands/index.js new file mode 100644 index 00000000..daab0153 --- /dev/null +++ b/sharedPlugins/remoteCommands/index.js @@ -0,0 +1,23 @@ +module.exports.main = class remoteCommands { + constructor(mergedConfig, messageInterface, extras){ + console.log = messageInterface; + this.messageInterface = messageInterface; + this.config = mergedConfig; + this.socket = extras.socket; + + if(this.config.allowRemoteCommandExecution){ + this.socket.on("runCommand", data => { + let {commandID} = data; + this.messageInterface(data.command); + if(commandID){ + this.socket.emit("runCommandReturnValue", { + commandID, + body: { + info: "Command was sent to factorio", + }, + }); + } + }); + } + } +} \ No newline at end of file