From b9f658dddf26b828dd9adf39af945bc73684d2ca Mon Sep 17 00:00:00 2001 From: Dvir <39403717+dvir001@users.noreply.github.com> Date: Sun, 10 Dec 2023 19:50:24 +0200 Subject: [PATCH] Merge pull request #625 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Basic rate limiting for chat messages (#21907) * Yep * Merge remote-tracking branch 'upstream/master' into 23-11-26-chat-rat… --- .../Chat/Managers/ChatManager.RateLimit.cs | 84 +++++++++++++++++++ Content.Server/Chat/Managers/ChatManager.cs | 15 +++- Content.Server/Chat/Managers/IChatManager.cs | 8 ++ Content.Server/Chat/Systems/ChatSystem.cs | 6 ++ Content.Shared.Database/LogType.cs | 5 ++ Content.Shared/CCVar/CCVars.cs | 33 ++++++++ .../en-US/chat/managers/chat-manager.ftl | 3 + 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 Content.Server/Chat/Managers/ChatManager.RateLimit.cs diff --git a/Content.Server/Chat/Managers/ChatManager.RateLimit.cs b/Content.Server/Chat/Managers/ChatManager.RateLimit.cs new file mode 100644 index 00000000000..cf87ab6322d --- /dev/null +++ b/Content.Server/Chat/Managers/ChatManager.RateLimit.cs @@ -0,0 +1,84 @@ +using System.Runtime.InteropServices; +using Content.Shared.CCVar; +using Content.Shared.Database; +using Robust.Shared.Enums; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server.Chat.Managers; + +internal sealed partial class ChatManager +{ + private readonly Dictionary _rateLimitData = new(); + + public bool HandleRateLimit(ICommonSession player) + { + ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(_rateLimitData, player, out _); + var time = _gameTiming.RealTime; + if (datum.CountExpires < time) + { + // Period expired, reset it. + var periodLength = _configurationManager.GetCVar(CCVars.ChatRateLimitPeriod); + datum.CountExpires = time + TimeSpan.FromSeconds(periodLength); + datum.Count = 0; + datum.Announced = false; + } + + var maxCount = _configurationManager.GetCVar(CCVars.ChatRateLimitCount); + datum.Count += 1; + + if (datum.Count <= maxCount) + return true; + + // Breached rate limits, inform admins if configured. + if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins)) + { + if (datum.NextAdminAnnounce < time) + { + SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name))); + var delay = _configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdminsDelay); + datum.NextAdminAnnounce = time + TimeSpan.FromSeconds(delay); + } + } + + if (!datum.Announced) + { + DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true); + _adminLogger.Add(LogType.ChatRateLimited, LogImpact.Medium, $"Player {player} breached chat rate limits"); + + datum.Announced = true; + } + + return false; + } + + private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Disconnected) + _rateLimitData.Remove(e.Session); + } + + private struct RateLimitDatum + { + /// + /// Time stamp (relative to ) this rate limit period will expire at. + /// + public TimeSpan CountExpires; + + /// + /// How many messages have been sent in the current rate limit period. + /// + public int Count; + + /// + /// Have we announced to the player that they've been blocked in this rate limit period? + /// + public bool Announced; + + /// + /// Time stamp (relative to ) of the + /// next time we can send an announcement to admins about rate limit breach. + /// + public TimeSpan NextAdminAnnounce; + } +} diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index 51aa1e3afc3..486fcc135d1 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -11,10 +11,12 @@ using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Mind; +using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Replays; +using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Chat.Managers @@ -22,7 +24,7 @@ namespace Content.Server.Chat.Managers /// /// Dispatches chat messages to clients. /// - internal sealed class ChatManager : IChatManager + internal sealed partial class ChatManager : IChatManager { private static readonly Dictionary PatronOocColors = new() { @@ -41,6 +43,8 @@ internal sealed class ChatManager : IChatManager [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly INetConfigurationManager _netConfigManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; /// /// The maximum length a player-sent message can be sent @@ -59,6 +63,8 @@ public void Initialize() _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true); + + _playerManager.PlayerStatusChanged += PlayerStatusChanged; } private void OnOocEnabledChanged(bool val) @@ -178,6 +184,9 @@ public void SendHookOOC(string sender, string message) /// The type of message. public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type) { + if (!HandleRateLimit(player)) + return; + // Check if message exceeds the character limit if (message.Length > MaxMessageLength) { @@ -215,7 +224,7 @@ private void SendOOC(ICommonSession player, string message) } Color? colorOverride = null; - var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message))); + var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName", player.Name), ("message", FormattedMessage.EscapeText(message))); if (_adminManager.HasAdminFlag(player, AdminFlags.Admin)) { var prefs = _preferencesManager.GetPreferences(player.UserId); @@ -224,7 +233,7 @@ private void SendOOC(ICommonSession player, string message) if (player.ConnectedClient.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor)) { - wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message))); + wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor), ("playerName", player.Name), ("message", FormattedMessage.EscapeText(message))); } //TODO: player.Name color, this will need to change the structure of the MsgChatMessage diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs index 34f16fe3111..e5fa8d5f4dc 100644 --- a/Content.Server/Chat/Managers/IChatManager.cs +++ b/Content.Server/Chat/Managers/IChatManager.cs @@ -41,5 +41,13 @@ void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessag [return: NotNullIfNotNull(nameof(author))] ChatUser? EnsurePlayer(NetUserId? author); + + /// + /// Called when a player sends a chat message to handle rate limits. + /// Will update counts and do necessary actions if breached. + /// + /// The player sending a chat message. + /// False if the player has violated rate limits and should be blocked from sending further messages. + bool HandleRateLimit(ICommonSession player); } } diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 61db8d82115..92ba01ee04d 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -183,6 +183,9 @@ public void TrySendInGameICMessage( return; } + if (player != null && !_chatManager.HandleRateLimit(player)) + return; + // Sus if (player?.AttachedEntity is { Valid: true } entity && source != entity) { @@ -267,6 +270,9 @@ public void TrySendInGameOOCMessage( if (!CanSendInGame(message, shell, player)) return; + if (player != null && !_chatManager.HandleRateLimit(player)) + return; + // It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending // in-game IC messages. if (player?.AttachedEntity is not { Valid: true } entity || source != entity) diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index cfe03b488d5..a0f789ca0b8 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -90,6 +90,11 @@ public enum LogType DeviceLinking = 85, Tile = 86, + /// + /// A client has sent too many chat messages recently and is temporarily blocked from sending more. + /// + ChatRateLimited = 87, + // Frontier Station Spesific ATMUsage = 200, ShipYardUsage = 201, diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index a91cba55bbd..63292a517a0 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1480,6 +1480,39 @@ public static readonly CVarDef * CHAT */ + /// + /// Chat rate limit values are accounted in periods of this size (seconds). + /// After the period has passed, the count resets. + /// + /// + public static readonly CVarDef ChatRateLimitPeriod = + CVarDef.Create("chat.rate_limit_period", 2, CVar.SERVERONLY); + + /// + /// How many chat messages are allowed in a single rate limit period. + /// + /// + /// The total rate limit throughput per second is effectively + /// divided by . + /// + /// + /// + public static readonly CVarDef ChatRateLimitCount = + CVarDef.Create("chat.rate_limit_count", 10, CVar.SERVERONLY); + + /// + /// If true, announce when a player breached chat rate limit to game administrators. + /// + /// + public static readonly CVarDef ChatRateLimitAnnounceAdmins = + CVarDef.Create("chat.rate_limit_announce_admins", true, CVar.SERVERONLY); + + /// + /// Minimum delay (in seconds) between announcements from . + /// + public static readonly CVarDef ChatRateLimitAnnounceAdminsDelay = + CVarDef.Create("chat.rate_limit_announce_admins_delay", 15, CVar.SERVERONLY); + public static readonly CVarDef ChatMaxMessageLength = CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED); diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index ac8e434e2da..2690a6dfdb2 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -47,6 +47,9 @@ chat-manager-send-hook-ooc-wrap-message = OOC: [bold](D){$senderName}:[/bold] {$ chat-manager-dead-channel-name = DEAD chat-manager-admin-channel-name = ADMIN +chat-manager-rate-limited = You are sending messages too quickly! +chat-manager-rate-limit-admin-announcement = Player { $player } breached chat rate limits. Watch them if this is a regular occurence. + ## Speech verbs for chat chat-speech-verb-suffix-exclamation = !