Update to Gatekeeper

This commit is contained in:
Evan
2025-03-02 09:17:26 -08:00
parent d09d4695f2
commit 74ce3b3187
5 changed files with 64 additions and 38 deletions
+2 -2
View File
@@ -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 {
@@ -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<any>,
limiterType: LimiterType,
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
// 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<SecurityMiddleware> {
async function getGatekeeper(): Promise<Gatekeeper> {
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<any>,
limiterType: LimiterType,
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
) {
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);
+7 -3
View File
@@ -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<void> {
const fetchPromises = [];
+14 -14
View File
@@ -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]