Skip to content

Commit

Permalink
Rate limit ahelps (space-wizards#29219)
Browse files Browse the repository at this point in the history
* Make chat rate limits a general-purpose system.

Intending to use this with ahelps next.

* Rate limt ahelps

Fixes space-wizards#28762

* Review comments
  • Loading branch information
PJB3005 authored and aspiringLich committed Jul 21, 2024
1 parent 5206ba3 commit fe8158c
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 74 deletions.
23 changes: 23 additions & 0 deletions Content.Server/Administration/Systems/BwoinkSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Content.Server.Afk;
using Content.Server.Discord;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Mind;
Expand All @@ -27,6 +28,8 @@ namespace Content.Server.Administration.Systems
[UsedImplicitly]
public sealed partial class BwoinkSystem : SharedBwoinkSystem
{
private const string RateLimitKey = "AdminHelp";

[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
Expand All @@ -35,6 +38,7 @@ public sealed partial class BwoinkSystem : SharedBwoinkSystem
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;

[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
private static partial Regex DiscordRegex();
Expand Down Expand Up @@ -80,6 +84,22 @@ public override void Initialize()

SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
SubscribeNetworkEvent<BwoinkClientTypingUpdated>(OnClientTypingUpdated);

_rateLimit.Register(
RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
CVarLimitCount = CCVars.AhelpRateLimitCount,
PlayerLimitedAction = PlayerRateLimitedAction
});
}

private void PlayerRateLimitedAction(ICommonSession obj)
{
RaiseNetworkEvent(
new BwoinkTextMessage(obj.UserId, default, Loc.GetString("bwoink-system-rate-limited"), playSound: false),
obj.Channel);
}

private void OnOverrideChanged(string obj)
Expand Down Expand Up @@ -395,6 +415,9 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes
return;
}

if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed)
return;

var escapedText = FormattedMessage.EscapeText(message.Text);

string bwoinkText;
Expand Down
85 changes: 21 additions & 64 deletions Content.Server/Chat/Managers/ChatManager.RateLimit.cs
Original file line number Diff line number Diff line change
@@ -1,84 +1,41 @@
using System.Runtime.InteropServices;
using Content.Server.Players.RateLimiting;
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();
private const string RateLimitKey = "Chat";

public bool HandleRateLimit(ICommonSession player)
private void RegisterRateLimits()
{
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)
_rateLimitManager.Register(RateLimitKey,
new RateLimitRegistration
{
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;
CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
CVarLimitCount = CCVars.ChatRateLimitCount,
CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
PlayerLimitedAction = RateLimitPlayerLimited,
AdminAnnounceAction = RateLimitAlertAdmins,
AdminLogType = LogType.ChatRateLimited,
});
}

private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
private void RateLimitPlayerLimited(ICommonSession player)
{
if (e.NewStatus == SessionStatus.Disconnected)
_rateLimitData.Remove(e.Session);
DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
}

private struct RateLimitDatum
private void RateLimitAlertAdmins(ICommonSession player)
{
/// <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;
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
}

/// <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;
public RateLimitStatus HandleRateLimit(ICommonSession player)
{
return _rateLimitManager.CountAction(player, RateLimitKey);
}
}
10 changes: 4 additions & 6 deletions Content.Server/Chat/Managers/ChatManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,17 @@
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.MoMMI;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Shared.Administration;
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
Expand All @@ -43,8 +42,7 @@ internal sealed partial 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!;
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;

/// <summary>
/// The maximum length a player-sent message can be sent
Expand All @@ -64,7 +62,7 @@ public void Initialize()
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);

_playerManager.PlayerStatusChanged += PlayerStatusChanged;
RegisterRateLimits();
}

private void OnOocEnabledChanged(bool val)
Expand Down Expand Up @@ -206,7 +204,7 @@ public void SendHookOOC(string sender, string message)
/// <param name="type">The type of message.</param>
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
{
if (!HandleRateLimit(player))
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
return;

// Check if message exceeds the character limit
Expand Down
4 changes: 3 additions & 1 deletion Content.Server/Chat/Managers/IChatManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Players;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Robust.Shared.Network;
Expand Down Expand Up @@ -50,6 +52,6 @@ void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessag
/// </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);
RateLimitStatus HandleRateLimit(ICommonSession player);
}
}
5 changes: 3 additions & 2 deletions Content.Server/Chat/Systems/ChatSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Content.Server.Chat.Managers;
using Content.Server.Examine;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Server.Speech.Components;
using Content.Server.Speech.EntitySystems;
using Content.Server.Station.Components;
Expand Down Expand Up @@ -183,7 +184,7 @@ public void TrySendInGameICMessage(
return;
}

if (player != null && !_chatManager.HandleRateLimit(player))
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
return;

// Sus
Expand Down Expand Up @@ -272,7 +273,7 @@ public void TrySendInGameOOCMessage(
if (!CanSendInGame(message, shell, player))
return;

if (player != null && !_chatManager.HandleRateLimit(player))
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
return;

// It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending
Expand Down
3 changes: 3 additions & 0 deletions Content.Server/Entry/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
using Content.Server.IoC;
using Content.Server.Maps;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Server.ServerInfo;
using Content.Server.ServerUpdates;
Expand Down Expand Up @@ -108,6 +110,7 @@ public override void Init()
_updateManager.Initialize();
_playTimeTracking.Initialize();
IoCManager.Resolve<JobWhitelistManager>().Initialize();
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
}
}

Expand Down
3 changes: 3 additions & 0 deletions Content.Server/IoC/ServerContentIoC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Server.ServerInfo;
using Content.Server.ServerUpdates;
Expand Down Expand Up @@ -63,6 +65,7 @@ public static void Register()
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
}
}
}
Loading

0 comments on commit fe8158c

Please sign in to comment.