restructure worker

This commit is contained in:
Ryan Barlow
2026-05-25 19:14:46 +01:00
parent edf1d03275
commit d9fedb6eb2
4 changed files with 67 additions and 83 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ export class GameManager {
persistentID: string,
gameID: GameID,
lastTurn: number = 0,
identityUpdate?: { username: string; clanTag?: string | null },
identityUpdate?: { username: string; clanTag: string | null },
): boolean {
const game = this.games.get(gameID);
if (!game) return false;
+2 -7
View File
@@ -286,7 +286,7 @@ export class GameServer {
ws: WebSocket,
persistentID: string,
lastTurn: number = 0,
identityUpdate?: { username: string; clanTag?: string | null },
identityUpdate?: { username: string; clanTag: string | null },
): boolean {
const clientID = this.getClientIdForPersistentId(persistentID);
if (!clientID) return false;
@@ -308,12 +308,7 @@ export class GameServer {
this.activeClients.push(client);
if (identityUpdate && !this.hasStarted()) {
client.username = identityUpdate.username;
// clanTag is only updated when explicitly provided. The reconnect
// fast-path omits it so a refreshed client can't swap to a tag the
// initial join didn't validate.
if (identityUpdate.clanTag !== undefined) {
client.clanTag = identityUpdate.clanTag;
}
client.clanTag = identityUpdate.clanTag;
}
client.lastPing = Date.now();
this.markClientDisconnected(client.clientID, false);
+60 -57
View File
@@ -10,6 +10,7 @@ import { z } from "zod";
import {
ClanExistsResponseSchema,
clanExistsApiPath,
type UserMeResponse,
} from "../core/ApiSchemas";
import { GameEnv } from "../core/configuration/Config";
import { GameType } from "../core/game/Game";
@@ -428,30 +429,18 @@ export async function startWorker() {
return;
}
// Normalize username and clan tag before any rejoin/join handling.
// Normalize username and clan tag.
const { clanTag: censoredClanTag, username: censoredUsername } =
privilegeRefresher
.get()
.censor(clientMsg.username, clientMsg.clanTag ?? null);
// Try to reconnect an existing client (e.g., page refresh).
// Username may have changed since initial join; clanTag is intentionally
// omitted so the reconnect can't swap to a tag that wasn't validated on
// the original join. To change clan tag, the player must fully rejoin.
if (
gm.rejoinClient(ws, persistentId, clientMsg.gameID, 0, {
username: censoredUsername,
})
) {
return;
}
let flares: string[] | undefined;
let publicId: string | undefined;
let friends: string[] = [];
let userClanTags: Set<string> = new Set();
// Fetch user profile up front. Needed here so the clan-tag ownership
// check can run before the reconnect fast-path (otherwise a refresh
// would let a player swap to an unvalidated tag), and reused below
// for flares/cosmetics on new joins.
const allowedFlares = ServerEnv.allowedFlares();
let userMeResponse: UserMeResponse | null = null;
if (claims === null) {
if (allowedFlares !== undefined) {
log.warn("Unauthorized: Anonymous user attempted to join game");
@@ -459,7 +448,6 @@ export async function startWorker() {
return;
}
} else {
// Verify token and get player permissions
const result = await getUserMe(clientMsg.token);
if (result.type === "error") {
log.warn(`Unauthorized: ${result.message}`, {
@@ -469,29 +457,64 @@ export async function startWorker() {
ws.close(1002, "Unauthorized: user me fetch failed");
return;
}
flares = result.response.player.flares;
publicId = result.response.player.publicId;
friends = result.response.player.friends;
userClanTags = new Set(
(result.response.player.clans ?? []).map((c) =>
c.tag.toUpperCase(),
),
);
userMeResponse = result.response;
}
if (allowedFlares !== undefined) {
const allowed =
allowedFlares.length === 0 ||
allowedFlares.some((f) => flares?.includes(f));
if (!allowed) {
log.warn(
"Forbidden: player without an allowed flare attempted to join game",
);
ws.close(1002, "Forbidden");
return;
// Enforce clan tag ownership. A player can wear a tag only if they're
// a member; if they're not and the tag belongs to a real clan, drop it
// to prevent impersonation. Fictional tags pass through.
let resolvedClanTag = censoredClanTag;
if (resolvedClanTag !== null) {
const userClanTags = new Set(
userMeResponse
? (userMeResponse.player.clans ?? []).map((c) =>
c.tag.toUpperCase(),
)
: [],
);
if (!userClanTags.has(resolvedClanTag.toUpperCase())) {
const exists = await clanExistsByTag(resolvedClanTag);
if (exists === true) {
log.warn("Dropped clan tag: player is not a member", {
persistentID: persistentId,
gameID: clientMsg.gameID,
clanTag: resolvedClanTag,
});
resolvedClanTag = null;
}
}
}
// Try to reconnect an existing client (e.g. page refresh). Pre-game,
// username and clan tag pick up the latest validated values from this
// connection.
if (
gm.rejoinClient(ws, persistentId, clientMsg.gameID, 0, {
username: censoredUsername,
clanTag: resolvedClanTag,
})
) {
return;
}
// New client — finish the join checks.
const flares = userMeResponse?.player.flares;
const publicId = userMeResponse?.player.publicId;
const friends = userMeResponse?.player.friends ?? [];
if (userMeResponse !== null && allowedFlares !== undefined) {
const allowed =
allowedFlares.length === 0 ||
allowedFlares.some((f) => flares?.includes(f));
if (!allowed) {
log.warn(
"Forbidden: player without an allowed flare attempted to join game",
);
ws.close(1002, "Forbidden");
return;
}
}
const cosmeticResult = privilegeRefresher
.get()
.isAllowed(flares ?? [], clientMsg.cosmetics ?? {});
@@ -531,26 +554,6 @@ export async function startWorker() {
}
}
// Enforce clan tag ownership. A player can wear a tag only if they're
// a member; if they're not and the tag belongs to a real clan, drop it
// to prevent impersonation. Fictional tags pass through. Runs after
// turnstile so we don't burn an API call on rejected bot joins.
let resolvedClanTag = censoredClanTag;
if (
resolvedClanTag !== null &&
!userClanTags.has(resolvedClanTag.toUpperCase())
) {
const exists = await clanExistsByTag(resolvedClanTag);
if (exists === true) {
log.warn("Dropped clan tag: player is not a member", {
persistentID: persistentId,
gameID: clientMsg.gameID,
clanTag: resolvedClanTag,
});
resolvedClanTag = null;
}
}
// Create client and add to game
const client = new Client(
generateID(),