Files
OpenFrontIO/src/server/ClientMsgRateLimiter.ts
T
FloPinguin 9821e8e041 Add host cheats for streamers (Specifically Enzo) (#3671)
## Description:

- Adds a "Host Cheats" toggle in the private lobby options section that
reveals a dedicated section with four host-only cheats: infinite gold,
infinite troops, gold multiplier, and starting gold
- Only the lobby creator receives the cheat effects in-game (checked via
`isLobbyCreator` in DefaultConfig)
- Joining players see active host cheats displayed as yellow badges in
the lobby UI
- Adds `hostCheats` optional object to `GameConfigSchema` and wires it
through the server config update whitelist
- Raises the intent size limit for `update_game_config` messages
(lobby-only, not stored in turn history) to prevent rate-limiter kicks
(I always got too-much-data-kicked after selecting "host cheats" lol)

<img width="861" height="525" alt="image"
src="https://github.com/user-attachments/assets/51e51ec4-c2e8-46ca-b258-11a93487964f"
/>


<img width="933" height="825" alt="image"
src="https://github.com/user-attachments/assets/5acbd38d-2097-42e1-ba78-0fb17d6afe82"
/>

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
2026-04-15 15:20:08 -07:00

77 lines
2.1 KiB
TypeScript

import { RateLimiter } from "limiter";
import { ClientID } from "../core/Schemas";
const INTENTS_PER_SECOND = 10;
const INTENTS_PER_MINUTE = 150;
const MAX_INTENT_SIZE = 500;
const MAX_CONFIG_INTENT_SIZE = 2000;
const TOTAL_BYTES = 2 * 1024 * 1024; // 2MB per client
export type RateLimitResult = "ok" | "limit" | "kick";
interface ClientBucket {
perSecond: RateLimiter;
perMinute: RateLimiter;
totalBytes: number;
}
export class ClientMsgRateLimiter {
private buckets = new Map<ClientID, ClientBucket>();
check(
clientID: ClientID,
type: string,
bytes: number,
intentType?: string,
): RateLimitResult {
const bucket = this.getOrCreate(clientID);
bucket.totalBytes += bytes;
if (bucket.totalBytes >= TOTAL_BYTES) return "kick";
if (type === "intent") {
// Config updates are lobby-only and not stored in turn history,
// so they can be larger than regular intents.
const maxSize =
intentType === "update_game_config"
? MAX_CONFIG_INTENT_SIZE
: MAX_INTENT_SIZE;
// Intents are stored in turn history for the duration of the game, so
// oversized intents would accumulate and fill up server RAM.
// Intents are also sent to all players, so it increase outgoing
// data.
// Intents should never be larger than MAX_INTENT_SIZE, so we assume the client is malicious.
if (bytes > maxSize) {
return "kick";
}
if (
!bucket.perSecond.tryRemoveTokens(1) ||
!bucket.perMinute.tryRemoveTokens(1)
) {
return "limit";
}
}
return "ok";
}
private getOrCreate(clientID: ClientID): ClientBucket {
const existing = this.buckets.get(clientID);
if (existing) {
return existing;
}
const bucket = {
perSecond: new RateLimiter({
tokensPerInterval: INTENTS_PER_SECOND,
interval: "second",
}),
perMinute: new RateLimiter({
tokensPerInterval: INTENTS_PER_MINUTE,
interval: "minute",
}),
totalBytes: 0,
};
this.buckets.set(clientID, bucket);
return bucket;
}
}