mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 01:18:07 +00:00
Merge branch 'v25'
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import WebSocket from "ws";
|
||||
import { TokenPayload } from "../core/ApiSchemas";
|
||||
import { Tick } from "../core/game/Game";
|
||||
import { ClientID, Winner } from "../core/Schemas";
|
||||
import { ClientID, PlayerCosmetics, Winner } from "../core/Schemas";
|
||||
|
||||
export class Client {
|
||||
public lastPing: number = Date.now();
|
||||
@@ -19,7 +19,6 @@ export class Client {
|
||||
public readonly ip: string,
|
||||
public readonly username: string,
|
||||
public readonly ws: WebSocket,
|
||||
public readonly flag: string | undefined,
|
||||
public readonly pattern: string | undefined,
|
||||
public readonly cosmetics: PlayerCosmetics | undefined,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,20 @@ export class Cloudflare {
|
||||
public async startCloudflared() {
|
||||
const cloudflared = spawn(
|
||||
"cloudflared",
|
||||
["tunnel", "--config", this.configPath, "--loglevel", "error", "run"],
|
||||
[
|
||||
"tunnel",
|
||||
"--config",
|
||||
this.configPath,
|
||||
"--loglevel",
|
||||
"error",
|
||||
"--protocol",
|
||||
"http2",
|
||||
"--retries",
|
||||
"15",
|
||||
"--no-autoupdate",
|
||||
"run",
|
||||
],
|
||||
|
||||
{
|
||||
detached: true,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
|
||||
@@ -399,8 +399,7 @@ export class GameServer {
|
||||
players: this.activeClients.map((c) => ({
|
||||
username: c.username,
|
||||
clientID: c.clientID,
|
||||
pattern: c.pattern,
|
||||
flag: c.flag,
|
||||
cosmetics: c.cosmetics,
|
||||
})),
|
||||
});
|
||||
if (!result.success) {
|
||||
@@ -686,6 +685,7 @@ export class GameServer {
|
||||
persistentID:
|
||||
this.allClients.get(player.clientID)?.persistentID ?? "",
|
||||
stats,
|
||||
cosmetics: player.cosmetics,
|
||||
} satisfies PlayerRecord;
|
||||
},
|
||||
);
|
||||
|
||||
+29
-12
@@ -1,15 +1,17 @@
|
||||
import { Cosmetics } from "../core/CosmeticSchemas";
|
||||
import { PatternDecoder } from "../core/PatternDecoder";
|
||||
import { decodePatternData } from "../core/PatternDecoder";
|
||||
import { PlayerPattern } from "../core/Schemas";
|
||||
|
||||
type PatternResult =
|
||||
| { type: "allowed"; pattern: string }
|
||||
| { type: "allowed"; pattern: PlayerPattern }
|
||||
| { type: "unknown" }
|
||||
| { type: "forbidden"; reason: string };
|
||||
|
||||
export interface PrivilegeChecker {
|
||||
isPatternAllowed(
|
||||
base64: string,
|
||||
flares: readonly string[] | undefined,
|
||||
flares: readonly string[],
|
||||
name: string,
|
||||
colorPaletteName: string | null,
|
||||
): PatternResult;
|
||||
isCustomFlagAllowed(
|
||||
flag: string,
|
||||
@@ -24,25 +26,39 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
||||
) {}
|
||||
|
||||
isPatternAllowed(
|
||||
flares: readonly string[],
|
||||
name: string,
|
||||
flares: readonly string[] | undefined,
|
||||
colorPaletteName: string | null,
|
||||
): PatternResult {
|
||||
// Look for the pattern in the cosmetics.json config
|
||||
const found = this.cosmetics.patterns[name];
|
||||
if (!found) return { type: "forbidden", reason: "pattern not found" };
|
||||
|
||||
try {
|
||||
new PatternDecoder(found.pattern, this.b64urlDecode);
|
||||
decodePatternData(found.pattern, this.b64urlDecode);
|
||||
} catch (e) {
|
||||
return { type: "forbidden", reason: "invalid pattern" };
|
||||
}
|
||||
|
||||
if (
|
||||
flares?.includes(`pattern:${found.name}`) ||
|
||||
flares?.includes("pattern:*")
|
||||
) {
|
||||
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
|
||||
|
||||
if (flares.includes("pattern:*")) {
|
||||
return {
|
||||
type: "allowed",
|
||||
pattern: { name: found.name, patternData: found.pattern, colorPalette },
|
||||
};
|
||||
}
|
||||
|
||||
const flareName =
|
||||
`pattern:${found.name}` +
|
||||
(colorPaletteName ? `:${colorPaletteName}` : "");
|
||||
|
||||
if (flares.includes(flareName)) {
|
||||
// Player has a flare for this pattern
|
||||
return { type: "allowed", pattern: found.pattern };
|
||||
return {
|
||||
type: "allowed",
|
||||
pattern: { name: found.name, patternData: found.pattern, colorPalette },
|
||||
};
|
||||
} else {
|
||||
return { type: "forbidden", reason: "no flares for pattern" };
|
||||
}
|
||||
@@ -124,8 +140,9 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
||||
|
||||
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
|
||||
isPatternAllowed(
|
||||
flares: readonly string[],
|
||||
name: string,
|
||||
flares: readonly string[] | undefined,
|
||||
colorPaletteName: string | null,
|
||||
): PatternResult {
|
||||
return { type: "unknown" };
|
||||
}
|
||||
|
||||
+84
-43
@@ -13,6 +13,9 @@ import {
|
||||
ClientMessageSchema,
|
||||
ID,
|
||||
PartialGameRecordSchema,
|
||||
PlayerCosmeticRefs,
|
||||
PlayerCosmetics,
|
||||
PlayerPattern,
|
||||
ServerErrorMessage,
|
||||
} from "../core/Schemas";
|
||||
import { replacer } from "../core/Util";
|
||||
@@ -363,47 +366,16 @@ export async function startWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the flag is allowed
|
||||
if (clientMsg.flag !== undefined) {
|
||||
if (clientMsg.flag.startsWith("!")) {
|
||||
const allowed = privilegeRefresher
|
||||
.get()
|
||||
.isCustomFlagAllowed(clientMsg.flag, flares);
|
||||
if (allowed !== true) {
|
||||
log.warn(`Custom flag ${allowed}: ${clientMsg.flag}`);
|
||||
ws.close(1002, `Custom flag ${allowed}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pattern: string | undefined;
|
||||
// Check if the pattern is allowed
|
||||
if (clientMsg.patternName !== undefined) {
|
||||
const result = privilegeRefresher
|
||||
.get()
|
||||
.isPatternAllowed(clientMsg.patternName, flares);
|
||||
switch (result.type) {
|
||||
case "allowed":
|
||||
pattern = result.pattern;
|
||||
break;
|
||||
case "unknown":
|
||||
log.warn(`Pattern ${clientMsg.patternName} unknown`);
|
||||
ws.close(
|
||||
1002,
|
||||
"Could not look up pattern, backend may be offline",
|
||||
);
|
||||
return;
|
||||
case "forbidden":
|
||||
log.warn(`Pattern ${clientMsg.patternName}: ${result.reason}`);
|
||||
ws.close(
|
||||
1002,
|
||||
`Pattern ${clientMsg.patternName}: ${result.reason}`,
|
||||
);
|
||||
return;
|
||||
default:
|
||||
assertNever(result);
|
||||
}
|
||||
const { perm, cosmetics, error } = checkCosmetics(
|
||||
clientMsg.cosmetics,
|
||||
flares ?? [],
|
||||
);
|
||||
if (perm === "forbidden") {
|
||||
log.warn(`Forbidden: ${error}`, {
|
||||
clientID: clientMsg.clientID,
|
||||
});
|
||||
ws.close(1002, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create client and add to game
|
||||
@@ -416,8 +388,7 @@ export async function startWorker() {
|
||||
ip,
|
||||
clientMsg.username,
|
||||
ws,
|
||||
clientMsg.flag,
|
||||
pattern,
|
||||
cosmetics,
|
||||
);
|
||||
|
||||
const wasFound = gm.addClient(
|
||||
@@ -453,6 +424,76 @@ export async function startWorker() {
|
||||
});
|
||||
});
|
||||
|
||||
function checkCosmetics(
|
||||
cosmetics: PlayerCosmeticRefs | undefined,
|
||||
flares: readonly string[],
|
||||
): {
|
||||
perm: "forbidden" | "allowed";
|
||||
cosmetics?: PlayerCosmetics | undefined;
|
||||
error?: string;
|
||||
} {
|
||||
if (cosmetics === undefined) {
|
||||
return {
|
||||
perm: "allowed",
|
||||
cosmetics: undefined,
|
||||
};
|
||||
}
|
||||
// Check if the flag is allowed
|
||||
if (cosmetics.flag !== undefined) {
|
||||
if (cosmetics.flag.startsWith("!")) {
|
||||
const allowed = privilegeRefresher
|
||||
.get()
|
||||
.isCustomFlagAllowed(cosmetics.flag, flares);
|
||||
if (allowed !== true) {
|
||||
log.warn(`Custom flag ${allowed}: ${cosmetics.flag}`);
|
||||
return {
|
||||
perm: "forbidden",
|
||||
error: `Custom flag ${allowed}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pattern: PlayerPattern | undefined;
|
||||
// Check if the pattern is allowed
|
||||
if (cosmetics.patternName !== undefined) {
|
||||
const result = privilegeRefresher
|
||||
.get()
|
||||
.isPatternAllowed(
|
||||
flares,
|
||||
cosmetics.patternName,
|
||||
cosmetics.patternColorPaletteName ?? null,
|
||||
);
|
||||
switch (result.type) {
|
||||
case "allowed":
|
||||
pattern = result.pattern;
|
||||
break;
|
||||
case "unknown":
|
||||
log.warn(`Pattern ${cosmetics.patternName} unknown`);
|
||||
return {
|
||||
perm: "forbidden",
|
||||
error: "Could not look up pattern, backend may be offline",
|
||||
};
|
||||
case "forbidden":
|
||||
log.warn(`Pattern ${cosmetics.patternName}: ${result.reason}`);
|
||||
return {
|
||||
perm: "forbidden",
|
||||
error: `Pattern ${cosmetics.patternName}: ${result.reason}`,
|
||||
};
|
||||
default:
|
||||
assertNever(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
perm: "allowed",
|
||||
cosmetics: {
|
||||
flag: cosmetics.flag,
|
||||
pattern: pattern,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// The load balancer will handle routing to this server based on path
|
||||
const PORT = config.workerPortByIndex(workerId);
|
||||
server.listen(PORT, () => {
|
||||
|
||||
Reference in New Issue
Block a user