]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Basic rate limiting for chat messages (#21907)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Mon, 27 Nov 2023 03:08:30 +0000 (04:08 +0100)
committerGitHub <noreply@github.com>
Mon, 27 Nov 2023 03:08:30 +0000 (14:08 +1100)
Content.Server/Chat/Managers/ChatManager.RateLimit.cs [new file with mode: 0644]
Content.Server/Chat/Managers/ChatManager.cs
Content.Server/Chat/Managers/IChatManager.cs
Content.Server/Chat/Systems/ChatSystem.cs
Content.Shared.Database/LogType.cs
Content.Shared/CCVar/CCVars.cs
Resources/Locale/en-US/chat/managers/chat-manager.ftl

diff --git a/Content.Server/Chat/Managers/ChatManager.RateLimit.cs b/Content.Server/Chat/Managers/ChatManager.RateLimit.cs
new file mode 100644 (file)
index 0000000..cf87ab6
--- /dev/null
@@ -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<ICommonSession, RateLimitDatum> _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
+    {
+        /// <summary>
+        /// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
+        /// </summary>
+        public TimeSpan CountExpires;
+
+        /// <summary>
+        /// How many messages have been sent in the current rate limit period.
+        /// </summary>
+        public int Count;
+
+        /// <summary>
+        /// Have we announced to the player that they've been blocked in this rate limit period?
+        /// </summary>
+        public bool Announced;
+
+        /// <summary>
+        /// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
+        /// next time we can send an announcement to admins about rate limit breach.
+        /// </summary>
+        public TimeSpan NextAdminAnnounce;
+    }
+}
index 51aa1e3afc32b73a1ebd39ee6a19bc5bbd08d6b4..b28fd881862cbd49a94d1ec15ea5928bd4723972 100644 (file)
@@ -11,10 +11,12 @@ using Content.Shared.CCVar;
 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
     /// <summary>
     ///     Dispatches chat messages to clients.
     /// </summary>
-    internal sealed class ChatManager : IChatManager
+    internal sealed partial class ChatManager : IChatManager
     {
         private static readonly Dictionary<string, string> PatronOocColors = new()
         {
@@ -41,6 +43,8 @@ namespace Content.Server.Chat.Managers
         [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!;
 
         /// <summary>
         /// The maximum length a player-sent message can be sent
@@ -59,6 +63,8 @@ namespace Content.Server.Chat.Managers
 
             _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
             _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
+
+            _playerManager.PlayerStatusChanged += PlayerStatusChanged;
         }
 
         private void OnOocEnabledChanged(bool val)
@@ -178,6 +184,9 @@ namespace Content.Server.Chat.Managers
         /// <param name="type">The type of message.</param>
         public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
         {
+            if (!HandleRateLimit(player))
+                return;
+
             // Check if message exceeds the character limit
             if (message.Length > MaxMessageLength)
             {
index 34f16fe31113735e028c67200854342a7ec3e285..e5fa8d5f4dc0077cbf1b3a24290eac3df9dcad49 100644 (file)
@@ -41,5 +41,13 @@ namespace Content.Server.Chat.Managers
 
         [return: NotNullIfNotNull(nameof(author))]
         ChatUser? EnsurePlayer(NetUserId? author);
+
+        /// <summary>
+        /// Called when a player sends a chat message to handle rate limits.
+        /// Will update counts and do necessary actions if breached.
+        /// </summary>
+        /// <param name="player">The player sending a chat message.</param>
+        /// <returns>False if the player has violated rate limits and should be blocked from sending further messages.</returns>
+        bool HandleRateLimit(ICommonSession player);
     }
 }
index d98424d0a544d104be53d04cb44b851bd1d201e3..578444a4648084bb3ad3ab1a9e06e573d9715ca5 100644 (file)
@@ -183,6 +183,9 @@ public sealed partial class ChatSystem : SharedChatSystem
             return;
         }
 
+        if (player != null && !_chatManager.HandleRateLimit(player))
+            return;
+
         // Sus
         if (player?.AttachedEntity is { Valid: true } entity && source != entity)
         {
@@ -267,6 +270,9 @@ public sealed partial class ChatSystem : SharedChatSystem
         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)
index 6671865472c077b2cade31601908075f4568ffff..1d5dbcc3b4828436349817cf82ab16efdaca6192 100644 (file)
@@ -89,4 +89,9 @@ public enum LogType
     ItemConfigure = 84,
     DeviceLinking = 85,
     Tile = 86,
+
+    /// <summary>
+    /// A client has sent too many chat messages recently and is temporarily blocked from sending more.
+    /// </summary>
+    ChatRateLimited = 87,
 }
index 5918729147df4895c27c91d80a8d3288bf57a2e6..a6f3f5df726ead75aeecbb0c9d48d3e8e46e7f5c 100644 (file)
@@ -1438,6 +1438,39 @@ namespace Content.Shared.CCVar
          * CHAT
          */
 
+        /// <summary>
+        /// Chat rate limit values are accounted in periods of this size (seconds).
+        /// After the period has passed, the count resets.
+        /// </summary>
+        /// <seealso cref="ChatRateLimitCount"/>
+        public static readonly CVarDef<int> ChatRateLimitPeriod =
+            CVarDef.Create("chat.rate_limit_period", 2, CVar.SERVERONLY);
+
+        /// <summary>
+        /// How many chat messages are allowed in a single rate limit period.
+        /// </summary>
+        /// <remarks>
+        /// The total rate limit throughput per second is effectively
+        /// <see cref="ChatRateLimitCount"/> divided by <see cref="ChatRateLimitCount"/>.
+        /// </remarks>
+        /// <seealso cref="ChatRateLimitPeriod"/>
+        /// <seealso cref="ChatRateLimitAnnounceAdmins"/>
+        public static readonly CVarDef<int> ChatRateLimitCount =
+            CVarDef.Create("chat.rate_limit_count", 10, CVar.SERVERONLY);
+
+        /// <summary>
+        /// If true, announce when a player breached chat rate limit to game administrators.
+        /// </summary>
+        /// <seealso cref="ChatRateLimitAnnounceAdminsDelay"/>
+        public static readonly CVarDef<bool> ChatRateLimitAnnounceAdmins =
+            CVarDef.Create("chat.rate_limit_announce_admins", true, CVar.SERVERONLY);
+
+        /// <summary>
+        /// Minimum delay (in seconds) between announcements from <see cref="ChatRateLimitAnnounceAdmins"/>.
+        /// </summary>
+        public static readonly CVarDef<int> ChatRateLimitAnnounceAdminsDelay =
+            CVarDef.Create("chat.rate_limit_announce_admins_delay", 15, CVar.SERVERONLY);
+
         public static readonly CVarDef<int> ChatMaxMessageLength =
             CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED);
 
index ac8e434e2da6c98a57da54d08f0e124767cf7a69..2690a6dfdb2e2934bf208898f2275c91dc806695 100644 (file)
@@ -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 = !