mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Convert player_auth to entrypoint functions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cfeb4b6a4a
commit
b2705b8788
4 files changed
+225
-226
No files matched your search
@@ -4,7 +4,7 @@ import util from "util";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { Static } from "@sinclair/typebox";
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import type { ControllerPluginContext } from "@clusterio/controller";
|
||||
import { basicType, RequestError } from "@clusterio/lib";
|
||||
|
||||
import { FetchPlayerCodeRequest, PlayerAuthServer, SetVerifyCodeRequest } from "./messages.js";
|
||||
@@ -28,75 +28,88 @@ async function generateCode(length: number): Promise<string> {
|
||||
|
||||
type PlayerCode = { playerCode: string, verifyCode: string | null, expiresMs: number };
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
players!: Map<string, PlayerCode>;
|
||||
export default async function loadControllerPlugin(context: ControllerPluginContext) {
|
||||
const { controller } = context;
|
||||
|
||||
async init() {
|
||||
// Store of validation attempts by players
|
||||
this.players = new Map();
|
||||
// Store of validation attempts by players
|
||||
const players = new Map<string, PlayerCode>();
|
||||
|
||||
// Periodically remove expired entries
|
||||
setInterval(() => {
|
||||
let now = Date.now();
|
||||
for (let [player, entry] of this.players) {
|
||||
if (entry.expiresMs < now) {
|
||||
this.players.delete(player);
|
||||
}
|
||||
// Periodically remove expired entries
|
||||
setInterval(() => {
|
||||
let now = Date.now();
|
||||
for (let [player, entry] of players) {
|
||||
if (entry.expiresMs < now) {
|
||||
players.delete(player);
|
||||
}
|
||||
}, 60e3).unref();
|
||||
}
|
||||
}, 60e3).unref();
|
||||
|
||||
this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => {
|
||||
const servers: Static<typeof PlayerAuthServer.jsonSchema>[] = [];
|
||||
controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => {
|
||||
const servers: Static<typeof PlayerAuthServer.jsonSchema>[] = [];
|
||||
|
||||
for (const instance of this.controller.instances.values()) {
|
||||
const pluginLoaded = instance.config.get("player_auth.load_plugin");
|
||||
const assignedHost = instance.config.get("instance.assigned_host");
|
||||
if (instance.status !== "running" || !pluginLoaded || assignedHost === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const host = this.controller.hosts.get(assignedHost);
|
||||
if (!host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const address = instance.gamePort !== undefined
|
||||
? `${host.publicAddress}:${instance.gamePort}`
|
||||
: host.publicAddress;
|
||||
|
||||
const settings = instance.config.get("factorio.settings");
|
||||
const includeAddress = this.controller.config.get("player_auth.show_connect_address");
|
||||
servers.push({
|
||||
name: settings["name"] as string || "unnamed server",
|
||||
factorioVersion: instance.factorioVersion,
|
||||
address: (includeAddress && host.publicAddress !== "") ? address : undefined,
|
||||
});
|
||||
for (const instance of controller.instances.values()) {
|
||||
const pluginLoaded = instance.config.get("player_auth.load_plugin");
|
||||
const assignedHost = instance.config.get("instance.assigned_host");
|
||||
if (instance.status !== "running" || !pluginLoaded || assignedHost === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res.send(servers);
|
||||
});
|
||||
|
||||
this.controller.app.post(
|
||||
"/api/player_auth/player_code",
|
||||
express.json(),
|
||||
(req: Request, res: Response, next: any) => {
|
||||
this.handlePlayerCode(req, res).catch(next);
|
||||
const host = controller.hosts.get(assignedHost);
|
||||
if (!host) {
|
||||
continue;
|
||||
}
|
||||
);
|
||||
|
||||
this.controller.app.post(
|
||||
"/api/player_auth/verify",
|
||||
express.json(),
|
||||
(req: Request, res: Response, next: any) => {
|
||||
this.handleVerify(req, res).catch(next);
|
||||
}
|
||||
);
|
||||
const address = instance.gamePort !== undefined
|
||||
? `${host.publicAddress}:${instance.gamePort}`
|
||||
: host.publicAddress;
|
||||
|
||||
this.controller.handle(FetchPlayerCodeRequest, this.handleFetchPlayerCodeRequest.bind(this));
|
||||
this.controller.handle(SetVerifyCodeRequest, this.handleSetVerifyCodeRequest.bind(this));
|
||||
}
|
||||
const settings = instance.config.get("factorio.settings");
|
||||
const includeAddress = controller.config.get("player_auth.show_connect_address");
|
||||
servers.push({
|
||||
name: settings["name"] as string || "unnamed server",
|
||||
factorioVersion: instance.factorioVersion,
|
||||
address: (includeAddress && host.publicAddress !== "") ? address : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async handlePlayerCode(req: Request, res: Response) {
|
||||
res.send(servers);
|
||||
});
|
||||
|
||||
controller.app.post(
|
||||
"/api/player_auth/player_code",
|
||||
express.json(),
|
||||
(req: Request, res: Response, next: any) => {
|
||||
handlePlayerCode(req, res).catch(next);
|
||||
}
|
||||
);
|
||||
|
||||
controller.app.post(
|
||||
"/api/player_auth/verify",
|
||||
express.json(),
|
||||
(req: Request, res: Response, next: any) => {
|
||||
handleVerify(req, res).catch(next);
|
||||
}
|
||||
);
|
||||
|
||||
controller.handle(FetchPlayerCodeRequest, async (request: FetchPlayerCodeRequest) => {
|
||||
let playerCode = await generateCode(controller.config.get("player_auth.code_length"));
|
||||
let expiresMs = Date.now() + controller.config.get("player_auth.code_timeout") * 1000;
|
||||
players.set(request.player, { playerCode, verifyCode: null, expiresMs });
|
||||
return { playerCode, controllerUrl: controller.getControllerUrl() };
|
||||
});
|
||||
|
||||
controller.handle(SetVerifyCodeRequest, async (request: SetVerifyCodeRequest) => {
|
||||
let { player, verifyCode } = request;
|
||||
|
||||
let entry = players.get(player);
|
||||
if (!entry || entry.expiresMs < Date.now()) {
|
||||
throw new RequestError("invalid player");
|
||||
}
|
||||
|
||||
entry.verifyCode = verifyCode;
|
||||
});
|
||||
|
||||
async function handlePlayerCode(req: Request, res: Response) {
|
||||
if (basicType(req.body) !== "object") {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
@@ -108,9 +121,9 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let entry of this.players.values()) {
|
||||
for (let entry of players.values()) {
|
||||
if (entry.playerCode === playerCode && entry.expiresMs > Date.now()) {
|
||||
let verifyCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let verifyCode = await generateCode(controller.config.get("player_auth.code_length"));
|
||||
let verifyToken = jwt.sign(
|
||||
{
|
||||
aud: "player_auth.verify_code",
|
||||
@@ -118,7 +131,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
verify_code: verifyCode,
|
||||
player_code: playerCode,
|
||||
},
|
||||
this.controller.authSecret
|
||||
controller.authSecret
|
||||
);
|
||||
|
||||
res.send({ verify_code: verifyCode, verify_token: verifyToken });
|
||||
@@ -129,7 +142,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
res.send({ error: true, message: "invalid player_code" });
|
||||
}
|
||||
|
||||
async handleVerify(req: Request, res: Response) {
|
||||
async function handleVerify(req: Request, res: Response) {
|
||||
if (basicType(req.body) !== "object") {
|
||||
res.sendStatus(400);
|
||||
return;
|
||||
@@ -155,7 +168,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
|
||||
try {
|
||||
let payload = jwt.verify(
|
||||
verifyToken, this.controller.authSecret, { audience: "player_auth.verify_code" }
|
||||
verifyToken, controller.authSecret, { audience: "player_auth.verify_code" }
|
||||
) as jwt.JwtPayload;
|
||||
|
||||
if (payload.verify_code !== verifyCode) {
|
||||
@@ -171,16 +184,16 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let [player, entry] of this.players) {
|
||||
for (let [player, entry] of players) {
|
||||
if (entry.playerCode === playerCode && entry.expiresMs > Date.now()) {
|
||||
if (entry.verifyCode === verifyCode) {
|
||||
let user = this.controller.users.getByName(player);
|
||||
let user = controller.users.getByName(player);
|
||||
if (!user) {
|
||||
res.send({ error: true, message: "invalid user" });
|
||||
return;
|
||||
}
|
||||
|
||||
let token = this.controller.users.signUserToken(user);
|
||||
let token = controller.users.signUserToken(user);
|
||||
res.send({ verified: true, token });
|
||||
return;
|
||||
|
||||
@@ -193,24 +206,6 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
|
||||
res.send({ error: true, message: "invalid player_code" });
|
||||
}
|
||||
|
||||
async handleFetchPlayerCodeRequest(request: FetchPlayerCodeRequest) {
|
||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let expiresMs = Date.now() + this.controller.config.get("player_auth.code_timeout") * 1000;
|
||||
this.players.set(request.player, { playerCode, verifyCode: null, expiresMs });
|
||||
return { playerCode, controllerUrl: this.controller.getControllerUrl() };
|
||||
}
|
||||
|
||||
async handleSetVerifyCodeRequest(request: SetVerifyCodeRequest) {
|
||||
let { player, verifyCode } = request;
|
||||
|
||||
let entry = this.players.get(player);
|
||||
if (!entry || entry.expiresMs < Date.now()) {
|
||||
throw new RequestError("invalid player");
|
||||
}
|
||||
|
||||
entry.verifyCode = verifyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// For testing only
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import type { InstancePluginContext } from "@clusterio/host";
|
||||
|
||||
import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages.js";
|
||||
|
||||
@@ -11,42 +11,46 @@ type IpcPlayerAuth = {
|
||||
verify_code: string,
|
||||
}
|
||||
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
async init() {
|
||||
this.instance.server.on(
|
||||
"ipc-player_auth",
|
||||
(ipcPlayerAuth: IpcPlayerAuth) => this.handleEvent(ipcPlayerAuth).catch(
|
||||
err => this.logger.error(`Error handling event:\n${err.stack}`)
|
||||
)
|
||||
);
|
||||
export default async function loadInstancePlugin(context: InstancePluginContext) {
|
||||
const { instance, host, logger, plugin } = context;
|
||||
|
||||
async function sendRcon(command: string) {
|
||||
await instance.sendRcon(command, false, plugin.name);
|
||||
}
|
||||
|
||||
async handleEvent(event: IpcPlayerAuth) {
|
||||
async function handleEvent(event: IpcPlayerAuth) {
|
||||
if (event.type === "open_dialog") {
|
||||
if (!this.host.connector.connected) {
|
||||
await this.sendRcon(`/web-login error ${event.player} login is temporarily unavailable`);
|
||||
if (!host.connector.connected) {
|
||||
await sendRcon(`/web-login error ${event.player} login is temporarily unavailable`);
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await this.instance.sendTo("controller", new FetchPlayerCodeRequest(event.player));
|
||||
response = await instance.sendTo("controller", new FetchPlayerCodeRequest(event.player));
|
||||
} catch (err: any) {
|
||||
await this.sendRcon(`/web-login error ${event.player} ${err.message}`);
|
||||
await sendRcon(`/web-login error ${event.player} ${err.message}`);
|
||||
return;
|
||||
}
|
||||
await this.sendRcon(`/web-login open ${event.player} ${response.controllerUrl} ${response.playerCode}`);
|
||||
await sendRcon(`/web-login open ${event.player} ${response.controllerUrl} ${response.playerCode}`);
|
||||
|
||||
} else if (event.type === "set_verify_code") {
|
||||
try {
|
||||
await this.instance.sendTo("controller", new SetVerifyCodeRequest(event.player, event.verify_code));
|
||||
await instance.sendTo("controller", new SetVerifyCodeRequest(event.player, event.verify_code));
|
||||
|
||||
} catch (err: any) {
|
||||
await this.sendRcon(`/web-login error ${event.player} ${err.message}`);
|
||||
await sendRcon(`/web-login error ${event.player} ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendRcon(`/web-login code_set ${event.player}`);
|
||||
await sendRcon(`/web-login code_set ${event.player}`);
|
||||
}
|
||||
}
|
||||
|
||||
instance.server.on(
|
||||
"ipc-player_auth",
|
||||
(ipcPlayerAuth: IpcPlayerAuth) => handleEvent(ipcPlayerAuth).catch(
|
||||
err => logger.error(`Error handling event:\n${err.stack}`)
|
||||
)
|
||||
);
|
||||
}
|
||||
+114
-113
@@ -4,7 +4,7 @@ import jwt from "jsonwebtoken";
|
||||
import * as mock from "../../../test/mock.js";
|
||||
|
||||
import * as controller from "../dist/node/controller.js";
|
||||
import * as instance from "../dist/node/instance.js";
|
||||
import * as instancePlugin from "../dist/node/instance.js";
|
||||
import { plugin as info } from "../dist/node/index.js";
|
||||
import { FetchPlayerCodeRequest, SetVerifyCodeRequest, PlayerAuthServer } from "../dist/node/messages.js";
|
||||
import { testRoundTripJsonSerialisable, testMatrix } from "../../../test/common.js";
|
||||
@@ -81,27 +81,54 @@ describe("player_auth", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ControllerPlugin", function() {
|
||||
let controllerPlugin;
|
||||
describe("entrypoint", function() {
|
||||
let mockController;
|
||||
let controllerUrl;
|
||||
let endpoint;
|
||||
let fetchPlayerCode;
|
||||
let setVerifyCode;
|
||||
|
||||
// Adds a pending code for player through the request handlers
|
||||
async function addPlayer(player, { timeout = 1, verifyCode } = {}) {
|
||||
mockController.mockConfigEntries.set("player_auth.code_timeout", timeout);
|
||||
try {
|
||||
const { playerCode } = await fetchPlayerCode(new FetchPlayerCodeRequest(player));
|
||||
if (verifyCode) {
|
||||
await setVerifyCode(new SetVerifyCodeRequest(player, verifyCode));
|
||||
}
|
||||
return playerCode;
|
||||
} finally {
|
||||
mockController.mockConfigEntries.set("player_auth.code_timeout", 1);
|
||||
}
|
||||
}
|
||||
|
||||
function signVerifyToken(playerCode, verifyCode) {
|
||||
return jwt.sign({
|
||||
aud: "player_auth.verify_code",
|
||||
player_code: playerCode,
|
||||
verify_code: verifyCode,
|
||||
}, Buffer.from("TestSecretDoNotUse", "base64"));
|
||||
}
|
||||
|
||||
before(async function() {
|
||||
controllerPlugin = await mock.createControllerPlugin(controller.ControllerPlugin, info);
|
||||
controllerPlugin.controller.mockConfigEntries.set("player_auth.code_length", 10);
|
||||
controllerPlugin.controller.mockConfigEntries.set("player_auth.code_timeout", 1);
|
||||
controllerPlugin.controller.mockConfigEntries.set("player_auth.show_connect_address", true);
|
||||
controllerUrl = await controllerPlugin.controller.startServer();
|
||||
mockController = await mock.loadControllerPlugin(controller.default, info);
|
||||
mockController.mockConfigEntries.set("player_auth.code_length", 10);
|
||||
mockController.mockConfigEntries.set("player_auth.code_timeout", 1);
|
||||
mockController.mockConfigEntries.set("player_auth.show_connect_address", true);
|
||||
fetchPlayerCode = mock.getHandler(mockController, FetchPlayerCodeRequest);
|
||||
setVerifyCode = mock.getHandler(mockController, SetVerifyCodeRequest);
|
||||
controllerUrl = await mockController.startServer();
|
||||
});
|
||||
after(async function() {
|
||||
if (controllerPlugin) {
|
||||
await controllerPlugin.controller.stopServer();
|
||||
if (mockController) {
|
||||
await mockController.stopServer();
|
||||
}
|
||||
});
|
||||
|
||||
describe("/api/player_auth/servers", function() {
|
||||
it("should return a list of running servers with player_auth loaded", async function() {
|
||||
function addInstance(id, status, load, name, version, gamePort, host=1) {
|
||||
controllerPlugin.controller.instances.records.set(id, {
|
||||
mockController.instances.records.set(id, {
|
||||
config: {
|
||||
get(field) {
|
||||
if (field === "player_auth.load_plugin") {
|
||||
@@ -135,7 +162,7 @@ describe("player_auth", function() {
|
||||
addInstance(8, "running", true, "invalid host", "1.1.8", 34197, 2);
|
||||
addInstance(9, "running", true, "no version", undefined, 34197);
|
||||
|
||||
controllerPlugin.controller.hosts.set(1, {
|
||||
mockController.hosts.set(1, {
|
||||
publicAddress: "127.0.0.1",
|
||||
});
|
||||
|
||||
@@ -170,15 +197,13 @@ describe("player_auth", function() {
|
||||
}
|
||||
});
|
||||
it("should return invalid player_code if the code is expired", async function() {
|
||||
let expiresMs = Date.now() - 1000;
|
||||
controllerPlugin.players.set("expired", { playerCode: "expired", verifyCode: null, expiresMs });
|
||||
const result = await postJSON(endpoint, { player_code: "expired" });
|
||||
const playerCode = await addPlayer("expired", { timeout: -1 });
|
||||
const result = await postJSON(endpoint, { player_code: playerCode });
|
||||
assert.deepEqual(await result.json(), { error: true, message: "invalid player_code" });
|
||||
});
|
||||
it("should return a verify code and token if code is valid", async function() {
|
||||
let expiresMs = Date.now() + 1000;
|
||||
controllerPlugin.players.set("valid", { playerCode: "valid", verifyCode: null, expiresMs });
|
||||
const result = await postJSON(endpoint, { player_code: "valid" });
|
||||
const playerCode = await addPlayer("valid");
|
||||
const result = await postJSON(endpoint, { player_code: playerCode });
|
||||
const body = await result.json();
|
||||
assert.equal(typeof body.verify_code, "string");
|
||||
assert.equal(typeof body.verify_token, "string");
|
||||
@@ -232,61 +257,38 @@ describe("player_auth", function() {
|
||||
await verify({ aud: "not_player_auth" });
|
||||
});
|
||||
it("should return invalid player_code if the code is expired", async function() {
|
||||
let expiresMs = Date.now() - 1000;
|
||||
controllerPlugin.players.set("expired", { playerCode: "expired", verifyCode: "verify", expiresMs });
|
||||
const playerCode = await addPlayer("expired", { timeout: -1 });
|
||||
const result = await postJSON(endpoint, {
|
||||
player_code: "expired",
|
||||
player_code: playerCode,
|
||||
verify_code: "verify",
|
||||
verify_token: jwt.sign({
|
||||
aud: "player_auth.verify_code",
|
||||
player_code: "expired",
|
||||
verify_code: "verify",
|
||||
}, Buffer.from("TestSecretDoNotUse", "base64")),
|
||||
verify_token: signVerifyToken(playerCode, "verify"),
|
||||
});
|
||||
assert.deepEqual(await result.json(), { error: true, message: "invalid player_code" });
|
||||
});
|
||||
it("should return verified false if verify code has not yet been set", async function() {
|
||||
let expiresMs = Date.now() + 1000;
|
||||
controllerPlugin.players.set(
|
||||
"unverified",
|
||||
{ playerCode: "unverified", verifyCode: null, expiresMs },
|
||||
);
|
||||
const playerCode = await addPlayer("unverified");
|
||||
const result = await postJSON(endpoint, {
|
||||
player_code: "unverified",
|
||||
player_code: playerCode,
|
||||
verify_code: "verify",
|
||||
verify_token: jwt.sign({
|
||||
aud: "player_auth.verify_code",
|
||||
player_code: "unverified",
|
||||
verify_code: "verify",
|
||||
}, Buffer.from("TestSecretDoNotUse", "base64")),
|
||||
verify_token: signVerifyToken(playerCode, "verify"),
|
||||
});
|
||||
assert.deepEqual(await result.json(), { verified: false });
|
||||
});
|
||||
it("should return error if user is missing", async function() {
|
||||
let expiresMs = Date.now() + 1000;
|
||||
controllerPlugin.players.set("missing", { playerCode: "missing", verifyCode: "verify", expiresMs });
|
||||
const playerCode = await addPlayer("missing", { verifyCode: "verify" });
|
||||
const result = await postJSON(endpoint, {
|
||||
player_code: "missing",
|
||||
player_code: playerCode,
|
||||
verify_code: "verify",
|
||||
verify_token: jwt.sign({
|
||||
aud: "player_auth.verify_code",
|
||||
player_code: "missing",
|
||||
verify_code: "verify",
|
||||
}, Buffer.from("TestSecretDoNotUse", "base64")),
|
||||
verify_token: signVerifyToken(playerCode, "verify"),
|
||||
});
|
||||
assert.deepEqual(await result.json(), { error: true, message: "invalid user" });
|
||||
});
|
||||
it("should return verified true with token if valid verification", async function() {
|
||||
let expiresMs = Date.now() + 1000;
|
||||
controllerPlugin.players.set("player", { playerCode: "player", verifyCode: "verify", expiresMs });
|
||||
const playerCode = await addPlayer("player", { verifyCode: "verify" });
|
||||
const result = await postJSON(endpoint, {
|
||||
player_code: "player",
|
||||
player_code: playerCode,
|
||||
verify_code: "verify",
|
||||
verify_token: jwt.sign({
|
||||
aud: "player_auth.verify_code",
|
||||
player_code: "player",
|
||||
verify_code: "verify",
|
||||
}, Buffer.from("TestSecretDoNotUse", "base64")),
|
||||
verify_token: signVerifyToken(playerCode, "verify"),
|
||||
});
|
||||
const body = await result.json();
|
||||
assert.equal(body.verified, true);
|
||||
@@ -294,35 +296,37 @@ describe("player_auth", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".handleFetchPlayerCodeRequest()", function() {
|
||||
describe("FetchPlayerCodeRequest handler", function() {
|
||||
it("should return a code", async function() {
|
||||
let result = await controllerPlugin.handleFetchPlayerCodeRequest(
|
||||
new FetchPlayerCodeRequest("test")
|
||||
);
|
||||
let result = await fetchPlayerCode(new FetchPlayerCodeRequest("test"));
|
||||
assert(typeof result.playerCode === "string", "no code returned");
|
||||
assert(result.playerCode.length === 10, "incorrect code length returned");
|
||||
let expiresMs = controllerPlugin.players.get("test").expiresMs;
|
||||
let msFromExpected = Math.abs(expiresMs - Date.now() - 1000);
|
||||
assert(msFromExpected < 100, `expiry time expected outside window (${msFromExpected}ms)`);
|
||||
assert.equal(result.controllerUrl, "http://controller.example/");
|
||||
});
|
||||
it("should expire the code after the timeout", async function() {
|
||||
const start = Date.now();
|
||||
const playerCode = await addPlayer("test", { timeout: 10 });
|
||||
const end = Date.now();
|
||||
const result = await postJSON(
|
||||
`${controllerUrl}/api/player_auth/player_code`, { player_code: playerCode }
|
||||
);
|
||||
const { exp } = jwt.decode((await result.json()).verify_token);
|
||||
assert(exp >= Math.floor((start + 10e3) / 1000), "expiry before expected window");
|
||||
assert(exp <= Math.floor((end + 10e3) / 1000), "expiry after expected window");
|
||||
});
|
||||
});
|
||||
|
||||
describe(".handleSetVerifyCodeRequest()", function() {
|
||||
describe("SetVerifyCodeRequest handler", function() {
|
||||
it("should throw if player does not exist", async function() {
|
||||
await assert.rejects(
|
||||
controllerPlugin.handleSetVerifyCodeRequest(
|
||||
new SetVerifyCodeRequest("invalid", "invalid")
|
||||
),
|
||||
setVerifyCode(new SetVerifyCodeRequest("invalid", "invalid")),
|
||||
new lib.RequestError("invalid player")
|
||||
);
|
||||
});
|
||||
it("should throw if player code has expired", async function() {
|
||||
let expiresMs = Date.now() - 1000;
|
||||
controllerPlugin.players.set("expired", { playerCode: "expired", verifyCode: null, expiresMs });
|
||||
await addPlayer("expired", { timeout: -1 });
|
||||
await assert.rejects(
|
||||
controllerPlugin.handleSetVerifyCodeRequest(
|
||||
new SetVerifyCodeRequest("expired", "expired")
|
||||
),
|
||||
setVerifyCode(new SetVerifyCodeRequest("expired", "expired")),
|
||||
new lib.RequestError("invalid player")
|
||||
);
|
||||
});
|
||||
@@ -330,10 +334,7 @@ describe("player_auth", function() {
|
||||
|
||||
describe("integration", function() {
|
||||
it("should verify a full login flow", async function() {
|
||||
let app = controllerPlugin.controller.app;
|
||||
let { playerCode } = await controllerPlugin.handleFetchPlayerCodeRequest(
|
||||
new FetchPlayerCodeRequest("test")
|
||||
);
|
||||
let { playerCode } = await fetchPlayerCode(new FetchPlayerCodeRequest("test"));
|
||||
|
||||
const playerCodeResult = await postJSON(
|
||||
`${controllerUrl}/api/player_auth/player_code`,
|
||||
@@ -341,12 +342,10 @@ describe("player_auth", function() {
|
||||
);
|
||||
|
||||
let { verify_code, verify_token } = await playerCodeResult.json();
|
||||
await controllerPlugin.handleSetVerifyCodeRequest(
|
||||
new SetVerifyCodeRequest("test", verify_code)
|
||||
);
|
||||
await setVerifyCode(new SetVerifyCodeRequest("test", verify_code));
|
||||
|
||||
const user = controllerPlugin.controller.users.getByName("test");
|
||||
const token = controllerPlugin.controller.users.signUserToken(user);
|
||||
const user = mockController.users.getByName("test");
|
||||
const token = mockController.users.signUserToken(user);
|
||||
const verifyResult = await postJSON(
|
||||
`${controllerUrl}/api/player_auth/verify`,
|
||||
{ player_code: playerCode, verify_code, verify_token }
|
||||
@@ -359,76 +358,78 @@ describe("player_auth", function() {
|
||||
});
|
||||
|
||||
describe("instance.js", function() {
|
||||
describe("class InstancePlugin", function() {
|
||||
let instancePlugin;
|
||||
describe("entrypoint", function() {
|
||||
let instance;
|
||||
let host;
|
||||
before(async function() {
|
||||
instancePlugin = await mock.createInstancePlugin(instance.InstancePlugin, info);
|
||||
try {
|
||||
lib.registerPluginMessages([info]);
|
||||
} catch (err) {
|
||||
// Already registered by the full test suite
|
||||
}
|
||||
({ instance, host } = await mock.loadInstancePlugin(instancePlugin.default, info));
|
||||
});
|
||||
|
||||
describe(".handleEvent()", async function() {
|
||||
describe("open_dialog", async function() {
|
||||
// Emits an ipc-player_auth event and returns the first RCON command it sent
|
||||
async function emitIpc(event) {
|
||||
instance.server.reset();
|
||||
instance.server.emit("ipc-player_auth", event);
|
||||
while (!instance.server.rconCommands.length) {
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
}
|
||||
return instance.server.rconCommands[0];
|
||||
}
|
||||
|
||||
describe("ipc-player_auth", function() {
|
||||
describe("open_dialog", function() {
|
||||
it("should call /web-login error if not connected to controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.host.connector.connected = false;
|
||||
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
instancePlugin.host.connector.connected = true;
|
||||
host.connector.connected = false;
|
||||
let command = await emitIpc({ type: "open_dialog", player: "test" });
|
||||
host.connector.connected = true;
|
||||
assert.equal(command, "/web-login error test login is temporarily unavailable");
|
||||
});
|
||||
it("should call /web-login error after error from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
instance.connector.once("send", message => {
|
||||
instance.connector.emit("message",
|
||||
new lib.MessageResponseError(1, message.dst, message.src,
|
||||
new lib.ResponseError("controller error")
|
||||
)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
let command = await emitIpc({ type: "open_dialog", player: "test" });
|
||||
assert.equal(command, "/web-login error test controller error");
|
||||
});
|
||||
it("should call /web-login open after a valid response from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
instance.connector.once("send", message => {
|
||||
instance.connector.emit("message",
|
||||
new lib.MessageResponse(1, message.dst, message.src,
|
||||
new FetchPlayerCodeRequest.Response("code", "controller-url")
|
||||
)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
let command = await emitIpc({ type: "open_dialog", player: "test" });
|
||||
assert.equal(command, "/web-login open test controller-url code");
|
||||
});
|
||||
});
|
||||
describe("open_dialog", async function() {
|
||||
describe("set_verify_code", function() {
|
||||
it("should call /web-login code_set after a valid response from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
instance.connector.once("send", message => {
|
||||
instance.connector.emit("message",
|
||||
new lib.MessageResponse(1, message.dst, message.src)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent(
|
||||
{ type: "set_verify_code", player: "test", verify_code: "verify" }
|
||||
);
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
let command = await emitIpc({ type: "set_verify_code", player: "test", verify_code: "verify" });
|
||||
assert.equal(command, "/web-login code_set test");
|
||||
});
|
||||
it("should call /web-login error after error from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
instance.connector.once("send", message => {
|
||||
instance.connector.emit("message",
|
||||
new lib.MessageResponseError(1, message.dst, message.src,
|
||||
new lib.ResponseError("controller error")
|
||||
)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent(
|
||||
{ type: "set_verify_code", player: "test", verify_code: "verify" }
|
||||
);
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
let command = await emitIpc({ type: "set_verify_code", player: "test", verify_code: "verify" });
|
||||
assert.equal(command, "/web-login error test controller error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { BaseWebPlugin, notifyErrorHandler } from "@clusterio/web_ui";
|
||||
import { notifyErrorHandler, type WebPluginContext } from "@clusterio/web_ui";
|
||||
import { Alert, Button, Form, Input, Modal, Space, Spin, Typography } from "antd";
|
||||
|
||||
import { PlayerAuthServer } from "../messages.js";
|
||||
@@ -251,13 +251,12 @@ function LoginForm(props: LoginFormProps) {
|
||||
}
|
||||
|
||||
|
||||
export class WebPlugin extends BaseWebPlugin {
|
||||
async init() {
|
||||
this.logger.info("Player Auth init");
|
||||
this.loginForms = [{
|
||||
name: "player_auth.factorio",
|
||||
title: "Factorio",
|
||||
Component: LoginForm,
|
||||
}];
|
||||
}
|
||||
export default async function loadWebPlugin(context: WebPluginContext) {
|
||||
const { control, logger, plugin } = context;
|
||||
logger.info("Player Auth init");
|
||||
control.hooks.loginForms.attach(plugin.name, () => [{
|
||||
name: "player_auth.factorio",
|
||||
title: "Factorio",
|
||||
Component: LoginForm,
|
||||
}]);
|
||||
}
|
||||
Reference in new issue
Block a user