diff --git a/client.js b/client.js index 839635f9..b4b3d8ec 100644 --- a/client.js +++ b/client.js @@ -42,6 +42,19 @@ const command = process.argv[2]; // and as the process name in ps/top on linux. process.title = "clusterioClient "+instance; +// make sure we have the master access token (can't write to master without it since clusterio 2.0) +if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){ + console.log("ERROR invalid config!"); + console.log("Master server now needs an access token for write operations. As clusterio slaves depends \ + upon this, please add your token to config.js in the field named masterAuthToken. \ + You can retrieve your auth token from the master in secret-api-token.txt after running it once."); +} +const needleOptionsWithTokenAuthHeader = { + headers: { + 'x-access-token': config.masterAuthToken + }, +}; + var instanceInfo = {}; // function to handle sending commands into the game @@ -604,7 +617,7 @@ function instanceManagement() { } else { payload.mac = mac console.log("Registered our precense with master "+config.masterIP+" at " + payload.time); - needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, function (err, response, body) { + needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) { if (err && err.code != "ECONNRESET"){ console.error("We got problems, something went wrong when contacting master"); console.error(err); @@ -637,7 +650,7 @@ function instanceManagement() { modName: modHashes[i].modName, hash: modHashes[i].hash, } - needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, function (err, response, body) { + needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) { if(err) console.log(new Error("Unable to contact master server! Please check your config.json.")); if(response && body && body == "found") { console.log("master has mod"); @@ -647,7 +660,11 @@ function instanceManagement() { console.log("Sending mod: " + mod); // Send mods master says it wants // response.body is a string which is a modName.zip - var req = request.post("http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod', function (err, resp, body) { + var req = request.post({url: "http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod', + headers: { + "x-access-token": config.masterAuthToken, + }, + }, function (err, resp, body) { if (err) { console.log(new Error("Unable to contact master server! Please check your config.json.")); } else { @@ -705,7 +722,7 @@ function instanceManagement() { } } console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]); - needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, function (err, response, body) { + needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) { // we did it, keep going }); } @@ -737,7 +754,7 @@ function instanceManagement() { count: g[1], instanceName: instance, // name of instance instanceID: instanceconfig.unique, // a hash computed from the randomly generated rcon password - }, function (err, resp, body) { + }, needleOptionsWithTokenAuthHeader, function (err, resp, body) { if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]); if(config.logItemTransfers){ if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`); @@ -794,7 +811,7 @@ function instanceManagement() { // request our items, one item at a time for (let i = 0; i < Object.keys(preparedPackage).length; i++) { // console.log(preparedPackage[Object.keys(preparedPackage)[i]]); - needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], function (err, response, body) { + needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) { if (response && response.body && typeof response.body == "object") { // buffer confirmed orders confirmedOrders[confirmedOrders.length] = { diff --git a/lib/authenticate.js b/lib/authenticate.js new file mode 100644 index 00000000..b13404c5 --- /dev/null +++ b/lib/authenticate.js @@ -0,0 +1,15 @@ +const jwt = require('jsonwebtoken'); + +const config = require("./../config"); + +var middleware = function (req, res, next) { + var token = req.headers['x-access-token']; + if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path}); + + jwt.verify(token, config.masterAuthSecret, function(err, decoded) { + if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path}); + next(); + }); +} + +module.exports = {middleware}; diff --git a/master.js b/master.js index d8cb67da..f7831405 100644 --- a/master.js +++ b/master.js @@ -28,6 +28,9 @@ var config = require('./config'); const getFactorioLocale = require("./lib/getFactorioLocale"); const stringUtils = require("./lib/stringUtils"); +// homemade express middleware for token auth +const authenticate = require("./lib/authenticate"); + // Library for create folder recursively if it does not exist const mkdirp = require("mkdirp"); mkdirp.sync("./database"); @@ -37,8 +40,13 @@ const deepmerge = require("deepmerge"); const path = require("path"); const fs = require("fs"); const nedb = require("nedb"); + const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); +// 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 +})); const express = require("express"); const ejs = require("ejs"); @@ -224,7 +232,7 @@ POST World ID management. Slaves post here to tell the server they exist @instance @alias /api/getID */ -app.post("/api/getID", function(req,res) { +app.post("/api/getID", authenticate.middleware, function(req,res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); // request.body should be an object @@ -253,7 +261,7 @@ POST Allows you to add metadata related to slaves for other tools, like owner da @instance @alias /api/editSlaveMeta */ -app.post("/api/editSlaveMeta", function(req,res) { +app.post("/api/editSlaveMeta", authenticate.middleware, function(req,res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); // request.body should be an object @@ -292,22 +300,6 @@ app.post("/api/getSlaveMeta", function (req, res) { res.send('{"INVALID REQUEST":1}'); } }); -// TODO: Remove deprecated function, also update researchSync to not use it -/** -POST DEPRECATED Get metadata from all slaves. This is already covered by /api/slaves, please use that one instead. -@memberof clusterioMaster -@instance -@alias /api/getSlavesMeta -*/ -app.post("/api/getSlavesMeta", function (req, res) { - if(req.body.password){ - let metas = {}; - res.send(JSON.stringify(Object.keys(slaves).map(key => metas[key] = slaves[key].meta))); - } else { - res.status(400); - res.send('{"INVALID REQUEST":1}'); - } -}); // mod management // should handle uploading and checking if mods are uploaded /** @@ -341,7 +333,7 @@ POST endpoint for uploading mods to the master server. Required for automatic mo @instance @alias /api/uploadMod */ -app.post("/api/uploadMod", function(req,res) { +app.post("/api/uploadMod", authenticate.middleware, function(req,res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); if (!req.files) { @@ -398,7 +390,7 @@ POST endpoint for storing items in master's inventory. @param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name] @returns {string} status either "success" or "failure" */ -app.post("/api/place", function(req, res) { +app.post("/api/place", authenticate.middleware, function(req, res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); res.header("Access-Control-Allow-Origin", "*"); @@ -465,7 +457,7 @@ POST endpoint to remove items from DB when client orders items. @returns {itemStack} the number of items actually removed, may be lower than what was asked for due to shortages. */ _doleDivisionFactor = {}; //If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down. -app.post("/api/remove", function(req, res) { +app.post("/api/remove", authenticate.middleware, function(req, res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); const doleDivisionRetardation = 10; //lower rates will equal more dramatic swings @@ -540,7 +532,7 @@ Gives no response @param {object} circuitFrame @param {number} circuitFrame.time */ -app.post("/api/setSignal", function(req,res) { +app.post("/api/setSignal", authenticate.middleware, function(req,res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); if(typeof req.body == "object" && req.body.time){ @@ -625,7 +617,7 @@ course the timeSeries data. @param {object} JSON {timestamp: Date.now(), instanceID: "string", data: {"item": number}} @returns {string} failure */ -app.post("/api/logStats", function(req,res) { +app.post("/api/logStats", authenticate.middleware, function(req,res) { endpointHitCounter.labels(req.route.path).inc(); let reqStartTime = Date.now(); if(typeof req.body == "object" && req.body.instanceID && req.body.timestamp && req.body.data) { @@ -793,10 +785,6 @@ Requires x-access-token header to be set. Find you api token in secret-api-token @param {object} JSON {instanceID:19412312, command:"/c game.print('hello')"} @returns {object} Status {auth: bool, message: "Informative error", data:{}} */ -// 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.' });