diff --git a/ConfigEditor.cs b/ConfigEditor.cs index c6006db..4b039b8 100644 --- a/ConfigEditor.cs +++ b/ConfigEditor.cs @@ -15,7 +15,6 @@ public static class ConfigEditor requireSteam = config.RequireSteam, publicAddress = config.PublicAddress, saveSyncMinutes = config.SaveSyncMinutes, - licenseGraceHours = config.LicenseGraceHours, autosaveSeconds = config.AutosaveSeconds, port = config.Port, panelEnabled = config.PanelEnabled, @@ -70,7 +69,6 @@ public static class ConfigEditor config.PublicAddress = Text("publicAddress", config.PublicAddress, 200); config.SaveSyncMinutes = Number("saveSyncMinutes", config.SaveSyncMinutes, 0, 240); config.AutosaveSeconds = Number("autosaveSeconds", config.AutosaveSeconds, 10, 3600); - config.LicenseGraceHours = Number("licenseGraceHours", config.LicenseGraceHours, 1, 720); int before = changed.Count; int oldPort = config.Port, oldPanelPort = config.PanelPort; diff --git a/GameServer.cs b/GameServer.cs index 0257d9e..8443340 100644 --- a/GameServer.cs +++ b/GameServer.cs @@ -287,7 +287,6 @@ public sealed class GameServer if (result.Status == 0 || result.Status >= 500) Log("Master server not reachable - server is not listed right now."); else if (result.Status >= 200 && result.Status < 300) ReportReachability(result); } - CheckLicenseGrace(); if (!offline && now >= nextHeartbeat && heartbeat == null) { nextHeartbeat = now + HeartbeatSeconds; @@ -1119,15 +1118,14 @@ public sealed class GameServer /// Wann der Master den Schlüssel zuletzt bestätigt hat (setzt der Host beim Start). public DateTime LicenseConfirmedUtc { get; set; } = DateTime.UtcNow; - /// Gesetzt, wenn der Server wegen fehlender Lizenzbestätigung gestoppt wurde. - public string LicenseStopReason { get; private set; } /// Hinweis fürs Panel, solange der Master den Schlüssel nicht bestätigt (null = alles gut). public string LicenseWarning { get; private set; } DateTime nextLicenseWarning = DateTime.MinValue, lastLicenseCacheWrite = DateTime.MinValue; - TimeSpan LicenseGrace => TimeSpan.FromHours(Math.Clamp(config.LicenseGraceHours, 1, 24 * 30)); - - /// Antwort des Masters auf den Heartbeat auswerten - nie sofort stoppen. + /// + /// Antwort des Masters auf den Heartbeat auswerten. Ein laufender Server wird deswegen nie gestoppt - er läuft bis zum + /// nächsten Neustart weiter; erst beim Start wird die Lizenz wieder geprüft (siehe ServerHost). + /// void OnLicenseResult(int status) { if (status >= 200 && status < 300) @@ -1145,9 +1143,8 @@ public sealed class GameServer } bool rejected = status == 401 || status == 403; var since = DateTime.UtcNow - LicenseConfirmedUtc; - var left = LicenseGrace - since; LicenseWarning = (rejected ? "The master server rejects the license key" : "The master server is not reachable") + - $" (last confirmed {LicenseCache.Span(since)} ago) - the server keeps running for {LicenseCache.Span(left)}"; + $" (last confirmed {LicenseCache.Span(since)} ago) - the server keeps running until the next restart"; if (DateTime.UtcNow >= nextLicenseWarning) { nextLicenseWarning = DateTime.UtcNow.AddMinutes(15); @@ -1155,16 +1152,6 @@ public sealed class GameServer } } - /// Kulanzzeit abgelaufen? Dann den Gameserver stoppen (das Panel bleibt, der Host versucht es weiter). - void CheckLicenseGrace() - { - if (LicenseStopReason != null || DateTime.UtcNow - LicenseConfirmedUtc <= LicenseGrace) return; - LicenseStopReason = $"The license key could not be confirmed by the master server for {LicenseCache.Span(LicenseGrace)}"; - Log(LicenseStopReason + " - stopping the game server. It starts again automatically as soon as the master confirms the key."); - stoppedBy = "the license check"; - running = false; - } - public void RequestExit() { ExitRequested = true; diff --git a/LicenseCache.cs b/LicenseCache.cs index e5f0122..0bfc8d4 100644 --- a/LicenseCache.cs +++ b/LicenseCache.cs @@ -7,10 +7,13 @@ namespace MSCO.Server; /// /// Letzte erfolgreiche Lizenzprüfung beim Master (data/license.json). Ist der Master mal nicht erreichbar oder lehnt /// den Schlüssel kurz ab (z.B. nach einem Umzug ohne keys.json), läuft bzw. startet der Server damit weiter - bis -/// LicenseGraceHours ohne Bestätigung vergangen sind. Gespeichert wird nur ein Hash des Schlüssels. +/// GraceHours ohne Bestätigung vergangen sind (fest, nicht einstellbar). Gespeichert wird nur ein Hash des Schlüssels. /// public static class LicenseCache { + /// So lange startet ein Server ohne Bestätigung des Masters noch (ein laufender läuft bis zum Neustart weiter). + public static readonly TimeSpan Grace = TimeSpan.FromHours(72); + public sealed class Entry { public string KeyHash { get; set; } diff --git a/Panel/app.js b/Panel/app.js index 13657d3..803c24b 100644 --- a/Panel/app.js +++ b/Panel/app.js @@ -401,9 +401,7 @@ function renderSettings(page) { h("div", { class: "section-head" }, h("h2", {}, "Saving")), h("div", { class: "section-body form-grid wide" }, number("saveSyncMinutes", "Take the save game from a player every … minutes", c.saveSyncMinutes, { min: 0, max: 240, hint: "0 = only when someone joins" }), - number("autosaveSeconds", "Write the world state every … seconds", c.autosaveSeconds, { min: 10, max: 3600 }), - number("licenseGraceHours", "Keep running without license confirmation for … hours", c.licenseGraceHours, { min: 1, max: 720, - hint: "If the master server is down or rejects the key, the server keeps running this long" }))), + number("autosaveSeconds", "Write the world state every … seconds", c.autosaveSeconds, { min: 10, max: 3600 }))), h("div", { class: "section" }, h("div", { class: "section-head" }, h("h2", {}, "Network & panel"), h("span", { class: "muted" }, "needs a restart")), h("div", { class: "section-body form-grid wide" }, diff --git a/Panel/i18n.js b/Panel/i18n.js index 29c3c72..8d84119 100644 --- a/Panel/i18n.js +++ b/Panel/i18n.js @@ -283,7 +283,6 @@ "Choose a ZIP, RAR or 7z file": "Wähle eine ZIP-, RAR- oder 7z-Datei", "License not confirmed": "Lizenz nicht bestätigt", "Keep running without license confirmation for … hours": "Ohne Lizenzbestätigung weiterlaufen für … Stunden", - "If the master server is down or rejects the key, the server keeps running this long": "Ist der Master-Server aus oder lehnt den Schlüssel ab, läuft der Server so lange weiter", "⟳ Restart panel": "⟳ Panel neu starten", "Restart the panel?": "Panel neu starten?", "Applies the saved port and panel settings. If the game port changed, the game server restarts too. You stay logged in.": @@ -340,10 +339,9 @@ [/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"], [/^Master server not reachable (.+)$/, "Master-Server nicht erreichbar $1"], [/^Crashed: (.+)$/, "Abgestürzt: $1"], - [/^The master server (rejects the license key|is not reachable) \(last confirmed (.+) ago\) - the server keeps running for (.+)$/, - (m, why, ago, left) => (why.startsWith("rejects") ? "Der Master-Server lehnt den Lizenzschlüssel ab" : "Der Master-Server ist nicht erreichbar") + - " (zuletzt bestätigt vor " + ago + ") - der Server läuft noch " + left + " weiter"], - [/^The license key could not be confirmed by the master server for (.+)$/, "Der Lizenzschlüssel konnte $1 lang nicht vom Master-Server bestätigt werden"], + [/^The master server (rejects the license key|is not reachable) \(last confirmed (.+) ago\) - the server keeps running until the next restart$/, + (m, why, ago) => (why.startsWith("rejects") ? "Der Master-Server lehnt den Lizenzschlüssel ab" : "Der Master-Server ist nicht erreichbar") + + " (zuletzt bestätigt vor " + ago + ") - der Server läuft bis zum nächsten Neustart weiter"], [/^This file could not be read - upload a ZIP, RAR or 7z archive (.+)$/, "Die Datei konnte nicht gelesen werden - lade ein ZIP-, RAR- oder 7z-Archiv hoch $1"], [/^No defaultES2File\.txt in the archive(.*)$/, "Keine defaultES2File.txt im Archiv - pack die Dateien aus %USERPROFILE%\\AppData\\LocalLow\\Amistech\\My Summer Car"], ]; diff --git a/README.md b/README.md index 61ed68f..22e778b 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,9 @@ Mit diesem Code legst du im Browser das **Owner**-Konto an (erneut anzeigen: `ms - Oberfläche auf **Deutsch oder Englisch** (Umschalter unten in der Seitenleiste). - **Port oder Panel-Einstellungen geändert?** In den Einstellungen unten **„Panel neu starten“** – übernimmt sie sofort, man bleibt angemeldet und wird auf die neue Adresse weitergeleitet. -- **Keymaster nicht erreichbar oder lehnt den Schlüssel ab?** Der Server läuft weiter und warnt im Log/Panel; erst nach - `LicenseGraceHours` (Standard 72 h) ohne Bestätigung stoppt der Gameserver und startet von selbst wieder, sobald der - Master den Schlüssel bestätigt. Auch ein Neustart klappt in dieser Zeit (letzte Bestätigung in `data/license.json`). +- **Keymaster nicht erreichbar oder lehnt den Schlüssel ab?** Ein laufender Server läuft bis zum nächsten Neustart weiter + und warnt im Log/Panel. Ein Neustart klappt noch bis 72 Stunden nach der letzten Bestätigung (gespeichert in + `data/license.json`); danach startet der Server von selbst, sobald der Master den Schlüssel wieder bestätigt. - Passwörter werden nur als PBKDF2-Hash in `data/panel_users.json` gespeichert, Login mit Sperre nach Fehlversuchen, Sitzungs-Cookie (HttpOnly, SameSite=Strict), Schutz gegen CSRF. diff --git a/ServerConfig.cs b/ServerConfig.cs index e6e9e66..0c374e0 100644 --- a/ServerConfig.cs +++ b/ServerConfig.cs @@ -33,12 +33,6 @@ public sealed class ServerConfig /// Admins (SteamID64): dürfen im Spiel mit /admin Spieler kicken und bannen. public List Admins { get; set; } = new(); - /// - /// So lange (Stunden) läuft bzw. startet der Server weiter, wenn der Master den Lizenzschlüssel nicht bestätigen - /// kann (nicht erreichbar oder lehnt ihn ab). Danach wird der Gameserver gestoppt, bis der Master ihn wieder bestätigt. - /// - public int LicenseGraceHours { get; set; } = 72; - // ---------------------------------------------------------------- Web-Panel /// Web-Panel einschalten. public bool PanelEnabled { get; set; } = true; diff --git a/ServerHost.cs b/ServerHost.cs index ee3adbb..4e789de 100644 --- a/ServerHost.cs +++ b/ServerHost.cs @@ -171,14 +171,6 @@ public sealed class ServerHost Server = null; if (server.ExitRequested) break; - if (server.LicenseStopReason != null) - { - // Lizenz zu lange nicht bestätigt: regelmäßig neu versuchen - startet von selbst, sobald der Master wieder bestätigt - SetState(HostState.Error, server.LicenseStopReason); - startRequested = true; - startAt = DateTime.UtcNow + LicenseRetryDelay; - continue; - } if (server.RestartRequested) { GameServer.Log("Restarting the server ..."); @@ -269,14 +261,14 @@ public sealed class ServerHost else { // Master nicht erreichbar oder lehnt ab: mit der letzten Bestätigung starten, solange sie frisch genug ist - var grace = TimeSpan.FromHours(Math.Clamp(Config.LicenseGraceHours, 1, 24 * 30)); + var grace = LicenseCache.Grace; var cached = LicenseCache.Load(dataDir, Config.LicenseKey); string problem = key.Unreachable ? $"Master server not reachable ({key.Error})" : "License key rejected: " + (key.Error ?? "unknown key"); if (cached != null && DateTime.UtcNow - cached.LastConfirmedUtc < grace) { var left = grace - (DateTime.UtcNow - cached.LastConfirmedUtc); GameServer.Log($"WARNING: {problem} - starting with the license confirmed {LicenseCache.Span(DateTime.UtcNow - cached.LastConfirmedUtc)} ago " + - $"(owner {cached.Owner}). The server keeps running for {LicenseCache.Span(left)} unless the master confirms it again."); + $"(owner {cached.Owner}). The server keeps running until the next restart; a restart works for {LicenseCache.Span(left)} more."); owner = cached.Owner ?? "?"; confirmed = cached.LastConfirmedUtc; } diff --git a/Tests/Program.cs b/Tests/Program.cs index fc56c86..80ac433 100644 --- a/Tests/Program.cs +++ b/Tests/Program.cs @@ -486,7 +486,7 @@ async Task PanelTests() using (var da = new System.IO.Compression.ZipArchive(new MemoryStream(downZip))) Check(down.IsSuccessStatusCode && da.GetEntry("defaultES2File.txt") != null, "The current save can be downloaded as ZIP"); - // Lizenz: Master fällt aus -> der Server läuft weiter, erst nach der Kulanzzeit wird er gestoppt + // Lizenz: Master fällt aus -> der laufende Server läuft bis zum nächsten Neustart weiter LicenseCache.Save(dir, "test", "Tester", "Test", DateTime.UtcNow.AddHours(-5)); var cachedLicense = LicenseCache.Load(dir, "test"); Check(cachedLicense != null && cachedLicense.Owner == "Tester" && LicenseCache.Load(dir, "other-key") == null @@ -494,14 +494,10 @@ async Task PanelTests() "The last license check is remembered (only a hash of the key)"); Srv().LicenseConfirmedUtc = DateTime.UtcNow.AddHours(-10); // innerhalb der 72 h: läuft weiter await Task.Delay(500); - Check(host.State == HostState.Running, "Server keeps running while the license is unconfirmed but within the grace period"); - Srv().LicenseConfirmedUtc = DateTime.UtcNow.AddHours(-100); // Kulanzzeit abgelaufen - await WaitForState(HostState.Error); - Check(host.State == HostState.Error && (host.LastError ?? "").Contains("license"), "After the grace period the game server stops: " + host.LastError); - Check((await Call(owner, "GET", "api/session")).status == 200, "The panel stays reachable after the license stop"); - Check((await Call(owner, "POST", "api/server/start")).status == 200, "It can be started again"); - await WaitForState(HostState.Running); - Check(host.State == HostState.Running, "Running again after the start"); + Check(host.State == HostState.Running, "Server keeps running while the license is unconfirmed"); + Srv().LicenseConfirmedUtc = DateTime.UtcNow.AddHours(-100); // länger als die 72 h für einen Start + await Task.Delay(1500); + Check(host.State == HostState.Running, "A running server is never stopped by the license check - it runs until the next restart"); // Panel neu starten: eigener Port, danach wieder zurück auf den Spiel-Port - angemeldet bleiben Check((await Call(owner, "POST", "api/config", new { panelPort = 17791 })).status == 200, "Panel port can be changed");