diff --git a/src/Empostor.Api/Config/AntiCheatConfig.cs b/src/Empostor.Api/Config/AntiCheatConfig.cs
index 82f5f2d..c7544c7 100644
--- a/src/Empostor.Api/Config/AntiCheatConfig.cs
+++ b/src/Empostor.Api/Config/AntiCheatConfig.cs
@@ -38,6 +38,8 @@ namespace Empostor.Api.Config
public bool EnableRateLimits { get; set; } = true;
+ public bool EnableDuplicateLoginCheck { get; set; } = true;
+
public bool ForbidProtocolExtensions { get; set; } = true;
public bool EnablePacketSizeChecks { get; set; } = true;
diff --git a/src/Empostor.Api/Games/GameJoinError.cs b/src/Empostor.Api/Games/GameJoinError.cs
index a516a07..f6842a4 100644
--- a/src/Empostor.Api/Games/GameJoinError.cs
+++ b/src/Empostor.Api/Games/GameJoinError.cs
@@ -54,5 +54,10 @@
/// A custom message can be set in .
///
Custom,
+
+ ///
+ /// The same account is already playing in this game.
+ ///
+ DuplicateConnection,
}
}
diff --git a/src/Empostor.Server/Net/Client.cs b/src/Empostor.Server/Net/Client.cs
index 209366e..c94888e 100644
--- a/src/Empostor.Server/Net/Client.cs
+++ b/src/Empostor.Server/Net/Client.cs
@@ -221,6 +221,9 @@ namespace Empostor.Server.Net
case GameJoinError.Custom:
await DisconnectAsync(DisconnectReason.Custom, result.Message);
break;
+ case GameJoinError.DuplicateConnection:
+ await DisconnectAsync(DisconnectReason.DuplicateConnectionDetected);
+ break;
default:
await DisconnectAsync(DisconnectReason.Custom, "Unknown error.");
break;
diff --git a/src/Empostor.Server/Net/State/Game.Incoming.cs b/src/Empostor.Server/Net/State/Game.Incoming.cs
index b7bde99..b1a8407 100644
--- a/src/Empostor.Server/Net/State/Game.Incoming.cs
+++ b/src/Empostor.Server/Net/State/Game.Incoming.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Empostor.Api.Config;
using Empostor.Api.Games;
using Empostor.Api.Net;
using Next.Hazel;
@@ -118,8 +119,7 @@ namespace Empostor.Server.Net.State
return GameJoinResult.FromError(GameJoinError.InvalidClient);
}
- private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew)
- {
+ private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew) {
var client = sender.Client;
var authority = client.GameVersion.HasDisableServerAuthorityFlag ? "true" : "false";
var version = client.GameVersion.ToString();
@@ -155,6 +155,29 @@ namespace Empostor.Server.Net.State
}
}
+ private bool IsDuplicateAccount(ClientBase client)
+ {
+ if (string.IsNullOrEmpty(client.ProductUserId))
+ {
+ return false;
+ }
+
+ foreach (var other in _players.Values)
+ {
+ if (other.Client == client)
+ {
+ continue;
+ }
+
+ if (string.Equals(other.Client.ProductUserId, client.ProductUserId, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private async ValueTask AddClientSafeAsync(ClientBase client)
{
if (_bannedIps.Contains(client.Connection.EndPoint.Address))
@@ -163,6 +186,15 @@ namespace Empostor.Server.Net.State
}
var player = client.Player;
+ if (AntiCheat.Config.EnableDuplicateLoginCheck && IsDuplicateAccount(client))
+ {
+ _logger.LogWarning(
+ "#{Id} {Name} rejected: account {Puid} is already playing in this game",
+ client.Id, client.Name, client.ProductUserId);
+
+ return GameJoinResult.FromError(GameJoinError.DuplicateConnection);
+ }
+
if (_compatibilityConfig.AllowVersionMixing == false &&
this.Host != null && client.GameVersion != this.Host.Client.GameVersion)
{
diff --git a/src/Empostor.Server/config.json b/src/Empostor.Server/config.json
index fa15008..4f8486a 100644
--- a/src/Empostor.Server/config.json
+++ b/src/Empostor.Server/config.json
@@ -32,6 +32,7 @@
"EnableMeetingChecks": true,
"EnableVotingChecks": true,
"EnableRateLimits": true,
+ "EnableDuplicateLoginCheck": true,
"ForbidProtocolExtensions": true,
"EnablePacketSizeChecks": true,
"PacketSizeLimit": 1203