From 1417808c14f6746afd9ef655aa8abdea5c58c425 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 17 Dec 2024 14:29:17 -0800 Subject: [PATCH] store user's persistent id in bigquery --- src/client/GameRunner.ts | 27 ++++++--------- src/client/LocalServer.ts | 27 +++++++++++---- src/client/Main.ts | 32 +++++++++++++++++- src/client/Transport.ts | 70 +++++++++++++++++++-------------------- src/client/Utils.ts | 25 -------------- src/core/Schemas.ts | 12 ++++--- src/server/Archive.ts | 9 +++-- src/server/Client.ts | 1 + src/server/GameServer.ts | 14 ++++++-- src/server/Server.ts | 1 + 10 files changed, 126 insertions(+), 92 deletions(-) diff --git a/src/client/GameRunner.ts b/src/client/GameRunner.ts index 70d7d5aef..af7dde298 100644 --- a/src/client/GameRunner.ts +++ b/src/client/GameRunner.ts @@ -12,21 +12,20 @@ import { WinCheckExecution } from "../core/execution/WinCheckExecution"; import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport"; import { createCanvas } from "./Utils"; import { DisplayMessageEvent, MessageType } from "./graphics/layers/EventsDisplay"; -import { v4 as uuidv4 } from 'uuid'; import { WorkerClient } from "../core/worker/WorkerClient"; - export interface LobbyConfig { - gameType: GameType playerName: () => string - gameID: GameID + clientID: ClientID, + playerID: PlayerID, + persistentID: string, + gameType: GameType + gameID: GameID, map: GameMap | null difficulty: Difficulty | null } export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void { - const clientID = generateID() - const playerID = generateID() const eventBus = new EventBus() const config = getConfig() @@ -40,14 +39,10 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v } const transport = new Transport( - lobbyConfig.gameType == GameType.Singleplayer, + lobbyConfig, gameConfig, eventBus, - lobbyConfig.gameID, - clientID, - playerID, config, - lobbyConfig.playerName ) const onconnect = () => { @@ -58,7 +53,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v if (message.type == "start") { console.log('lobby: game started') onjoin() - createClientGame(message.config, eventBus, transport, lobbyConfig.gameID, clientID).then(r => r.start()) + createClientGame(lobbyConfig, message.config, eventBus, transport).then(r => r.start()) }; } transport.connect(onconnect, onmessage) @@ -69,7 +64,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v } -export async function createClientGame(gameConfig: GameConfig, eventBus: EventBus, transport: Transport, gameID: GameID, clientID: ClientID): Promise { +export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise { const config = getConfig() const terrainMap = await loadTerrainMap(gameConfig.gameMap); @@ -82,18 +77,18 @@ export async function createClientGame(gameConfig: GameConfig, eventBus: EventBu await worker.initialize() console.log('inited path finder') const canvas = createCanvas() - let gameRenderer = createRenderer(canvas, game, eventBus, clientID) + let gameRenderer = createRenderer(canvas, game, eventBus, lobbyConfig.clientID) console.log(`creating private game got difficulty: ${gameConfig.difficulty}`) return new GameRunner( - clientID, + lobbyConfig.clientID, eventBus, game, gameRenderer, new InputHandler(canvas, eventBus), - new Executor(game, gameID, worker), + new Executor(game, lobbyConfig.gameID, worker), transport, ) } diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 1fc2a678b..8d97dfa83 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -1,6 +1,8 @@ import { Config } from "../core/configuration/Config"; import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas"; import { CreateGameRecord, generateID } from "../core/Util"; +import { LobbyConfig } from "./GameRunner"; +import { getPersistentIDFromCookie } from "./Main"; export class LocalServer { @@ -11,10 +13,14 @@ export class LocalServer { private endTurnIntervalID - private gameID: GameID - constructor(private clientID: ClientID, private config: Config, private gameConfig: GameConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) { - this.gameID = generateID() + constructor( + private config: Config, + private gameConfig: GameConfig, + private lobbyConfig: LobbyConfig, + private clientConnect: () => void, + private clientMessage: (message: ServerMessage) => void + ) { } start() { @@ -38,7 +44,7 @@ export class LocalServer { private endTurn() { const pastTurn: Turn = { turnNumber: this.turns.length, - gameID: this.gameID, + gameID: this.lobbyConfig.gameID, intents: this.intents } this.turns.push(pastTurn) @@ -54,9 +60,18 @@ export class LocalServer { clearInterval(this.endTurnIntervalID) const players: PlayerRecord[] = [{ ip: null, - clientID: this.clientID + persistentID: getPersistentIDFromCookie(), + username: this.lobbyConfig.playerName(), + clientID: this.lobbyConfig.clientID }] - const record = CreateGameRecord(this.gameID, this.gameConfig, players, this.turns, this.startedAt, Date.now()) + const record = CreateGameRecord( + this.lobbyConfig.gameID, + this.gameConfig, + players, + this.turns, + this.startedAt, + Date.now() + ) // Clear turns because beacon only supports up to 64kb record.turns = [] // For unload events, sendBeacon is the only reliable method diff --git a/src/client/Main.ts b/src/client/Main.ts index be73dc2e1..a40f10933 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -9,6 +9,7 @@ import { UsernameInput } from "./UsernameInput"; import { SinglePlayerModal } from "./SinglePlayerModal"; import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal"; +import { generateID } from "../core/Util"; @@ -71,6 +72,9 @@ class Client { gameType: event.detail.gameType, playerName: (): string => this.usernameInput.getCurrentUsername(), gameID: lobby.id, + persistentID: getPersistentIDFromCookie(), + playerID: generateID(), + clientID: generateID(), map: event.detail.map, difficulty: event.detail.difficulty, }, @@ -105,4 +109,30 @@ function setFavicon(): void { link.rel = 'shortcut icon'; link.href = favicon; document.head.appendChild(link); -} \ No newline at end of file +} + +// 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 +} diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 15019b260..46b9897be 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,9 +1,9 @@ import { Config } from "../core/configuration/Config" import { EventBus, GameEvent } from "../core/EventBus" -import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" +import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig } from "../core/Schemas" +import { LobbyConfig } from "./GameRunner" import { LocalServer } from "./LocalServer" -import { getPersistentIDFromCookie } from "./Utils" export class SendAllianceRequestIntentEvent implements GameEvent { @@ -96,18 +96,17 @@ export class Transport { private pingInterval: number | null = null - private lastPingTime: number | null = null + private isLocal: boolean constructor( - private isLocal: boolean, + private lobbyConfig: LobbyConfig, + // gameConfig only set on private games private gameConfig: GameConfig | null, private eventBus: EventBus, - private gameID: GameID, - private clientID: ClientID, - private playerID: PlayerID, private config: Config, - private playerName: () => string, ) { + this.isLocal = lobbyConfig.gameType == GameType.Singleplayer + this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e)) this.eventBus.on(SendAllianceReplyIntentEvent, (e) => this.onAllianceRequestReplyUIEvent(e)) this.eventBus.on(SendBreakAllianceIntentEvent, (e) => this.onBreakAllianceRequestUIEvent(e)) @@ -128,8 +127,8 @@ export class Transport { if (this.socket != null && this.socket.readyState === WebSocket.OPEN) { this.sendMsg(JSON.stringify(ClientPingMessageSchema.parse({ type: 'ping', - clientID: this.clientID, - gameID: this.gameID, + clientID: this.lobbyConfig.clientID, + gameID: this.lobbyConfig.gameID, }))) } }, 5 * 1000); @@ -152,7 +151,7 @@ export class Transport { } private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) { - this.localServer = new LocalServer(this.clientID, this.config, this.gameConfig, onconnect, onmessage) + this.localServer = new LocalServer(this.config, this.gameConfig, this.lobbyConfig, onconnect, onmessage) this.localServer.start() } @@ -193,10 +192,11 @@ export class Transport { JSON.stringify( ClientJoinMessageSchema.parse({ type: "join", - gameID: this.gameID, - clientID: this.clientID, + gameID: this.lobbyConfig.gameID, + clientID: this.lobbyConfig.clientID, lastTurn: numTurns, - persistentID: getPersistentIDFromCookie(), + persistentID: this.lobbyConfig.persistentID, + username: this.lobbyConfig.playerName() }) ) ) @@ -221,7 +221,7 @@ export class Transport { private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) { this.sendIntent({ type: "allianceRequest", - clientID: this.clientID, + clientID: this.lobbyConfig.clientID, requestor: event.requestor.id(), recipient: event.recipient.id(), }) @@ -230,7 +230,7 @@ export class Transport { private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) { this.sendIntent({ type: "allianceRequestReply", - clientID: this.clientID, + clientID: this.lobbyConfig.clientID, requestor: event.allianceRequest.requestor().id(), recipient: event.allianceRequest.recipient().id(), accept: event.accepted, @@ -240,7 +240,7 @@ export class Transport { private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) { this.sendIntent({ type: "breakAlliance", - clientID: this.clientID, + clientID: this.lobbyConfig.clientID, requestor: event.requestor.id(), recipient: event.recipient.id(), }) @@ -249,9 +249,9 @@ export class Transport { private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) { this.sendIntent({ type: "spawn", - clientID: this.clientID, - playerID: this.playerID, - name: this.playerName(), + clientID: this.lobbyConfig.clientID, + playerID: this.lobbyConfig.playerID, + name: this.lobbyConfig.playerName(), playerType: PlayerType.Human, x: event.cell.x, y: event.cell.y @@ -261,8 +261,8 @@ export class Transport { private onSendAttackIntent(event: SendAttackIntentEvent) { this.sendIntent({ type: "attack", - clientID: this.clientID, - attackerID: this.playerID, + clientID: this.lobbyConfig.clientID, + attackerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, sourceX: null, @@ -275,8 +275,8 @@ export class Transport { private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) { this.sendIntent({ type: "boat", - clientID: this.clientID, - attackerID: this.playerID, + clientID: this.lobbyConfig.clientID, + attackerID: this.lobbyConfig.playerID, targetID: event.targetID, troops: event.troops, x: event.cell.x, @@ -287,8 +287,8 @@ export class Transport { private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) { this.sendIntent({ type: "targetPlayer", - clientID: this.clientID, - requestor: this.playerID, + clientID: this.lobbyConfig.clientID, + requestor: this.lobbyConfig.playerID, target: event.targetID, }) } @@ -296,8 +296,8 @@ export class Transport { private onSendEmojiIntent(event: SendEmojiIntentEvent) { this.sendIntent({ type: "emoji", - clientID: this.clientID, - sender: this.playerID, + clientID: this.lobbyConfig.clientID, + sender: this.lobbyConfig.playerID, recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(), emoji: event.emoji }) @@ -306,7 +306,7 @@ export class Transport { private onSendDonateIntent(event: SendDonateIntentEvent) { this.sendIntent({ type: "donate", - clientID: this.clientID, + clientID: this.lobbyConfig.clientID, sender: event.sender.id(), recipient: event.recipient.id(), troops: event.troops, @@ -316,8 +316,8 @@ export class Transport { private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) { this.sendIntent({ type: "troop_ratio", - clientID: this.clientID, - player: this.playerID, + clientID: this.lobbyConfig.clientID, + player: this.lobbyConfig.playerID, ratio: event.ratio, }) } @@ -325,8 +325,8 @@ export class Transport { private onBuildUnitIntent(event: BuildUnitIntentEvent) { this.sendIntent({ type: "build_unit", - clientID: this.clientID, - player: this.playerID, + clientID: this.lobbyConfig.clientID, + player: this.lobbyConfig.playerID, unit: event.unit, x: event.cell.x, y: event.cell.y, @@ -337,8 +337,8 @@ export class Transport { if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ type: "intent", - clientID: this.clientID, - gameID: this.gameID, + clientID: this.lobbyConfig.clientID, + gameID: this.lobbyConfig.gameID, intent: intent }) this.sendMsg(JSON.stringify(msg)) diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 73e2c6e6c..c88ea87fd 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -35,28 +35,3 @@ 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/core/Schemas.ts b/src/core/Schemas.ts index 80f939718..6689e322a 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -222,10 +222,12 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({ intent: IntentSchema }) +// WARNING: never send this message to clients. export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal('join'), - persistentID: z.string(), - lastTurn: z.number() // The last turn the client saw. + persistentID: z.string(), // WARNING: persistent id is private. + lastTurn: z.number(), // The last turn the client saw. + username: z.string(), }) export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]); @@ -233,12 +235,12 @@ export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientInten export const PlayerRecordSchema = z.object({ clientID: z.string(), username: z.string(), - ip: z.string().nullable(), + ip: z.string().nullable(), // WARNING: PII + persistentID: z.string(), // WARNING: PII }) export const GameRecordSchema = z.object({ - id: z.string(), // WARNING: PII - persistentID: z.string(), // WARNING: PII + id: z.string(), gameConfig: GameConfigSchema, players: z.array(PlayerRecordSchema), startTimestampMS: z.number(), diff --git a/src/server/Archive.ts b/src/server/Archive.ts index 2271d94b7..7889b8e62 100644 --- a/src/server/Archive.ts +++ b/src/server/Archive.ts @@ -22,6 +22,8 @@ export async function archive(gameRecord: GameRecord) { players: gameRecord.players.map(p => ({ username: p.username, ip: p.ip, + persistentID: p.persistentID, + clientID: p.clientID, })), }; @@ -32,8 +34,11 @@ export async function archive(gameRecord: GameRecord) { console.log(`wrote game metadata to BigQuery: ${gameRecord.id}`); if (gameRecord.turns.length > 0) { - // Players will see this so make sure to clear PII. - gameRecord.players.forEach(p => p.ip = "REDACTED") + // Players may see this so make sure to clear PII. + gameRecord.players.forEach(p => { + p.ip = "REDACTED" + p.persistentID = "REDACTED" + }) console.log(`writing game ${gameRecord.id} to gcs`) const bucket = storage.bucket("openfront-games"); const file = bucket.file(gameRecord.id); diff --git a/src/server/Client.ts b/src/server/Client.ts index b1d1eff59..8d708d7a4 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -10,6 +10,7 @@ export class Client { public readonly clientID: ClientID, public readonly persistentID: string, public readonly ip: string | null, + public readonly username: string, public readonly ws: WebSocket, ) { } } \ No newline at end of file diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index ecc8a85c3..778bf18b9 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -166,9 +166,19 @@ export class GameServer { const playerRecords: PlayerRecord[] = Array.from(this.allClients.values()).map(client => ({ ip: client.ip, clientID: client.clientID, + username: client.username, + persistentID: client.persistentID, })); - const record = CreateGameRecord(this.id, this.gameConfig, playerRecords, this.turns, this._startTime, Date.now()) - archive(record) + archive( + CreateGameRecord( + this.id, + this.gameConfig, + playerRecords, + this.turns, + this._startTime, + Date.now() + ) + ) } else { console.log(`${this.id}: no clients joined, not archiving game`) } diff --git a/src/server/Server.ts b/src/server/Server.ts index 7af735bea..8a5d8d620 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -115,6 +115,7 @@ wss.on('connection', (ws, req) => { clientMsg.clientID, clientMsg.persistentID, ip, + clientMsg.username, ws ), clientMsg.gameID,