Files
factorio-clusterio/test/master/routes.js
T
Hornwitser dbe01c6c23 Decode master secret from base64 (#398)
The jsonwebtoken accept strings as input for secrets which is
nonsensical from a cryptographic standpoint as the algorithms all
operate on byte sequences.  Worse still, strings are encoded into bytes
using UTF-8, which disallows certain sequences of bytes from appearing
and potentially weakening the cryptography.

For legacy reasons the master secret was encoded into base64 _and passed
as a string to jsonwebtoken_.  This further weakened the crypto as the
bytes that can appear in base 64 is only 1/4 of the 256 possible values
for bytes.  This could significantly weaken the cryptographic strength
of the tokens.

I have known of this issue for quite some time but has put off actually
fixing it for too long now.  Since the master secret is already
documented as being in base 64, decode it as base 64 and pass it as a
byte Buffer to jsonwebtoken.  This will ensure every byte value is
possible in the byte sequence passed to the jsonwebtoken crypto.

The impact of this change is unfortunately that all tokens will become
invalid and will have to be reissued.
2021-07-13 17:16:10 +02:00

356 lines
13 KiB
JavaScript

"use strict";
const assert = require("assert").strict;
const events = require("events");
const FormData = require("form-data");
const jwt = require("jsonwebtoken");
const http = require("http");
const phin = require("phin");
const { wait } = require("@clusterio/lib/helpers");
const routes = require("@clusterio/master/src/routes");
const mock = require("../mock");
describe("master/src/routes", function() {
let master;
let server;
let port;
beforeEach(async function() {
master = new mock.MockMaster();
routes.addRouteHandlers(master.app);
server = http.createServer(master.app);
server.listen(0, "localhost");
await events.once(server, "listening");
port = server.address().port;
});
afterEach(function() {
server.close();
});
describe("/api/stream/:id", function() {
it("should respond with 404 if stream doesn't exist", async function() {
let response;
response = await phin(`http://localhost:${port}/api/stream/01020304`);
assert.equal(response.statusCode, 404);
response = await phin({
url: `http://localhost:${port}/api/stream/01020304`, method: "PUT",
data: "test stream",
});
assert.equal(response.statusCode, 404);
});
it("should respond with 500 if stream times out", async function() {
let stream = await routes.createProxyStream(master.app);
let response = await phin(`http://localhost:${port}/api/stream/${stream.id}`);
assert.equal(response.statusCode, 500);
});
it("should passthrough a stream", async function() {
let stream;
let responses;
stream = await routes.createProxyStream(master.app);
responses = await Promise.all([
(async function() {
await wait(100);
return await phin({
url: `http://localhost:${port}/api/stream/${stream.id}`, method: "PUT",
data: "test content",
});
}()),
phin(`http://localhost:${port}/api/stream/${stream.id}`),
]);
assert.equal(responses[0].statusCode, 200);
assert.equal(responses[1].statusCode, 200);
assert.equal(responses[1].body.toString(), "test content");
stream = await routes.createProxyStream(master.app);
responses = await Promise.all([
phin({
url: `http://localhost:${port}/api/stream/${stream.id}`, method: "PUT",
data: "test content",
}),
(async function() {
await wait(100);
return await phin(`http://localhost:${port}/api/stream/${stream.id}`);
}()),
]);
assert.equal(responses[0].statusCode, 200);
assert.equal(responses[1].statusCode, 200);
assert.equal(responses[1].body.toString(), "test content");
});
});
describe("/api/upload-save", function() {
it("should respond with 401 with invalid token", async function() {
let response;
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: { "Content-Type": "application/zip" },
data: "totally a zip file",
});
assert.equal(response.statusCode, 401);
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": "invalid",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 401);
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": jwt.sign(
{ aud: "user", user: "invalid" }, Buffer.from("TestSecretDoNotUse", "base64")
),
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 401);
master.userManager.users.get("test").tokenValidAfter = Math.floor((Date.now() + 60e3) / 1000);
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": jwt.sign(
{ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")
),
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 401);
});
it("should respond with 403 when user has insufficient permission", async function() {
let response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"Content-Type": "application/zip",
"X-Access-Token": jwt.sign(
{ aud: "user", user: "player" }, Buffer.from("TestSecretDoNotUse", "base64")
),
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 403);
});
let testToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64"));
it("should respond with 415 if content type is missing or invalid", async function() {
let response;
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 415);
assert.deepEqual(JSON.parse(response.body), { request_errors: ["invalid Content-Type"] });
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "invalid",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 415);
assert.deepEqual(JSON.parse(response.body), { request_errors: ["invalid Content-Type"] });
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=file.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "text/plain",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 415);
assert.deepEqual(JSON.parse(response.body), { request_errors: ["invalid Content-Type"] });
});
it("should respond with 400 if invalid multipart request", async function() {
let response;
let form;
form = new FormData();
form.append("instance_id", "123");
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "text/plain" });
response = await phin({
url: `http://localhost:${port}/api/upload-save`, method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
data: form.getBuffer(),
});
assert.equal(response.statusCode, 400);
assert.deepEqual(JSON.parse(response.body), { request_errors: ["invalid file Content-Type"] });
form = new FormData();
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "application/zip" });
form.append("instance_id", "123");
response = await phin({
url: `http://localhost:${port}/api/upload-save`, method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
data: form.getBuffer(),
});
assert.equal(response.statusCode, 400);
assert.deepEqual(
JSON.parse(response.body), { request_errors: ["instance_id must come before files uploaded"] }
);
form = new FormData();
form.append("instance_id", "123");
form.append("file", "totally a zip file", { filename: "test.txt", contentType: "application/zip" });
response = await phin({
url: `http://localhost:${port}/api/upload-save`, method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
data: form.getBuffer(),
});
assert.equal(response.statusCode, 400);
assert.deepEqual(
JSON.parse(response.body), { request_errors: ["filename must end with .zip"] }
);
form = new FormData();
form.append("instance_id", "invalid");
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "application/zip" });
response = await phin({
url: `http://localhost:${port}/api/upload-save`, method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
data: form.getBuffer(),
});
assert.equal(response.statusCode, 400);
assert.deepEqual(
JSON.parse(response.body), { request_errors: ["invalid instance_id"] }
);
});
it("should respond with 400 if invalid file request", async function() {
let response;
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 400);
assert.deepEqual(JSON.parse(response.body), { request_errors: ["Missing or invalid filename parameter"] });
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=test.txt`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 400);
assert.deepEqual(JSON.parse(response.body), { request_errors: ["filename must end with .zip"] });
response = await phin({
url: `http://localhost:${port}/api/upload-save?filename=test.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 400);
assert.deepEqual(
JSON.parse(response.body), { request_errors: ["Missing or invalid instance_id parameter"] }
);
});
it("should respond with 500 if the transfer failed", async function() {
master.forwardRequestToInstance = async (data, request) => {
throw new Error("Something went wrong");
};
let response;
let form;
form = new FormData();
form.append("instance_id", "123");
form.append("file", "totally a zip file", { filename: "test.zip", contentType: "application/zip" });
response = await phin({
url: `http://localhost:${port}/api/upload-save`, method: "POST",
headers: {
"X-Access-Token": testToken,
...form.getHeaders(),
},
data: form.getBuffer(),
});
assert.equal(response.statusCode, 500);
assert.deepEqual(JSON.parse(response.body), { errors: ["Something went wrong"], request_errors: [] });
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=test.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 500);
assert.deepEqual(JSON.parse(response.body), { errors: ["Something went wrong"], request_errors: [] });
master.forwardRequestToInstance = async (data, request) => {
await new Promise(() => {});
};
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=test.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
data: "totally a zip file",
});
assert.equal(response.statusCode, 500);
assert.deepEqual(
JSON.parse(response.body), { errors: ["Timed out establishing stream to slave"], request_errors: [] }
);
});
it("should complete a valid transfer", async function() {
master.forwardRequestToInstance = async (request, data) => {
let stream = await phin({
url: `http://localhost:${port}/api/stream/${data.stream_id}`,
});
assert.equal(stream.body.toString(), "totally a zip file");
return { save: data.filename };
};
let response;
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=test.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/zip",
},
data: "totally a zip file",
});
assert.deepEqual(JSON.parse(response.body), { saves: ["test.zip"] });
assert.equal(response.statusCode, 200);
});
it("should complete a valid transfer with non-standerd mime type", async function() {
master.forwardRequestToInstance = async (request, data) => {
let stream = await phin({
url: `http://localhost:${port}/api/stream/${data.stream_id}`,
});
assert.equal(stream.body.toString(), "totally a zip file");
return { save: data.filename };
};
let response;
response = await phin({
url: `http://localhost:${port}/api/upload-save?instance_id=123&filename=test.zip`, method: "POST",
headers: {
"X-Access-Token": testToken,
"Content-Type": "application/x-zip-compressed",
},
data: "totally a zip file",
});
assert.deepEqual(JSON.parse(response.body), { saves: ["test.zip"] });
assert.equal(response.statusCode, 200);
});
});
});