3 using System.Collections.Generic;
4 using System.Collections.Immutable;
10 internal abstract class ServerPeer<TConnection> : ServerPeer where TConnection : NetworkConnection
12 private readonly ImmutableArray<ContentPackage> contentPackages;
13 protected ServerPeer(Callbacks callbacks, ServerSettings serverSettings) : base(callbacks)
15 this.serverSettings = serverSettings;
16 this.connectedClients =
new List<ConnectedClient>();
17 this.pendingClients =
new List<PendingClient>();
19 List<ContentPackage> contentPackageList =
new List<ContentPackage>();
20 foreach (var cp
in ContentPackageManager.EnabledPackages.All)
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)
28 DebugConsole.AddWarning($
"The content package \"{existingPackage.Name}\" ({existingPackage.Path}) has the same id as \"{cp.Name}\" ({cp.Path}). Ignoring the latter package.");
31 contentPackageList.Add(cp);
33 contentPackages = contentPackageList.ToImmutableArray();
57 UpdateTime = Timing.TotalTime + Timing.Step * 3.0;
82 protected readonly List<ConnectedClient> connectedClients;
83 protected readonly List<PendingClient> pendingClients;
86 protected TConnection? OwnerConnection;
87 protected Option<int> ownerKey = Option.None;
89 protected void ReadConnectionInitializationStep(PendingClient pendingClient,
IReadMessage inc, ConnectionInitialization initializationStep)
93 if (pendingClient.InitializationStep != initializationStep) {
return; }
95 pendingClient.UpdateTime = Timing.TotalTime + Timing.Step;
97 switch (initializationStep)
99 case ConnectionInitialization.AuthInfoAndVersion:
100 if (!INetSerializableStruct.TryRead(inc, pendingClient.AccountInfo, out ClientAuthTicketAndVersionPacket authPacket))
102 RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
106 if (!Client.IsValidName(authPacket.Name, serverSettings))
108 RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.InvalidName));
112 bool isCompatibleVersion =
113 Version.TryParse(authPacket.GameVersion, out var remoteVersion)
114 && NetworkMember.IsCompatible(remoteVersion, GameMain.Version);
115 if (!isCompatibleVersion)
117 RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion());
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);
124 pendingClient.Connection.Language = authPacket.Language.ToLanguageIdentifier();
126 Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), authPacket.Name.ToLower()));
127 if (nameTaken !=
null)
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);
134 if (!pendingClient.AuthSessionStarted)
136 ProcessAuthTicket(authPacket, pendingClient);
140 case ConnectionInitialization.Password:
141 if (!INetSerializableStruct.TryRead(inc, pendingClient.AccountInfo, out ClientPeerPasswordPacket passwordPacket))
143 RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.MalformedData));
147 if (pendingClient.PasswordSalt is
null)
149 DebugConsole.ThrowError(
"Received password message from client without salt");
153 if (serverSettings.IsPasswordCorrect(passwordPacket.Password, pendingClient.PasswordSalt.Value))
155 pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder;
159 pendingClient.Retries++;
160 if (serverSettings.BanAfterWrongPassword && pendingClient.Retries > serverSettings.MaxPasswordRetriesBeforeBan)
162 const string banMsg =
"Failed to enter correct password too many times";
163 BanPendingClient(pendingClient, banMsg,
null);
164 RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banMsg));
169 pendingClient.UpdateTime = Timing.TotalTime;
171 case ConnectionInitialization.ContentPackageOrder:
172 pendingClient.InitializationStep = ConnectionInitialization.Success;
173 pendingClient.UpdateTime = Timing.TotalTime;
178 protected abstract void ProcessAuthTicket(ClientAuthTicketAndVersionPacket packet, PendingClient pendingClient);
180 protected void BanPendingClient(PendingClient pendingClient,
string banReason, TimeSpan? duration)
182 void banAccountId(AccountId accountId)
184 serverSettings.BanList.BanPlayer(pendingClient.Name ??
"Player", accountId, banReason, duration);
187 if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var
id)) { banAccountId(
id); }
189 pendingClient.AccountInfo.OtherMatchingIds.ForEach(banAccountId);
190 if (pendingClient.AccountInfo.AccountId.TryUnwrap(out var accountId))
192 serverSettings.BanList.BanPlayer(pendingClient.Name ??
"Player", accountId, banReason, duration);
196 serverSettings.BanList.BanPlayer(pendingClient.Name ??
"Player", pendingClient.Connection.Endpoint, banReason, duration);
200 protected bool IsPendingClientBanned(PendingClient pendingClient, out
string? banReason)
202 bool isAccountIdBanned(AccountId accountId, out
string? banReason)
204 return serverSettings.BanList.IsBanned(accountId, out banReason);
208 bool isBanned = pendingClient.AccountInfo.AccountId.TryUnwrap(out var
id)
209 && isAccountIdBanned(
id, out banReason);
210 foreach (var otherId
in pendingClient.AccountInfo.OtherMatchingIds)
212 if (isBanned) {
break; }
214 isBanned |= isAccountIdBanned(otherId, out banReason);
220 protected abstract void SendMsgInternal(TConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body);
222 protected void UpdatePendingClient(PendingClient pendingClient)
224 var skipRemove =
false;
225 var result = GameMain.LuaCs.Hook.Call<
bool?>(
"handlePendingClient", pendingClient);
227 if (result !=
null) skipRemove = result.Value;
229 if (!skipRemove && connectedClients.Count >= serverSettings.MaxPlayers)
231 RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull));
234 if (IsPendingClientBanned(pendingClient, out
string? banReason))
236 RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
240 if (pendingClient.InitializationStep == ConnectionInitialization.Success)
242 TConnection newConnection = pendingClient.Connection;
243 connectedClients.Add(
new ConnectedClient(newConnection));
244 pendingClients.Remove(pendingClient);
246 callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name);
248 CheckOwnership(pendingClient);
251 pendingClient.TimeOut -= Timing.Step;
252 if (pendingClient.TimeOut < 0.0)
254 RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
257 if (Timing.TotalTime < pendingClient.UpdateTime) {
return; }
259 pendingClient.UpdateTime = Timing.TotalTime + 1.0;
261 PeerPacketHeaders headers =
new PeerPacketHeaders
263 DeliveryMethod = DeliveryMethod.Reliable,
264 PacketHeader = PacketHeader.IsConnectionInitializationStep | PacketHeader.IsServerMessage,
265 Initialization = pendingClient.InitializationStep
268 INetSerializableStruct? structToSend =
null;
270 switch (pendingClient.InitializationStep)
272 case ConnectionInitialization.ContentPackageOrder:
274 SerializableDateTime timeNow = SerializableDateTime.UtcNow;
275 structToSend =
new ServerPeerContentPackageOrderPacket
277 ServerName = GameMain.Server.ServerName,
278 ContentPackages = contentPackages
279 .Select(contentPackage =>
new ServerContentPackage(contentPackage, timeNow))
281 AllowModDownloads = serverSettings.AllowModDownloads
285 case ConnectionInitialization.Password:
286 structToSend =
new ServerPeerPasswordPacket
288 Salt = GetSalt(pendingClient),
289 RetriesLeft = Option<int>.Some(pendingClient.Retries)
292 static Option<int> GetSalt(PendingClient client)
294 if (client.PasswordSalt is { } salt) {
return Option<int>.Some(salt); }
296 salt = Lidgren.Network.CryptoRandom.Instance.Next();
297 client.PasswordSalt = salt;
298 return Option<int>.Some(salt);
304 SendMsgInternal(pendingClient.Connection, headers, structToSend);
307 protected virtual void CheckOwnership(PendingClient pendingClient) { }
309 public void RemovePendingClient(PendingClient pendingClient, PeerDisconnectPacket peerDisconnectPacket)
311 if (pendingClients.Contains(pendingClient))
313 Disconnect(pendingClient.Connection, peerDisconnectPacket);
315 pendingClients.Remove(pendingClient);
317 pendingClient.Connection.SetAccountInfo(AccountInfo.None);
318 pendingClient.AuthSessionStarted =
false;
323 internal abstract class ServerPeer
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)
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);
339 protected readonly Callbacks callbacks;
341 protected ServerPeer(Callbacks callbacks)
343 this.callbacks = callbacks;
346 public abstract void Start();
347 public abstract void Close();
348 public abstract void Update(
float deltaTime);
350 public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod,
bool compressPastThreshold =
true);
351 public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket);
353 private void LogMalformedMessage(NetworkConnection conn)
355 foreach (Client c
in GameMain.Server.ConnectedClients)
357 if (c.Connection == conn)
359 DebugConsole.ThrowError($
"Received malformed message from {c.Name}.");
363 DebugConsole.ThrowError(
"Received malformed message from remote peer.");
365 protected static void LogMalformedMessage()
366 => DebugConsole.ThrowError(
"Received malformed message from remote peer.");
368 protected bool ShouldAskForPassword(ServerSettings serverSettings, NetworkConnection connection)
370 if (!serverSettings.HasPassword) {
return false; }
372 if (GameMain.Server is { } server && server.FindAndRemoveRecentlyDisconnectedConnection(connection))
static double TimeoutThreshold
readonly MessageFragmenter Fragmenter
readonly MessageDefragmenter Defragmenter
readonly TConnection Connection
ConnectedClient(TConnection connection)
ConnectionInitialization InitializationStep
PendingClient(TConnection conn)
readonly TConnection Connection