Client LuaCsForBarotrauma
SpamServerFilter.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Immutable;
5 using System.Linq;
6 using System.Net;
7 using System.Net.Cache;
8 using System.Threading.Tasks;
9 using System.Xml;
10 using System.Xml.Linq;
11 using Barotrauma.IO;
13 using RestSharp;
15 
16 namespace Barotrauma
17 {
19  {
20  Invalid,
21  NameEquals,
31  Endpoint,
33  }
34 
35  internal readonly record struct SpamFilter(ImmutableHashSet<(SpamServerFilterType Type, string Value)> Filters)
36  {
37  public bool IsFiltered(ServerInfo info)
38  {
39  if (!Filters.Any()) { return false; }
40 
41  foreach (var (type, value) in Filters)
42  {
43  if (!IsFiltered(info, type, value)) { return false; }
44  }
45 
46  return true;
47  }
48 
49  private static bool IsFiltered(ServerInfo info, SpamServerFilterType type, string value)
50  {
51  string desc = info.ServerMessage,
52  name = info.ServerName;
53 
54  int.TryParse(value, out int parsedInt);
55 
56  return type switch
57  {
58  SpamServerFilterType.NameEquals => CompareEquals(name, value),
59  SpamServerFilterType.NameContains => CompareContains(name, value),
60 
61  SpamServerFilterType.MessageEquals => CompareEquals(desc, value),
62  SpamServerFilterType.MessageContains => CompareContains(desc, value),
63 
64  SpamServerFilterType.Endpoint => info.Endpoints.First().StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase),
65 
66  SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt,
67  SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt,
68 
69  SpamServerFilterType.MaxPlayersLarger => info.MaxPlayers > parsedInt,
70  SpamServerFilterType.MaxPlayersExact => info.MaxPlayers == parsedInt,
71 
72  SpamServerFilterType.GameModeEquals => info.GameMode == value,
73  SpamServerFilterType.PlayStyleEquals => info.PlayStyle.ToIdentifier() == value,
74 
75  SpamServerFilterType.LanguageEquals => info.Language.Value == value,
76  _ => false
77  };
78 
79  static bool CompareEquals(string a, string b)
80  => a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b);
81 
82  static bool CompareContains(string a, string b)
83  => a.Contains(b, StringComparison.OrdinalIgnoreCase);
84  }
85 
86  public XElement Serialize()
87  {
88  var element = new XElement("Filter");
89 
90  foreach (var (type, value) in Filters)
91  {
92  element.Add(new XAttribute(type.ToString().ToLowerInvariant(), value));
93  }
94 
95  return element;
96  }
97 
98  public static bool TryParse(XElement element, out SpamFilter filter)
99  {
100  var builder = ImmutableHashSet.CreateBuilder<(SpamServerFilterType Type, string Value)>();
101  foreach (var attribute in element.Attributes())
102  {
103  if (!Enum.TryParse(attribute.Name.ToString(), ignoreCase: true, out SpamServerFilterType e))
104  {
105  DebugConsole.ThrowError($"Failed to parse spam filter attribute \"{attribute.Name}\"");
106  continue;
107  }
108  if (e is SpamServerFilterType.Invalid) { continue; }
109  builder.Add((e, attribute.Value));
110  }
111 
112  if (builder.Any())
113  {
114  filter = new SpamFilter(builder.ToImmutable());
115  return true;
116  }
117 
118  filter = default;
119  return false;
120  }
121 
122  public override string ToString()
123  {
124  return !Filters.Any() ? "Invalid Filter" : string.Join(", ", Filters.Select(static f => $"{f.Type}: {f.Value}"));
125  }
126  }
127 
128  internal sealed class SpamServerFilter
129  {
130  public readonly ImmutableArray<SpamFilter> Filters;
131 
132  public bool IsFiltered(ServerInfo info)
133  {
134  foreach (var f in Filters)
135  {
136  if (f.IsFiltered(info)) { return true; }
137  }
138 
139  return false;
140  }
141 
142  public SpamServerFilter(XElement element)
143  {
144  var builder = ImmutableArray.CreateBuilder<SpamFilter>();
145  foreach (var subElement in element.Elements())
146  {
147  if (SpamFilter.TryParse(subElement, out var filter))
148  {
149  builder.Add(filter);
150  }
151  }
152  Filters = builder.ToImmutable();
153  }
154 
155  public SpamServerFilter(ImmutableArray<SpamFilter> filters)
156  => Filters = filters;
157 
158  public readonly static string SavePath = Path.Combine("Data", "serverblacklist.xml");
159 
160  public void Save(string path)
161  {
162  var comment = new XComment(SpamServerFilters.LocalFilterComment);
163  var doc = new XDocument(comment, new XElement("Filters"));
164  foreach (var filter in Filters)
165  {
166  doc.Root?.Add(filter.Serialize());
167  }
168 
169  try
170  {
171  using var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true });
172  doc.SaveSafe(writer);
173  }
174  catch (Exception e)
175  {
176  DebugConsole.ThrowError("Saving spam filter failed.", e);
177  }
178  }
179  }
180 
181  internal static class SpamServerFilters
182  {
183  public static Option<SpamServerFilter> LocalSpamFilter;
184  public static Option<SpamServerFilter> GlobalSpamFilter;
185 
186  public const string LocalFilterComment = @"
187 This file contains a list of filters that can be used to hide servers from the server list.
188 You can add filters by right-clicking a server in the server list and selecting ""Hide server"" or by reporting the server and choosing ""Report and hide server"".
189 The filters are saved in this file, which you can edit manually if you want to.
190 
191 The available filter types are:
192 - NameEquals: The server name must equal the specified value. Homoglyphs are also checked.
193 - NameContains: The server name must contain the specified value.
194 - MessageEquals: The server description must equal the specified value. Homoglyphs are also checked.
195 - MessageContains: The server description must contain the specified value.
196 - PlayerCountLarger: The player count must be larger than the specified value.
197 - PlayerCountExact: The player count must match the specified value exactly.
198 - MaxPlayersLarger: The max player count must be larger than the specified value.
199 - MaxPlayersExact: The max player count must match the specified value exactly.
200 - GameModeEquals: The game mode identifier must match the specified value exactly.
201 - PlayStyleEquals: The play style must match the specified value exactly.
202 - Endpoint: The server endpoint, which is a Steam ID or an IP address, must match the specified value exactly. Steam ID is in the format of STEAM_X:Y:Z.
203 - LanguageEquals: The server language must match the specified value exactly.
204 
205 The filter values are case-insensitive and adding multiple conditions on one filter will require all of them to be met.
206 Homoglyph comparison is used for NameEquals and MessageEquals filters, which means that it checks whether the words look the same, meaning you can't abuse identical-looking but different symbols to work around the filter. For example ""lmaobox"" and ""lmаobox"" (with a cyrillic a) are considered equal.
207 
208 Examples:
209 <Filters>
210  <Filter namecontains=""discord.gg"" />
211  <Filter messagecontains=""discord.gg"" />
212  <Filter nameequals=""get good get lmaobox"" maxplayersexact=""999"" />
213 </Filters>
214 These will hide all servers that have a discord.gg link in their name or description and servers with the name ""get good get lmaobox"" that have 999 max players.
215 ";
216  static SpamServerFilters()
217  {
218  XDocument? doc;
219  if (!File.Exists(SpamServerFilter.SavePath))
220  {
221  var comment = new XComment(LocalFilterComment);
222 
223  doc = new XDocument(comment, new XElement("Filters"));
224 
225  try
226  {
227  using var writer = XmlWriter.Create(SpamServerFilter.SavePath, new XmlWriterSettings { Indent = true });
228  doc.SaveSafe(writer);
229  }
230  catch (Exception e)
231  {
232  DebugConsole.ThrowError("Saving spam filter failed.", e);
233  }
234  }
235  else
236  {
237  doc = XMLExtensions.TryLoadXml(SpamServerFilter.SavePath);
238  }
239 
240  if (doc?.Root is { } root)
241  {
242  LocalSpamFilter = Option.Some(new SpamServerFilter(root));
243  }
244  }
245 
246  public static bool IsFiltered(ServerInfo info)
247  {
248  if (LocalSpamFilter.TryUnwrap(out var localFilter) && localFilter.IsFiltered(info)) { return true; }
249  if (GlobalSpamFilter.TryUnwrap(out var globalFilter) && globalFilter.IsFiltered(info)) { return true; }
250  return false;
251  }
252 
253  public static void AddServerToLocalSpamList(ServerInfo info)
254  {
255  if (!LocalSpamFilter.TryUnwrap(out var localFilter)) { return; }
256  if (localFilter.IsFiltered(info)) { return; }
257 
258  var filters = localFilter.Filters.Add(new SpamFilter(ImmutableHashSet.Create((NameExact: SpamServerFilterType.NameEquals, info.ServerName))));
259  var newFilter = new SpamServerFilter(filters);
260  newFilter.Save(SpamServerFilter.SavePath);
261  LocalSpamFilter = Option.Some(newFilter);
262  }
263 
264  public static void ClearLocalSpamFilter()
265  {
266  var newFilter = new SpamServerFilter(ImmutableArray<SpamFilter>.Empty);
267  newFilter.Save(SpamServerFilter.SavePath);
268  LocalSpamFilter = Option.Some(newFilter);
269  }
270 
271  public static void RequestGlobalSpamFilter()
272  {
273  if (GameSettings.CurrentConfig.DisableGlobalSpamList) { return; }
274 
275  string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl;
276  if (string.IsNullOrEmpty(remoteContentUrl)) { return; }
277 
278  try
279  {
280  var client = new RestClient($"{remoteContentUrl}spamfilter")
281  {
282  CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore)
283  };
284  client.AddDefaultHeader("Cache-Control", "no-cache");
285  client.AddDefaultHeader("Pragma", "no-cache");
286  var request = new RestRequest("serve_spamlist.php", Method.GET);
287  TaskPool.Add("RequestGlobalSpamFilter", client.ExecuteAsync(request), RemoteContentReceived);
288  }
289  catch (Exception e)
290  {
291 #if DEBUG
292  DebugConsole.ThrowError("Fetching global spam list failed.", e);
293 #endif
294  GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RequestGlobalSpamFilter:Exception", GameAnalyticsManager.ErrorSeverity.Error,
295  "Fetching global spam list failed. " + e.Message);
296  }
297 
298  static void RemoteContentReceived(Task t)
299  {
300  try
301  {
302  if (!t.TryGetResult(out IRestResponse? remoteContentResponse)) { throw new Exception("Task did not return a valid result"); }
303  if (remoteContentResponse.StatusCode != HttpStatusCode.OK)
304  {
305  DebugConsole.AddWarning(
306  "Failed to receive global spam filter." +
307  "There may be an issue with your internet connection, or the master server might be temporarily unavailable " +
308  $"(error code: {remoteContentResponse.StatusCode})");
309  return;
310  }
311  string data = remoteContentResponse.Content;
312  if (string.IsNullOrWhiteSpace(data)) { return; }
313 
314  if (XDocument.Parse(data).Root is { } root)
315  {
316  GlobalSpamFilter = Option.Some(new SpamServerFilter(root));
317  }
318  }
319  catch (Exception e)
320  {
321 #if DEBUG
322  DebugConsole.ThrowError("Reading received global spam filter failed.", e);
323 #endif
324  GameAnalyticsManager.AddErrorEventOnce("SpamServerFilters.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error,
325  "Reading received global spam filter failed. " + e.Message);
326  }
327  }
328  }
329  }
330 }
static XmlWriter Create(string path, System.Xml.XmlWriterSettings settings)
Definition: SafeIO.cs:163
ImmutableArray< Endpoint > Endpoints
Definition: ServerInfo.cs:25
LanguageIdentifier Language
Definition: ServerInfo.cs:76
readonly Identifier Value
Definition: TextPack.cs:14