Security: admin rights only for Steam-verified SteamIDs; protocol test suite (Tests/)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 23:51:18 +02:00
co-authored by Claude Opus 5.5
parent 1cfe139949
commit c70b284e17
6 changed files with 332 additions and 14 deletions
+32 -11
View File
@@ -27,6 +27,8 @@ public sealed class GameServer
public ClientState State;
public string Name = "?";
public string SteamId;
/// <summary>SteamID von Steam bestätigt (Ticket per Web API geprüft). Nur dann gibt es Admin-Rechte.</summary>
public bool SteamVerified;
public bool IsAdmin;
public Task<string> Verification;
}
@@ -53,8 +55,12 @@ public sealed class GameServer
static readonly TimeSpan ClientTimeout = TimeSpan.FromSeconds(30);
bool running = true;
public GameServer(ServerConfig config, string dataDir, string configPath, string ownerName)
/// <summary>Ohne Master-Server (nur für automatische Tests): keine Heartbeats, keine Serverliste.</summary>
readonly bool offline;
public GameServer(ServerConfig config, string dataDir, string configPath, string ownerName, bool offline = false)
{
this.offline = offline;
this.ownerName = ownerName;
this.config = config;
this.configPath = configPath;
@@ -109,6 +115,9 @@ public sealed class GameServer
if (!config.RequireSteam) Log("Non-Steam server: players without Steam can join (RequireSteam = false)");
else Log(steam.Enabled ? "Steam required - tickets are verified with the Steam Web API" : "Steam required (no Web API key set, tickets are not verified online)");
Log($"World: {world.Objects.Count} objects, {world.Events.Count} part/bolt actions");
if (!steam.Enabled && config.Admins.Count > 0)
Log("WARNING: Admins are disabled - without SteamWebApiKey anyone could pretend to be an admin. " +
"Get a free key at https://steamcommunity.com/dev/apikey and put it into server.json (\"SteamWebApiKey\").");
Log("Type 'help' for commands.");
nextHeartbeat = 0;
@@ -126,8 +135,11 @@ public sealed class GameServer
foreach (var c in clients.Values) c.Conn.Close("Server shutting down");
listener.Stop();
SaveWorld();
master.UnregisterAsync(config.LicenseKey).Wait(TimeSpan.FromSeconds(4)); // sofort aus der Serverliste
Log("Removed from the server list.");
if (!offline)
{
master.UnregisterAsync(config.LicenseKey).Wait(TimeSpan.FromSeconds(4)); // sofort aus der Serverliste
Log("Removed from the server list.");
}
Log("Server stopped.");
}
@@ -196,7 +208,7 @@ public sealed class GameServer
else if (result.Status == 0) Log("Master server not reachable - server is not listed right now.");
else ReportReachability(result);
}
if (now >= nextHeartbeat && heartbeat == null)
if (!offline && now >= nextHeartbeat && heartbeat == null)
{
nextHeartbeat = now + HeartbeatSeconds;
heartbeat = master.HeartbeatAsync(config, JoinedCount, ModVersion);
@@ -362,10 +374,15 @@ public sealed class GameServer
if (error != null) { Reject(c, error); return; }
if (JoinedCount >= config.MaxPlayers) { Reject(c, "Server is full"); return; }
// Gleicher Steam-Account nochmal verbunden: alte Verbindung trennen
// Nur eine von Steam bestätigte SteamID gilt als Beweis. Ohne SteamWebApiKey kann jeder eine beliebige
// SteamID behaupten - damit darf er weder Admin werden noch einen verbundenen Spieler verdrängen.
c.SteamVerified = steam.Enabled && !c.SteamId.StartsWith("name:");
foreach (var other in clients.Values)
if (other != c && other.State == ClientState.Joined && other.SteamId == c.SteamId)
other.Conn.Close("Joined from another location");
{
if (other == c || other.State != ClientState.Joined || other.SteamId != c.SteamId) continue;
if (c.SteamVerified) other.Conn.Close("Joined from another location"); // echter Account: alte Verbindung trennen
else { Reject(c, "A player with this identity is already connected"); return; }
}
c.State = ClientState.Joined;
var joined = clients.Values.Where(x => x.State == ClientState.Joined && !x.Conn.IsClosed).ToList();
@@ -379,7 +396,9 @@ public sealed class GameServer
Broadcast(MsgType.PlayerJoined, Wire.Build(w => { w.Write(c.Id); w.Write(c.Name); }), c.Id);
// Admin? Nur Spieler mit (geprüfter) SteamID können Admin sein
c.IsAdmin = !c.SteamId.StartsWith("name:") && config.Admins.Contains(c.SteamId);
c.IsAdmin = c.SteamVerified && config.Admins.Contains(c.SteamId);
if (!c.SteamVerified && config.Admins.Contains(c.SteamId))
Log($"{c.Name} uses admin SteamID {c.SteamId}, but it cannot be verified (no SteamWebApiKey) - no admin rights");
bool isAdmin = c.IsAdmin;
c.Conn.Send(MsgType.AdminStatus, Wire.Build(w => w.Write(isAdmin)));
@@ -462,7 +481,7 @@ public sealed class GameServer
config.Admins.Remove(target);
if (makeAdmin) config.Admins.Add(target);
config.Save(configPath);
foreach (var online in clients.Values.Where(x => x.SteamId == target))
foreach (var online in clients.Values.Where(x => x.SteamId == target && x.SteamVerified))
{
online.IsAdmin = makeAdmin;
online.Conn.Send(MsgType.AdminStatus, Wire.Build(w => w.Write(makeAdmin)));
@@ -475,9 +494,11 @@ public sealed class GameServer
Log("Unknown player id (see 'players') - or use a SteamID64");
break;
}
if (who.SteamId.StartsWith("name:"))
if (!who.SteamVerified)
{
Log("Only players with Steam can be admins (their SteamID is verified)");
Log(steam.Enabled
? "Only players with Steam can be admins"
: "Admins need a verified SteamID - set SteamWebApiKey in server.json (https://steamcommunity.com/dev/apikey)");
break;
}
config.Admins.Remove(who.SteamId);