diff --git a/client/src/ui/gameOver/GameOverMenu.cpp b/client/src/ui/gameOver/GameOverMenu.cpp index 4f2ceb38..56e724ba 100644 --- a/client/src/ui/gameOver/GameOverMenu.cpp +++ b/client/src/ui/gameOver/GameOverMenu.cpp @@ -47,8 +47,8 @@ namespace Engine if (_scoresTitle) _scoresTitle->setPosition(v.cx - 30.f, titleY); for (std::size_t i = 0; i < _scoreLines.size(); ++i) - if (_scoreLines[i]) - _scoreLines[i]->setPosition(v.cx - 90.f, lineStartY + static_cast(i) * 34.f); + if (_scoreLines.at(i)) + _scoreLines.at(i)->setPosition(v.cx - 90.f, lineStartY + static_cast(i) * 34.f); layoutColumnCentered(v.w, v.h * 0.60f, 110.f, {_back.get(), _quit.get()}); } @@ -59,26 +59,34 @@ namespace Engine handleMousePressed(frame); if (frame.mouseReleased) handleMouseReleased(frame); - if (const auto w = _world.lock()) { - auto scores = w->getRoomScores(); - if (scores.empty()) { - if (!_scoreLines.empty()) - _scoreLines[0]->setString("Player 1: 0 pts"); - for (std::size_t i = 1; i < _scoreLines.size(); ++i) - _scoreLines[i]->setString(""); - return; - } - std::ranges::sort(scores, [](const auto &a, const auto &b) { - return a.second > b.second; - }); - const std::size_t n = std::min(scores.size(), _scoreLines.size()); - for (std::size_t i = 0; i < n; ++i) { - const std::string name = "Player " + std::to_string(i + 1); - _scoreLines[i]->setString(name + ": " + std::to_string(scores[i].second) + " pts"); - } - for (std::size_t i = n; i < _scoreLines.size(); ++i) - _scoreLines[i]->setString(""); + if (_backRequested || _quitRequested) + return; + const auto w = _world.lock(); + if (!w) + return; + const auto scoresRefOrValue = w->getRoomScores(); + auto scores = std::vector(scoresRefOrValue.begin(), scoresRefOrValue.end()); + if (scores.empty()) { + if (!_scoreLines.empty() && _scoreLines.at(0)) + _scoreLines.at(0)->setString("Player 1: 0 pts"); + for (std::size_t i = 1; i < _scoreLines.size(); ++i) + if (_scoreLines.at(i)) + _scoreLines.at(i)->setString(""); + return; } + std::ranges::sort(scores, [](const auto &a, const auto &b) { + return a.second > b.second; + }); + const std::size_t n = std::min(scores.size(), _scoreLines.size()); + for (std::size_t i = 0; i < n; ++i) { + if (!_scoreLines.at(i)) + continue; + const std::string name = "Player " + std::to_string(i + 1); + _scoreLines.at(i)->setString(name + ": " + std::to_string(scores.at(i).second) + " pts"); + } + for (std::size_t i = n; i < _scoreLines.size(); ++i) + if (_scoreLines.at(i)) + _scoreLines.at(i)->setString(""); } void GameOverMenu::handleMousePressed(const InputFrame &frame) const diff --git a/server/src/admin/AdminConsole.cpp b/server/src/admin/AdminConsole.cpp new file mode 100644 index 00000000..4bf4af07 --- /dev/null +++ b/server/src/admin/AdminConsole.cpp @@ -0,0 +1,360 @@ +/* +** EPITECH PROJECT, 2026 +** RType +** File description: +** AdminConsole.cpp +*/ + +#include "AdminConsole.hpp" + +namespace +{ + [[nodiscard]] std::optional readRoomId( + std::istringstream &iss, const std::function(const std::string &)> &parseRoomId) + { + std::string s; + if (!(iss >> s)) + return std::nullopt; + return parseRoomId(s); + } + + [[nodiscard]] std::string toLower(std::string s) + { + std::ranges::transform(s, s.begin(), [](const unsigned char c) { + return static_cast(std::tolower(c)); + }); + return s; + } + +#ifndef _WIN32 + [[nodiscard]] bool tryReadLineWithTimeout(std::string &out, const int timeoutMs) + { + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(STDIN_FILENO, &readfds); + timeval tv{}; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (timeoutMs % 1000) * 1000; + if (const int r = select(STDIN_FILENO + 1, &readfds, nullptr, nullptr, &tv); r <= 0) + return false; + if (!FD_ISSET(STDIN_FILENO, &readfds)) + return false; + return static_cast(std::getline(std::cin, out)); + } +#else + [[nodiscard]] bool tryReadLineWithTimeout(std::string &out, int /*timeoutMs*/) + { + return static_cast(std::getline(std::cin, out)); + } +#endif +} // namespace + +namespace Net::Admin +{ + AdminConsole::AdminConsole(std::shared_ptr sessions, + std::shared_ptr rooms, ShutdownFn shutdownFn) + : _sessions(std::move(sessions)), _rooms(std::move(rooms)), _shutdown(std::move(shutdownFn)) + { + } + + AdminConsole::~AdminConsole() + { + stop(); + } + + void AdminConsole::start() + { + if (!_sessions || !_rooms) + return; + if (_running.exchange(true, std::memory_order_relaxed)) + return; + _thread = std::thread(&AdminConsole::run, this); + } + + void AdminConsole::stop() + { + _running.store(false, std::memory_order_relaxed); + if (_thread.joinable() && std::this_thread::get_id() != _thread.get_id()) + _thread.join(); + } + + bool AdminConsole::isAllDigits(const std::string &s) + { + if (s.empty()) + return false; + return std::ranges::all_of(s, [](unsigned char c) { + return std::isdigit(c) != 0; + }); + } + + std::optional AdminConsole::resolveSessionId(const std::string &who) const + { + if (who.empty()) + return std::nullopt; + + if (isAllDigits(who)) { + try { + return std::stoi(who); + } catch (...) { + return std::nullopt; + } + } + return _sessions ? _sessions->findSessionIdByUsername(who) : std::nullopt; + } + + std::optional AdminConsole::parseRoomId(const std::string &s) const + { + if (!isAllDigits(s)) + return std::nullopt; + try { + const auto v = std::stoul(s); + if (v > 0xFFFFFFFFu) + return std::nullopt; + return static_cast(v); + } catch (...) { + return std::nullopt; + } + } + + std::optional AdminConsole::resolveUsername(const std::string &who) const + { + if (who.empty()) + return std::nullopt; + + if (isAllDigits(who)) { + int sid = -1; + try { + sid = std::stoi(who); + } catch (...) { + return std::nullopt; + } + if (sid < 0) + return std::nullopt; + + const auto name = _sessions ? _sessions->getUsername(sid) : std::string(); + if (name.empty()) + return std::nullopt; + return name; + } + + return who; + } + + void AdminConsole::printHelp() + { + std::cout << "Admin commands:\n" + << " help\n" + << " status\n" + << " rooms\n" + << " sessions\n" + << " kickroom \n" + << " banroom \n" + << " unbanroom \n" + << " bans\n" + << " shutdown\n"; + } + + std::string AdminConsole::ipToString(const uint32_t ipNbo) + { + in_addr addr{}; + addr.s_addr = ipNbo; + char buf[INET_ADDRSTRLEN] = {0}; + const char *res = inet_ntop(AF_INET, &addr, buf, sizeof(buf)); + return res ? std::string(res) : std::string("?"); + } + + bool AdminConsole::parseIPv4(const std::string &s, uint32_t &outIpNbo) + { + in_addr addr{}; + if (inet_pton(AF_INET, s.c_str(), &addr) != 1) + return false; + outIpNbo = addr.s_addr; + return true; + } + + void AdminConsole::cmdStatus() const + { + const auto rooms = _rooms->listRooms(); + const auto sessions = _sessions->getAllSessions(); + std::cout << "rooms=" << rooms.size() << " sessions=" << sessions.size() + << " bans=" << _sessions->listBans().size() << "\n"; + } + + void AdminConsole::cmdRooms() const + { + const auto rooms = _rooms->listRooms(); + if (rooms.empty()) { + std::cout << "no rooms\n"; + return; + } + for (const auto &r : rooms) { + std::cout << "roomId=" << r.roomId << " name=\"" << r.roomName << "\" players=" << r.currentPlayers << "/" + << r.maxPlayers << "\n"; + } + } + + void AdminConsole::cmdSessions() const + { + const auto sessions = _sessions->getAllSessions(); + if (sessions.empty()) { + std::cout << "no sessions\n"; + return; + } + for (const auto &[id, tcp] : sessions) { + const auto *udp = _sessions->getUdpAddress(id); + const auto roomId = _rooms->getRoomIdOfPlayer(id); + const auto username = _sessions->getUsername(id); + std::cout << "sid=" << id << " tcp=" << ipToString(tcp.sin_addr.s_addr) << ":" << ntohs(tcp.sin_port); + if (udp) + std::cout << " udp=" << ipToString(udp->sin_addr.s_addr) << ":" << ntohs(udp->sin_port); + else + std::cout << " udp=-"; + std::cout << " authed=" << (_sessions->isAuthed(id) ? "1" : "0") << " user=\"" << username << "\"" + << " roomId=" << roomId << "\n"; + } + } + + bool AdminConsole::cmdKickRoom(const Engine::RoomId roomId, const std::string &who) const + { + const auto room = _rooms->getRoomById(roomId); + if (!room) { + std::cout << "not found\n"; + return false; + } + const auto sidOpt = resolveSessionId(who); + if (!sidOpt.has_value()) { + std::cout << "not found\n"; + return false; + } + const int sessionId = *sidOpt; + if (_rooms->getRoomIdOfPlayer(sessionId) != roomId) { + std::cout << "not found\n"; + return false; + } + (void) _rooms->removePlayer(sessionId); + std::cout << "room-kicked sid=" << sessionId << " roomId=" << roomId << "\n"; + return true; + } + + bool AdminConsole::cmdBanRoom(const Engine::RoomId roomId, const std::string &who) const + { + const auto room = _rooms->getRoomById(roomId); + if (!room) { + std::cout << "not found\n"; + return false; + } + const auto usernameOpt = resolveUsername(who); + if (!usernameOpt.has_value()) { + std::cout << "not found\n"; + return false; + } + const auto &username = *usernameOpt; + room->banUsername(username); + if (const auto sidOpt = _sessions->findSessionIdByUsername(username); sidOpt.has_value()) { + const int sid = *sidOpt; + if (_rooms->getRoomIdOfPlayer(sid) == roomId) + (void) _rooms->removePlayer(sid); + } + std::cout << "room-banned user=\"" << username << "\" roomId=" << roomId << "\n"; + return true; + } + + bool AdminConsole::cmdUnbanRoom(const Engine::RoomId roomId, const std::string &who) const + { + const auto room = _rooms->getRoomById(roomId); + if (!room) { + std::cout << "not found\n"; + return false; + } + const auto usernameOpt = resolveUsername(who); + if (!usernameOpt.has_value()) { + std::cout << "not found\n"; + return false; + } + const auto &username = *usernameOpt; + room->unbanUsername(username); + std::cout << "room-unbanned user=\"" << username << "\" roomId=" << roomId << "\n"; + return true; + } + + void AdminConsole::run() + { + printHelp(); + const auto usage = [](const char *u) { + std::cout << u << "\n"; + }; + + const std::unordered_map handlers = { + {"help", + [&](std::istringstream &) { + printHelp(); + }}, + {"status", + [&](std::istringstream &) { + cmdStatus(); + }}, + {"rooms", + [&](std::istringstream &) { + cmdRooms(); + }}, + {"sessions", + [&](std::istringstream &) { + cmdSessions(); + }}, + {"shutdown", + [&](std::istringstream &) { + if (_shutdown) + _shutdown(); + }}, + {"kickroom", + [&](std::istringstream &iss) { + const auto rid = readRoomId(iss, [&](const std::string &s) { + return parseRoomId(s); + }); + std::string who; + if (!rid || !(iss >> who)) + return usage("usage: kickroom "); + (void) cmdKickRoom(*rid, who); + }}, + {"banroom", + [&](std::istringstream &iss) { + const auto rid = readRoomId(iss, [&](const std::string &s) { + return parseRoomId(s); + }); + std::string who; + if (!rid || !(iss >> who)) + return usage("usage: banroom "); + (void) cmdBanRoom(*rid, who); + }}, + {"unbanroom", + [&](std::istringstream &iss) { + const auto rid = readRoomId(iss, [&](const std::string &s) { + return parseRoomId(s); + }); + std::string who; + if (!rid || !(iss >> who)) + return usage("usage: unbanroom "); + (void) cmdUnbanRoom(*rid, who); + }}, + }; + + while (_running.load(std::memory_order_relaxed)) { + std::string line; + if (!tryReadLineWithTimeout(line, 50)) + continue; + while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) + line.pop_back(); + if (line.empty()) + continue; + std::istringstream iss(line); + std::string cmd; + if (!(iss >> cmd)) + continue; + cmd = toLower(cmd); + if (const auto it = handlers.find(cmd); it != handlers.end()) + it->second(iss); + else + std::cout << "unknown command\n"; + } + } +} // namespace Net::Admin \ No newline at end of file diff --git a/server/src/admin/AdminConsole.hpp b/server/src/admin/AdminConsole.hpp new file mode 100644 index 00000000..348f218d --- /dev/null +++ b/server/src/admin/AdminConsole.hpp @@ -0,0 +1,170 @@ +/* +** EPITECH PROJECT, 2026 +** RType +** File description: +** AdminConsole.hpp +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "RoomManager.hpp" +#include "SessionManager.hpp" +#include + +#ifndef _WIN32 + #include + #include + #include +#else + #include + #include +#endif + +using ShutdownFn = std::function; ///> Shutdown function type +using Handler = std::function; ///> Command handler type + +namespace Net::Admin +{ + /** + * @class AdminConsole + * @brief Text-mode administrative console for server management. + */ + class AdminConsole { + public: + /** + * @brief Constructs an AdminConsole. + * @param sessions Shared pointer to the session manager. + * @param rooms Shared pointer to the room manager. + * @param shutdownFn Function to call for server shutdown. + */ + AdminConsole(std::shared_ptr sessions, std::shared_ptr rooms, + ShutdownFn shutdownFn); + + /** + * @brief Destructor for AdminConsole. + */ + ~AdminConsole(); + + /** + * @brief Starts the admin console thread. + */ + void start(); + + /** + * @brief Stops the admin console thread. + */ + void stop(); + + private: + /** + * @brief Main loop for the admin console thread. + */ + void run(); + + /** + * @brief Prints the help message. + */ + void printHelp(); + + /** + * @brief Displays server status information. + */ + void cmdStatus() const; + + /** + * @brief Lists all active rooms. + */ + void cmdRooms() const; + + /** + * @brief Lists all active sessions. + */ + void cmdSessions() const; + + /** + * @brief Kicks a user from a specific room. + * @param roomId ID of the room. + * @param who Session ID or username of the user to kick. + * @return True if the user was successfully kicked from the room, false otherwise. + */ + [[nodiscard]] bool cmdKickRoom(Engine::RoomId roomId, const std::string &who) const; + + /** + * @brief Bans a user from a specific room. + * @param roomId ID of the room. + * @param who Session ID or username of the user to ban. + * @return True if the user was successfully banned from the room, false otherwise. + */ + [[nodiscard]] bool cmdBanRoom(Engine::RoomId roomId, const std::string &who) const; + + /** + * @brief Unbans a user from a specific room. + * @param roomId ID of the room. + * @param who Session ID or username of the user to unban. + * @return True if the user was successfully unbanned from the room, false otherwise. + */ + [[nodiscard]] bool cmdUnbanRoom(Engine::RoomId roomId, const std::string &who) const; + + /** + * @brief Converts an IP address from network byte order to string format. + * @param ipNbo IP address in network byte order. + * @return IP address as a string. + */ + [[nodiscard]] static std::string ipToString(uint32_t ipNbo); + + /** + * @brief Parses an IPv4 address from string format to network byte order. + * @param s IP address as a string. + * @param outIpNbo Output parameter for the IP address in network byte order. + * @return True if parsing was successful, false otherwise. + */ + [[nodiscard]] static bool parseIPv4(const std::string &s, uint32_t &outIpNbo); + + /** + * @brief Checks if a string consists entirely of digits. + * @param s Input string. + * @return True if the string is all digits, false otherwise. + */ + [[nodiscard]] static bool isAllDigits(const std::string &s); + + /** + * @brief Resolves a session ID from a session ID string or username. + * @param who Session ID string or username. + * @return Optional session ID if found, std::nullopt otherwise. + */ + [[nodiscard]] std::optional resolveSessionId(const std::string &who) const; + + /** + * @brief Parses a room ID from a string. + * @param s Room ID as a string. + * @return Optional room ID if parsing was successful, std::nullopt otherwise. + */ + [[nodiscard]] std::optional parseRoomId(const std::string &s) const; + + /** + * @brief Resolves a username from a session ID string or username. + * @param who Session ID string or username. + * @return Optional username if found, std::nullopt otherwise. + */ + [[nodiscard]] std::optional resolveUsername(const std::string &who) const; + + std::shared_ptr _sessions; ///> Session manager + std::shared_ptr _rooms; ///> Room manager + ShutdownFn _shutdown; ///> Function to call for server shutdown + + std::atomic _running{false}; ///> Flag indicating if the console is running + std::thread _thread; ///> Thread for the admin console + }; +} // namespace Net::Admin diff --git a/server/src/network/SessionManager/Manager/SessionManager.cpp b/server/src/network/SessionManager/Manager/SessionManager.cpp index 0c31de8e..1ae1c34b 100644 --- a/server/src/network/SessionManager/Manager/SessionManager.cpp +++ b/server/src/network/SessionManager/Manager/SessionManager.cpp @@ -25,6 +25,59 @@ namespace using namespace Net::Server; static constexpr std::chrono::seconds kDefaultAuthTtl{24 * 60 * 60}; +static constexpr std::chrono::seconds kDefaultBanTtl{24 * 60 * 60}; + +void SessionManager::cleanupBansLocked() const +{ + const auto now = Clock::now(); + for (auto it = _bannedIpUntil.begin(); it != _bannedIpUntil.end();) { + if (now >= it->second) + it = _bannedIpUntil.erase(it); + else + ++it; + } +} + +void SessionManager::banIp(const uint32_t ip, std::chrono::seconds duration) +{ + if (duration.count() <= 0) + duration = kDefaultBanTtl; + std::unique_lock lock(_mutex); + cleanupBansLocked(); + _bannedIpUntil[ip] = Clock::now() + duration; +} + +void SessionManager::unbanIp(const uint32_t ip) +{ + std::unique_lock lock(_mutex); + _bannedIpUntil.erase(ip); +} + +bool SessionManager::isIpBanned(const uint32_t ip) const +{ + std::unique_lock lock(_mutex); + cleanupBansLocked(); + const auto it = _bannedIpUntil.find(ip); + if (it == _bannedIpUntil.end()) + return false; + return Clock::now() < it->second; +} + +std::vector> SessionManager::listBans() const +{ + std::unique_lock lock(_mutex); + cleanupBansLocked(); + + std::vector> out; + out.reserve(_bannedIpUntil.size()); + + const auto now = Clock::now(); + for (const auto &[ip, until] : _bannedIpUntil) { + const auto rem = (until > now) ? std::chrono::duration_cast(until - now).count() : 0; + out.emplace_back(ip, static_cast(std::max(0, rem))); + } + return out; +} bool SessionManager::isExpiredLocked(const int sessionId) const { @@ -42,6 +95,8 @@ void SessionManager::clearAuthLocked(const int sessionId) int SessionManager::getOrCreateSession(const sockaddr_in &address) { + if (isIpBanned(address.sin_addr.s_addr)) + return -1; const AddressKey key{address.sin_addr.s_addr, address.sin_port}; { @@ -63,6 +118,8 @@ int SessionManager::getOrCreateSession(const sockaddr_in &address) int SessionManager::getSessionId(const sockaddr_in &address) const { + if (isIpBanned(address.sin_addr.s_addr)) + return -1; const AddressKey key{address.sin_addr.s_addr, address.sin_port}; std::shared_lock lock(_mutex); @@ -149,6 +206,8 @@ uint64_t SessionManager::getUdpToken(const int sessionId) const bool SessionManager::bindUdp(const int sessionId, const sockaddr_in &udpAddr) { + if (isIpBanned(udpAddr.sin_addr.s_addr)) + return false; std::unique_lock lock(_mutex); if (!_idToTcpAddress.contains(sessionId)) @@ -179,6 +238,8 @@ const sockaddr_in *SessionManager::getUdpAddress(const int sessionId) const int SessionManager::getSessionIdFromUdp(const sockaddr_in &udpAddr) const { + if (isIpBanned(udpAddr.sin_addr.s_addr)) + return -1; const AddressKey key{udpAddr.sin_addr.s_addr, udpAddr.sin_port}; std::shared_lock lock(_mutex); @@ -225,6 +286,8 @@ bool SessionManager::isAuthed(const int sessionId) const bool SessionManager::consumeUdp(const sockaddr_in &addr) { + if (isIpBanned(addr.sin_addr.s_addr)) + return false; const AddressKey key{addr.sin_addr.s_addr, addr.sin_port}; const uint64_t now = nowNs(); @@ -294,4 +357,23 @@ std::string SessionManager::getUsername(const int sessionId) const if (const auto it = _identityById.find(sessionId); it != _identityById.end()) return it->second.username; return ""; +} + +std::optional SessionManager::findSessionIdByUsername(const std::string &username) const +{ + if (username.empty()) + return std::nullopt; + + std::shared_lock lock(_mutex); + for (const auto &[sid, ident] : _identityById) { + const auto itExp = _authExpiryById.find(sid); + if (itExp == _authExpiryById.end()) + continue; + if (Clock::now() >= itExp->second) + continue; + + if (ident.username == username) + return sid; + } + return std::nullopt; } \ No newline at end of file diff --git a/server/src/network/SessionManager/Manager/SessionManager.hpp b/server/src/network/SessionManager/Manager/SessionManager.hpp index c4725583..00bf8fb8 100644 --- a/server/src/network/SessionManager/Manager/SessionManager.hpp +++ b/server/src/network/SessionManager/Manager/SessionManager.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "Endian.hpp" #include "ISessionManager.hpp" #include "UserStorage.hpp" @@ -172,12 +173,58 @@ namespace Net::Server */ [[nodiscard]] std::string getUsername(int sessionId) const override; + /** + * @brief Find a session ID by username. + * @param username The username to search for. + * @return An optional containing the session ID if found, otherwise std::nullopt. + */ + [[nodiscard]] std::optional findSessionIdByUsername(const std::string &username) const override; + + /** + * @brief Ban an IP address (sin_addr.s_addr format). + * @param ip IPv4 address in network byte order. + * @param duration Duration of the ban. If <= 0, defaults to 24h. + */ + void banIp(uint32_t ip, std::chrono::seconds duration) override; + + /** + * @brief Remove a ban for an IP. + * @param ip IPv4 address in network byte order. + */ + void unbanIp(uint32_t ip) override; + + /** + * @brief Check if an IP is currently banned. + * @param ip IPv4 address in network byte order. + */ + [[nodiscard]] bool isIpBanned(uint32_t ip) const override; + + /** + * @brief Snapshot of banned IPs and remaining seconds. + */ + [[nodiscard]] std::vector> listBans() const override; + private: mutable std::shared_mutex _mutex{}; ///> Mutex for thread-safe access using Clock = std::chrono::steady_clock; ///> Clock type for time management - void clearAuthLocked(int sessionId); ///> Clear authentication data for a session ID - bool isExpiredLocked(int sessionId) const; ///> Check if the authentication for a session ID has expired + /** + * @brief Clear the authentication for a session ID (locked version). + * @param sessionId The ID of the session. + */ + void clearAuthLocked(int sessionId); + + /** + * @brief Check if the authentication for a session ID has expired. + * @param sessionId The ID of the session. + * @return True if the authentication has expired, false otherwise. + */ + [[nodiscard]] bool isExpiredLocked(int sessionId) const; + + /** + * @brief Clean up expired bans from the banned IP list. + */ + void cleanupBansLocked() const; std::unordered_map _identityById{}; ///> Identity storage std::unordered_map _authExpiryById{}; ///> Authentication expiry storage @@ -193,7 +240,8 @@ namespace Net::Server std::unordered_map _lastScoreById{}; ///> Last score storage - int _nextId = 1; ///> Next available session ID + mutable std::unordered_map _bannedIpUntil{}; ///> Banned IPs storage + int _nextId = 1; ///> Next available session ID /** * @brief Structure to manage UDP rate limiting tokens and timestamps. diff --git a/server/src/network/SessionManager/interfaces/ISessionManager.hpp b/server/src/network/SessionManager/interfaces/ISessionManager.hpp index 9f67c8c5..3b0512c1 100644 --- a/server/src/network/SessionManager/interfaces/ISessionManager.hpp +++ b/server/src/network/SessionManager/interfaces/ISessionManager.hpp @@ -174,6 +174,38 @@ namespace Net::Server * @param sessionId The ID of the session. * @return The username as a string. */ - virtual std::string getUsername(int sessionId) const = 0; + [[nodiscard]] virtual std::string getUsername(int sessionId) const = 0; + + /** + * @brief Find a session ID by username. + * @param username The username to search for. + * @return An optional containing the session ID if found, otherwise std::nullopt. + */ + [[nodiscard]] virtual std::optional findSessionIdByUsername(const std::string &username) const = 0; + + /** + * @brief Ban an IP address (sin_addr.s_addr format). + * @param ip IPv4 address in network byte order. + * @param duration Duration of the ban. If <= 0, defaults to 24h. + */ + virtual void banIp(uint32_t ip, std::chrono::seconds duration) = 0; + + /** + * @brief Remove a ban for an IP. + * @param ip IPv4 address in network byte order. + */ + virtual void unbanIp(uint32_t ip) = 0; + + /** + * @brief Check if an IP is currently banned. + * @param ip IPv4 address in network byte order. + */ + [[nodiscard]] virtual bool isIpBanned(uint32_t ip) const = 0; + + /** + * @brief Get a list of banned IPs and their remaining ban durations. + * @return A vector of pairs containing the banned IP and remaining seconds of the ban. + */ + [[nodiscard]] virtual std::vector> listBans() const = 0; }; } // namespace Net::Server diff --git a/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp b/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp index 8513a0fa..3379dc95 100644 --- a/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp +++ b/server/src/packet/PacketRouter/TCP/TCPPacketRouter.cpp @@ -67,7 +67,6 @@ namespace Net } const int sessionId = _sessions->getOrCreateSession(*addr); - const bool authFree = h.type == Protocol::TCP::HELLO || h.type == Protocol::TCP::AUTH_REGISTER || h.type == Protocol::TCP::AUTH_LOGIN; diff --git a/server/src/room/manager/RoomManager.cpp b/server/src/room/manager/RoomManager.cpp index 59fbfd83..c662612d 100644 --- a/server/src/room/manager/RoomManager.cpp +++ b/server/src/room/manager/RoomManager.cpp @@ -84,6 +84,10 @@ namespace Engine return false; try { const auto name = _sessionManager->getUsername(sessionId); + if (name.empty()) + return false; + if (room->isUsernameBanned(name)) + return false; room->join(sessionId, name); } catch (...) { return false; diff --git a/server/src/room/room/Room.cpp b/server/src/room/room/Room.cpp index 1c15b4be..f8368e06 100644 --- a/server/src/room/room/Room.cpp +++ b/server/src/room/room/Room.cpp @@ -104,6 +104,11 @@ namespace Engine void Room::join(const int sessionId, std::string_view username) { + std::scoped_lock lock(_sessionsMutex); + + if (!username.empty() && _bannedUsernames.contains(std::string(username))) + throw RoomError("{Room::join} username is banned from room"); + if (_sessions.contains(sessionId)) throw RoomError("{Room::join} session " + std::to_string(sessionId) + " already in room"); _sessions.insert(sessionId); @@ -113,6 +118,7 @@ namespace Engine void Room::leave(const int sessionId, std::string_view username) { + std::scoped_lock lock(_sessionsMutex); _sessions.erase(sessionId); _gameServer->onPlayerDisconnect(sessionId); auto &player = _roomData.playerNames; @@ -121,8 +127,29 @@ namespace Engine }); } - bool Room::empty() const + void Room::banUsername(std::string_view username) + { + std::scoped_lock lock(_sessionsMutex); + if (username.empty()) + return; + _bannedUsernames.insert(std::string(username)); + } + + void Room::unbanUsername(std::string_view username) + { + std::scoped_lock lock(_sessionsMutex); + _bannedUsernames.erase(std::string(username)); + } + + bool Room::isUsernameBanned(const std::string_view username) + { + std::scoped_lock lock(_sessionsMutex); + return _bannedUsernames.contains(std::string(username)); + } + + bool Room::empty() { + std::scoped_lock lock(_sessionsMutex); return _sessions.empty(); } @@ -136,8 +163,9 @@ namespace Engine return *_gameServer; } - size_t Room::getCurrentPlayers() const noexcept + size_t Room::getCurrentPlayers() noexcept { + std::scoped_lock lock(_sessionsMutex); return _sessions.size(); } @@ -158,6 +186,7 @@ namespace Engine RoomData Room::getRoomData() noexcept { + std::scoped_lock lock(_sessionsMutex); _roomData.currentPlayers = _sessions.size(); return _roomData; } diff --git a/server/src/room/room/Room.hpp b/server/src/room/room/Room.hpp index 783f7c0b..a325c414 100644 --- a/server/src/room/room/Room.hpp +++ b/server/src/room/room/Room.hpp @@ -108,11 +108,30 @@ namespace Engine */ void leave(int sessionId, std::string_view username); + /** + * @brief Bans a username from the room + * @param username The username to be banned + */ + void banUsername(std::string_view username); + + /** + * @brief Unbans a username from the room + * @param username The username to be unbanned + */ + void unbanUsername(std::string_view username); + + /** + * @brief Checks if a username is banned from the room + * @param username The username to check + * @return true if the username is banned, false otherwise + */ + [[nodiscard]] bool isUsernameBanned(std::string_view username); + /** * @brief Checks if the room is empty (no player sessions) * @return true if the room has no player sessions, false otherwise */ - [[nodiscard]] bool empty() const; + [[nodiscard]] bool empty(); /** * @brief Gets the set of player session IDs in the room * @return A constant reference to the set of session IDs @@ -129,7 +148,7 @@ namespace Engine * @brief Gets the current number of players in the room * @return The number of player sessions in the room */ - [[nodiscard]] size_t getCurrentPlayers() const noexcept; + [[nodiscard]] size_t getCurrentPlayers() noexcept; /** * @brief Gets the maximum number of players allowed in the room @@ -163,7 +182,8 @@ namespace Engine std::mutex _sessionsMutex; ///> Mutex for synchronizing access to the sessions set - std::unordered_set _sessions; ///> Set of player session IDs in the room + std::unordered_set _sessions; ///> Set of player session IDs in the room + std::unordered_set _bannedUsernames; ///> Set of banned usernames std::unique_ptr _gameServer = nullptr; ///> Unique pointer to the room's game server std::shared_ptr _sessionsManager; ///> Shared pointer to the session manager diff --git a/server/src/thread/ServerRuntime.cpp b/server/src/thread/ServerRuntime.cpp index 5dd1fc42..0019a2e0 100644 --- a/server/src/thread/ServerRuntime.cpp +++ b/server/src/thread/ServerRuntime.cpp @@ -62,6 +62,10 @@ void ServerRuntime::start() _stopRequested.store(false); _running.store(true); + _adminConsole = std::make_unique(_sessionManager, _roomManager, [this]() { + requestStop(); + }); + _adminConsole->start(); _receiverThread = std::thread(&ServerRuntime::runReceiver, this); _processorThread = std::thread(&ServerRuntime::runProcessor, this); _snapshotThread = std::thread(&ServerRuntime::runSnapshot, this); @@ -89,6 +93,9 @@ void ServerRuntime::stop() { requestStop(); + if (_adminConsole) + _adminConsole->stop(); + _roomManager->forEachRoom([](Engine::Room &room) { room.stop(); }); diff --git a/server/src/thread/ServerRuntime.hpp b/server/src/thread/ServerRuntime.hpp index adc16268..5e284e56 100644 --- a/server/src/thread/ServerRuntime.hpp +++ b/server/src/thread/ServerRuntime.hpp @@ -10,6 +10,7 @@ #include #include #include +#include "AdminConsole.hpp" #include "AuthService.hpp" #include "GameServer.hpp" #include "IServer.hpp" @@ -132,6 +133,7 @@ namespace Net::Thread std::shared_ptr _sessionManager; ///> Manages client sessions std::shared_ptr _roomManager; ///> Manages game rooms + std::unique_ptr _adminConsole; ///> Text-mode admin dashboard std::thread _receiverThread; ///> Thread for receiving packets std::thread _processorThread; ///> Thread for processing packets diff --git a/server/tests/packet/router/TCP/testTCProuter.cpp b/server/tests/packet/router/TCP/testTCProuter.cpp index 411a0e5a..64ba1f47 100644 --- a/server/tests/packet/router/TCP/testTCProuter.cpp +++ b/server/tests/packet/router/TCP/testTCProuter.cpp @@ -216,8 +216,55 @@ namespace bool _running = false; }; - class FakeSessions final : public Net::Server::ISessionManager { + class FakeSessions : public Net::Server::ISessionManager { public: + void banIp(uint32_t ip, std::chrono::seconds duration) override + { + const auto until = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + duration) + .count(); + _bans[ip] = static_cast(until); + } + + void unbanIp(uint32_t ip) override + { + _bans.erase(ip); + } + + [[nodiscard]] bool isIpBanned(uint32_t ip) const override + { + const auto it = _bans.find(ip); + if (it == _bans.end()) + return false; + + const auto now = static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); + + if (it->second <= now) + return false; + + return true; + } + + [[nodiscard]] std::vector> listBans() const override + { + std::vector> out; + out.reserve(_bans.size()); + for (const auto &kv : _bans) + out.push_back(kv); + return out; + } + + std::optional findSessionIdByUsername(const std::string &username) const override + { + for (const auto &kv : _identities) { + if (kv.second.username == username) + return kv.first; + } + return std::nullopt; + } + int getOrCreateSession(const sockaddr_in &addr) override { _sessions[_sessionId] = addr; @@ -371,6 +418,8 @@ namespace } private: + std::unordered_map _bans; + static uint64_t addrKey(const sockaddr_in &a) { const uint64_t ip = static_cast(a.sin_addr.s_addr); diff --git a/server/tests/packet/router/UDP/testUDProuter.cpp b/server/tests/packet/router/UDP/testUDProuter.cpp index f277a1f1..dfd68b1a 100644 --- a/server/tests/packet/router/UDP/testUDProuter.cpp +++ b/server/tests/packet/router/UDP/testUDProuter.cpp @@ -207,6 +207,49 @@ namespace class FakeSessions final : public Net::Server::ISessionManager { public: + void banIp(uint32_t ip, std::chrono::seconds duration) override + { + const auto until = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + duration) + .count(); + _bans[ip] = static_cast(until); + } + + void unbanIp(uint32_t ip) override + { + _bans.erase(ip); + } + + [[nodiscard]] bool isIpBanned(uint32_t ip) const override + { + const auto it = _bans.find(ip); + if (it == _bans.end()) + return false; + + const auto now = static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); + + if (it->second <= now) + return false; + + return true; + } + + [[nodiscard]] std::vector> listBans() const override + { + std::vector> out; + out.reserve(_bans.size()); + for (const auto &kv : _bans) + out.push_back(kv); + return out; + } + + std::optional findSessionIdByUsername(const std::string &username) const override + { + return std::nullopt; + } + void setConsumeUdp(bool v) { _consumeUdp = v; @@ -335,6 +378,8 @@ namespace bool _bindUdpOk = true; private: + std::unordered_map _bans; + bool _consumeUdp = true; int _sessionFromUdp = 10; bool _seqValid = true; diff --git a/server/tests/room/room/testRoom.cpp b/server/tests/room/room/testRoom.cpp index bba1b968..cafb3f29 100644 --- a/server/tests/room/room/testRoom.cpp +++ b/server/tests/room/room/testRoom.cpp @@ -172,6 +172,53 @@ namespace class FakeSessions final : public Net::Server::ISessionManager { public: + void banIp(uint32_t ip, std::chrono::seconds duration) override + { + const auto until = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + duration) + .count(); + _bans[ip] = static_cast(until); + } + + void unbanIp(uint32_t ip) override + { + _bans.erase(ip); + } + + [[nodiscard]] bool isIpBanned(uint32_t ip) const override + { + const auto it = _bans.find(ip); + if (it == _bans.end()) + return false; + + const auto now = static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count()); + + if (it->second <= now) + return false; + + return true; + } + + [[nodiscard]] std::vector> listBans() const override + { + std::vector> out; + out.reserve(_bans.size()); + for (const auto &kv : _bans) + out.push_back(kv); + return out; + } + + std::optional findSessionIdByUsername(const std::string &username) const override + { + for (const auto &kv : usernames) { + if (kv.second == username) + return kv.first; + } + return std::nullopt; + } + int getOrCreateSession(const sockaddr_in &addr) override { lastAddr = addr; @@ -277,6 +324,8 @@ namespace return (it == usernames.end()) ? std::string{} : it->second; } + std::unordered_map _bans; + sockaddr_in lastAddr{}; std::unordered_map udpTokens; std::unordered_map udpAddrs;