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:
+32
-11
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user