Server 2.10.3: browse the MSCO mod list in the web panel

Mods -> MSCO mod list: search the approved mods from msco.mbm.wtf/mods and add or
update one with a click. The server lists and downloads only from the official
keymaster (never from a URL in the answer), checks the SHA-256 and stores it in
data/mods like an uploaded mod. Admins only. 6 new tests (154 pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 18:08:37 +02:00
co-authored by Claude Opus 5.5
parent 266a462fc3
commit d946c95198
7 changed files with 228 additions and 2 deletions
+59
View File
@@ -431,6 +431,65 @@ async Task PanelTests()
var cmd = await Call(owner, "POST", "api/console", new { command = "status" });
Check(cmd.status == 200 && cmd.json.GetProperty("message").GetString().Contains("players"), "Console command runs and returns its output");
// Mod-Liste des Keymasters: kleiner Ersatz-Keymaster auf localhost
{
var modBytes = new byte[3000];
Random.Shared.NextBytes(modBytes);
modBytes[0] = 0x4d; modBytes[1] = 0x5a;
string modSha = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(modBytes)).ToLowerInvariant();
bool serveBroken = false;
int catalogPort = 39000 + Random.Shared.Next(900);
var listener = new System.Net.HttpListener();
listener.Prefixes.Add($"http://127.0.0.1:{catalogPort}/");
listener.Start();
_ = Task.Run(async () =>
{
while (listener.IsListening)
{
System.Net.HttpListenerContext c;
try { c = await listener.GetContextAsync(); } catch { break; }
byte[] body;
if (c.Request.Url.AbsolutePath == "/api/mods")
{
body = System.Text.Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(new
{
mods = new[] { new { id = "catalogmod", name = "Catalog Mod", description = "From the list", author = "Tester", version = "1.0.0",
sha256 = modSha, size = modBytes.Length, changelog = "", updatedUtc = "2026-10-03", downloads = 3,
url = "http://evil.example/steal.dll" } }
}));
c.Response.ContentType = "application/json";
}
else if (c.Request.Url.AbsolutePath == "/mods/download/catalogmod/1.0.0")
{
body = serveBroken ? new byte[] { 0x4d, 0x5a, 1, 2, 3 } : modBytes;
}
else { c.Response.StatusCode = 404; body = Array.Empty<byte>(); }
await c.Response.OutputStream.WriteAsync(body);
c.Response.Close();
}
});
ModCatalog.BaseUrl = $"http://127.0.0.1:{catalogPort}";
var catalog = await Call(owner, "GET", "api/mods/catalog");
Check(catalog.status == 200 && catalog.json.EnumerateArray().Any(m => m.GetProperty("id").GetString() == "catalogmod" && !m.GetProperty("installed").GetBoolean()),
"Panel lists the MSCO mod list (not yet added)");
serveBroken = true;
var broken = await Call(owner, "POST", "api/mods/catalog/catalogmod/install");
Check(broken.status == 400 && !File.Exists(Path.Combine(dir, "mods", "catalogmod.dll")), "A download with the wrong checksum is not added");
serveBroken = false;
Check((await Call(mod1Placeholder(), "POST", "api/mods/catalog/catalogmod/install")).status is 401 or 403, "Without admin rights nothing can be added");
var installMod = await Call(owner, "POST", "api/mods/catalog/catalogmod/install");
Check(installMod.status == 200 && File.Exists(Path.Combine(dir, "mods", "catalogmod.dll"))
&& File.ReadAllBytes(Path.Combine(dir, "mods", "catalogmod.dll")).SequenceEqual(modBytes),
"Admin adds a mod from the list (downloaded from the keymaster, checksum checked)");
catalog = await Call(owner, "GET", "api/mods/catalog");
Check(catalog.json.EnumerateArray().Any(m => m.GetProperty("id").GetString() == "catalogmod" && m.GetProperty("upToDate").GetBoolean()),
"The list shows the mod as added");
Check((await Call(owner, "POST", "api/mods/catalog/doesnotexist/install")).status == 400, "Unknown mods cannot be added");
await Call(owner, "POST", "api/mods/catalogmod/remove");
listener.Stop();
ModCatalog.BaseUrl = MasterServer.Url;
}
HttpClient mod1Placeholder() => Browser();
var log = await Call(owner, "GET", "api/console?after=0");
Check(log.status == 200 && log.json.GetProperty("lines").GetArrayLength() > 0, "Live console log is readable");