diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 8667cc5b9..07bbe2c37 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -19,7 +19,7 @@ import { GameType } from "../core/game/Game"; import { archive } from "./Archive"; import { Client } from "./Client"; import { slog } from "./StructuredLog"; -import { securityMiddleware } from "./Security"; +import { gatekeeper } from "./Gatekeeper"; export enum GamePhase { Lobby = "LOBBY", @@ -123,7 +123,7 @@ export class GameServer { client.ws.on( "message", - securityMiddleware.wsHandler(client.ip, async (message: string) => { + gatekeeper.wsHandler(client.ip, async (message: string) => { try { let clientMsg: ClientMessage = null; try { diff --git a/src/server/Security.ts b/src/server/Gatekeeper.ts similarity index 64% rename from src/server/Security.ts rename to src/server/Gatekeeper.ts index 5f38a427d..bfbb8b4a9 100644 --- a/src/server/Security.ts +++ b/src/server/Gatekeeper.ts @@ -1,8 +1,9 @@ -// src/server/middleware/securityInterface.ts +// src/server/Security.ts import { Request, Response, NextFunction } from "express"; import http from "http"; import path from "path"; import { fileURLToPath } from "url"; +import fs from "fs"; export enum LimiterType { Get = "get", @@ -11,11 +12,11 @@ export enum LimiterType { WebSocket = "websocket", } -export interface SecurityMiddleware { +export interface Gatekeeper { // The wrapper for request handlers with optional rate limiting httpHandler: ( - fn: (req: Request, res: Response, next: NextFunction) => Promise, limiterType: LimiterType, + fn: (req: Request, res: Response, next: NextFunction) => Promise, ) => (req: Request, res: Response, next: NextFunction) => Promise; // The wrapper for WebSocket message handlers with rate limiting @@ -26,41 +27,63 @@ export interface SecurityMiddleware { } // Function to get the appropriate security middleware implementation -async function getSecurityMiddleware(): Promise { +async function getGatekeeper(): Promise { try { // Get the current file's directory const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); try { - // Use dynamic import for ES modules - without file extension - // ts-node will resolve this correctly - const module = await import( - "./security-middleware/RealSecurityMiddleware" + // Check if the file exists before attempting to import it + const realMiddlewarePath = path.resolve( + __dirname, + "./gatekeeper/RealSecurityMiddleware.js", + ); + const tsMiddlewarePath = path.resolve( + __dirname, + "./gatekeeper/RealSecurityMiddleware.ts", ); - if (!module.RealSecurityMiddleware) { - throw new Error("RealSecurityMiddleware class not found in module"); + if ( + !fs.existsSync(realMiddlewarePath) && + !fs.existsSync(tsMiddlewarePath) + ) { + console.log( + "RealSecurityMiddleware file not found, using NoOpSecurityMiddleware", + ); + return new NoOpGatekeeper(); + } + + // Use dynamic import for ES modules + const module = await import("./gatekeeper/RealGatekeeper.js").catch( + () => import("./gatekeeper/RealGatekeeper.js"), + ); + + if (!module || !module.RealSecurityMiddleware) { + console.log( + "RealSecurityMiddleware class not found in module, using NoOpSecurityMiddleware", + ); + return new NoOpGatekeeper(); } console.log("Successfully loaded real security middleware"); return new module.RealSecurityMiddleware(); } catch (error) { console.log("Failed to load real security middleware:", error); - return new NoOpSecurityMiddleware(); + return new NoOpGatekeeper(); } } catch (e) { // Fall back to no-op if real implementation isn't available console.log("using no-op security middleware", e); - return new NoOpSecurityMiddleware(); + return new NoOpGatekeeper(); } } -export class NoOpSecurityMiddleware implements SecurityMiddleware { +export class NoOpGatekeeper implements Gatekeeper { // Simple pass-through with no rate limiting httpHandler( - fn: (req: Request, res: Response, next: NextFunction) => Promise, limiterType: LimiterType, + fn: (req: Request, res: Response, next: NextFunction) => Promise, ) { return async (req: Request, res: Response, next: NextFunction) => { try { @@ -89,14 +112,13 @@ export class NoOpSecurityMiddleware implements SecurityMiddleware { // Initialize the security middleware with a default implementation // We'll use the NoOpSecurityMiddleware initially and then replace it // with the real implementation once it's loaded -export const securityMiddleware: SecurityMiddleware = - new NoOpSecurityMiddleware(); +export const gatekeeper: Gatekeeper = new NoOpGatekeeper(); // Immediately try to load the real middleware -getSecurityMiddleware() +getGatekeeper() .then((middleware) => { // Replace the methods of securityMiddleware with those from the loaded middleware - Object.assign(securityMiddleware, middleware); + Object.assign(gatekeeper, middleware); }) .catch((error) => { console.error("Failed to initialize security middleware:", error); diff --git a/src/server/Master.ts b/src/server/Master.ts index 9215be323..802c9f869 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -10,6 +10,7 @@ import path from "path"; import rateLimit from "express-rate-limit"; import { fileURLToPath } from "url"; import { isHighTrafficTime } from "./Util"; +import { gatekeeper, LimiterType } from "./Gatekeeper"; const config = getServerConfig(); const readyWorkers = new Set(); @@ -122,9 +123,12 @@ export async function startMaster() { } // Add lobbies endpoint to list public games for this worker -app.get("/public_lobbies", (req, res) => { - res.send(publicLobbiesJsonStr); -}); +app.get( + "/public_lobbies", + gatekeeper.httpHandler(LimiterType.Get, async (req, res) => { + res.send(publicLobbiesJsonStr); + }), +); async function fetchLobbies(): Promise { const fetchPromises = []; diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 18efc82b2..7fa77e03a 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -13,7 +13,7 @@ import { GameConfig, GameRecord, LogSeverity } from "../core/Schemas"; import { slog } from "./StructuredLog"; import { GameType } from "../core/game/Game"; import { archive } from "./Archive"; -import { LimiterType, securityMiddleware } from "./Security"; +import { LimiterType, gatekeeper } from "./Gatekeeper"; const config = getServerConfig(); @@ -80,7 +80,7 @@ export function startWorker() { // Endpoint to create a private lobby app.post( "/create_game/:id", - securityMiddleware.httpHandler(async (req, res) => { + gatekeeper.httpHandler(LimiterType.Post, async (req, res) => { const id = req.params.id; if (!id) { console.warn(`cannot create game, id not found`); @@ -111,13 +111,13 @@ export function startWorker() { `Worker ${workerId}: IP ${clientIP} creating game ${game.isPublic() ? "Public" : "Private"} with id ${id}`, ); res.json(game.gameInfo()); - }, LimiterType.Post), + }), ); // Add other endpoints from your original server app.post( "/start_game/:id", - securityMiddleware.httpHandler(async (req, res) => { + gatekeeper.httpHandler(LimiterType.Post, async (req, res) => { console.log(`starting private lobby with id ${req.params.id}`); const game = gm.game(req.params.id); if (!game) { @@ -132,12 +132,12 @@ export function startWorker() { } game.start(); res.status(200).json({ success: true }); - }, LimiterType.Post), + }), ); app.put( "/game/:id", - securityMiddleware.httpHandler(async (req, res) => { + gatekeeper.httpHandler(LimiterType.Put, async (req, res) => { // TODO: only update public game if from local host const lobbyID = req.params.id; if (req.body.gameType == GameType.Public) { @@ -163,34 +163,34 @@ export function startWorker() { disableNPCs: req.body.disableNPCs, }); res.status(200).json({ success: true }); - }, LimiterType.Put), + }), ); app.get( "/game/:id/exists", - securityMiddleware.httpHandler(async (req, res) => { + gatekeeper.httpHandler(LimiterType.Get, async (req, res) => { const lobbyId = req.params.id; res.json({ exists: gm.game(lobbyId) != null, }); - }, LimiterType.Get), + }), ); app.get( "/game/:id", - securityMiddleware.httpHandler(async (req, res) => { + gatekeeper.httpHandler(LimiterType.Get, async (req, res) => { const game = gm.game(req.params.id); if (game == null) { console.log(`lobby ${req.params.id} not found`); return res.status(404).json({ error: "Game not found" }); } res.json(game.gameInfo()); - }, LimiterType.Get), + }), ); app.post( "/archive_singleplayer_game", - securityMiddleware.httpHandler(async (req, res) => { + gatekeeper.httpHandler(LimiterType.Post, async (req, res) => { const gameRecord: GameRecord = req.body; const clientIP = req.ip || req.socket.remoteAddress || "unknown"; @@ -204,14 +204,14 @@ export function startWorker() { res.json({ success: true, }); - }, LimiterType.Post), + }), ); // WebSocket handling wss.on("connection", (ws: WebSocket, req) => { ws.on( "message", - securityMiddleware.wsHandler(req, async (message: string) => { + gatekeeper.wsHandler(req, async (message: string) => { const forwarded = req.headers["x-forwarded-for"]; const ip = Array.isArray(forwarded) ? forwarded[0] diff --git a/src/server/gatekeeper b/src/server/gatekeeper index 3192c20cd..392e2b70a 160000 --- a/src/server/gatekeeper +++ b/src/server/gatekeeper @@ -1 +1 @@ -Subproject commit 3192c20cde75a17725dd4bbaab64463c95056768 +Subproject commit 392e2b70af97ec652090f6ca92b46387d4103795