mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 16:30:16 +00:00
use git commit hash verification when replaying archived games (#204)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# Build stage - will use your native architecture
|
||||
FROM --platform=$BUILDPLATFORM oven/bun:1 AS builder
|
||||
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
|
||||
# Set the working directory for the build
|
||||
WORKDIR /build
|
||||
|
||||
@@ -28,6 +31,8 @@ FROM oven/bun:1
|
||||
ARG GAME_ENV=prod
|
||||
ENV GAME_ENV=$GAME_ENV
|
||||
ENV NODE_ENV=production
|
||||
ARG GIT_COMMIT=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
|
||||
# Install Nginx, Supervisor and Git (for Husky)
|
||||
RUN apt-get update && apt-get install -y nginx supervisor && \
|
||||
|
||||
@@ -364,65 +364,101 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async joinLobby() {
|
||||
private async joinLobby(): Promise<void> {
|
||||
const lobbyId = this.lobbyIdInput.value;
|
||||
consolex.log(`Joining lobby with ID: ${lobbyId}`);
|
||||
this.message = "Checking lobby..."; // Set initial message
|
||||
this.message = "Checking lobby...";
|
||||
|
||||
const config = await getServerConfigFromClient();
|
||||
const url = `/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const gameInfo = await response.json();
|
||||
if (gameInfo.exists) {
|
||||
this.message = "Joined successfully! Waiting for game to start...";
|
||||
this.hasJoined = true;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
} else {
|
||||
const archive_url = `/${config.workerPath(lobbyId)}/api/archived_game/${lobbyId}`;
|
||||
const archive_response = await fetch(archive_url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const archive_data = await archive_response.json();
|
||||
if (archive_data.exists) {
|
||||
const gr = archive_data.gameRecord as GameRecord;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
gameRecord: gr,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.message = "Lobby not found. Please check the ID and try again.";
|
||||
}
|
||||
}
|
||||
// First, check if the game exists in active lobbies
|
||||
const gameExists = await this.checkActiveLobby(lobbyId);
|
||||
if (gameExists) return;
|
||||
|
||||
// If not active, check archived games
|
||||
const archivedGame = await this.checkArchivedGame(lobbyId);
|
||||
if (archivedGame) return;
|
||||
|
||||
this.message = "Lobby not found. Please check the ID and try again.";
|
||||
} catch (error) {
|
||||
consolex.error("Error checking lobby existence:", error);
|
||||
this.message = "An error occurred. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
private async checkActiveLobby(lobbyId: string): Promise<boolean> {
|
||||
const config = await getServerConfigFromClient();
|
||||
const url = `/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const gameInfo = await response.json();
|
||||
|
||||
if (gameInfo.exists) {
|
||||
this.message = "Joined successfully! Waiting for game to start...";
|
||||
this.hasJoined = true;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: { gameID: lobbyId } as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async checkArchivedGame(lobbyId: string): Promise<boolean> {
|
||||
const config = await getServerConfigFromClient();
|
||||
const archiveUrl = `/${config.workerPath(lobbyId)}/api/archived_game/${lobbyId}`;
|
||||
|
||||
const archiveResponse = await fetch(archiveUrl, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const archiveData = await archiveResponse.json();
|
||||
|
||||
if (
|
||||
archiveData.success === false &&
|
||||
archiveData.error === "Version mismatch"
|
||||
) {
|
||||
consolex.warn(
|
||||
`Git commit hash mismatch for game ${lobbyId}`,
|
||||
archiveData.details,
|
||||
);
|
||||
this.message =
|
||||
"This game was created with a different version. Cannot join.";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (archiveData.exists) {
|
||||
const gameRecord = archiveData.gameRecord as GameRecord;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
gameID: lobbyId,
|
||||
gameRecord: gameRecord,
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async pollPlayers() {
|
||||
if (!this.lobbyIdInput?.value) return;
|
||||
const config = await getServerConfigFromClient();
|
||||
|
||||
@@ -395,4 +395,5 @@ export const GameRecordSchema = z.object({
|
||||
winner: ID.nullable(),
|
||||
allPlayersStats: z.record(ID, PlayerStatsSchema),
|
||||
version: z.enum(["v0.0.1"]),
|
||||
gitCommit: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
@@ -100,6 +100,8 @@ export interface ServerConfig {
|
||||
env(): GameEnv;
|
||||
adminToken(): string;
|
||||
adminHeader(): string;
|
||||
// Only available on the server
|
||||
gitCommit(): string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
|
||||
@@ -22,6 +22,9 @@ import { pastelTheme } from "./PastelTheme";
|
||||
import { pastelThemeDark } from "./PastelThemeDark";
|
||||
|
||||
export abstract class DefaultServerConfig implements ServerConfig {
|
||||
gitCommit(): string {
|
||||
return process.env.GIT_COMMIT;
|
||||
}
|
||||
adminHeader(): string {
|
||||
return "x-admin-key";
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ export class DevServerConfig extends DefaultServerConfig {
|
||||
numWorkers(): number {
|
||||
return 2;
|
||||
}
|
||||
gitCommit(): string {
|
||||
return "DEV";
|
||||
}
|
||||
}
|
||||
|
||||
export class DevConfig extends DefaultConfig {
|
||||
|
||||
@@ -15,6 +15,7 @@ const analyticsBucket = "openfront-analytics";
|
||||
|
||||
export async function archive(gameRecord: GameRecord) {
|
||||
try {
|
||||
gameRecord.gitCommit = config.gitCommit();
|
||||
// Archive to Redshift Serverless
|
||||
await archiveAnalyticsToS3(gameRecord);
|
||||
|
||||
|
||||
@@ -478,7 +478,6 @@ export class GameServer {
|
||||
for (const client of this.activeClients) {
|
||||
if (client.hashes.has(turnNumber)) {
|
||||
const clientHash = client.hashes.get(turnNumber)!;
|
||||
console.log(`clientHash: ${clientHash}`);
|
||||
counts.set(clientHash, (counts.get(clientHash) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-4
@@ -4,7 +4,10 @@ import { WebSocketServer } from "ws";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { GameManager } from "./GameManager";
|
||||
import { getServerConfigFromServer } from "../core/configuration/Config";
|
||||
import {
|
||||
GameEnv,
|
||||
getServerConfigFromServer,
|
||||
} from "../core/configuration/Config";
|
||||
import { WebSocket } from "ws";
|
||||
import { Client } from "./Client";
|
||||
import rateLimit from "express-rate-limit";
|
||||
@@ -185,13 +188,35 @@ export function startWorker() {
|
||||
"/api/archived_game/:id",
|
||||
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
||||
const gameRecord = await readGameRecord(req.params.id);
|
||||
|
||||
if (!gameRecord) {
|
||||
res.json({
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Game not found",
|
||||
exists: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
|
||||
if (
|
||||
config.env() != GameEnv.Dev &&
|
||||
gameRecord.gitCommit != config.gitCommit()
|
||||
) {
|
||||
console.warn(
|
||||
`git commit mismatch for game ${req.params.id}, expected ${config.gitCommit()}, got ${gameRecord.gitCommit}`,
|
||||
);
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: "Version mismatch",
|
||||
exists: true,
|
||||
details: {
|
||||
expectedCommit: config.gitCommit(),
|
||||
actualCommit: gameRecord.gitCommit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
exists: true,
|
||||
gameRecord: gameRecord,
|
||||
});
|
||||
|
||||
@@ -54,13 +54,17 @@ if [ $? -ne 0 ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
GIT_COMMIT=$(git rev-parse HEAD)
|
||||
echo "Git commit: $GIT_COMMIT"
|
||||
|
||||
# Build the Docker image
|
||||
echo "Building Docker image..."
|
||||
docker buildx build --platform linux/amd64 -t $ECR_REPO_NAME:$VERSION_TAG .
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Docker build failed."
|
||||
exit 1
|
||||
fi
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg GIT_COMMIT=$GIT_COMMIT \
|
||||
-t $ECR_REPO_NAME:$VERSION_TAG \
|
||||
.
|
||||
|
||||
|
||||
# Authenticate to ECR
|
||||
echo "Authenticating to ECR..."
|
||||
|
||||
Reference in New Issue
Block a user