From 4cf80058297e5e16e9ffc96ea42f7256d124a022 Mon Sep 17 00:00:00 2001 From: past-due <30942300+past-due@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:54:44 -0400 Subject: [PATCH] Refactor admin status, initial support for admins changing settings via UI Store admin status as a per-player flag --- lib/netplay/netplay.cpp | 6 +++++ lib/netplay/netplay.h | 2 ++ src/multiint.cpp | 47 ++++++++++++++++++++++++++------------ src/multilobbycommands.cpp | 17 +++++++------- src/multilobbycommands.h | 2 ++ src/multiplay.cpp | 11 +++++++-- src/multistat.cpp | 15 ++++++++++++ src/multisync.cpp | 13 +++++++++++ 8 files changed, 88 insertions(+), 25 deletions(-) diff --git a/lib/netplay/netplay.cpp b/lib/netplay/netplay.cpp index fa2cd4a5432..b4f18602371 100644 --- a/lib/netplay/netplay.cpp +++ b/lib/netplay/netplay.cpp @@ -33,6 +33,7 @@ #include "src/console.h" #include "src/component.h" // FIXME: we need to handle this better #include "src/modding.h" // FIXME: we need to handle this better +#include "src/multilobbycommands.h" #include // for stats #include @@ -668,6 +669,7 @@ void NET_InitPlayer(uint32_t i, bool initPosition, bool initTeams, bool initSpec { NetPlay.players[i].isSpectator = false; } + NetPlay.players[i].isAdmin = false; } uint8_t NET_numHumanPlayers(void) @@ -736,6 +738,7 @@ static void NETSendNPlayerInfoTo(uint32_t *index, uint32_t indexLen, unsigned to NETint8_t(reinterpret_cast(&NetPlay.players[index[n]].difficulty)); NETuint8_t(reinterpret_cast(&NetPlay.players[index[n]].faction)); NETbool(&NetPlay.players[index[n]].isSpectator); + NETbool(&NetPlay.players[index[n]].isAdmin); } NETend(); ActivityManager::instance().updateMultiplayGameData(game, ingame, NETGameIsLocked()); @@ -2533,6 +2536,7 @@ static bool NETprocessSystemMessage(NETQUEUE playerQueue, uint8_t *type) int8_t difficulty = 0; uint8_t faction = FACTION_NORMAL; bool isSpectator = false; + bool isAdmin = false; bool error = false; NETbeginDecode(playerQueue, NET_PLAYER_INFO); @@ -2577,6 +2581,7 @@ static bool NETprocessSystemMessage(NETQUEUE playerQueue, uint8_t *type) NETint8_t(&difficulty); NETuint8_t(&faction); NETbool(&isSpectator); + NETbool(&isAdmin); auto newFactionId = uintToFactionID(faction); if (!newFactionId.has_value()) @@ -2596,6 +2601,7 @@ static bool NETprocessSystemMessage(NETQUEUE playerQueue, uint8_t *type) NetPlay.players[index].difficulty = static_cast(difficulty); NetPlay.players[index].faction = newFactionId.value(); NetPlay.players[index].isSpectator = isSpectator; + NetPlay.players[index].isAdmin = isAdmin; } debug(LOG_NET, "%s for player %u (%s)", n == 0 ? "Receiving MSG_PLAYER_INFO" : " and", (unsigned int)index, NetPlay.players[index].allocated ? "human" : "AI"); diff --git a/lib/netplay/netplay.h b/lib/netplay/netplay.h index e16e5d21b34..04c56310fc6 100644 --- a/lib/netplay/netplay.h +++ b/lib/netplay/netplay.h @@ -275,6 +275,7 @@ struct PLAYER bool autoGame; ///< if we are running a autogame (AI controls us) FactionID faction; ///< which faction the player has bool isSpectator; ///< whether this slot is a spectator slot + bool isAdmin; ///< whether this slot has admin privs // used on host-ONLY (not transmitted to other clients): std::shared_ptr> wzFiles = std::make_shared>(); ///< for each player, we keep track of map/mod download progress @@ -302,6 +303,7 @@ struct PLAYER IPtextAddress[0] = '\0'; faction = FACTION_NORMAL; isSpectator = false; + isAdmin = false; } }; diff --git a/src/multiint.cpp b/src/multiint.cpp index 86258b9b6ea..ee83bd73a69 100644 --- a/src/multiint.cpp +++ b/src/multiint.cpp @@ -1443,13 +1443,18 @@ static void addGameOptions() updateLimitIcons(); } +static bool isHostOrAdmin() +{ + return NetPlay.isHost || NetPlay.players[selectedPlayer].isAdmin; +} + // //////////////////////////////////////////////////////////////////////////// // Colour functions static bool safeToUseColour(unsigned player, unsigned otherPlayer) { - // Player wants to take the colour from otherPlayer. May not take from a human otherPlayer, unless we're the host. - return player == otherPlayer || NetPlay.isHost || !isHumanPlayer(otherPlayer); + // Player wants to take the colour from otherPlayer. May not take from a human otherPlayer, unless we're the host/admin. + return player == otherPlayer || isHostOrAdmin() || !isHumanPlayer(otherPlayer); } static int getPlayerTeam(int i) @@ -1982,7 +1987,7 @@ static std::set validPlayerIdxTargetsForPlayerPositionMove(uint32_t pl for (uint32_t i = 0; i < game.maxPlayers; i++) { if (player != i - && (NetPlay.isHost || !isHumanPlayer(i)) // host can move a player to any slot, player can only move to empty slots + && (isHostOrAdmin() || !isHumanPlayer(i)) // host/admin can move a player to any slot, player can only move to empty slots && !isSpectatorOnlySlot(i)) // target cannot be a spectator only slot (for player position changes) { validTargetPlayerIdx.insert(i); @@ -2114,9 +2119,9 @@ void WzMultiplayerOptionsTitleUI::openTeamChooser(uint32_t player) UDWORD i; int disallow = allPlayersOnSameTeam(player); - if (bIsTrueMultiplayerGame && NetPlay.isHost) + if (bIsTrueMultiplayerGame && isHostOrAdmin()) { - // allow configuration of all teams in true multiplayer mode (by host), even if they would block the game starting + // allow configuration of all teams in true multiplayer mode (by host/admin), even if they would block the game starting // (i.e. even if all players would be configured to be on the same team) disallow = -1; } @@ -2513,7 +2518,7 @@ bool recvTeamRequest(NETQUEUE queue) return false; } - if (whosResponsible(player) != queue.index) + if (whosResponsible(player) != queue.index && !NetPlay.players[queue.index].isAdmin) { HandleBadParam("NET_TEAMREQUEST given incorrect params.", player, queue.index); return false; @@ -2616,6 +2621,7 @@ bool changeReadyStatus(UBYTE player, bool bReady) static bool changePosition(UBYTE player, UBYTE position) { + ASSERT_HOST_ONLY(return false); ASSERT(player < MAX_PLAYERS, "Invalid player idx: %" PRIu8, player); int i; @@ -2767,7 +2773,7 @@ bool recvFactionRequest(NETQUEUE queue) return false; } - if (whosResponsible(player) != queue.index) + if (whosResponsible(player) != queue.index && !NetPlay.players[queue.index].isAdmin) { HandleBadParam("NET_FACTIONREQUEST given incorrect params.", player, queue.index); return false; @@ -2805,7 +2811,7 @@ bool recvColourRequest(NETQUEUE queue) return false; } - if (whosResponsible(player) != queue.index) + if (whosResponsible(player) != queue.index && !NetPlay.players[queue.index].isAdmin) { HandleBadParam("NET_COLOURREQUEST given incorrect params.", player, queue.index); return false; @@ -2813,7 +2819,7 @@ bool recvColourRequest(NETQUEUE queue) resetReadyStatus(false, true); - return changeColour(player, col, false); + return changeColour(player, col, NetPlay.players[queue.index].isAdmin); } bool recvPositionRequest(NETQUEUE queue) @@ -2835,7 +2841,7 @@ bool recvPositionRequest(NETQUEUE queue) return false; } - if (whosResponsible(player) != queue.index) + if (whosResponsible(player) != queue.index && !NetPlay.players[queue.index].isAdmin) { HandleBadParam("NET_POSITIONREQUEST given incorrect params.", player, queue.index); return false; @@ -3269,7 +3275,7 @@ static SwapPlayerIndexesResult recvSwapPlayerIndexes(NETQUEUE queue, const std:: static bool canChooseTeamFor(int i) { - return (i == selectedPlayer || NetPlay.isHost); + return (i == selectedPlayer || isHostOrAdmin()); } // //////////////////////////////////////////////////////////////////////////// @@ -3996,7 +4002,7 @@ class WzPlayerRow : public WIDGET widget->colorButton->addOnClickHandler([playerIdx, titleUI](W_BUTTON& button){ auto strongTitleUI = titleUI.lock(); ASSERT_OR_RETURN(, strongTitleUI != nullptr, "Title UI is gone?"); - if (playerIdx == selectedPlayer || NetPlay.isHost) + if (playerIdx == selectedPlayer || isHostOrAdmin()) { if (!NetPlay.players[playerIdx].isSpectator) // not a spectator { @@ -4031,11 +4037,12 @@ class WzPlayerRow : public WIDGET widget->playerInfo->addOnClickHandler([playerIdx, titleUI](W_BUTTON& button){ auto strongTitleUI = titleUI.lock(); ASSERT_OR_RETURN(, strongTitleUI != nullptr, "Title UI is gone?"); - if (playerIdx == selectedPlayer || NetPlay.isHost) + if (playerIdx == selectedPlayer || isHostOrAdmin()) { uint32_t player = playerIdx; - // host can move any player, clients can request to move themselves if there are available slots - if (((player == selectedPlayer && validPlayerIdxTargetsForPlayerPositionMove(player).size() > 0) || (NetPlay.players[player].allocated && NetPlay.isHost)) + // host/admin can move any player, clients can request to move themselves if there are available slots + if (((player == selectedPlayer && validPlayerIdxTargetsForPlayerPositionMove(player).size() > 0) || + (NetPlay.players[player].allocated && isHostOrAdmin())) && !locked.position && player < MAX_PLAYERS && !isSpectatorOnlySlot(player)) @@ -8827,6 +8834,7 @@ inline void to_json(nlohmann::json& j, const PLAYER& p) { // Do not persist IPtextAddress j["faction"] = static_cast(p.faction); j["isSpectator"] = p.isSpectator; + j["isAdmin"] = p.isAdmin; } inline void from_json(const nlohmann::json& j, PLAYER& p) { @@ -8857,6 +8865,15 @@ inline void from_json(const nlohmann::json& j, PLAYER& p) { auto factionUint = j.at("faction").get(); p.faction = static_cast(factionUint); // TODO CHECK p.isSpectator = j.at("isSpectator").get(); + if (j.contains("isAdmin")) + { + p.isAdmin = j.at("isAdmin").get(); + } + else + { + // default to the old (pre-4.5.4) default value of false + p.isAdmin = false; + } } static nlohmann::json DataHashToJSON() diff --git a/src/multilobbycommands.cpp b/src/multilobbycommands.cpp index 57f735007a1..3f312871973 100644 --- a/src/multilobbycommands.cpp +++ b/src/multilobbycommands.cpp @@ -57,6 +57,14 @@ bool removeLobbyAdminPublicKey(const std::string& publicKeyB64Str) return lobbyAdminPublicKeys.erase(publicKeyB64Str) > 0; } +// checks for specific identity being an admin +bool identityMatchesAdmin(const EcKey& identity) +{ + std::string senderIdentityHash = identity.publicHashString(); + std::string senderPublicKeyB64 = base64Encode(identity.toBytes(EcKey::Public)); + return lobbyAdminPublicKeys.count(senderPublicKeyB64) != 0 || lobbyAdminPublicHashStrings.count(senderIdentityHash) != 0; +} + // NOTE: **IMPORTANT** this should *NOT* be used for determining whether a sender has permission to execute admin commands // (Use senderHasLobbyCommandAdminPrivs instead) static bool senderApparentlyMatchesAdmin(uint32_t playerIdx) @@ -75,14 +83,7 @@ static bool senderApparentlyMatchesAdmin(uint32_t playerIdx) { return false; } - std::string senderIdentityHash = identity.publicHashString(); - std::string senderPublicKeyB64 = base64Encode(identity.toBytes(EcKey::Public)); - if (lobbyAdminPublicKeys.count(senderPublicKeyB64) == 0 && lobbyAdminPublicHashStrings.count(senderIdentityHash) == 0) - { - return false; // identity hash is not in permitted lists - } - - return true; + return identityMatchesAdmin(identity); } // **THIS** is the function that should be used to determine whether a sender currently has permission to execute admin commands diff --git a/src/multilobbycommands.h b/src/multilobbycommands.h index 112bab5d0bf..b7ba7b00177 100644 --- a/src/multilobbycommands.h +++ b/src/multilobbycommands.h @@ -52,6 +52,8 @@ void cmdInterfaceLogChatMsg(const NetworkTextMessage& message, const char* log_p bool processChatLobbySlashCommands(const NetworkTextMessage& message, HostLobbyOperationsInterface& cmdInterface); +bool identityMatchesAdmin(const EcKey& identity); + bool addLobbyAdminIdentityHash(const std::string& playerIdentityHash); bool removeLobbyAdminIdentityHash(const std::string& playerIdentityHash); diff --git a/src/multiplay.cpp b/src/multiplay.cpp index 932937c6bb9..5e363449435 100644 --- a/src/multiplay.cpp +++ b/src/multiplay.cpp @@ -1031,6 +1031,7 @@ HandleMessageAction getMessageHandlingAction(NETQUEUE& queue, uint8_t type) } bool senderIsSpectator = NetPlay.players[queue.index].isSpectator; + bool senderIsAdmin = NetPlay.players[queue.index].isAdmin; if (type > NET_MIN_TYPE && type < NET_MAX_TYPE) { @@ -1049,10 +1050,16 @@ HandleMessageAction getMessageHandlingAction(NETQUEUE& queue, uint8_t type) } break; case NET_KICK: - case NET_AITEXTMSG: - case NET_BEACONMSG: case NET_TEAMREQUEST: // spectators should not be allowed to request a team / non-spectator slot status + case NET_FACTIONREQUEST: case NET_POSITIONREQUEST: + if (senderIsSpectator && !senderIsAdmin) + { + return HandleMessageAction::Disallow_And_Kick_Sender; + } + break; + case NET_AITEXTMSG: + case NET_BEACONMSG: if (senderIsSpectator) { return HandleMessageAction::Disallow_And_Kick_Sender; diff --git a/src/multistat.cpp b/src/multistat.cpp index d0b172304f4..10a661ed75b 100644 --- a/src/multistat.cpp +++ b/src/multistat.cpp @@ -40,6 +40,7 @@ #include "multistat.h" #include "urlrequest.h" #include "stdinreader.h" +#include "multilobbycommands.h" #include #include @@ -377,12 +378,26 @@ bool multiStatsSetIdentity(uint32_t playerIndex, const EcKey::Key &identity, boo { // Record that the identity has been verified ingame.VerifiedIdentity[playerIndex] = true; + if (NetPlay.isHost) + { + NetPlay.players[playerIndex].isAdmin = identityMatchesAdmin(playerStats[playerIndex].identity); + // Do not broadcast player info here, as it's assumed caller will do it + } // Do *not* output to stdinterface here - the join event is still being processed } else { ingame.VerifiedIdentity[playerIndex] = false; + if (NetPlay.isHost) + { + // when verified identity is cleared, so is admin status (until new identity is verified) + if (NetPlay.players[playerIndex].isAdmin) + { + NetPlay.players[playerIndex].isAdmin = false; + NETBroadcastPlayerInfo(playerIndex); + } + } // Output to stdinterface, if enabled if (!identity.empty()) diff --git a/src/multisync.cpp b/src/multisync.cpp index 3ca72cf608d..d1233f90e89 100644 --- a/src/multisync.cpp +++ b/src/multisync.cpp @@ -38,6 +38,7 @@ #include "multistat.h" #include "multirecv.h" #include "stdinreader.h" +#include "multilobbycommands.h" #include @@ -330,6 +331,18 @@ bool recvPing(NETQUEUE queue) // Record that they've verified the identity ingame.VerifiedIdentity[sender] = true; + if (NetPlay.isHost) + { + // check if verified identity is an admin, and handle changes to admin status + bool oldIsAdminStatus = NetPlay.players[sender].isAdmin; + NetPlay.players[sender].isAdmin = identityMatchesAdmin(senderIdentity); + if (oldIsAdminStatus != NetPlay.players[sender].isAdmin) + { + // then send info about admin status changes to all players + NETBroadcastPlayerInfo(sender); + } + } + // Output to stdinterface, if enabled std::string senderPublicKeyB64 = base64Encode(senderIdentity.toBytes(EcKey::Public)); std::string senderIdentityHash = senderIdentity.publicHashString();