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>
82 lines
3.8 KiB
C#
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 <id>.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);
|
|
}
|
|
}
|