diff --git a/dNet/ChatRouting.h b/dNet/ChatRouting.h new file mode 100644 index 000000000..663dc8672 --- /dev/null +++ b/dNet/ChatRouting.h @@ -0,0 +1,67 @@ +#ifndef __CHATROUTING__H__ +#define __CHATROUTING__H__ + +#include +#include + +#include "MessageType/Chat.h" +#include "eGameMasterLevel.h" + +namespace ChatRouting { + // A routed packet is the world header, the size of the chat packet and then the chat packet itself. + // The chat packet's message ID sits at this offset and its contents start after its own header. + constexpr uint32_t MessageIdOffset = 14; + constexpr uint32_t ContentOffset = 23; + + /** + * The chat messages a client sends through a world server, with the GM level each one needs. + * Every other chat message only ever comes from a server, so it has no level and is not routed. + */ + constexpr std::optional GetRequiredGMLevel(const MessageType::Chat messageId) { + switch (messageId) { + case MessageType::Chat::GENERAL_CHAT_MESSAGE: + case MessageType::Chat::PRIVATE_CHAT_MESSAGE: + case MessageType::Chat::USER_CHANNEL_CHAT_MESSAGE: + case MessageType::Chat::ADD_FRIEND_REQUEST: + case MessageType::Chat::ADD_FRIEND_RESPONSE: + case MessageType::Chat::REMOVE_FRIEND: + case MessageType::Chat::GET_FRIENDS_LIST: + case MessageType::Chat::ADD_IGNORE: + case MessageType::Chat::REMOVE_IGNORE: + case MessageType::Chat::GET_IGNORE_LIST: + case MessageType::Chat::TEAM_MISSED_INVITE_CHECK: + case MessageType::Chat::TEAM_INVITE: + case MessageType::Chat::TEAM_INVITE_RESPONSE: + case MessageType::Chat::TEAM_KICK: + case MessageType::Chat::TEAM_LEAVE: + case MessageType::Chat::TEAM_SET_LOOT: + case MessageType::Chat::TEAM_SET_LEADER: + case MessageType::Chat::TEAM_GET_STATUS: + case MessageType::Chat::GUILD_CREATE: + case MessageType::Chat::GUILD_INVITE: + case MessageType::Chat::GUILD_INVITE_RESPONSE: + case MessageType::Chat::GUILD_LEAVE: + case MessageType::Chat::GUILD_KICK: + case MessageType::Chat::GUILD_GET_STATUS: + case MessageType::Chat::GUILD_GET_ALL: + case MessageType::Chat::MAIL: + case MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE: + case MessageType::Chat::REQUEST_MINIMUM_CHAT_MODE_PRIVATE: + case MessageType::Chat::WHO: + return eGameMasterLevel::CIVILIAN; + // The same level the showall chat command needs + case MessageType::Chat::SHOW_ALL: + case MessageType::Chat::GM_CLOSE_PRIVATE_CHAT_WINDOW: + return eGameMasterLevel::JUNIOR_MODERATOR; + default: + return std::nullopt; + } + } + + constexpr bool CanRoute(const MessageType::Chat messageId, const eGameMasterLevel senderLevel) { + const auto requiredLevel = GetRequiredGMLevel(messageId); + return requiredLevel && senderLevel >= *requiredLevel; + } +} + +#endif //!__CHATROUTING__H__ diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index 21dfd8078..6d931eea0 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -70,6 +70,7 @@ #include "Entity.h" #include "Character.h" #include "ChatPackets.h" +#include "ChatRouting.h" #include "ScriptedActivityComponent.h" #include "eReplicaComponentType.h" #include "WorldRoutePacket.h" @@ -1564,14 +1565,24 @@ namespace { return; } - //We need to insert the player's objectID so the chat server can find who originated this request: - LWOOBJID objectID = 0; - auto user = UserManager::Instance()->GetUser(sysAddr); - if (user) { - const auto* const lastChar = user->GetLastUsedChar(); - if (lastChar) objectID = lastChar->GetObjectID(); + // The chat server looks the sender up by this ID, so there has to be a character to send as + auto* const user = UserManager::Instance()->GetUser(sysAddr); + const auto* const lastChar = user ? user->GetLastUsedChar() : nullptr; + if (!lastChar) { + LOG("Tried to route a chat packet without a character."); + return; } + // The whole 32 bit ID is checked, the chat server gets its first byte + const auto chatMessageId = static_cast(routedMessageID); + if (!ChatRouting::CanRoute(chatMessageId, lastChar->GetGMLevel())) { + LOG("Character %llu tried to route chat message %u, which they may not send.", lastChar->GetObjectID(), routedMessageID); + return; + } + + //We need to insert the player's objectID so the chat server can find who originated this request: + const LWOOBJID objectID = lastChar->GetObjectID(); + ChatServerLink::Send(ToChat(objectID), SYSTEM_PRIORITY, RELIABLE_ORDERED); } }; diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index 81c2710ff..d4509a568 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -18,7 +18,8 @@ SET(DGAMEMESSAGES_TESTS "GameMessageDecoderTests.cpp" "HotPropertyMessageTests.cpp" "ClientMessagesTests.cpp" - "LoadSequenceTests.cpp") + "LoadSequenceTests.cpp" + "ChatRoutingTests.cpp") # Get the folder name and prepend it to the files above get_filename_component(thisFolderName ${CMAKE_CURRENT_SOURCE_DIR} NAME) diff --git a/tests/dGameTests/dGameMessagesTests/ChatRoutingTests.cpp b/tests/dGameTests/dGameMessagesTests/ChatRoutingTests.cpp new file mode 100644 index 000000000..6372bc35b --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/ChatRoutingTests.cpp @@ -0,0 +1,47 @@ +#include + +#include "ChatRouting.h" + +TEST(ChatRoutingTest, PlayersRouteTheMessagesTheClientSends) { + const auto civilian = eGameMasterLevel::CIVILIAN; + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::GENERAL_CHAT_MESSAGE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::PRIVATE_CHAT_MESSAGE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::ADD_FRIEND_REQUEST, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::ADD_FRIEND_RESPONSE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::REMOVE_FRIEND, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::GET_FRIENDS_LIST, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::ADD_IGNORE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::REMOVE_IGNORE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::GET_IGNORE_LIST, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_INVITE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_INVITE_RESPONSE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_KICK, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_LEAVE, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_SET_LOOT, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_SET_LEADER, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::TEAM_GET_STATUS, civilian)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::WHO, civilian)); +} + +TEST(ChatRoutingTest, ServerMessagesAreNeverRouted) { + for (const auto level : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::OPERATOR }) { + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::LOGIN_SESSION_NOTIFY, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::GMLEVEL_UPDATE, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::GM_KICK, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::GM_ANNOUNCE, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::GM_MUTE, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::WORLD_ROUTE_PACKET, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::ACHIEVEMENT_NOTIFY, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::UNEXPECTED_DISCONNECT, level)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::CREATE_TEAM, level)); + // Not a message at all + EXPECT_FALSE(ChatRouting::CanRoute(static_cast(0x100 | 1), level)); + } +} + +TEST(ChatRoutingTest, ShowAllNeedsTheChatCommandLevel) { + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::SHOW_ALL, eGameMasterLevel::CIVILIAN)); + EXPECT_FALSE(ChatRouting::CanRoute(MessageType::Chat::SHOW_ALL, eGameMasterLevel::FORUM_MODERATOR)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::SHOW_ALL, eGameMasterLevel::JUNIOR_MODERATOR)); + EXPECT_TRUE(ChatRouting::CanRoute(MessageType::Chat::SHOW_ALL, eGameMasterLevel::OPERATOR)); +}