+ add validation server side

This commit is contained in:
Mittani
2024-12-31 13:06:43 -08:00
committed by evanpelle
parent aa01ea7694
commit aba8e5dd81
10 changed files with 166 additions and 36 deletions
+1 -1
View File
@@ -267,4 +267,4 @@ async function createLobby(): Promise<Lobby> {
throw error; // Re-throw the error so the caller can handle it
}
}
}
+35 -2
View File
@@ -5,6 +5,10 @@ import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerTy
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig, ClientLogMessageSchema } from "../core/Schemas"
import { LobbyConfig } from "./GameRunner"
import { LocalServer } from "./LocalServer"
import {UsernameInput} from "./UsernameInput";
import {HostLobbyModal as HostPrivateLobbyModal} from "./HostLobbyModal";
import {JoinPrivateLobbyModal} from "./JoinPrivateLobbyModal";
import {SinglePlayerModal} from "./SinglePlayerModal";
export class PauseGameEvent implements GameEvent {
constructor(public readonly paused: boolean) { }
@@ -179,7 +183,20 @@ export class Transport {
onconnect()
};
this.socket.onmessage = (event: MessageEvent) => {
onmessage(ServerMessageSchema.parse(JSON.parse(event.data)))
try {
const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data));
// Handle validation errors
if (serverMsg.type === 'validationError' && serverMsg.input === 'username-input') {
this.handleValidationError(serverMsg.message);
return;
}
// Process other message types
this.onmessage(serverMsg);
} catch (error) {
console.error('Failed to process server message:', error);
}
};
this.socket.onerror = (err) => {
console.error('Socket encountered error: ', err, 'Closing socket');
@@ -240,7 +257,23 @@ export class Transport {
this.socket.onclose = (event: CloseEvent) => { }
}
private handleValidationError(message: string) {
const usernameInput = document.querySelector('username-input') as UsernameInput;
const hostModal = document.querySelector('host-lobby-modal') as HostPrivateLobbyModal;
const joinModal = document.querySelector('join-private-lobby-modal') as JoinPrivateLobbyModal;
const singlePlayerModal = document.querySelector('single-player-modal') as SinglePlayerModal;
if (usernameInput) {
this.closeAllModals([hostModal, joinModal, singlePlayerModal]);
usernameInput.validationError = message;
}
}
private closeAllModals(modals: (HostPrivateLobbyModal | JoinPrivateLobbyModal | SinglePlayerModal)[]) {
modals.forEach(modal => {
modal.close();
});
}
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
this.sendIntent({
type: "allianceRequest",
@@ -416,4 +449,4 @@ export class Transport {
this.socket = null
}
}
}
+2 -4
View File
@@ -47,12 +47,11 @@ export class UsernameInput extends LitElement {
connectedCallback() {
super.connectedCallback();
this.username = this.getStoredUsername();
this.dispatchUsernameEvent();
this.dispatchUsernameEvent()
}
render() {
return html`
<div>
<input
type="text"
.value=${this.username}
@@ -63,7 +62,6 @@ export class UsernameInput extends LitElement {
${this.validationError
? html`<div class="error">${this.validationError}</div>`
: null}
</div>
`;
}
@@ -91,7 +89,7 @@ export class UsernameInput extends LitElement {
private dispatchUsernameEvent() {
this.dispatchEvent(new CustomEvent('username-change', {
detail: { username: this.username },
detail: {username: this.username},
bubbles: true,
composed: true
}));
+1 -1
View File
@@ -227,7 +227,7 @@ export class NameLayer implements Layer {
if (myPlayer != null) {
const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer);
const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer)
if (emojis.length > 0) {
context.font = `${render.fontSize * 4}px ${this.theme.font()}`;
context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex();
+13 -3
View File
@@ -32,11 +32,12 @@ export type Turn = z.infer<typeof TurnSchema>
export type GameConfig = z.infer<typeof GameConfigSchema>
export type ClientMessage = ClientPingMessage | ClientIntentMessage | ClientJoinMessage | ClientLogMessage
export type ServerMessage = ServerSyncMessage | ServerStartGameMessage | ServerPingMessage
export type ServerMessage = ServerSyncMessage | ServerStartGameMessage | ServerPingMessage | ServerValidationErrorMessageSchema
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>
export type ServerStartGameMessage = z.infer<typeof ServerStartGameMessageSchema>
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>
export type ServerValidationErrorMessageSchema = z.infer<typeof ServerValidationErrorMessageSchema>
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
@@ -210,9 +211,18 @@ export const ServerStartGameMessageSchema = ServerBaseMessageSchema.extend({
config: GameConfigSchema
})
export const ServerValidationErrorMessageSchema = ServerBaseMessageSchema.extend({
type: z.literal('validationError'),
input: z.string(),
message: z.string()
});
export const ServerMessageSchema = z.union([ServerTurnMessageSchema, ServerStartGameMessageSchema, ServerPingMessageSchema]);
export const ServerMessageSchema = z.union([
ServerTurnMessageSchema,
ServerStartGameMessageSchema,
ServerPingMessageSchema,
ServerValidationErrorMessageSchema,
]);
// Client
+1 -1
View File
@@ -10,7 +10,7 @@ import { customAlphabet, nanoid } from 'nanoid';
export const MIN_USERNAME_LENGTH = 3;
export const MAX_USERNAME_LENGTH = 12;
export const MAX_USERNAME_LENGTH = 15;
export function manhattanDist(c1: Cell, c2: Cell): number {
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
+20 -22
View File
@@ -7,49 +7,43 @@ import {BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES} from "./utils/BotNames";
export class BotSpawner {
private random: PseudoRandom;
private random: PseudoRandom
private bots: SpawnIntent[] = [];
constructor(private gs: Game, gameID: GameID) {
this.random = new PseudoRandom(simpleHash(gameID));
this.random = new PseudoRandom(simpleHash(gameID))
}
spawnBots(numBots: number): SpawnIntent[] {
let tries = 0;
let tries = 0
while (this.bots.length < numBots) {
if (tries > 10000) {
consolex.log("too many retries while spawning bots, giving up");
return this.bots;
consolex.log('too many retries while spawning bots, giving up')
return this.bots
}
const botName = this.randomBotName();
const spawn = this.spawnBot(botName);
if (spawn != null) {
this.bots.push(spawn);
} else {
tries++;
tries++
}
}
return this.bots;
}
private randomBotName(): string {
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
}
spawnBot(botName: string): SpawnIntent | null {
const tile = this.randTile();
const tile = this.randTile()
if (!tile.isLand()) {
return null;
return null
}
for (const spawn of this.bots) {
if (manhattanDist(new Cell(spawn.x, spawn.y), tile.cell()) < 30) {
return null;
return null
}
}
return {
type: "spawn",
type: 'spawn',
playerID: this.random.nextID(),
name: botName,
playerType: PlayerType.Bot,
@@ -58,12 +52,16 @@ export class BotSpawner {
};
}
private randomBotName(): string {
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
}
private randTile(): Tile {
return this.gs.tile(
new Cell(
this.random.nextInt(0, this.gs.width()),
this.random.nextInt(0, this.gs.height())
)
);
return this.gs.tile(new Cell(
this.random.nextInt(0, this.gs.width()),
this.random.nextInt(0, this.gs.height())
))
}
}
+43
View File
@@ -0,0 +1,43 @@
export const BOT_NAME_PREFIXES = [
"Akkadian", "Babylonian", "Assyrian", "Sumerian", "Hittite", "Phoenician",
"Canaanite", "Minoan", "Mycenaean", "Etruscan", "Scythian", "Thracian",
"Dacian", "Illyrian", "Median", "Chaldean",
"Roman", "Greek", "Byzantine", "Persian", "Parthian", "Seleucid",
"Ptolemaic", "Palmyrene", "Macedonian", "Carthaginian",
"Ming", "Tang", "Song", "Yuan",
"Mauryan", "Kushan", "Rajput", "Mughal", "Satavahana", "Vijayanagara",
"Egyptian", "Nubian", "Kushite", "Aksumite", "Ethiopian", "Songhai",
"Malian", "Ghanaian", "Benin", "Ashanti", "Zulu", "Tuareg", "Berber",
"Kanem-Bornu", "Buganda", "Mossi", "Swahili", "Somali", "Wolof",
"Umayyad", "Abbasid", "Ayyubid", "Fatimid", "Mamluk", "Seljuk",
"Safavid", "Ottoman", "Almoravid", "Almohad", "Rashidun", "Ziyarid",
"Frankish", "Visigothic", "Ostrogothic", "Viking", "Norman", "Saxon",
"Anglo-Saxon", "Celtic", "Gaulish", "Carolingian", "Merovingian",
"Capetian", "Plantagenet", "Tudor", "Stuart", "Habsburg", "Romanov",
"Lancaster", "York", "Bourbon", "Napoleonic",
"British", "French", "Spanish", "Portuguese", "Dutch", "Russian",
"German", "Italian", "Swedish", "Norwegian", "Danish", "Polish",
"Hungarian", "Austrian", "Swiss", "Czech", "Slovak", "Serbian",
"Croatian", "Bosnian", "Montenegrin", "Bulgarian", "Romanian",
"Apache", "Sioux", "Cherokee", "Navajo", "Iroquois", "Inuit", "Arawak",
"Carib", "Taino", "Aztec", "Mayan", "Incan", "Mapuche", "Guarani",
"Tupi", "Yanomami", "Zuni", "Hopi", "Kiowa", "Comanche", "Shoshone",
"Japanese", "Ryukyu", "Ainu", "Cham", "Khmer", "Thai", "Vietnamese",
"Burmese", "Balinese", "Malay", "Filipino", "Mongolian",
"Korean", "Tibetan", "Manchu", "Uyghur", "Hmong", "Karen", "Pyu",
"Hawaiian", "Fijian", "Tongan", "Samoan", "Maori", "Micronesian",
"Hebrew", "Armenian", "Georgian", "Phoenician", "Assyrian", "Chaldean",
"Kurdish", "Turkic", "Kazakh", "Uzbek", "Kyrgyz", "Tajik", "Uighur",
"Pashtun", "Baloch", "Afghan", "Persian",
]
export const BOT_NAME_SUFFIXES = [
"Empire", "Dynasty", "Kingdom", "Sultanate", "Confederation", "Union",
"Republic", "Caliphate", "Dominion", "Realm", "State",
"Federation", "Territory", "Commonwealth", "League", "Duchy", "Province",
"Protectorate", "Colony", "Mandate", "Free State","Canton", "Region", "Nation",
"Assembly", "Hierarchy", "Archduchy", "Grand Duchy","Metropolis", "Cluster",
"Alliance", "Tribunal", "Council", "Confederacy", "Order", "Regime",
"Dominion", "Syndicate","Guild", "Corporation", "Patriarchy",
"Matriarchy","Legion", "Horde", "Clan", "Brotherhood", "Sisterhood","Ascendancy", "Supremacy",
"Province","Kingdoms", "Tribes", "Dominion", "Assembly", "Republics"
];
+33
View File
@@ -0,0 +1,33 @@
import { MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH } from "../Util";
export function validateUsername(username: string): { isValid: boolean; error?: string } {
const validPattern = /^[a-zA-Z0-9_]+$/; // Alphanumeric and underscores
if (typeof username !== 'string') {
return { isValid: false, error: "Username must be a string." };
}
if (username.length < MIN_USERNAME_LENGTH) {
return {
isValid: false,
error: `Username must be at least ${MIN_USERNAME_LENGTH} characters long.`,
};
}
if (username.length > MAX_USERNAME_LENGTH) {
return {
isValid: false,
error: `Username must not exceed ${MAX_USERNAME_LENGTH} characters.`,
};
}
if (!validPattern.test(username)) {
return {
isValid: false,
error: "Username can only contain letters, numbers, and underscores.",
};
}
// All checks passed
return { isValid: true };
}
+17 -2
View File
@@ -12,6 +12,7 @@ import { GamePhase, GameServer } from './GameServer';
import { archive } from './Archive';
import { DiscordBot } from './DiscordBot';
import {MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH} from "../core/Util";
import {validateUsername} from "../core/validations/username";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -148,12 +149,26 @@ wss.on('connection', (ws, req) => {
if (Array.isArray(ip)) {
ip = ip[0]
}
gm.addClient(
const username = clientMsg.username;
const { isValid, error } = validateUsername(username);
if (!isValid) {
const errorMsg = error || "Invalid username.";
// Send error back to the client
ws.send(JSON.stringify({
type: 'error',
input: 'username-input',
message: errorMsg,
}));
return;
}
// If username is valid, add the client
gm.addClient(
new Client(
clientMsg.clientID,
clientMsg.persistentID,
ip,
clientMsg.username,
username,
ws
),
clientMsg.gameID,