diff --git a/marketplace/Empostor.Plugins.Monitor.dll b/marketplace/Empostor.Plugins.Monitor.dll index 84084cc..42bc88a 100644 Binary files a/marketplace/Empostor.Plugins.Monitor.dll and b/marketplace/Empostor.Plugins.Monitor.dll differ diff --git a/marketplace/Empostor.Plugins.Privacy.dll b/marketplace/Empostor.Plugins.Privacy.dll index fd8927b..9bb97c7 100644 Binary files a/marketplace/Empostor.Plugins.Privacy.dll and b/marketplace/Empostor.Plugins.Privacy.dll differ diff --git a/src/Empostor.Plugins.Monitor/MonitorController.cs b/src/Empostor.Plugins.Monitor/MonitorController.cs deleted file mode 100644 index 2d31994..0000000 --- a/src/Empostor.Plugins.Monitor/MonitorController.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text.Json.Serialization; -using Empostor.Api.Games.Managers; -using Empostor.Api.Net.Manager; -using Microsoft.AspNetCore.Mvc; - -namespace Empostor.Plugins.Monitor; - -[ApiController] -public sealed class MonitorController : ControllerBase -{ - private static readonly DateTime StartTime = Process.GetCurrentProcess().StartTime.ToUniversalTime(); - - private readonly IGameManager _gameManager; - private readonly IClientManager _clientManager; - - public MonitorController(IGameManager gameManager, IClientManager clientManager) - { - _gameManager = gameManager; - _clientManager = clientManager; - } - - [HttpGet("/api/monitor/status")] - public IActionResult GetStatus() - { - var process = Process.GetCurrentProcess(); - var uptime = DateTime.UtcNow - StartTime; - - var games = _gameManager.Games.ToList(); - var clients = _clientManager.Clients.ToList(); - - var status = new MonitorStatus - { - Status = "running", - Timestamp = DateTime.UtcNow.ToString("o"), - UptimeSeconds = (long)uptime.TotalSeconds, - UptimeDisplay = $"{uptime.Days}d {uptime.Hours}h {uptime.Minutes}m", - Version = GetVersion(), - Runtime = RuntimeInformation.FrameworkDescription, - Platform = RuntimeInformation.OSDescription, - Processors = Environment.ProcessorCount, - MemoryMb = process.WorkingSet64 / 1024 / 1024, - GameCount = games.Count, - PlayerCount = clients.Count(c => c.Player != null), - ActiveConnections = clients.Count, - Games = games.Select(g => new MonitorGame - { - Code = g.Code.ToString(), - State = g.GameState.ToString(), - Map = g.Options.Map.ToString(), - PlayerCount = g.PlayerCount, - Host = g.Host?.Character?.PlayerInfo?.PlayerName ?? "(none)", - }).ToList(), - }; - - return new JsonResult(status); - } - - [HttpGet("/api/monitor/health")] - public IActionResult GetHealth() - { - return Ok(new { status = "healthy", timestamp = DateTime.UtcNow.ToString("o") }); - } - - private static string GetVersion() - { - try - { - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - var info = System.Diagnostics.FileVersionInfo.GetVersionInfo(assembly.Location); - return info.ProductVersion ?? "unknown"; - } - catch - { - return "unknown"; - } - } -} - -public sealed class MonitorStatus -{ - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - [JsonPropertyName("timestamp")] - public string Timestamp { get; set; } = string.Empty; - - [JsonPropertyName("uptime_seconds")] - public long UptimeSeconds { get; set; } - - [JsonPropertyName("uptime_display")] - public string UptimeDisplay { get; set; } = string.Empty; - - [JsonPropertyName("version")] - public string Version { get; set; } = string.Empty; - - [JsonPropertyName("runtime")] - public string Runtime { get; set; } = string.Empty; - - [JsonPropertyName("platform")] - public string Platform { get; set; } = string.Empty; - - [JsonPropertyName("processors")] - public int Processors { get; set; } - - [JsonPropertyName("memory_mb")] - public long MemoryMb { get; set; } - - [JsonPropertyName("game_count")] - public int GameCount { get; set; } - - [JsonPropertyName("player_count")] - public int PlayerCount { get; set; } - - [JsonPropertyName("active_connections")] - public int ActiveConnections { get; set; } - - [JsonPropertyName("games")] - public List Games { get; set; } = new(); -} - -public sealed class MonitorGame -{ - [JsonPropertyName("code")] - public string Code { get; set; } = string.Empty; - - [JsonPropertyName("state")] - public string State { get; set; } = string.Empty; - - [JsonPropertyName("map")] - public string Map { get; set; } = string.Empty; - - [JsonPropertyName("player_count")] - public int PlayerCount { get; set; } - - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; -} diff --git a/src/Empostor.Plugins.Monitor/MonitorMiddleware.cs b/src/Empostor.Plugins.Monitor/MonitorMiddleware.cs new file mode 100644 index 0000000..1e6061c --- /dev/null +++ b/src/Empostor.Plugins.Monitor/MonitorMiddleware.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Empostor.Api.Games.Managers; +using Empostor.Api.Net.Manager; +using Microsoft.AspNetCore.Http; + +namespace Empostor.Plugins.Monitor; + +public sealed class MonitorMiddleware +{ + private static readonly DateTime StartTime = Process.GetCurrentProcess().StartTime.ToUniversalTime(); + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = null, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + }; + + private readonly RequestDelegate _next; + + public MonitorMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, IGameManager gameManager, IClientManager clientManager) + { + var path = context.Request.Path; + + if (path.Equals("/api/monitor/status", StringComparison.OrdinalIgnoreCase) + && HttpMethods.IsGet(context.Request.Method)) + { + await WriteJsonAsync(context, BuildStatus(gameManager, clientManager)); + return; + } + + if (path.Equals("/api/monitor/health", StringComparison.OrdinalIgnoreCase) + && HttpMethods.IsGet(context.Request.Method)) + { + await WriteJsonAsync(context, new + { + status = "healthy", + timestamp = DateTime.UtcNow.ToString("o"), + }); + return; + } + + await _next(context); + } + + private static MonitorStatus BuildStatus(IGameManager gameManager, IClientManager clientManager) + { + var process = Process.GetCurrentProcess(); + var uptime = DateTime.UtcNow - StartTime; + var games = gameManager.Games.ToList(); + var clients = clientManager.Clients.ToList(); + + return new MonitorStatus + { + Status = "running", + Timestamp = DateTime.UtcNow.ToString("o"), + UptimeSeconds = (long)uptime.TotalSeconds, + UptimeDisplay = $"{uptime.Days}d {uptime.Hours}h {uptime.Minutes}m", + Version = GetVersion(), + Runtime = RuntimeInformation.FrameworkDescription, + Platform = RuntimeInformation.OSDescription, + Processors = Environment.ProcessorCount, + MemoryMb = process.WorkingSet64 / 1024 / 1024, + GameCount = games.Count, + PlayerCount = clients.Count(c => c.Player != null), + ActiveConnections = clients.Count, + Games = games.Select(g => new MonitorGame + { + Code = g.Code.ToString(), + State = g.GameState.ToString(), + Map = g.Options.Map.ToString(), + PlayerCount = g.PlayerCount, + Host = g.Host?.Character?.PlayerInfo?.PlayerName ?? "(none)", + }).ToList(), + }; + } + + private static async Task WriteJsonAsync(HttpContext context, object value) + { + context.Response.ContentType = "application/json; charset=utf-8"; + await context.Response.WriteAsync(JsonSerializer.Serialize(value, JsonOptions)); + } + + private static string GetVersion() + { + try + { + var assembly = typeof(MonitorMiddleware).Assembly; + var info = FileVersionInfo.GetVersionInfo(assembly.Location); + return info.ProductVersion ?? "unknown"; + } + catch + { + return "unknown"; + } + } +} diff --git a/src/Empostor.Plugins.Monitor/MonitorModels.cs b/src/Empostor.Plugins.Monitor/MonitorModels.cs new file mode 100644 index 0000000..4afdbe7 --- /dev/null +++ b/src/Empostor.Plugins.Monitor/MonitorModels.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Empostor.Plugins.Monitor; + +public sealed class MonitorStatus +{ + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } = string.Empty; + + [JsonPropertyName("uptime_seconds")] + public long UptimeSeconds { get; set; } + + [JsonPropertyName("uptime_display")] + public string UptimeDisplay { get; set; } = string.Empty; + + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName("runtime")] + public string Runtime { get; set; } = string.Empty; + + [JsonPropertyName("platform")] + public string Platform { get; set; } = string.Empty; + + [JsonPropertyName("processors")] + public int Processors { get; set; } + + [JsonPropertyName("memory_mb")] + public long MemoryMb { get; set; } + + [JsonPropertyName("game_count")] + public int GameCount { get; set; } + + [JsonPropertyName("player_count")] + public int PlayerCount { get; set; } + + [JsonPropertyName("active_connections")] + public int ActiveConnections { get; set; } + + [JsonPropertyName("games")] + public List Games { get; set; } = new(); +} + +public sealed class MonitorGame +{ + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; + + [JsonPropertyName("state")] + public string State { get; set; } = string.Empty; + + [JsonPropertyName("map")] + public string Map { get; set; } = string.Empty; + + [JsonPropertyName("player_count")] + public int PlayerCount { get; set; } + + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; +} diff --git a/src/Empostor.Plugins.Monitor/MonitorPluginStartup.cs b/src/Empostor.Plugins.Monitor/MonitorPluginStartup.cs index 0a7d90e..cc9cf45 100644 --- a/src/Empostor.Plugins.Monitor/MonitorPluginStartup.cs +++ b/src/Empostor.Plugins.Monitor/MonitorPluginStartup.cs @@ -1,6 +1,5 @@ using Empostor.Api.Plugins; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -12,11 +11,10 @@ public sealed class MonitorPluginStartup : IPluginStartup, IPluginHttpStartup public void ConfigureServices(IServiceCollection services) { - services.AddControllers() - .AddApplicationPart(typeof(MonitorPluginStartup).Assembly); } public void ConfigureWebApplication(IApplicationBuilder app) { + app.UseMiddleware(); } } diff --git a/src/Empostor.Plugins.Privacy/PrivacyController.cs b/src/Empostor.Plugins.Privacy/PrivacyController.cs deleted file mode 100644 index 33c5f48..0000000 --- a/src/Empostor.Plugins.Privacy/PrivacyController.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.IO; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace Empostor.Plugins.Privacy; - -[ApiController] -[Route("/privacy")] -public sealed class PrivacyController : ControllerBase -{ - private readonly PrivacyStore _store; - private readonly ILogger _logger; - - public PrivacyController(PrivacyStore store, ILogger logger) - { - _store = store; - _logger = logger; - } - - [HttpGet] - public IActionResult GetPrivacy() - { - var html = _store.GetContent(); - return Content(html, "text/html; charset=utf-8"); - } - - [HttpPost] - [Route("/admin/api/privacy")] - public async Task UpdatePrivacy() - { - using var reader = new StreamReader(Request.Body); - var body = await reader.ReadToEndAsync(); - - try - { - var doc = JsonDocument.Parse(body); - var content = doc.RootElement.GetProperty("content").GetString() ?? string.Empty; - var token = doc.RootElement.GetProperty("token").GetString() ?? string.Empty; - - var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN") - ?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN") - ?? "empostor"; - - if (token != adminToken) - { - return Unauthorized(new { error = "Invalid token." }); - } - - _store.SaveContent(content); - return Ok(new { success = true }); - } - catch (Exception ex) - { - return BadRequest(new { error = ex.Message }); - } - } -} diff --git a/src/Empostor.Plugins.Privacy/PrivacyMiddleware.cs b/src/Empostor.Plugins.Privacy/PrivacyMiddleware.cs new file mode 100644 index 0000000..1758b7f --- /dev/null +++ b/src/Empostor.Plugins.Privacy/PrivacyMiddleware.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Empostor.Plugins.Privacy; + +public sealed class PrivacyMiddleware +{ + private readonly RequestDelegate _next; + + public PrivacyMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, PrivacyStore store) + { + var path = context.Request.Path; + + if (path.Equals("/privacy", StringComparison.OrdinalIgnoreCase) + && HttpMethods.IsGet(context.Request.Method)) + { + context.Response.ContentType = "text/html; charset=utf-8"; + await context.Response.WriteAsync(store.GetContent()); + return; + } + + if (path.Equals("/admin/api/privacy", StringComparison.OrdinalIgnoreCase) + && HttpMethods.IsPost(context.Request.Method)) + { + await HandleUpdateAsync(context, store); + return; + } + + await _next(context); + } + + private static async Task HandleUpdateAsync(HttpContext context, PrivacyStore store) + { + using var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + + try + { + var doc = JsonDocument.Parse(body); + var content = doc.RootElement.GetProperty("content").GetString() ?? string.Empty; + var token = doc.RootElement.GetProperty("token").GetString() ?? string.Empty; + + var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN") + ?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN") + ?? "empostor"; + + if (token != adminToken) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync("{\"error\":\"Invalid token.\"}"); + return; + } + + store.SaveContent(content); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync("{\"success\":true}"); + } + catch (Exception ex) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = "application/json"; + var error = JsonSerializer.Serialize(new { error = ex.Message }); + await context.Response.WriteAsync(error); + } + } +} diff --git a/src/Empostor.Plugins.Privacy/PrivacyPluginStartup.cs b/src/Empostor.Plugins.Privacy/PrivacyPluginStartup.cs index 821259b..6042f20 100644 --- a/src/Empostor.Plugins.Privacy/PrivacyPluginStartup.cs +++ b/src/Empostor.Plugins.Privacy/PrivacyPluginStartup.cs @@ -14,11 +14,10 @@ public sealed class PrivacyPluginStartup : IPluginStartup, IPluginHttpStartup { services.AddSingleton(); services.AddSingleton(); - services.AddControllers() - .AddApplicationPart(typeof(PrivacyPluginStartup).Assembly); } public void ConfigureWebApplication(IApplicationBuilder app) { + app.UseMiddleware(); } } diff --git a/src/Empostor.Server/Plugins/PluginLoader.cs b/src/Empostor.Server/Plugins/PluginLoader.cs index aed827f..7f7d975 100644 --- a/src/Empostor.Server/Plugins/PluginLoader.cs +++ b/src/Empostor.Server/Plugins/PluginLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Loader; using Empostor.Api.Plugins; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Hosting; @@ -16,6 +17,8 @@ namespace Empostor.Server.Plugins { private static readonly ILogger Logger = Log.ForContext(typeof(PluginLoader)); + internal static IReadOnlyList LoadedPluginAssemblies { get; private set; } = Array.Empty(); + public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config) { var assemblyInfos = new List(); @@ -105,6 +108,10 @@ namespace Empostor.Server.Plugins } var orderedPlugins = LoadOrderPlugins(plugins); + LoadedPluginAssemblies = orderedPlugins + .Select(p => p.PluginType.Assembly) + .Distinct() + .ToList(); foreach (var plugin in orderedPlugins) { @@ -120,11 +127,50 @@ namespace Empostor.Server.Plugins { plugin.Startup?.ConfigureServices(services); } + + EnsureMvcApplicationParts(services, orderedPlugins); }); return builder; } + private static void EnsureMvcApplicationParts(IServiceCollection services, IReadOnlyList plugins) + { + var manager = services + .LastOrDefault(d => d.ServiceType == typeof(ApplicationPartManager)) + ?.ImplementationInstance as ApplicationPartManager; + if (manager == null) + { + return; + } + + EnsureAssemblyPart(manager, typeof(PluginLoader).Assembly, insertFirst: true); + foreach (var plugin in plugins) + { + EnsureAssemblyPart(manager, plugin.PluginType.Assembly, insertFirst: false); + } + } + + private static void EnsureAssemblyPart(ApplicationPartManager manager, Assembly assembly, bool insertFirst) + { + if (manager.ApplicationParts.OfType().Any(p => p.Assembly == assembly)) + { + return; + } + + var part = new AssemblyPart(assembly); + if (insertFirst) + { + manager.ApplicationParts.Insert(0, part); + } + else + { + manager.ApplicationParts.Add(part); + } + + Logger.Information("Registered MVC controllers from {Assembly}.", assembly.GetName().Name); + } + private static void CheckPaths(IEnumerable paths) { foreach (var path in paths) diff --git a/src/Empostor.Server/Program.cs b/src/Empostor.Server/Program.cs index 323de58..fa47f76 100644 --- a/src/Empostor.Server/Program.cs +++ b/src/Empostor.Server/Program.cs @@ -33,6 +33,7 @@ using Empostor.Server.Service.Firewall; using Empostor.Server.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -269,7 +270,8 @@ namespace Empostor.Server { builder.ConfigureServices(s => { - s.AddControllers(); + s.AddControllers() + .ConfigureApplicationPartManager(EnsureServerApplicationPart); }); builder.Configure(app => { @@ -289,5 +291,32 @@ namespace Empostor.Server return hostBuilder; } + + private static void EnsureServerApplicationPart(ApplicationPartManager manager) + { + EnsureAssemblyPart(manager, typeof(Program).Assembly, insertFirst: true); + foreach (var assembly in PluginLoader.LoadedPluginAssemblies) + { + EnsureAssemblyPart(manager, assembly, insertFirst: false); + } + } + + private static void EnsureAssemblyPart(ApplicationPartManager manager, Assembly assembly, bool insertFirst) + { + if (manager.ApplicationParts.OfType().Any(p => p.Assembly == assembly)) + { + return; + } + + var part = new AssemblyPart(assembly); + if (insertFirst) + { + manager.ApplicationParts.Insert(0, part); + } + else + { + manager.ApplicationParts.Add(part); + } + } } }