Files
MrMahoandClaude Opus 5.5 d946c95198 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>
2026-10-03 18:08:37 +02:00

82 lines
3.8 KiB
C#

using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
namespace MSCO.Server;
/// <summary>
/// Mod-Liste des Keymasters (msco.mbm.wtf/mods: Mods von Spielern, von MSCO-Admins geprüft) im Web-Panel durchsuchen
/// und mit einem Klick als Server-Mod übernehmen. Geladen wird nur vom offiziellen Master, die SHA-256 wird geprüft;
/// danach liegt die Mod wie eine hochgeladene in data/mods (Datei &lt;id&gt;.dll).
/// </summary>
public static class ModCatalog
{
public sealed record Entry(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("author")] string Author,
[property: JsonPropertyName("version")] string Version,
[property: JsonPropertyName("sha256")] string Sha256,
[property: JsonPropertyName("size")] long Size,
[property: JsonPropertyName("changelog")] string Changelog,
[property: JsonPropertyName("updatedUtc")] string UpdatedUtc,
[property: JsonPropertyName("downloads")] int Downloads);
record ListResponse([property: JsonPropertyName("mods")] List<Entry> Mods);
static readonly HttpClient http = CreateHttp();
/// <summary>Adresse der Mod-Liste: der offizielle Master (nur die Tests setzen hier einen eigenen).</summary>
public static string BaseUrl { get; set; } = MasterServer.Url;
static HttpClient CreateHttp()
{
var c = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
c.DefaultRequestHeaders.UserAgent.ParseAdd("MSCO-Server");
return c;
}
/// <summary>Freigegebene Mods vom Keymaster. Wirft bei Netzwerkfehlern.</summary>
public static async Task<List<Entry>> ListAsync()
{
var res = await http.GetFromJsonAsync<ListResponse>(BaseUrl + "/api/mods");
return res?.Mods ?? new List<Entry>();
}
/// <summary>Mod aus der Liste in data/mods übernehmen (bzw. aktualisieren). Fehlertext oder null.</summary>
public static async Task<string> InstallAsync(string dataDir, string id)
{
List<Entry> list;
try { list = await ListAsync(); }
catch (Exception e) { return "The MSCO mod list is not reachable (" + e.Message + ")"; }
var entry = list.FirstOrDefault(m => m.Id == id);
if (entry == null) return "This mod is not in the MSCO mod list (any more)";
if (entry.Size > ModStore.MaxModBytes) return "This mod is too large";
// Immer vom offiziellen Master laden - nie von einer Adresse aus der Antwort
string url = BaseUrl + "/mods/download/" + Uri.EscapeDataString(entry.Id) + "/" + Uri.EscapeDataString(entry.Version);
byte[] data;
try
{
using var res = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!res.IsSuccessStatusCode) return "Download failed (HTTP " + (int)res.StatusCode + ")";
await using var stream = await res.Content.ReadAsStreamAsync();
using var ms = new MemoryStream();
var chunk = new byte[81920];
int read;
while ((read = await stream.ReadAsync(chunk)) > 0)
{
ms.Write(chunk, 0, read);
if (ms.Length > ModStore.MaxModBytes) return "This mod is too large";
}
data = ms.ToArray();
}
catch (Exception e) { return "Download failed (" + e.Message + ")"; }
string sha = Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();
if (!string.Equals(sha, entry.Sha256, StringComparison.OrdinalIgnoreCase)) return "The download is damaged (checksum mismatch) - try again";
return ModStore.Add(dataDir, entry.Id + ".dll", data);
}
}