using System.Net.Http.Json; using System.Security.Cryptography; using System.Text.Json.Serialization; namespace MSCO.Server; /// /// 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 <id>.dll). /// 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 Mods); static readonly HttpClient http = CreateHttp(); /// Adresse der Mod-Liste: der offizielle Master (nur die Tests setzen hier einen eigenen). 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; } /// Freigegebene Mods vom Keymaster. Wirft bei Netzwerkfehlern. public static async Task> ListAsync() { var res = await http.GetFromJsonAsync(BaseUrl + "/api/mods"); return res?.Mods ?? new List(); } /// Mod aus der Liste in data/mods übernehmen (bzw. aktualisieren). Fehlertext oder null. public static async Task InstallAsync(string dataDir, string id) { List 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); } }