From b558bd722db2c15b51679cfed3af417824c6f28d Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Sun, 18 May 2025 00:03:05 -0400 Subject: [PATCH] Prevent multiple clients from using the same account (#706) ## Description: This change blocks a persistent ID from being reused by more than one client in the same game. This setting is only enabled in staging and prod. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- src/client/SinglePlayerModal.ts | 12 ++++-------- src/server/GameServer.ts | 27 +++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index b1030c21f..c93c1a363 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -31,7 +31,6 @@ export class SinglePlayerModal extends LitElement { @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNPCs: boolean = false; - @state() private disableNukes: boolean = false; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @state() private infiniteTroops: boolean = false; @@ -390,10 +389,6 @@ export class SinglePlayerModal extends LitElement { this.disableNPCs = Boolean((e.target as HTMLInputElement).checked); } - private handleDisableNukesChange(e: Event) { - this.disableNukes = Boolean((e.target as HTMLInputElement).checked); - } - private handleGameModeSelection(value: GameMode) { this.gameMode = value; } @@ -456,15 +451,16 @@ export class SinglePlayerModal extends LitElement { playerTeams: this.teamCount, difficulty: this.selectedDifficulty, disableNPCs: this.disableNPCs, - disableNukes: this.disableNukes, bots: this.bots, infiniteGold: this.infiniteGold, infiniteTroops: this.infiniteTroops, instantBuild: this.instantBuild, - disabledUnits: this.disabledUnits, + disabledUnits: this.disabledUnits + .map((u) => Object.values(UnitType).find((ut) => ut === u)) + .filter((ut): ut is UnitType => ut !== undefined), }, }, - } as JoinLobbyEvent, + } satisfies JoinLobbyEvent, bubbles: true, composed: true, }), diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index b1f82c83d..b747bebda 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -20,7 +20,7 @@ import { Turn, } from "../core/Schemas"; import { createGameRecord } from "../core/Util"; -import { ServerConfig } from "../core/configuration/Config"; +import { GameEnv, ServerConfig } from "../core/configuration/Config"; import { GameType } from "../core/game/Game"; import { archive } from "./Archive"; import { Client } from "./Client"; @@ -132,6 +132,25 @@ export class GameServer { return; } + if (this.config.env() === GameEnv.Prod) { + // Prevent multiple clients from using the same account in prod + const conflicting = this.activeClients.find( + (c) => + c.persistentID === client.persistentID && + c.clientID !== client.clientID, + ); + if (conflicting !== undefined) { + this.log.error("client ids do not match", { + clientID: client.clientID, + clientIP: ipAnonymize(client.ip), + clientPersistentID: client.persistentID, + existingIP: ipAnonymize(conflicting.ip), + existingPersistentID: conflicting.persistentID, + }); + return; + } + } + // Remove stale client if this is a reconnect const existing = this.activeClients.find( (c) => c.clientID === client.clientID, @@ -148,10 +167,10 @@ export class GameServer { return; } existing.ws.removeAllListeners("message"); - this.activeClients = this.activeClients.filter( - (c) => c.clientID !== client.clientID, - ); + this.activeClients = this.activeClients.filter((c) => c !== existing); } + + // Client connection accepted this.activeClients.push(client); client.lastPing = Date.now();