Basic rate limiting for chat messages (#21907)
This commit is contained in:
committed by
GitHub
parent
dbb3da9232
commit
8bf807a9b5
84
Content.Server/Chat/Managers/ChatManager.RateLimit.cs
Normal file
84
Content.Server/Chat/Managers/ChatManager.RateLimit.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,12 @@ using Content.Shared.CCVar;
|
|||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Replays;
|
using Robust.Shared.Replays;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Chat.Managers
|
namespace Content.Server.Chat.Managers
|
||||||
@@ -22,7 +24,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispatches chat messages to clients.
|
/// Dispatches chat messages to clients.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ChatManager : IChatManager
|
internal sealed partial class ChatManager : IChatManager
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, string> PatronOocColors = new()
|
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 IConfigurationManager _configurationManager = default!;
|
||||||
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum length a player-sent message can be sent
|
/// 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.OocEnabled, OnOocEnabledChanged, true);
|
||||||
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
||||||
|
|
||||||
|
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOocEnabledChanged(bool val)
|
private void OnOocEnabledChanged(bool val)
|
||||||
@@ -178,6 +184,9 @@ namespace Content.Server.Chat.Managers
|
|||||||
/// <param name="type">The type of message.</param>
|
/// <param name="type">The type of message.</param>
|
||||||
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
||||||
{
|
{
|
||||||
|
if (!HandleRateLimit(player))
|
||||||
|
return;
|
||||||
|
|
||||||
// Check if message exceeds the character limit
|
// Check if message exceeds the character limit
|
||||||
if (message.Length > MaxMessageLength)
|
if (message.Length > MaxMessageLength)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,5 +41,13 @@ namespace Content.Server.Chat.Managers
|
|||||||
|
|
||||||
[return: NotNullIfNotNull(nameof(author))]
|
[return: NotNullIfNotNull(nameof(author))]
|
||||||
ChatUser? EnsurePlayer(NetUserId? 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (player != null && !_chatManager.HandleRateLimit(player))
|
||||||
|
return;
|
||||||
|
|
||||||
// Sus
|
// Sus
|
||||||
if (player?.AttachedEntity is { Valid: true } entity && source != entity)
|
if (player?.AttachedEntity is { Valid: true } entity && source != entity)
|
||||||
{
|
{
|
||||||
@@ -267,6 +270,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
if (!CanSendInGame(message, shell, player))
|
if (!CanSendInGame(message, shell, player))
|
||||||
return;
|
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
|
// 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.
|
// in-game IC messages.
|
||||||
if (player?.AttachedEntity is not { Valid: true } entity || source != entity)
|
if (player?.AttachedEntity is not { Valid: true } entity || source != entity)
|
||||||
|
|||||||
@@ -89,4 +89,9 @@ public enum LogType
|
|||||||
ItemConfigure = 84,
|
ItemConfigure = 84,
|
||||||
DeviceLinking = 85,
|
DeviceLinking = 85,
|
||||||
Tile = 86,
|
Tile = 86,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A client has sent too many chat messages recently and is temporarily blocked from sending more.
|
||||||
|
/// </summary>
|
||||||
|
ChatRateLimited = 87,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1438,6 +1438,39 @@ namespace Content.Shared.CCVar
|
|||||||
* CHAT
|
* 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 =
|
public static readonly CVarDef<int> ChatMaxMessageLength =
|
||||||
CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED);
|
CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED);
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ chat-manager-send-hook-ooc-wrap-message = OOC: [bold](D){$senderName}:[/bold] {$
|
|||||||
chat-manager-dead-channel-name = DEAD
|
chat-manager-dead-channel-name = DEAD
|
||||||
chat-manager-admin-channel-name = ADMIN
|
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
|
## Speech verbs for chat
|
||||||
|
|
||||||
chat-speech-verb-suffix-exclamation = !
|
chat-speech-verb-suffix-exclamation = !
|
||||||
|
|||||||
Reference in New Issue
Block a user