Files
factorio-clusterio/test/mock.js
T
Hornwitser 72d0994ca6 Add Player Auth plugin (#368)
Add authentication plugin that does a simple challenge response protocol
to verify that the player trying to log in is who they claim to be.  The
process goes as follows:

1. A player code is generated for a player that wants to log in and is
   displayed in-game.
2. The player code is input into the web interface and the master
   generates a verification code as a challenge back along with a token.
3. The player inputs the verification code in-game and is logged in to
   the web interface in doing so.

To do the actual login the web interface polls a verify endpoint every 2
seconds until the code has either been verified or expired.  The
challenge comes from being unable to control the verification code and
having to input it as a player logging into one of the servers in order
to login.

Some additional steps are taken in order to increase the difficulty for
an attacker to subvert the challenge:

- The login process must be started from in-game.
- Generated codes are only valid for a limited timespan which is
  configurable and 2 minutes by default.
- The verification code comes with a token that only works with one
  player code and verification code combo and must be provided to pass
  validation.
2021-03-17 18:48:22 +01:00

237 lines
4.8 KiB
JavaScript

"use strict";
const events = require("events");
const http = require("http");
const express = require("express");
const bodyParser = require("body-parser");
const libLink = require("@clusterio/lib/link");
const libPlugin = require("@clusterio/lib/plugin");
const libUsers = require("@clusterio/lib/users");
const libPrometheus = require("@clusterio/lib/prometheus");
class MockLogger {
child() { return this; }
fatal() { }
error() { }
warn() { }
audit() { }
info() { }
server() { }
verbose() { }
}
class MockSocket {
constructor() {
this.sentMessages = [];
this.events = new Map();
}
send(message) {
this.sentMessages.push(message);
}
on(event, fn) {
this.events.set(event, fn);
}
/* eslint-disable accessor-pairs */
set onclose(fn) {
this.on("close", (code, reason) => fn({ code, reason }));
}
set onerror(fn) {
this.on("error", fn);
}
set onopen(fn) {
this.on("open", fn);
}
set onmessage(fn) {
this.on("message", (data) => fn({ data }));
}
terminate() {
this.terminateCalled = true;
}
close() {
this.closeCalled = true;
}
}
class MockConnector extends events.EventEmitter {
constructor() {
super();
this._seq = 1;
this.sentMessages = [];
this.events = new Map();
this.handshake = { address: "socket.test" };
this.connected = true;
this.closing = false;
}
send(type, data) {
let seq = this._seq;
this._seq += 1;
let message = { seq, type, data };
this.sentMessages.push(message);
setImmediate(() => this.emit("send", message));
return seq;
}
}
class MockServer extends events.EventEmitter {
constructor() {
super();
this.reset();
}
reset() {
this.rconCommands = [];
this.rconCommandResults = new Map();
}
async sendRcon(command) {
this.rconCommands.push(command);
return this.rconCommandResults.get(command) || "";
}
}
class MockInstance extends libLink.Link {
constructor() {
super("instance", "slave", new MockConnector());
this.logger = new MockLogger();
this.server = new MockServer();
this.name = "test";
this.id = 7357;
this.mockConfigEntries = new Map([
["instance.id", 7357],
["factorio.enable_save_patching", true],
]);
this.config = {
get: (name) => {
if (this.mockConfigEntries.has(name)) {
return this.mockConfigEntries.get(name);
}
throw Error(`mock for field ${name} is not implemented`);
},
};
}
async sendRcon(command, expectEmpty, plugin) {
return await this.server.sendRcon(command);
}
}
class MockSlave extends libLink.Link {
constructor() {
super("slave", "master", new MockConnector());
}
}
class MockControl extends libLink.Link {
constructor(connector) {
super("control", "master", connector);
}
}
class MockMaster {
constructor() {
this.app = express();
this.app.use(bodyParser.json({ limit: "10mb" }));
this.mockConfigEntries = new Map([
["master.external_address", "test"],
["master.auth_secret", "TestSecretDoNotUse"],
]);
this.config = {
get: (name) => {
if (this.mockConfigEntries.has(name)) {
return this.mockConfigEntries.get(name);
}
throw Error(`mock for field ${name} is not implemented`);
},
};
let roles = new Map([
[0, new libUsers.Role({ id: 0, name: "Admin", description: "admin", permissions: ["core.admin"] })],
[1, new libUsers.Role({ id: 1, name: "Player", description: "player", permissions: [] })],
]);
roles.get(1).grantDefaultPermissions();
let users = new Map([
["test", new libUsers.User({ name: "test", roles: [0, 1] }, roles)],
["player", new libUsers.User({ name: "player", roles: [1] }, roles)],
]);
this.db = {
instances: new Map(),
slaves: new Map(),
roles,
users,
};
}
getMasterUrl() {
return "http://master.example/";
}
async startServer() {
this.server = http.createServer(this.app);
await new Promise(resolve => {
this.server.listen(0, "127.0.0.1", resolve);
});
let address = this.server.address();
return `http://127.0.0.1:${address.port}`;
}
async stopServer() {
if (this.server) {
this.server.close();
this.server.unref();
}
}
}
async function createMasterPlugin(MasterPluginClass, info) {
let master = new MockMaster();
let metrics = {
endpointHitCounter: new libPrometheus.Counter("hit_counter", "Hit Counter", { labels: ["route"] }),
};
let logger = new MockLogger();
let plugin = new MasterPluginClass(info, master, metrics, logger);
await plugin.init();
return plugin;
}
async function createInstancePlugin(InstancePluginClass, info) {
let instance = new MockInstance();
let slave = new MockSlave();
let plugin = new InstancePluginClass(info, instance, slave);
libPlugin.attachPluginMessages(instance, plugin);
await plugin.init();
return plugin;
}
module.exports = {
MockLogger,
MockSocket,
MockConnector,
MockInstance,
MockSlave,
MockControl,
createMasterPlugin,
createInstancePlugin,
};