From 722165c4012b2b8c5db4e0fd33f77922c1acd517 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 16 Dec 2024 20:55:33 -0800 Subject: [PATCH] adding persistent id --- TODO.txt | 4 ++- src/client/GameRunner.ts | 2 +- src/client/PersistentID.ts | 0 src/client/Transport.ts | 4 ++- src/client/{graphics => }/Utils.ts | 26 +++++++++++++++++++ src/client/graphics/layers/ControlPanel.ts | 2 +- src/client/graphics/layers/NameLayer.ts | 2 +- .../graphics/layers/radial/BuildMenu.ts | 2 +- src/core/Schemas.ts | 4 ++- src/core/execution/AttackExecution.ts | 2 +- src/core/execution/TradeShipExecution.ts | 2 +- src/core/game/PlayerImpl.ts | 2 +- src/server/Client.ts | 3 ++- src/server/GameServer.ts | 22 ++++++++-------- src/server/Server.ts | 11 +++++++- 15 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 src/client/PersistentID.ts rename src/client/{graphics => }/Utils.ts (56%) diff --git a/TODO.txt b/TODO.txt index 10996c5fe..6432690b0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -239,9 +239,11 @@ * add capture alert DONE 12/13/2024 * better emojis 🏳️🤦‍♂️🖕☮️🫡😡😈🤡 DONE 12/13/2024 * store ips in bigquery table DONE 12/14/2024 -* better error logging in server +* better error logging in server DONE 12/16/2024 * store and archive player cookies +* make ips less precise * send client logs back to server +* seperate server config from client config * right click brings up player info menu * give naval units health * bug: player names not updating sometimes diff --git a/src/client/GameRunner.ts b/src/client/GameRunner.ts index 8bbcf9985..70d7d5aef 100644 --- a/src/client/GameRunner.ts +++ b/src/client/GameRunner.ts @@ -10,7 +10,7 @@ import { createMiniMap, loadTerrainMap, TerrainMapImpl } from "../core/game/Terr import { and, bfs, dist, generateID, manhattanDist } from "../core/Util"; import { WinCheckExecution } from "../core/execution/WinCheckExecution"; import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport"; -import { createCanvas } from "./graphics/Utils"; +import { createCanvas } from "./Utils"; import { DisplayMessageEvent, MessageType } from "./graphics/layers/EventsDisplay"; import { v4 as uuidv4 } from 'uuid'; import { WorkerClient } from "../core/worker/WorkerClient"; diff --git a/src/client/PersistentID.ts b/src/client/PersistentID.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/client/Transport.ts b/src/client/Transport.ts index d76107ef2..15019b260 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -3,6 +3,7 @@ import { EventBus, GameEvent } from "../core/EventBus" import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig } from "../core/Schemas" import { LocalServer } from "./LocalServer" +import { getPersistentIDFromCookie } from "./Utils" export class SendAllianceRequestIntentEvent implements GameEvent { @@ -194,7 +195,8 @@ export class Transport { type: "join", gameID: this.gameID, clientID: this.clientID, - lastTurn: numTurns + lastTurn: numTurns, + persistentID: getPersistentIDFromCookie(), }) ) ) diff --git a/src/client/graphics/Utils.ts b/src/client/Utils.ts similarity index 56% rename from src/client/graphics/Utils.ts rename to src/client/Utils.ts index 46cbb983e..73e2c6e6c 100644 --- a/src/client/graphics/Utils.ts +++ b/src/client/Utils.ts @@ -34,3 +34,29 @@ export function createCanvas(): HTMLCanvasElement { return canvas } + +// WARNING: DO NOT EXPOSE THIS ID +export function getPersistentIDFromCookie(): string { + const COOKIE_NAME = 'player_persistent_id'; + + // Try to get existing cookie + const cookies = document.cookie.split(';'); + for (let cookie of cookies) { + const [cookieName, cookieValue] = cookie.split('=').map(c => c.trim()); + if (cookieName === COOKIE_NAME) { + return cookieValue; + } + } + + // If no cookie exists, create new ID and set cookie + const newId = crypto.randomUUID(); // Using built-in UUID generator + document.cookie = [ + `${COOKIE_NAME}=${newId}`, + `max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years + 'path=/', + 'SameSite=Strict', + 'Secure' + ].join(';'); + + return newId; +} \ No newline at end of file diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index ebb6c6aef..c7060aa20 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -3,7 +3,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { Layer } from './Layer'; import { Game } from '../../../core/game/Game'; import { ClientID } from '../../../core/Schemas'; -import { renderNumber, renderTroops } from '../Utils'; +import { renderNumber, renderTroops } from '../../Utils'; import { EventBus } from '../../../core/EventBus'; import { UIState } from '../UIState'; import { SendSetTargetTroopRatioEvent } from '../../Transport'; diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 3873a2043..4e3cdaf8f 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -5,7 +5,7 @@ import { Theme } from "../../../core/configuration/Config" import { Layer } from "./Layer" import { placeName } from "../NameBoxCalculator" import { TransformHandler } from "../TransformHandler" -import { renderTroops } from "../Utils" +import { renderTroops } from "../../Utils" import traitorIcon from '../../../../resources/images/TraitorIcon.png'; import allianceIcon from '../../../../resources/images/AllianceIcon.png'; import crownIcon from '../../../../resources/images/CrownIcon.png'; diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index b57f5c343..33baa008e 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -12,7 +12,7 @@ import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import portIcon from '../../../../../resources/images/PortIcon.svg'; import shieldIcon from '../../../../../resources/images/ShieldIconWhite.svg'; import cityIcon from '../../../../../resources/images/CityIconWhite.svg'; -import { renderNumber } from '../../Utils'; +import { renderNumber } from '../../../Utils'; import { ContextMenuEvent } from '../../../InputHandler'; interface BuildItemDisplay { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 0a395ba35..80f939718 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -224,6 +224,7 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({ export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal('join'), + persistentID: z.string(), lastTurn: z.number() // The last turn the client saw. }) @@ -236,7 +237,8 @@ export const PlayerRecordSchema = z.object({ }) export const GameRecordSchema = z.object({ - id: z.string(), + id: z.string(), // WARNING: PII + persistentID: z.string(), // WARNING: PII gameConfig: GameConfigSchema, players: z.array(PlayerRecordSchema), startTimestampMS: z.number(), diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 4c8937fd7..7b5fae3bd 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -3,7 +3,7 @@ import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerTy import { PseudoRandom } from "../PseudoRandom"; import { manhattanDist } from "../Util"; import { MessageType } from "../../client/graphics/layers/EventsDisplay"; -import { renderNumber } from "../../client/graphics/Utils"; +import { renderNumber } from "../../client/Utils"; export class AttackExecution implements Execution { private breakAlliance = false diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 922b0dc53..f8f94d12c 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -1,5 +1,5 @@ import { MessageType } from "../../client/graphics/layers/EventsDisplay"; -import { renderNumber } from "../../client/graphics/Utils"; +import { renderNumber } from "../../client/Utils"; import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, Player, PlayerID, Tile, Unit, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 594c24bed..d6e13a7f0 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -5,7 +5,7 @@ import { CellString, GameImpl } from "./GameImpl"; import { UnitImpl } from "./UnitImpl"; import { TileImpl } from "./TileImpl"; import { MessageType } from "../../client/graphics/layers/EventsDisplay"; -import { renderTroops } from "../../client/graphics/Utils"; +import { renderTroops } from "../../client/Utils"; interface Target { tick: Tick diff --git a/src/server/Client.ts b/src/server/Client.ts index 78cca60be..b1d1eff59 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -7,7 +7,8 @@ export class Client { public lastPing: number constructor( - public readonly id: ClientID, + public readonly clientID: ClientID, + public readonly persistentID: string, public readonly ip: string | null, public readonly ws: WebSocket, ) { } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 08259a3fe..ecc8a85c3 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -50,23 +50,23 @@ export class GameServer { } public addClient(client: Client, lastTurn: number) { - console.log(`${this.id}: adding client ${client.id}`) - slog('client_joined_game', `client ${client.id} (re)joining game ${this.id}`, { - clientID: client.id, + console.log(`${this.id}: adding client ${client.clientID}`) + slog('client_joined_game', `client ${client.clientID} (re)joining game ${this.id}`, { + clientID: client.clientID, clientIP: client.ip, gameID: this.id, isRejoin: lastTurn > 0 }) // Remove stale client if this is a reconnect - const existing = this.activeClients.find(c => c.id == client.id) + const existing = this.activeClients.find(c => c.clientID == client.clientID) if (existing != null) { existing.ws.removeAllListeners('message') } - this.activeClients = this.activeClients.filter(c => c.id != client.id) + this.activeClients = this.activeClients.filter(c => c.clientID != client.clientID) this.activeClients.push(client) client.lastPing = Date.now() - this.allClients.set(client.id, client) + this.allClients.set(client.clientID, client) client.ws.on('message', (message: string) => { const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message)) @@ -83,8 +83,8 @@ export class GameServer { } }) client.ws.on('close', () => { - console.log(`${this.id}: client ${client.id} disconnected`) - this.activeClients = this.activeClients.filter(c => c.id != client.id) + console.log(`${this.id}: client ${client.clientID} disconnected`) + this.activeClients = this.activeClients.filter(c => c.clientID != client.clientID) }) // In case a client joined the game late and missed the start message. @@ -112,7 +112,7 @@ export class GameServer { this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs()); this.activeClients.forEach(c => { - console.log(`${this.id}: sending start message to ${c.id}`) + console.log(`${this.id}: sending start message to ${c.clientID}`) this.sendStartGameMsg(c.ws, 0) }) } @@ -165,7 +165,7 @@ export class GameServer { if (this.allClients.size > 0) { const playerRecords: PlayerRecord[] = Array.from(this.allClients.values()).map(client => ({ ip: client.ip, - clientID: client.id, + clientID: client.clientID, })); const record = CreateGameRecord(this.id, this.gameConfig, playerRecords, this.turns, this._startTime, Date.now()) archive(record) @@ -202,7 +202,7 @@ export class GameServer { const alive = [] for (const client of this.activeClients) { if (now - client.lastPing > 60_000) { - console.log(`${this.id}: no pings from ${client.id}, terminating connection`) + console.log(`${this.id}: no pings from ${client.clientID}, terminating connection`) if (client.ws.readyState === WebSocket.OPEN) { client.ws.close(1000, "no heartbeats received, closing connection"); } diff --git a/src/server/Server.ts b/src/server/Server.ts index 151129c3e..7af735bea 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -110,7 +110,16 @@ wss.on('connection', (ws, req) => { ? forwarded[0] // Get the first IP if it's an array : forwarded || req.socket.remoteAddress; - gm.addClient(new Client(clientMsg.clientID, ip, ws), clientMsg.gameID, clientMsg.lastTurn) + gm.addClient( + new Client( + clientMsg.clientID, + clientMsg.persistentID, + ip, + ws + ), + clientMsg.gameID, + clientMsg.lastTurn + ) } // TODO: send error message })