Merge branch 'v25'

This commit is contained in:
evanpelle
2025-09-26 10:53:57 -07:00
31 changed files with 679 additions and 496 deletions
+2 -3
View File
@@ -1,7 +1,7 @@
import WebSocket from "ws";
import { TokenPayload } from "../core/ApiSchemas";
import { Tick } from "../core/game/Game";
import { ClientID, Winner } from "../core/Schemas";
import { ClientID, PlayerCosmetics, Winner } from "../core/Schemas";
export class Client {
public lastPing: number = Date.now();
@@ -19,7 +19,6 @@ export class Client {
public readonly ip: string,
public readonly username: string,
public readonly ws: WebSocket,
public readonly flag: string | undefined,
public readonly pattern: string | undefined,
public readonly cosmetics: PlayerCosmetics | undefined,
) {}
}
+14 -1
View File
@@ -237,7 +237,20 @@ export class Cloudflare {
public async startCloudflared() {
const cloudflared = spawn(
"cloudflared",
["tunnel", "--config", this.configPath, "--loglevel", "error", "run"],
[
"tunnel",
"--config",
this.configPath,
"--loglevel",
"error",
"--protocol",
"http2",
"--retries",
"15",
"--no-autoupdate",
"run",
],
{
detached: true,
stdio: ["ignore", "pipe", "pipe"],
+2 -2
View File
@@ -399,8 +399,7 @@ export class GameServer {
players: this.activeClients.map((c) => ({
username: c.username,
clientID: c.clientID,
pattern: c.pattern,
flag: c.flag,
cosmetics: c.cosmetics,
})),
});
if (!result.success) {
@@ -686,6 +685,7 @@ export class GameServer {
persistentID:
this.allClients.get(player.clientID)?.persistentID ?? "",
stats,
cosmetics: player.cosmetics,
} satisfies PlayerRecord;
},
);
+29 -12
View File
@@ -1,15 +1,17 @@
import { Cosmetics } from "../core/CosmeticSchemas";
import { PatternDecoder } from "../core/PatternDecoder";
import { decodePatternData } from "../core/PatternDecoder";
import { PlayerPattern } from "../core/Schemas";
type PatternResult =
| { type: "allowed"; pattern: string }
| { type: "allowed"; pattern: PlayerPattern }
| { type: "unknown" }
| { type: "forbidden"; reason: string };
export interface PrivilegeChecker {
isPatternAllowed(
base64: string,
flares: readonly string[] | undefined,
flares: readonly string[],
name: string,
colorPaletteName: string | null,
): PatternResult;
isCustomFlagAllowed(
flag: string,
@@ -24,25 +26,39 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
) {}
isPatternAllowed(
flares: readonly string[],
name: string,
flares: readonly string[] | undefined,
colorPaletteName: string | null,
): PatternResult {
// Look for the pattern in the cosmetics.json config
const found = this.cosmetics.patterns[name];
if (!found) return { type: "forbidden", reason: "pattern not found" };
try {
new PatternDecoder(found.pattern, this.b64urlDecode);
decodePatternData(found.pattern, this.b64urlDecode);
} catch (e) {
return { type: "forbidden", reason: "invalid pattern" };
}
if (
flares?.includes(`pattern:${found.name}`) ||
flares?.includes("pattern:*")
) {
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
if (flares.includes("pattern:*")) {
return {
type: "allowed",
pattern: { name: found.name, patternData: found.pattern, colorPalette },
};
}
const flareName =
`pattern:${found.name}` +
(colorPaletteName ? `:${colorPaletteName}` : "");
if (flares.includes(flareName)) {
// Player has a flare for this pattern
return { type: "allowed", pattern: found.pattern };
return {
type: "allowed",
pattern: { name: found.name, patternData: found.pattern, colorPalette },
};
} else {
return { type: "forbidden", reason: "no flares for pattern" };
}
@@ -124,8 +140,9 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
isPatternAllowed(
flares: readonly string[],
name: string,
flares: readonly string[] | undefined,
colorPaletteName: string | null,
): PatternResult {
return { type: "unknown" };
}
+84 -43
View File
@@ -13,6 +13,9 @@ import {
ClientMessageSchema,
ID,
PartialGameRecordSchema,
PlayerCosmeticRefs,
PlayerCosmetics,
PlayerPattern,
ServerErrorMessage,
} from "../core/Schemas";
import { replacer } from "../core/Util";
@@ -363,47 +366,16 @@ export async function startWorker() {
}
}
// Check if the flag is allowed
if (clientMsg.flag !== undefined) {
if (clientMsg.flag.startsWith("!")) {
const allowed = privilegeRefresher
.get()
.isCustomFlagAllowed(clientMsg.flag, flares);
if (allowed !== true) {
log.warn(`Custom flag ${allowed}: ${clientMsg.flag}`);
ws.close(1002, `Custom flag ${allowed}`);
return;
}
}
}
let pattern: string | undefined;
// Check if the pattern is allowed
if (clientMsg.patternName !== undefined) {
const result = privilegeRefresher
.get()
.isPatternAllowed(clientMsg.patternName, flares);
switch (result.type) {
case "allowed":
pattern = result.pattern;
break;
case "unknown":
log.warn(`Pattern ${clientMsg.patternName} unknown`);
ws.close(
1002,
"Could not look up pattern, backend may be offline",
);
return;
case "forbidden":
log.warn(`Pattern ${clientMsg.patternName}: ${result.reason}`);
ws.close(
1002,
`Pattern ${clientMsg.patternName}: ${result.reason}`,
);
return;
default:
assertNever(result);
}
const { perm, cosmetics, error } = checkCosmetics(
clientMsg.cosmetics,
flares ?? [],
);
if (perm === "forbidden") {
log.warn(`Forbidden: ${error}`, {
clientID: clientMsg.clientID,
});
ws.close(1002, error);
return;
}
// Create client and add to game
@@ -416,8 +388,7 @@ export async function startWorker() {
ip,
clientMsg.username,
ws,
clientMsg.flag,
pattern,
cosmetics,
);
const wasFound = gm.addClient(
@@ -453,6 +424,76 @@ export async function startWorker() {
});
});
function checkCosmetics(
cosmetics: PlayerCosmeticRefs | undefined,
flares: readonly string[],
): {
perm: "forbidden" | "allowed";
cosmetics?: PlayerCosmetics | undefined;
error?: string;
} {
if (cosmetics === undefined) {
return {
perm: "allowed",
cosmetics: undefined,
};
}
// Check if the flag is allowed
if (cosmetics.flag !== undefined) {
if (cosmetics.flag.startsWith("!")) {
const allowed = privilegeRefresher
.get()
.isCustomFlagAllowed(cosmetics.flag, flares);
if (allowed !== true) {
log.warn(`Custom flag ${allowed}: ${cosmetics.flag}`);
return {
perm: "forbidden",
error: `Custom flag ${allowed}`,
};
}
}
}
let pattern: PlayerPattern | undefined;
// Check if the pattern is allowed
if (cosmetics.patternName !== undefined) {
const result = privilegeRefresher
.get()
.isPatternAllowed(
flares,
cosmetics.patternName,
cosmetics.patternColorPaletteName ?? null,
);
switch (result.type) {
case "allowed":
pattern = result.pattern;
break;
case "unknown":
log.warn(`Pattern ${cosmetics.patternName} unknown`);
return {
perm: "forbidden",
error: "Could not look up pattern, backend may be offline",
};
case "forbidden":
log.warn(`Pattern ${cosmetics.patternName}: ${result.reason}`);
return {
perm: "forbidden",
error: `Pattern ${cosmetics.patternName}: ${result.reason}`,
};
default:
assertNever(result);
}
}
return {
perm: "allowed",
cosmetics: {
flag: cosmetics.flag,
pattern: pattern,
},
};
}
// The load balancer will handle routing to this server based on path
const PORT = config.workerPortByIndex(workerId);
server.listen(PORT, () => {