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);
}
}