Server LuaCsForBarotrauma
ServerPeer.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Linq;
7 
8 namespace Barotrauma.Networking
9 {
10  internal abstract class ServerPeer<TConnection> : ServerPeer where TConnection : NetworkConnection
11  {
12  private readonly ImmutableArray<ContentPackage> contentPackages;
13  protected ServerPeer(Callbacks callbacks, ServerSettings serverSettings) : base(callbacks)
14  {
15  this.serverSettings = serverSettings;
16  this.connectedClients = new List<ConnectedClient>();
17  this.pendingClients = new List<PendingClient>();
18 
19  List<ContentPackage> contentPackageList = new List<ContentPackage>();
20  foreach (var cp in ContentPackageManager.EnabledPackages.All)
21  {
22  if (!cp.Files.Any()) { continue; }
23  if (!cp.HasMultiplayerSyncedContent && !cp.Files.All(f => f is SubmarineFile)) { continue; }
24  if (cp.UgcId.TryUnwrap(out var id1) &&
25  contentPackageList.FirstOrDefault(cp => cp.UgcId.TryUnwrap(out var id2) && id1.Equals(id2)) is ContentPackage existingPackage)
26  {
27  //there can be multiple enabled mods with the same UgcId if the player has e.g. created a local copy of a workshop mod
28  DebugConsole.AddWarning($"The content package \"{existingPackage.Name}\" ({existingPackage.Path}) has the same id as \"{cp.Name}\" ({cp.Path}). Ignoring the latter package.");
29  continue;
30  }
31  contentPackageList.Add(cp);
32  }
33  contentPackages = contentPackageList.ToImmutableArray();
34  }
35 
36  public sealed class PendingClient
37  {
38  public string? Name;
39  public Option<int> OwnerKey;
40  public readonly TConnection Connection;
41  public ConnectionInitialization InitializationStep;
42  public double UpdateTime;
43  public double TimeOut;
44  public int Retries;
45  public Int32? PasswordSalt;
46  public bool AuthSessionStarted;
47 
48  public AccountInfo AccountInfo => Connection.AccountInfo;
49 
50  public PendingClient(TConnection conn)
51  {
52  OwnerKey = Option.None;
53  Connection = conn;
54  InitializationStep = ConnectionInitialization.AuthInfoAndVersion;
55  Retries = 0;
56  PasswordSalt = null;
57  UpdateTime = Timing.TotalTime + Timing.Step * 3.0;
59  AuthSessionStarted = false;
60  }
61 
62  public void Heartbeat()
63  {
65  }
66  }
67 
68  protected sealed class ConnectedClient
69  {
70  public readonly TConnection Connection;
71  public readonly MessageFragmenter Fragmenter;
73 
74  public ConnectedClient(TConnection connection)
75  {
76  Connection = connection;
77  Fragmenter = new();
78  Defragmenter = new();
79  }
80  }
81 
82  protected readonly List<ConnectedClient> connectedClients;
83  protected readonly List<PendingClient> pendingClients;
84  protected readonly ServerSettings serverSettings;
85 
86  protected TConnection? OwnerConnection;
87  protected Option<int> ownerKey = Option.None;
88 
89  protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep)
90  {
91  pendingClient.TimeOut = NetworkConnection.TimeoutThreshold;
92 
93  if (pendingClient.InitializationStep != initializationStep) { return; }
94 
95  pendingClient.UpdateTime = Timing.TotalTime + Timing.Step;
96 
97  switch (initializationStep)
98  {
99  case ConnectionInitialization.AuthInfoAndVersion:
100  if (!INetSerializableStruct.TryRead(inc, pendingClient.AccountInfo, out ClientAuthTicketAndVersionPacket authPacket))
101  {
102  RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
103  return;
104  }
105 
106  if (!Client.IsValidName(authPacket.Name, serverSettings))
107  {
108  RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.InvalidName));
109  return;
110  }
111 
112  bool isCompatibleVersion =
113  Version.TryParse(authPacket.GameVersion, out var remoteVersion)
114  && NetworkMember.IsCompatible(remoteVersion, GameMain.Version);
115  if (!isCompatibleVersion)
116  {
117  RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion());
118 
119  GameServer.Log($"{authPacket.Name} ({authPacket.AccountId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error);
120  DebugConsole.NewMessage($"{authPacket.Name} ({authPacket.AccountId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red);
121  return;
122  }
123 
124  pendingClient.Connection.Language = authPacket.Language.ToLanguageIdentifier();
125 
126  Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), authPacket.Name.ToLower()));
127  if (nameTaken != null)
128  {
129  RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.NameTaken));
130  GameServer.Log($"{authPacket.Name} ({authPacket.AccountId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error);
131  return;
132  }
133 
134  if (!pendingClient.AuthSessionStarted)
135  {
136  ProcessAuthTicket(authPacket, pendingClient);
137  }
138 
139  break;
140  case ConnectionInitialization.Password:
141  if (!INetSerializableStruct.TryRead(inc, pendingClient.AccountInfo, out ClientPeerPasswordPacket passwordPacket))
142  {
143  RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
144  return;
145  }
146 
147  if (pendingClient.PasswordSalt is null)
148  {
149  DebugConsole.ThrowError("Received password message from client without salt");
150  return;
151  }
152 
153  if (serverSettings.IsPasswordCorrect(passwordPacket.Password, pendingClient.PasswordSalt.Value))
154  {
155  pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder;
156  }
157  else
158  {
159  pendingClient.Retries++;
160  if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan)
161  {
162  const string banMsg = "Failed to enter correct password too many times";
163  BanPendingClient(pendingClient, banMsg, null);
164  RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banMsg));
165  return;
166  }
167  }
168 
169  pendingClient.UpdateTime = Timing.TotalTime;
170  break;
171  case ConnectionInitialization.ContentPackageOrder:
172  pendingClient.InitializationStep = ConnectionInitialization.Success;
173  pendingClient.UpdateTime = Timing.TotalTime;
174  break;
175  }
176  }
177 
178  protected abstract void ProcessAuthTicket(ClientAuthTicketAndVersionPacket packet, PendingClient pendingClient);
179 
180  protected void BanPendingClient(PendingClient pendingClient, string banReason, TimeSpan? duration)
181  {
182  void banAccountId(AccountId accountId)
183  {
184  serverSettings.BanList.BanPlayer(pendingClient.Name ?? "Player", accountId, banReason, duration);
185  }
186 
187  if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var id)) { banAccountId(id); }
188 
189  pendingClient.AccountInfo.OtherMatchingIds.ForEach(banAccountId);
190  if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var accountId))
191  {
192  serverSettings.BanList.BanPlayer(pendingClient.Name ?? "Player", accountId, banReason, duration);
193  }
194  else
195  {
196  serverSettings.BanList.BanPlayer(pendingClient.Name ?? "Player", pendingClient.Connection.Endpoint, banReason, duration);
197  }
198  }
199 
200  protected bool IsPendingClientBanned(PendingClient pendingClient, out string? banReason)
201  {
202  bool isAccountIdBanned(AccountId accountId, out string? banReason)
203  {
204  return serverSettings.BanList.IsBanned(accountId, out banReason);
205  }
206 
207  banReason = default;
208  bool isBanned = pendingClient.AccountInfo.AccountId.TryUnwrap(out var id)
209  && isAccountIdBanned(id, out banReason);
210  foreach (var otherId in pendingClient.AccountInfo.OtherMatchingIds)
211  {
212  if (isBanned) { break; }
213 
214  isBanned |= isAccountIdBanned(otherId, out banReason);
215  }
216 
217  return isBanned;
218  }
219 
220  protected abstract void SendMsgInternal(TConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body);
221 
222  protected void UpdatePendingClient(PendingClient pendingClient)
223  {
224  var skipRemove = false;
225  var result = GameMain.LuaCs.Hook.Call<bool?>("handlePendingClient", pendingClient);
226 
227  if (result != null) skipRemove = result.Value;
228 
229  if (!skipRemove && connectedClients.Count >= serverSettings.MaxPlayers)
230  {
231  RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull));
232  }
233 
234  if (IsPendingClientBanned(pendingClient, out string? banReason))
235  {
236  RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
237  return;
238  }
239 
240  if (pendingClient.InitializationStep == ConnectionInitialization.Success)
241  {
242  TConnection newConnection = pendingClient.Connection;
243  connectedClients.Add(new ConnectedClient(newConnection));
244  pendingClients.Remove(pendingClient);
245 
246  callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name);
247 
248  CheckOwnership(pendingClient);
249  }
250 
251  pendingClient.TimeOut -= Timing.Step;
252  if (pendingClient.TimeOut < 0.0)
253  {
254  RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
255  }
256 
257  if (Timing.TotalTime < pendingClient.UpdateTime) { return; }
258 
259  pendingClient.UpdateTime = Timing.TotalTime + 1.0;
260 
261  PeerPacketHeaders headers = new PeerPacketHeaders
262  {
263  DeliveryMethod = DeliveryMethod.Reliable,
264  PacketHeader = PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage,
265  Initialization = pendingClient.InitializationStep
266  };
267 
268  INetSerializableStruct? structToSend = null;
269 
270  switch (pendingClient.InitializationStep)
271  {
272  case ConnectionInitialization.ContentPackageOrder:
273 
274  SerializableDateTime timeNow = SerializableDateTime.UtcNow;
275  structToSend = new ServerPeerContentPackageOrderPacket
276  {
277  ServerName = GameMain.Server.ServerName,
278  ContentPackages = contentPackages
279  .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
280  .ToImmutableArray(),
281  AllowModDownloads = serverSettings.AllowModDownloads
282  };
283 
284  break;
285  case ConnectionInitialization.Password:
286  structToSend = new ServerPeerPasswordPacket
287  {
288  Salt = GetSalt(pendingClient),
289  RetriesLeft = Option<int>.Some(pendingClient.Retries)
290  };
291 
292  static Option<int> GetSalt(PendingClient client)
293  {
294  if (client.PasswordSalt is { } salt) { return Option<int>.Some(salt); }
295 
296  salt = Lidgren.Network.CryptoRandom.Instance.Next();
297  client.PasswordSalt = salt;
298  return Option<int>.Some(salt);
299  }
300 
301  break;
302  }
303 
304  SendMsgInternal(pendingClient.Connection, headers, structToSend);
305  }
306 
307  protected virtual void CheckOwnership(PendingClient pendingClient) { }
308 
309  public void RemovePendingClient(PendingClient pendingClient, PeerDisconnectPacket peerDisconnectPacket)
310  {
311  if (pendingClients.Contains(pendingClient))
312  {
313  Disconnect(pendingClient.Connection, peerDisconnectPacket);
314 
315  pendingClients.Remove(pendingClient);
316 
317  pendingClient.Connection.SetAccountInfo(AccountInfo.None);
318  pendingClient.AuthSessionStarted = false;
319  }
320  }
321  }
322 
323  internal abstract class ServerPeer
324  {
325  public readonly record struct Callbacks(
326  Callbacks.MessageCallback OnMessageReceived,
327  Callbacks.DisconnectCallback OnDisconnect,
328  Callbacks.InitializationCompleteCallback OnInitializationComplete,
329  Callbacks.ShutdownCallback OnShutdown,
330  Callbacks.OwnerDeterminedCallback OnOwnerDetermined)
331  {
332  public delegate void MessageCallback(NetworkConnection connection, IReadMessage message);
333  public delegate void DisconnectCallback(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket);
334  public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName);
335  public delegate void ShutdownCallback();
336  public delegate void OwnerDeterminedCallback(NetworkConnection connection);
337  }
338 
339  protected readonly Callbacks callbacks;
340 
341  protected ServerPeer(Callbacks callbacks)
342  {
343  this.callbacks = callbacks;
344  }
345 
346  public abstract void Start();
347  public abstract void Close();
348  public abstract void Update(float deltaTime);
349 
350  public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true);
351  public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket);
352 
353  private void LogMalformedMessage(NetworkConnection conn)
354  {
355  foreach (Client c in GameMain.Server.ConnectedClients)
356  {
357  if (c.Connection == conn)
358  {
359  DebugConsole.ThrowError($"Received malformed message from {c.Name}.");
360  return;
361  }
362  }
363  DebugConsole.ThrowError("Received malformed message from remote peer.");
364  }
365  protected static void LogMalformedMessage()
366  => DebugConsole.ThrowError("Received malformed message from remote peer.");
367 
368  protected bool ShouldAskForPassword(ServerSettings serverSettings, NetworkConnection connection)
369  {
370  if (!serverSettings.HasPassword) { return false; }
371 
372  if (GameMain.Server is { } server && server.FindAndRemoveRecentlyDisconnectedConnection(connection))
373  {
374  // do not ask passwords from clients that have recently disconnected
375  return false;
376  }
377 
378  return true;
379  }
380  }
381 }
readonly MessageDefragmenter Defragmenter
Definition: ServerPeer.cs:72
ConnectionInitialization InitializationStep
Definition: ServerPeer.cs:41