From 36cf14074ea0aeb1feda05d77678aa235bc1df96 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Tue, 26 May 2020 15:31:42 +0200 Subject: [PATCH] Automatically upload export to master server Upload the created export data zip file to a new /api/upload-export endpoint on the master server which unpacks it into the appropriate directory on the master. --- lib/factorio/export.js | 4 ++-- master.js | 40 +++++++++++++++++++++++++++++++++++ package.json | 1 + slave.js | 19 ++++++++++++++++- test/integration/clusterio.js | 2 +- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/lib/factorio/export.js b/lib/factorio/export.js index 31f81b0f..4c2d08cb 100644 --- a/lib/factorio/export.js +++ b/lib/factorio/export.js @@ -306,6 +306,7 @@ async function exportLocale(server, modVersions, modOrder, languageCode) { * * @param {module:lib/factorio.FactorioServer} server - * The server to export the data from. + * @returns {JSZip} zip file with exported data. * @memberof module:lib/factorio */ async function exportData(server) { @@ -347,8 +348,7 @@ async function exportData(server) { zip.file("export/item-spritesheet.png", await iconSheet.getBufferAsync(Jimp.MIME_PNG)); zip.file("export/item-metadata.json", JSON.stringify([...itemData.entries()])); zip.file("export/locale.json", JSON.stringify([...locale.entries()])); - let content = await zip.generateAsync({ type: "nodebuffer" }); - await fs.outputFile("temp/export.zip", content); + return zip; } diff --git a/master.js b/master.js index 365063bc..f0abf712 100644 --- a/master.js +++ b/master.js @@ -27,6 +27,7 @@ const setBlocking = require("set-blocking"); const events = require("events"); const yargs = require("yargs"); const WebSocket = require("ws"); +const JSZip = require("jszip"); const version = require("./package").version; // ugly globals @@ -174,6 +175,45 @@ async function getMetrics(req, res, next) { } app.get('/metrics', (req, res, next) => getMetrics(req, res, next).catch(next)); +// Handle an uploaded export package. +async function uploadExport(req, res, next) { + endpointHitCounter.labels(req.route.path).inc(); + if (req.get("Content-Type") !== "application/zip") { + res.sendStatus(415); + return; + } + + let data = []; + for await (let chunk of req) { + data.push(chunk); + } + data = Buffer.concat(data); + let zip = await JSZip.loadAsync(data); + delete data; + + // This is hardcoded to prevent path expansion attacks + let exportFiles = [ + "export/item-spritesheet.png", + "export/item-metadata.json", + "export/locale.json", + ]; + + for (let filePath of exportFiles) { + let file = zip.file(filePath); + if (!file) { + continue; + } + + let name = path.posix.basename(filePath); + await fs.outputFile(path.join("static", "export", name), await file.async("nodebuffer")); + } + + res.sendStatus(200); +} +app.put("/api/upload-export", + authenticate.middleware, + (req, res, next) => uploadExport(req, res, next).catch(next) +); const masterConnectedClientsCount = new prometheus.Gauge( 'clusterio_master_connected_clients_count', "How many clients are currently connected to this master server", diff --git a/package.json b/package.json index 831572ca..fc2842fb 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "needle": "^2.4.0", "node-factorio-api": "^0.3.8", "node-forge": "^0.7.1", + "phin": "^3.4.1", "pidusage": "^2.0.18", "rcon-client": "^4.2.0", "request": "^2.83.0", diff --git a/slave.js b/slave.js index ca2301ac..4001d10b 100644 --- a/slave.js +++ b/slave.js @@ -16,6 +16,7 @@ const yargs = require("yargs"); const events = require("events"); const pidusage = require("pidusage"); const setBlocking = require("set-blocking"); +const phin = require("phin"); const version = require("./package").version; // internal libraries @@ -446,7 +447,23 @@ class Instance extends link.Link{ console.log("Exporting data ....."); await symlinkMods(this, "sharedMods", console); - await factorio.exportData(this.server); + let zip = await factorio.exportData(this.server); + + let content = await zip.generateAsync({ type: "nodebuffer" }); + let url = new URL(this._slave.config.get("slave.master_url")); + url.pathname += "api/upload-export"; + let response = await phin({ + url, method: "PUT", + data: content, + core: { rejectUnauthorized: false }, + headers: { + "Content-Type": "application/zip", + "x-access-token": this._slave.config.get("slave.master_token"), + }, + }); + if (response.statusCode !== 200) { + throw Error(`Upload failed: ${response.statusCode} ${response.statusMessage}: ${response.body}`) + } } finally { this.notifyExit(); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 2d5bd828..f48353b4 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -62,7 +62,7 @@ describe("Integration of Clusterio", function() { it("exports the data", async function() { slowTest(this); await exec(`node clusterctl --config ${controlConfigPath} export-data --instance test`); - assert(await fs.exists(path.join("temp", "export.zip")), "Export was not created"); + assert(await fs.exists(path.join("static", "export", "locale.json")), "Export was not created"); }); });