Files
cactoz.su/gameserver/src/Lobby.hpp
T
cacto 35041c7d48 Security-хардening: XSS в никах, non-root контейнер, rate-limiting, спавн-фикс
Клиент: ники других игроков рендерились через innerHTML без экранирования — XSS.
Добавлен escapeHtml() для nickname и в списке игроков комнаты.

Сервер:
- Санитайзинг ника и на бэкенде (обрезка длины, вырезание control/HTML
  символов) — защита не только для этого клиента, но и для любых будущих
  потребителей (логи, другие рендеры).
- onMessage обёрнут в try/catch: payload.value<T>() кидает json::type_error
  на несовпадении типа поля — одно кривое сообщение могло уронить процесс
  со всеми активными играми.
- Лимит на общее число комнат (200) — иначе клиент, спамящий
  lobby.create_room, разгонял rooms_ до исчерпания памяти.
- Fix: одна из раскладок coop-карты клала укрытие поверх стартовой позиции
  игрока — танк рождался внутри кирпича и не мог выехать. Тайл под каждым
  стартовым танком теперь расчищается после построения карты.

Docker: gameserver теперь работает от непривилегированного пользователя,
а не от root.

nginx: rate-limiting на /ws (5 подключений/с на IP, до 20 одновременных) —
защита от флуда соединениями/комнатами; CSP и security-заголовки
(X-Content-Type-Options, X-Frame-Options, Referrer-Policy).
2026-08-06 14:00:31 +05:00

80 lines
2.4 KiB
C++

#pragma once
#include <App.h>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
using json = nlohmann::json;
struct PerSocketData {
std::string player_id;
std::string nickname;
std::string room_id; // пусто, если игрок в лобби, а не в комнате
};
using WS = uWS::WebSocket<false, true, PerSocketData>;
class Game;
struct RoomPlayer {
std::string player_id;
std::string nickname;
WS *ws;
bool ready = false;
};
struct Room {
std::string id;
std::string mode; // "coop" | "pvp"
std::string state = "waiting"; // waiting | playing | finished
std::vector<RoomPlayer> players;
};
// Лобби и реестр комнат (этап 3 из TZ.md). Игровая логика — этапы 4-5.
class Lobby {
public:
Lobby();
~Lobby();
void onOpen(WS *ws);
void onMessage(WS *ws, std::string_view message);
void onClose(WS *ws);
private:
static constexpr size_t kMaxPlayersPerRoom = 2;
// Верхняя граница на общее число комнат — без неё клиент, создающий и не
// покидающий комнаты, мог бы разогнать rooms_ до исчерпания памяти.
static constexpr size_t kMaxRooms = 200;
std::unordered_map<std::string, Room> rooms_;
std::unordered_map<std::string, std::unique_ptr<Game>> games_;
std::unordered_set<WS *> sockets_;
int next_player_id_ = 1;
int next_room_id_ = 1;
void handleHello(WS *ws, const json &payload);
void handleListRooms(WS *ws);
void handleCreateRoom(WS *ws, const json &payload);
void handleJoinRoom(WS *ws, const json &payload);
void handleLeaveRoom(WS *ws);
void handleGameReady(WS *ws);
void handleGameInput(WS *ws, const json &payload);
void removePlayerFromRoom(WS *ws);
void maybeStartGame(const std::string &room_id);
void endActiveGameIfAny(const std::string &room_id, WS *leavingWs);
void broadcastRoomsToLobby();
void broadcastRoomUpdated(const Room &room, WS *exclude = nullptr);
json roomsSummary() const;
json roomPlayers(const Room &room) const;
std::string generateRoomId();
static void send(WS *ws, const std::string &type, json payload);
static void sendError(WS *ws, const std::string &code, const std::string &message);
};