Server LuaCsForBarotrauma
RateLimiter.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Generic;
5 using System.Collections.Immutable;
7 
8 namespace Barotrauma
9 {
10  public enum RateLimitAction
11  {
12  Invalid,
15  }
16 
17  public enum RateLimitPunishment
18  {
19  None, // just ignore
20  Announce, // announce to the server
21  Kick, // kick the player
22  Ban // ban the player
23  }
24 
25  internal sealed class RateLimiter
26  {
27  private sealed record RateLimit(DateTimeOffset Expiry)
28  {
29  public int RequestAmount;
30  }
31 
32  private readonly Dictionary<Client, RateLimit> rateLimits = new();
33  private readonly HashSet<Client> expiredRateLimits = new();
34  private readonly Dictionary<Client, DateTimeOffset> recentlyAnnouncedOffenders = new();
35 
36  private readonly int maxRequests, expiryInSeconds;
37 
38  private readonly ImmutableDictionary<RateLimitAction, RateLimitPunishment> punishments;
39 
40  public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules)
41  {
42  this.maxRequests = maxRequests;
43  this.expiryInSeconds = expiryInSeconds;
44 
45  punishments = punishmentRules.ToImmutableDictionary(
46  static pair => pair.Action,
47  static pair => pair.Punishment);
48  }
49 
50  public bool IsLimitReached(Client client)
51  {
52 #if !DEBUG
53  if (IsExempt(client)) { return false; }
54 #endif
55  expiredRateLimits.Clear();
56 
57  foreach (var (c, limit) in rateLimits)
58  {
59  if (limit.Expiry < DateTimeOffset.Now)
60  {
61  expiredRateLimits.Add(c);
62  }
63  }
64 
65  foreach (Client c in expiredRateLimits)
66  {
67  rateLimits.Remove(c);
68  }
69 
70  if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit))
71  {
72  rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds));
73  rateLimits.Add(client, rateLimit);
74  }
75 
76  rateLimit.RequestAmount++;
77 
78  if (rateLimit.RequestAmount > maxRequests)
79  {
80  ProcessPunishment(client, rateLimit.RequestAmount);
81  return true;
82  }
83 
84  return false;
85  }
86 
87  private void ProcessPunishment(Client client, int requests)
88  {
89  bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true };
90 
91  foreach (var (action, punishment) in punishments)
92  {
93  switch (action)
94  {
95  case RateLimitAction.Invalid:
96  continue;
97  case RateLimitAction.OnLimitReached when requests >= maxRequests:
98  case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2:
99  switch (punishment)
100  {
101  case RateLimitPunishment.None:
102  continue;
103  case RateLimitPunishment.Announce:
104  AnnounceOffender(client);
105  break;
106  case RateLimitPunishment.Ban when isDosProtectionEnabled:
107  GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value);
108  break;
109  case RateLimitPunishment.Kick when isDosProtectionEnabled:
110  GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value);
111  break;
112  }
113  break;
114  }
115  }
116  }
117 
118  private void AnnounceOffender(Client client)
119  {
120  if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry))
121  {
122  if (expiry > DateTimeOffset.Now) { return; }
123 
124  recentlyAnnouncedOffenders.Remove(client);
125  }
126 
127  GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection);
128  recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds));
129  }
130 
131  public static bool IsExempt(Client client) =>
132  (GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection)
133  || client.HasPermission(ClientPermissions.SpamImmunity);
134  }
135 }
static void Log(string line, ServerLog.MessageType messageType)
Definition: GameServer.cs:4609