diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 941e07a46..374cc8e86 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -82,7 +82,7 @@ export function joinLobby( if (message.type == "start") { // Trigger prestart for singleplayer games onPrestart(); - consolex.log(`lobby: game started: ${JSON.stringify(message)}`); + consolex.log(`lobby: game started: ${JSON.stringify(message, null, 2)}`); onJoin(); // For multiplayer games, GameStartInfo is not known until game starts. lobbyConfig.gameStartInfo = message.gameStartInfo; diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 49830cfef..60465df09 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -57,6 +57,8 @@ export class GameServer { private _hasPrestarted = false; + private kickedClients: Set = new Set(); + constructor( public readonly id: string, readonly log_: Logger, @@ -103,6 +105,12 @@ export class GameServer { } public addClient(client: Client, lastTurn: number) { + if (this.kickedClients.has(client.clientID)) { + this.log.warn(`cannot add client, already kicked`, { + clientID: client.clientID, + }); + return; + } this.log.info("client (re)joining game", { clientID: client.clientID, persistentID: client.persistentID, @@ -492,6 +500,31 @@ export class GameServer { return this.gameConfig.gameType == GameType.Public; } + public kickClient(clientID: ClientID): void { + if (this.kickedClients.has(clientID)) { + this.log.warn(`cannot kick client, already kicked`, { + clientID, + }); + return; + } + const client = this.activeClients.find((c) => c.clientID === clientID); + if (client) { + this.log.info("Kicking client from game", { + clientID: client.clientID, + persistentID: client.persistentID, + }); + client.ws.close(1000, "Kicked from game"); + this.activeClients = this.activeClients.filter( + (c) => c.clientID !== clientID, + ); + this.kickedClients.add(clientID); + } else { + this.log.warn(`cannot kick client, not found in game`, { + clientID, + }); + } + } + private handleSynchronization() { if (this.activeClients.length <= 1) { return; diff --git a/src/server/Master.ts b/src/server/Master.ts index bcab69fff..ae49de34d 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -160,6 +160,39 @@ app.get( }), ); +app.post( + "/api/kick_player/:gameID/:clientID", + gatekeeper.httpHandler(LimiterType.Post, async (req, res) => { + if (req.headers[config.adminHeader()] !== config.adminToken()) { + res.status(401).send("Unauthorized"); + return; + } + + const { gameID, clientID } = req.params; + + try { + const response = await fetch( + `http://localhost:${config.workerPort(gameID)}/api/kick_player/${gameID}/${clientID}`, + { + method: "POST", + headers: { + [config.adminHeader()]: config.adminToken(), + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to kick player: ${response.statusText}`); + } + + res.status(200).send("Player kicked successfully"); + } catch (error) { + log.error(`Error kicking player from game ${gameID}:`, error); + res.status(500).send("Failed to kick player"); + } + }), +); + async function fetchLobbies(): Promise { const fetchPromises = []; diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 0ce554f16..60ce1c8da 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -250,6 +250,27 @@ export function startWorker() { }), ); + app.post( + "/api/kick_player/:gameID/:clientID", + gatekeeper.httpHandler(LimiterType.Post, async (req, res) => { + if (req.headers[config.adminHeader()] !== config.adminToken()) { + res.status(401).send("Unauthorized"); + return; + } + + const { gameID, clientID } = req.params; + + const game = gm.game(gameID); + if (!game) { + res.status(404).send("Game not found"); + return; + } + + game.kickClient(clientID); + res.status(200).send("Player kicked successfully"); + }), + ); + // WebSocket handling wss.on("connection", (ws: WebSocket, req) => { ws.on( diff --git a/webpack.config.js b/webpack.config.js index 5bb60050c..4b4096141 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -228,6 +228,7 @@ export default async (env, argv) => { "/api/archive_singleplayer_game", "/api/auth/callback", "/api/auth/discord", + "/api/kick_player", ], target: "http://localhost:3000", secure: false,