This commit is contained in:
evanpelle
2026-05-03 22:34:00 -06:00
parent 853216f965
commit a2eae98d6b
3 changed files with 40 additions and 35 deletions
+1 -11
View File
@@ -259,7 +259,6 @@ class Client {
private storeModal: StoreModal;
private tokenLoginModal: TokenLoginModal;
private matchmakingModal: MatchmakingModal;
private mostRecentJoinEvent: number;
private turnstileTokenPromise: Promise<{
token: string;
@@ -756,7 +755,6 @@ class Client {
private async handleJoinLobby(event: CustomEvent<JoinLobbyEvent>) {
const lobby = event.detail;
this.mostRecentJoinEvent = event.timeStamp;
if (this.usernameInput && !this.usernameInput.validateOrShowError()) {
return;
}
@@ -777,7 +775,7 @@ class Client {
}
const auth = await userAuth();
const playerRole = auth !== false ? (auth.claims.role ?? null) : null;
const newLobbyHandle = joinLobby(this.eventBus, {
this.lobbyHandle = joinLobby(this.eventBus, {
gameID: lobby.gameID,
serverConfig: config,
cosmetics: await getPlayerCosmeticsRefs(),
@@ -789,14 +787,6 @@ class Client {
gameRecord: lobby.gameRecord,
});
if (this.mostRecentJoinEvent !== event.timeStamp) {
newLobbyHandle.stop(true);
console.warn("Join requested, but was superseded");
return;
}
this.lobbyHandle = newLobbyHandle;
this.lobbyHandle.prestart.then(() => {
console.log("Closing modals");
document.getElementById("settings-button")?.classList.add("hidden");
+12 -19
View File
@@ -593,22 +593,19 @@ export class GameServer {
(c) => c.clientID !== client.clientID,
);
if (!this._hasStarted) {
// Remove persistentId if the game has not started to prevent going over max players
this.persistentIdToClientId.delete(client.persistentID);
// Close lobby when host leaves before game starts
if (
!this.isPublic() &&
client.persistentID === this.creatorPersistentID
) {
this.log.info("Host left, closing lobby", {
gameID: this.id,
});
for (const c of [...this.activeClients]) {
this.kickClient(c.clientID, KICK_REASON_HOST_LEFT);
}
this._hasEnded = true;
// Close lobby when host leaves before game starts
if (
!this._hasStarted &&
!this.isPublic() &&
client.persistentID === this.creatorPersistentID
) {
this.log.info("Host left, closing lobby", {
gameID: this.id,
});
for (const c of [...this.activeClients]) {
this.kickClient(c.clientID, KICK_REASON_HOST_LEFT);
}
this._hasEnded = true;
}
});
client.ws.on("error", (error: Error) => {
@@ -626,10 +623,6 @@ export class GameServer {
this.activeClients = this.activeClients.filter(
(c) => c.clientID !== client.clientID,
);
// Remove persistentId if the game has not started to prevent going over max players
if (!this._hasStarted) {
this.persistentIdToClientId.delete(client.persistentID);
}
}
}
+27 -5
View File
@@ -3,6 +3,7 @@ import express, { NextFunction, Request, Response } from "express";
import rateLimit from "express-rate-limit";
import http from "http";
import ipAnonymize from "ip-anonymize";
import { RateLimiter } from "limiter";
import path from "path";
import { fileURLToPath } from "url";
import { WebSocket, WebSocketServer } from "ws";
@@ -286,7 +287,16 @@ export async function startWorker() {
// WebSocket handling
wss.on("connection", (ws: WebSocket, req) => {
ws.on("message", async (message: string) => {
const ip = getClientIp(req);
const forwarded = req.headers["x-forwarded-for"];
const ip = Array.isArray(forwarded)
? forwarded[0]
: // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
forwarded || req.socket.remoteAddress || "unknown";
if (!getWsIpLimiter(ip).tryRemoveTokens(1)) {
ws.close(1008, "Rate limit exceeded");
return;
}
try {
// Parse and handle client messages
@@ -619,8 +629,20 @@ function generateGameIdForWorker(): GameID | null {
return null;
}
function getClientIp(req: http.IncomingMessage): string {
const cfIp = req.headers["cf-connecting-ip"];
if (typeof cfIp === "string" && cfIp) return cfIp;
return req.socket.remoteAddress ?? "unknown";
// Per-IP rate limiter for pre-join WebSocket messages.
// Prevents unauthenticated connections from spamming messages
// (e.g. pings) before joining a game.
const wsIpLimiters = new Map<string, RateLimiter>();
function getWsIpLimiter(ip: string): RateLimiter {
let limiter = wsIpLimiters.get(ip);
if (!limiter) {
limiter = new RateLimiter({
tokensPerInterval: 5,
interval: "second",
});
wsIpLimiters.set(ip, limiter);
}
return limiter;
}
// Clean up stale IP limiters every 10 minutes
setInterval(() => wsIpLimiters.clear(), 10 * 60 * 1000);