fix: route only the chat messages a client sends

A world server passed every routed packet on to the chat server under
whatever message ID it carried. It now passes on the messages a client
sends, and SHOW_ALL only for the level the showall command needs. The
message ID is read as the full 32 bit number, the packet has to be long
enough to hold a chat packet and the sender needs a character.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 12:56:54 -06:00
parent 5f3b10f466
commit b88168f2ed
4 changed files with 133 additions and 7 deletions

67
dNet/ChatRouting.h Normal file
View File

@@ -0,0 +1,67 @@
#ifndef __CHATROUTING__H__
#define __CHATROUTING__H__
#include <cstdint>
#include <optional>
#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<eGameMasterLevel> 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__

View File

@@ -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<MessageType::Chat>(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);
}
};

View File

@@ -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)

View File

@@ -0,0 +1,47 @@
#include <gtest/gtest.h>
#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<MessageType::Chat>(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));
}