Files
factorio-clusterio/plugins/player_auth/master.js
T
Hornwitser 5ad89f072f Replace endpointHitCounter
Track summary statistic of total HTTP response durations using a
middleware that applies for all requests instead of having every request
needing to update the hit counter manually.  Deprecates
endpointHitCounter.
2023-02-15 20:31:02 +01:00

190 lines
5.0 KiB
JavaScript

/**
* @module
*/
"use strict";
const crypto = require("crypto");
const express = require("express");
const util = require("util");
const jwt = require("jsonwebtoken");
const libPlugin = require("@clusterio/lib/plugin");
const libErrors = require("@clusterio/lib/errors");
const { basicType } = require("@clusterio/lib/helpers");
async function generateCode(length) {
// ji1lI, 0oOQ, and 2Z are not present to ease reading.
let letters = "abcdefghkmnpqrstuvwxyzABCDEFGHJKLMNPRSTUVWXY3456789";
let asyncRandomBytes = util.promisify(crypto.randomBytes);
let code = [];
for (let byte of await asyncRandomBytes(length)) {
// Due to the 51 characters in the letters not fitting perfectly in
// 256 there's an ever so slight bias towards a with this algorithm.
code.push(letters[byte % letters.length]);
}
return code.join("");
}
class MasterPlugin extends libPlugin.BaseMasterPlugin {
async init() {
// Store of validation attempts by players
this.players = new Map();
// Periodically remove expired entries
setInterval(() => {
let now = Date.now();
for (let [player, entry] of this.players) {
if (entry.expires < now) {
this.players.delete(player);
}
}
}, 60e3).unref();
this.master.app.get("/api/player_auth/servers", (req, res) => {
let servers = [];
for (let instance of this.master.instances.values()) {
if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) {
servers.push(instance.config.get("factorio.settings")["name"] || "unnamed server");
}
}
res.send(servers);
});
this.master.app.post("/api/player_auth/player_code", express.json(), (req, res, next) => {
this.handlePlayerCode(req, res).catch(next);
});
this.master.app.post("/api/player_auth/verify", express.json(), (req, res, next) => {
this.handleVerify(req, res).catch(next);
});
}
async handlePlayerCode(req, res) {
if (basicType(req.body) !== "object") {
res.sendStatus(400);
return;
}
let playerCode = req.body.player_code;
if (typeof playerCode !== "string") {
res.sendStatus(400);
return;
}
for (let entry of this.players.values()) {
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
let verifyCode = await generateCode(this.master.config.get("player_auth.code_length"));
let secret = Buffer.from(this.master.config.get("master.auth_secret"), "base64");
let verifyToken = jwt.sign(
{
aud: "player_auth.verify_code",
exp: Math.floor(entry.expires / 1000),
verify_code: verifyCode,
player_code: playerCode,
},
secret
);
res.send({ verify_code: verifyCode, verify_token: verifyToken });
return;
}
}
res.send({ error: true, message: "invalid player_code" });
}
async handleVerify(req, res) {
if (basicType(req.body) !== "object") {
res.sendStatus(400);
return;
}
let playerCode = req.body.player_code;
if (typeof playerCode !== "string") {
res.sendStatus(400);
return;
}
let verifyCode = req.body.verify_code;
if (typeof verifyCode !== "string") {
res.sendStatus(400);
return;
}
let verifyToken = req.body.verify_token;
if (typeof verifyToken !== "string") {
res.sendStatus(400);
return;
}
let secret = Buffer.from(this.master.config.get("master.auth_secret"), "base64");
try {
let payload = jwt.verify(verifyToken, secret, { audience: "player_auth.verify_code" });
if (payload.verify_code !== verifyCode) {
throw new Error("invalid verify_code");
}
if (payload.player_code !== playerCode) {
throw new Error("invalid player_code");
}
} catch (err) {
res.send({ error: true, message: err.message });
return;
}
for (let [player, entry] of this.players) {
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
if (entry.verifyCode === verifyCode) {
let user = this.master.userManager.users.get(player);
if (!user) {
res.send({ error: true, message: "invalid user" });
return;
}
let token = user.createToken(secret);
res.send({ verified: true, token });
return;
}
res.send({ verified: false });
return;
}
}
res.send({ error: true, message: "invalid player_code" });
}
async fetchPlayerCodeRequestHandler(message) {
let playerCode = await generateCode(this.master.config.get("player_auth.code_length"));
let expires = Date.now() + this.master.config.get("player_auth.code_timeout") * 1000;
this.players.set(message.data.player, { playerCode, verifyCode: null, expires });
return { player_code: playerCode, master_url: this.master.getMasterUrl() };
}
async setVerifyCodeRequestHandler(message) {
let { player, verify_code } = message.data;
if (!this.players.has(player)) {
throw new libErrors.RequestError("invalid player");
}
let entry = this.players.get(player);
if (entry.expires < Date.now()) {
throw new libErrors.RequestError("invalid player");
}
entry.verifyCode = verify_code;
}
}
module.exports = {
MasterPlugin,
// For testing only
_generateCode: generateCode,
};