diff --git a/OpenRA.Game/Server/PlayerMessageTracker.cs b/OpenRA.Game/Server/PlayerMessageTracker.cs new file mode 100644 index 0000000000..33c802394a --- /dev/null +++ b/OpenRA.Game/Server/PlayerMessageTracker.cs @@ -0,0 +1,86 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; + +namespace OpenRA.Server +{ + class PlayerMessageTracker + { + [TranslationReference("remaining")] + static readonly string ChatDisabled = "chat-disabled"; + + readonly Dictionary> messageTracker = new Dictionary>(); + readonly Server server; + readonly Action dispatchOrdersToClient; + readonly Action> sendLocalizedMessageTo; + + public PlayerMessageTracker(Server server, Action dispatchOrdersToClient, Action> sendLocalizedMessageTo) + { + this.server = server; + this.dispatchOrdersToClient = dispatchOrdersToClient; + this.sendLocalizedMessageTo = sendLocalizedMessageTo; + } + + public void DisableChatUI(Connection conn, int time) + { + dispatchOrdersToClient(conn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)time }.Serialize()); + } + + public bool IsPlayerAtFloodLimit(Connection conn) + { + if (!messageTracker.ContainsKey(conn.PlayerIndex)) + messageTracker.Add(conn.PlayerIndex, new List()); + + var isAdmin = server.GetClient(conn)?.IsAdmin ?? false; + var settings = server.Settings; + var time = conn.ConnectionTimer.ElapsedMilliseconds; + var tracker = messageTracker[conn.PlayerIndex]; + tracker.RemoveAll(t => t + settings.FloodLimitInterval < time); + + long CalculateRemaining(long cooldown) + { + return (cooldown - time + 999) / 1000; + } + + // Block messages until join cooldown times out + if (!isAdmin && time < settings.FloodLimitJoinCooldown) + { + var remaining = CalculateRemaining(settings.FloodLimitJoinCooldown); + sendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining)); + return true; + } + + // Block messages if above flood limit + if (tracker.Count >= settings.FloodLimitMessageCount) + { + var remaining = CalculateRemaining(tracker[0] + settings.FloodLimitInterval); + sendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining)); + return true; + } + + tracker.Add(time); + + // Disable chat when player has reached the flood limit + if (tracker.Count >= settings.FloodLimitMessageCount) + { + var cooldownDelta = Math.Max(0, settings.FloodLimitCooldown - settings.FloodLimitInterval); + for (var i = 0; i < tracker.Count; i++) + tracker[i] = time + cooldownDelta; + + DisableChatUI(conn, settings.FloodLimitCooldown); + } + + return false; + } + } +} diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 351a25f016..81e00f9fd5 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -79,6 +79,7 @@ namespace OpenRA.Server GameInformation gameInfo; readonly List worldPlayers = new List(); readonly Stopwatch pingUpdated = Stopwatch.StartNew(); + readonly PlayerMessageTracker playerMessageTracker; [TranslationReference] static readonly string CustomRules = "custom-rules"; @@ -128,9 +129,6 @@ namespace OpenRA.Server [TranslationReference("command")] static readonly string UnknownServerCommand = "unknown-server-command"; - [TranslationReference("remaining")] - static readonly string ChatDisabled = "chat-disabled"; - [TranslationReference("player")] static readonly string LobbyDisconnected = "lobby-disconnected"; @@ -316,6 +314,8 @@ namespace OpenRA.Server Map = ModData.MapCache[settings.Map]; MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks); + playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo); + LobbyInfo = new Session { GlobalSettings = @@ -558,8 +558,8 @@ namespace OpenRA.Server newConn.Validated = true; // Disable chat UI to stop the client sending messages that we know we will reject - if (!client.IsAdmin && Settings.JoinChatDelay > 0) - DispatchOrdersToClient(newConn, 0, 0, new Order("DisableChatEntry", null, false) { ExtraData = (uint)Settings.JoinChatDelay }.Serialize()); + if (!client.IsAdmin && Settings.FloodLimitJoinCooldown > 0) + playerMessageTracker.DisableChatUI(newConn, Settings.FloodLimitJoinCooldown); Log.Write("server", $"Client {newConn.PlayerIndex}: Accepted connection from {newConn.EndPoint}."); @@ -1006,14 +1006,7 @@ namespace OpenRA.Server case "Chat": { - var isAdmin = GetClient(conn)?.IsAdmin ?? false; - var connected = conn.ConnectionTimer.ElapsedMilliseconds; - if (!isAdmin && connected < Settings.JoinChatDelay) - { - var remaining = (Settings.JoinChatDelay - connected + 999) / 1000; - SendLocalizedMessageTo(conn, ChatDisabled, Translation.Arguments("remaining", remaining)); - } - else + if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn)) DispatchOrdersToClients(conn, 0, o.Serialize()); break; diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index c6fe72ae10..4987218cee 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -103,7 +103,16 @@ namespace OpenRA public bool EnableLintChecks = true; [Desc("Delay in milliseconds before newly joined players can send chat messages.")] - public int JoinChatDelay = 5000; + public int FloodLimitJoinCooldown = 5000; + + [Desc("Amount of miliseconds player chat messages are tracked for.")] + public int FloodLimitInterval = 5000; + + [Desc("Amount of chat messages per FloodLimitInterval a players can send before flood is detected.")] + public int FloodLimitMessageCount = 5; + + [Desc("Delay in milliseconds before players can send chat messages after flood was detected.")] + public int FloodLimitCooldown = 15000; public ServerSettings Clone() { diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl index da95e4af07..fc47d218e8 100644 --- a/mods/common/languages/en.ftl +++ b/mods/common/languages/en.ftl @@ -8,11 +8,6 @@ no-start-until-required-slots-full = Unable to start the game until required slo no-start-without-players = Unable to start the game with no players. insufficient-enabled-spawnPoints = Unable to start the game until more spawn points are enabled. malformed-command = Malformed { $command } command -chat-disabled = - { $remaining -> - [one] Chat is disabled. Please try again in { $remaining } second. - *[other] Chat is disabled. Please try again in { $remaining } seconds. - } state-unchanged-ready = Cannot change state when marked as ready. invalid-faction-selected = Invalid faction selected: { $faction } supported-factions = Supported values: { $factions } @@ -91,6 +86,13 @@ game-started = Game started ## Server also LobbyUtils bots-disabled = Bots Disabled +## PlayerMessageTracker +chat-disabled = + { $remaining -> + [one] Chat is disabled. Please try again in { $remaining } second. + *[other] Chat is disabled. Please try again in { $remaining } seconds. + } + ## ActorEditLogic duplicate-actor-id = Duplicate Actor ID enter-actor-id = Enter an Actor ID