From 173bff67df812b99ffc430ffd0ec168fb680b14a Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sat, 22 Jul 2023 02:08:39 +0200 Subject: [PATCH 1/5] Add basic miner check --- .../Services/Background/MalwareScanService.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Moonlight/App/Services/Background/MalwareScanService.cs b/Moonlight/App/Services/Background/MalwareScanService.cs index 7db31660..d80fd2c2 100644 --- a/Moonlight/App/Services/Background/MalwareScanService.cs +++ b/Moonlight/App/Services/Background/MalwareScanService.cs @@ -162,6 +162,29 @@ public class MalwareScanService } } + async Task ScanMinerJar() + { + var access = await ServerService.CreateFileAccess(server, null!); + var fileElements = await access.Ls(); + + if (fileElements.Any(x => x.Name == "libraries" && !x.IsFile)) + { + await access.Cd("libraries"); + + fileElements = await access.Ls(); + + if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile)) + { + results.Add(new () + { + Title = "Found Miner", + Description = "Detected suspicious library directory which may contain a script for miners", + Author = "Marcel Baumgartner" + }); + } + } + } + async Task ScanFakePlayerPlugins() { var access = await ServerService.CreateFileAccess(server, null!); @@ -190,6 +213,7 @@ public class MalwareScanService // Execute scans await ScanSelfBot(); await ScanFakePlayerPlugins(); + await ScanMinerJar(); return results.ToArray(); } From 21bea974a9f2c3f7ae37ae932491139c08126202 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sat, 22 Jul 2023 23:44:45 +0200 Subject: [PATCH 2/5] Implemented a basic plugin system --- Moonlight/App/Helpers/ComponentHelper.cs | 12 + Moonlight/App/Plugin/MoonlightPlugin.cs | 15 + .../Plugin/UI/Servers/ServerPageContext.cs | 11 + .../App/Plugin/UI/Servers/ServerSetting.cs | 9 + Moonlight/App/Plugin/UI/Servers/ServerTab.cs | 11 + .../UI/Webspaces/WebspacePageContext.cs | 10 + .../App/Plugin/UI/Webspaces/WebspaceTab.cs | 10 + Moonlight/App/Services/PluginService.cs | 69 +++++ Moonlight/Moonlight.csproj | 1 + Moonlight/Program.cs | 2 + .../WebsiteControl/WebSpaceNavigation.razor | 34 +-- Moonlight/Shared/Views/Server/Index.razor | 259 +++++++++++------- .../Views/Server/ServerNavigation.razor | 80 ++---- .../Shared/Views/Server/ServerSettings.razor | 100 ++----- Moonlight/Shared/Views/Webspace/Index.razor | 98 ++++--- 15 files changed, 412 insertions(+), 309 deletions(-) create mode 100644 Moonlight/App/Helpers/ComponentHelper.cs create mode 100644 Moonlight/App/Plugin/MoonlightPlugin.cs create mode 100644 Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs create mode 100644 Moonlight/App/Plugin/UI/Servers/ServerSetting.cs create mode 100644 Moonlight/App/Plugin/UI/Servers/ServerTab.cs create mode 100644 Moonlight/App/Plugin/UI/Webspaces/WebspacePageContext.cs create mode 100644 Moonlight/App/Plugin/UI/Webspaces/WebspaceTab.cs create mode 100644 Moonlight/App/Services/PluginService.cs diff --git a/Moonlight/App/Helpers/ComponentHelper.cs b/Moonlight/App/Helpers/ComponentHelper.cs new file mode 100644 index 00000000..830c5958 --- /dev/null +++ b/Moonlight/App/Helpers/ComponentHelper.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components; + +namespace Moonlight.App.Helpers; + +public class ComponentHelper +{ + public static RenderFragment FromType(Type type) => builder => + { + builder.OpenComponent(0, type); + builder.CloseComponent(); + }; +} \ No newline at end of file diff --git a/Moonlight/App/Plugin/MoonlightPlugin.cs b/Moonlight/App/Plugin/MoonlightPlugin.cs new file mode 100644 index 00000000..702dddc7 --- /dev/null +++ b/Moonlight/App/Plugin/MoonlightPlugin.cs @@ -0,0 +1,15 @@ +using Moonlight.App.Plugin.UI; +using Moonlight.App.Plugin.UI.Servers; +using Moonlight.App.Plugin.UI.Webspaces; + +namespace Moonlight.App.Plugin; + +public abstract class MoonlightPlugin +{ + public string Name { get; set; } = "N/A"; + public string Author { get; set; } = "N/A"; + public string Version { get; set; } = "N/A"; + + public Func? OnBuildServerPage { get; set; } + public Func? OnBuildWebspacePage { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs b/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs new file mode 100644 index 00000000..0905be1d --- /dev/null +++ b/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs @@ -0,0 +1,11 @@ +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Plugin.UI.Servers; + +public class ServerPageContext +{ + public List Tabs { get; set; } = new(); + public List Settings { get; set; } = new(); + public Server Server { get; set; } + public User User { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Servers/ServerSetting.cs b/Moonlight/App/Plugin/UI/Servers/ServerSetting.cs new file mode 100644 index 00000000..b032c66d --- /dev/null +++ b/Moonlight/App/Plugin/UI/Servers/ServerSetting.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Components; + +namespace Moonlight.App.Plugin.UI.Servers; + +public class ServerSetting +{ + public string Name { get; set; } + public RenderFragment Component { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Servers/ServerTab.cs b/Moonlight/App/Plugin/UI/Servers/ServerTab.cs new file mode 100644 index 00000000..8a31e6f9 --- /dev/null +++ b/Moonlight/App/Plugin/UI/Servers/ServerTab.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components; + +namespace Moonlight.App.Plugin.UI.Servers; + +public class ServerTab +{ + public string Name { get; set; } + public string Route { get; set; } + public string Icon { get; set; } + public RenderFragment Component { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Webspaces/WebspacePageContext.cs b/Moonlight/App/Plugin/UI/Webspaces/WebspacePageContext.cs new file mode 100644 index 00000000..a8b8d700 --- /dev/null +++ b/Moonlight/App/Plugin/UI/Webspaces/WebspacePageContext.cs @@ -0,0 +1,10 @@ +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Plugin.UI.Webspaces; + +public class WebspacePageContext +{ + public List Tabs { get; set; } = new(); + public User User { get; set; } + public WebSpace WebSpace { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Webspaces/WebspaceTab.cs b/Moonlight/App/Plugin/UI/Webspaces/WebspaceTab.cs new file mode 100644 index 00000000..70d02c88 --- /dev/null +++ b/Moonlight/App/Plugin/UI/Webspaces/WebspaceTab.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Components; + +namespace Moonlight.App.Plugin.UI.Webspaces; + +public class WebspaceTab +{ + public string Name { get; set; } = "N/A"; + public string Route { get; set; } = "/"; + public RenderFragment Component { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Services/PluginService.cs b/Moonlight/App/Services/PluginService.cs new file mode 100644 index 00000000..fb4f04eb --- /dev/null +++ b/Moonlight/App/Services/PluginService.cs @@ -0,0 +1,69 @@ +using System.Reflection; +using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers; +using Moonlight.App.Plugin; +using Moonlight.App.Plugin.UI; +using Moonlight.App.Plugin.UI.Servers; +using Moonlight.App.Plugin.UI.Webspaces; + +namespace Moonlight.App.Services; + +public class PluginService +{ + public List Plugins { get; set; } + + public PluginService() + { + LoadPlugins(); + } + + private void LoadPlugins() + { + Plugins = new(); + + var pluginType = typeof(MoonlightPlugin); + + foreach (var pluginFile in Directory.EnumerateFiles( + PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins")) + .Where(x => x.EndsWith(".dll"))) + { + var assembly = Assembly.LoadFile(pluginFile); + + foreach (var type in assembly.GetTypes()) + { + if (type.IsSubclassOf(pluginType)) + { + var plugin = (Activator.CreateInstance(type) as MoonlightPlugin)!; + + Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}"); + + Plugins.Add(plugin); + } + } + } + + Logger.Info($"Loaded {Plugins.Count} plugins"); + } + + public async Task BuildServerPage(ServerPageContext context) + { + foreach (var plugin in Plugins) + { + if (plugin.OnBuildServerPage != null) + await plugin.OnBuildServerPage.Invoke(context); + } + + return context; + } + + public async Task BuildWebspacePage(WebspacePageContext context) + { + foreach (var plugin in Plugins) + { + if (plugin.OnBuildWebspacePage != null) + await plugin.OnBuildWebspacePage.Invoke(context); + } + + return context; + } +} \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index e8a71f2d..d8410558 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -90,6 +90,7 @@ + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index bd2c59ec..4f504771 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -239,6 +239,7 @@ namespace Moonlight builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Other builder.Services.AddSingleton(); @@ -289,6 +290,7 @@ namespace Moonlight _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); + _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); diff --git a/Moonlight/Shared/Components/WebsiteControl/WebSpaceNavigation.razor b/Moonlight/Shared/Components/WebsiteControl/WebSpaceNavigation.razor index 92c0f2f8..6bce8879 100644 --- a/Moonlight/Shared/Components/WebsiteControl/WebSpaceNavigation.razor +++ b/Moonlight/Shared/Components/WebsiteControl/WebSpaceNavigation.razor @@ -1,4 +1,5 @@ @using Moonlight.App.Database.Entities +@using Moonlight.App.Plugin.UI.Webspaces @using Moonlight.App.Services @using Moonlight.App.Services.Interop @@ -34,26 +35,14 @@
@@ -61,10 +50,13 @@ @code { [Parameter] - public int Index { get; set; } + public string Route { get; set; } [Parameter] public WebSpace WebSpace { get; set; } + + [CascadingParameter] + public WebspacePageContext Context { get; set; } private async Task Delete() { diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index d91238e8..3e0b4689 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -4,12 +4,16 @@ @using Microsoft.EntityFrameworkCore @using Moonlight.App.Database.Entities @using Moonlight.App.Events +@using Moonlight.App.Helpers @using Moonlight.App.Helpers.Wings @using Moonlight.App.Helpers.Wings.Enums +@using Moonlight.App.Plugin.UI +@using Moonlight.App.Plugin.UI.Servers @using Moonlight.App.Repositories @using Moonlight.App.Services @using Moonlight.App.Services.Sessions @using Moonlight.Shared.Components.Xterm +@using Moonlight.Shared.Views.Server.Settings @using Newtonsoft.Json @inject ImageRepository ImageRepository @@ -21,10 +25,11 @@ @inject DynamicBackgroundService DynamicBackgroundService @inject SmartTranslateService SmartTranslateService @inject IdentityService IdentityService +@inject PluginService PluginService @implements IDisposable - + @if (CurrentServer == null) { @@ -33,7 +38,7 @@ { if (NodeOnline) { - if (Console.ConsoleState == ConsoleState.Connected) + if (Console != null && Console.ConsoleState == ConsoleState.Connected) { if (Console.ServerState == ServerState.Installing || CurrentServer.Installing) { @@ -75,38 +80,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @foreach (var tab in Context.Tabs) + { + + + @(tab.Component) + + + } + + @@ -114,8 +99,17 @@ } else { -
- Connecting +
+
+
+

+ Connecting +

+

+ Connecting to the servers console to stream logs and the current resource usage +

+
+
} } @@ -144,21 +138,19 @@ [Parameter] public string ServerUuid { get; set; } - - [Parameter] public string? Route { get; set; } private WingsConsole? Console; private Server? CurrentServer; - private Node Node; private bool NodeOnline = false; - private Image Image; private NodeAllocation NodeAllocation; private string[] Tags; private Terminal? InstallConsole; + private ServerPageContext Context; + protected override void OnInitialized() { Console = new(); @@ -171,109 +163,166 @@ Console.OnMessage += async (_, s) => { - if (Console.ServerState == ServerState.Installing) + if (Console.ServerState == ServerState.Installing && InstallConsole != null) { - if (InstallConsole != null) - { - if (s.IsInternal) - await InstallConsole.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + s.Content + "\x1b[0m"); - else - await InstallConsole.WriteLine(s.Content); - } + if (s.IsInternal) + await InstallConsole.WriteLine("\x1b[38;5;16;48;5;135m\x1b[39m\x1b[1m Moonlight \x1b[0m " + s.Content + "\x1b[0m"); + else + await InstallConsole.WriteLine(s.Content); } }; } - private async Task LoadData(LazyLoader lazyLoader) + private async Task Load(LazyLoader lazyLoader) { await lazyLoader.SetText("Requesting server data"); - try - { - var uuid = Guid.Parse(ServerUuid); + if (!Guid.TryParse(ServerUuid, out var uuid)) + return; - CurrentServer = ServerRepository - .Get() - .Include(x => x.Allocations) - .Include(x => x.Image) - .Include(x => x.Node) - .Include(x => x.Variables) - .Include(x => x.MainAllocation) - .Include(x => x.Owner) - .First(x => x.Uuid == uuid); - } - catch (Exception) - { - // ignored - } + CurrentServer = ServerRepository + .Get() + .Include(x => x.Allocations) + .Include(x => x.Image) + .Include("Image.Variables") + .Include(x => x.Node) + .Include(x => x.Variables) + .Include(x => x.MainAllocation) + .Include(x => x.Owner) + .First(x => x.Uuid == uuid); if (CurrentServer != null) { if (CurrentServer.Owner.Id != IdentityService.User.Id && !IdentityService.User.Admin) + { CurrentServer = null; - } + return; + } + + if (string.IsNullOrEmpty(CurrentServer.Image.BackgroundImageUrl)) + await DynamicBackgroundService.Reset(); + else + await DynamicBackgroundService.Change(CurrentServer.Image.BackgroundImageUrl); - if (CurrentServer != null) - { await lazyLoader.SetText("Checking node online status"); NodeOnline = await ServerService.IsHostUp(CurrentServer); - if (NodeOnline) - { - await lazyLoader.SetText("Checking server variables"); + if (!NodeOnline) + return; - var image = ImageRepository - .Get() - .Include(x => x.Variables) - .First(x => x.Id == CurrentServer.Image.Id); + await lazyLoader.SetText("Checking server variables"); // Live variable migration - foreach (var variable in image.Variables) + foreach (var variable in CurrentServer.Image.Variables) + { + if (CurrentServer.Variables.All(x => x.Key != variable.Key)) { - if (!CurrentServer.Variables.Any(x => x.Key == variable.Key)) + CurrentServer.Variables.Add(new ServerVariable() { - CurrentServer.Variables.Add(new ServerVariable() - { - Key = variable.Key, - Value = variable.DefaultValue - }); + Key = variable.Key, + Value = variable.DefaultValue + }); - ServerRepository.Update(CurrentServer); - } + ServerRepository.Update(CurrentServer); } + } // Tags - await lazyLoader.SetText("Requesting tags"); + await lazyLoader.SetText("Reading tags"); - Tags = JsonConvert.DeserializeObject(image.TagsJson) ?? Array.Empty(); - Image = image; + Tags = JsonConvert.DeserializeObject(CurrentServer.Image.TagsJson) ?? Array.Empty(); - await lazyLoader.SetText("Connecting to console"); + // Build server pages and settings - await ReconnectConsole(); + Context = new ServerPageContext() + { + Server = CurrentServer, + User = IdentityService.User + }; - await Event.On($"server.{CurrentServer.Uuid}.installComplete", this, server => - { - NavigationManager.NavigateTo(NavigationManager.Uri, true); + Context.Tabs.Add(new() + { + Name = "Console", + Route = "/", + Icon = "terminal", + Component = ComponentHelper.FromType(typeof(ServerConsole)) + }); - return Task.CompletedTask; - }); + Context.Tabs.Add(new() + { + Name = "Files", + Route = "/files", + Icon = "folder", + Component = ComponentHelper.FromType(typeof(ServerFiles)) + }); - await Event.On($"server.{CurrentServer.Uuid}.archiveStatusChanged", this, server => - { - NavigationManager.NavigateTo(NavigationManager.Uri, true); + Context.Tabs.Add(new() + { + Name = "Backups", + Route = "/backups", + Icon = "box", + Component = ComponentHelper.FromType(typeof(ServerBackups)) + }); - return Task.CompletedTask; - }); + Context.Tabs.Add(new() + { + Name = "Network", + Route = "/network", + Icon = "wifi", + Component = ComponentHelper.FromType(typeof(ServerNetwork)) + }); - if (string.IsNullOrEmpty(Image.BackgroundImageUrl)) - await DynamicBackgroundService.Reset(); - else - await DynamicBackgroundService.Change(Image.BackgroundImageUrl); - } + Context.Tabs.Add(new() + { + Name = "Settings", + Route = "/settings", + Icon = "cog", + Component = ComponentHelper.FromType(typeof(ServerSettings)) + }); + + // Add default settings + + Context.Settings.Add(new() + { + Name = "Rename", + Component = ComponentHelper.FromType(typeof(ServerRenameSetting)) + }); + + Context.Settings.Add(new() + { + Name = "Reset", + Component = ComponentHelper.FromType(typeof(ServerResetSetting)) + }); + + Context.Settings.Add(new() + { + Name = "Delete", + Component = ComponentHelper.FromType(typeof(ServerDeleteSetting)) + }); + + Context = await PluginService.BuildServerPage(Context); + + await lazyLoader.SetText("Connecting to console"); + await ReconnectConsole(); + + // Register event system + + await Event.On($"server.{CurrentServer.Uuid}.installComplete", this, server => + { + NavigationManager.NavigateTo(NavigationManager.Uri, true); + + return Task.CompletedTask; + }); + + await Event.On($"server.{CurrentServer.Uuid}.archiveStatusChanged", this, server => + { + NavigationManager.NavigateTo(NavigationManager.Uri, true); + + return Task.CompletedTask; + }); } } diff --git a/Moonlight/Shared/Views/Server/ServerNavigation.razor b/Moonlight/Shared/Views/Server/ServerNavigation.razor index 7f84ce7c..ebb37a75 100644 --- a/Moonlight/Shared/Views/Server/ServerNavigation.razor +++ b/Moonlight/Shared/Views/Server/ServerNavigation.razor @@ -3,6 +3,8 @@ @using Moonlight.App.Helpers @using Moonlight.App.Helpers.Wings @using Moonlight.App.Helpers.Wings.Enums +@using Moonlight.App.Plugin.UI +@using Moonlight.App.Plugin.UI.Servers @using Moonlight.App.Services.Sessions @inject SmartTranslateService TranslationService @@ -122,66 +124,19 @@
@@ -198,16 +153,17 @@ [CascadingParameter] public Server CurrentServer { get; set; } - - [CascadingParameter] public WingsConsole Console { get; set; } + + [CascadingParameter] + public ServerPageContext Context { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] - public int Index { get; set; } = 0; + public string Route { get; set; } = "/"; //TODO: NodeIpService which loads and caches raw ips for nodes (maybe) diff --git a/Moonlight/Shared/Views/Server/ServerSettings.razor b/Moonlight/Shared/Views/Server/ServerSettings.razor index 25999848..82960044 100644 --- a/Moonlight/Shared/Views/Server/ServerSettings.razor +++ b/Moonlight/Shared/Views/Server/ServerSettings.razor @@ -1,91 +1,29 @@ -@using Moonlight.App.Database.Entities -@using Moonlight.Shared.Views.Server.Settings -@using Microsoft.AspNetCore.Components.Rendering +@using Moonlight.App.Plugin.UI.Servers - -
- @foreach (var setting in Settings) - { -
-
-
-

- -

-
-
- @(GetComponent(setting.Value)) -
+
+ @foreach (var setting in Context.Settings) + { +
+
+
+

+ +

+
+
+ @(setting.Component)
- } -
- +
+ } +
@code { [CascadingParameter] - public Server CurrentServer { get; set; } - - [CascadingParameter] - public string[] Tags { get; set; } - - private Dictionary Settings = new(); - - private Task Load(LazyLoader lazyLoader) - { - if (Tags.Contains("paperversion")) - Settings.Add("Paper version", typeof(PaperVersionSetting)); - - if (Tags.Contains("forgeversion")) - Settings.Add("Forge version", typeof(ForgeVersionSetting)); - - if (Tags.Contains("fabricversion")) - Settings.Add("Fabric version", typeof(FabricVersionSetting)); - - if (Tags.Contains("join2start")) - Settings.Add("Join2Start", typeof(Join2StartSetting)); - - if (Tags.Contains("javascriptversion")) - Settings.Add("Javascript version", typeof(JavascriptVersionSetting)); - - if (Tags.Contains("javascriptfile")) - Settings.Add("Javascript file", typeof(JavascriptFileSetting)); - - if (Tags.Contains("pythonversion")) - Settings.Add("Python version", typeof(PythonVersionSetting)); - - if (Tags.Contains("javaversion")) - Settings.Add("Java version", typeof(JavaRuntimeVersionSetting)); - - if (Tags.Contains("dotnetversion")) - Settings.Add("Dotnet version", typeof(DotnetVersionSetting)); - - if (Tags.Contains("pythonfile")) - Settings.Add("Python file", typeof(PythonFileSetting)); - - if (Tags.Contains("javafile")) - Settings.Add("Jar file", typeof(JavaFileSetting)); - - if (Tags.Contains("dotnetfile")) - Settings.Add("Dll file", typeof(DotnetFileSetting)); - - Settings.Add("Rename", typeof(ServerRenameSetting)); - - Settings.Add("Reset", typeof(ServerResetSetting)); - - Settings.Add("Delete", typeof(ServerDeleteSetting)); - - return Task.CompletedTask; - } - - private RenderFragment GetComponent(Type type) => builder => - { - builder.OpenComponent(0, type); - builder.CloseComponent(); - }; + public ServerPageContext Context { get; set; } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Webspace/Index.razor b/Moonlight/Shared/Views/Webspace/Index.razor index 1de867e0..d3e17f36 100644 --- a/Moonlight/Shared/Views/Webspace/Index.razor +++ b/Moonlight/Shared/Views/Webspace/Index.razor @@ -4,10 +4,13 @@ @using Moonlight.App.Services @using Moonlight.Shared.Components.WebsiteControl @using Microsoft.EntityFrameworkCore +@using Moonlight.App.Helpers +@using Moonlight.App.Plugin.UI.Webspaces @using Moonlight.App.Services.Sessions @inject Repository WebSpaceRepository @inject WebSpaceService WebSpaceService +@inject PluginService PluginService @inject IdentityService IdentityService @@ -32,43 +35,17 @@ if (HostOnline) { - @{ - var index = 0; - - switch (Route) - { - case "files": - index = 1; - break; - case "sftp": - index = 2; - break; - case "databases": - index = 3; - break; - default: - index = 0; - break; - } - - - - @switch (Route) - { - case "files": - - break; - case "sftp": - - break; - case "databases": - - break; - default: - - break; - } - } + + + @foreach (var tab in Context.Tabs) + { + + + @(tab.Component) + + } + + } else @@ -101,6 +78,8 @@ private WebSpace? CurrentWebspace; private bool HostOnline = false; + private WebspacePageContext Context; + private async Task Load(LazyLoader lazyLoader) { CurrentWebspace = WebSpaceRepository @@ -112,14 +91,53 @@ if (CurrentWebspace != null) { if (CurrentWebspace.Owner.Id != IdentityService.User.Id && !IdentityService.User.Admin) + { CurrentWebspace = null; - } + return; + } - if (CurrentWebspace != null) - { await lazyLoader.SetText("Checking host system online status"); HostOnline = await WebSpaceService.IsHostUp(CurrentWebspace); + + if (!HostOnline) + return; + + Context = new WebspacePageContext() + { + WebSpace = CurrentWebspace, + User = IdentityService.User + }; + + Context.Tabs.Add(new() + { + Route = "/", + Name = "Dashboard", + Component = ComponentHelper.FromType(typeof(WebSpaceDashboard)) + }); + + Context.Tabs.Add(new() + { + Route = "/files", + Name = "Files", + Component = ComponentHelper.FromType(typeof(WebSpaceFiles)) + }); + + Context.Tabs.Add(new() + { + Route = "/sftp", + Name = "SFTP", + Component = ComponentHelper.FromType(typeof(WebSpaceSftp)) + }); + + Context.Tabs.Add(new() + { + Route = "/databases", + Name = "Databases", + Component = ComponentHelper.FromType(typeof(WebSpaceDatabases)) + }); + + Context = await PluginService.BuildWebspacePage(Context); } } } \ No newline at end of file From 0658e55a78e75524de29eff0065e49579d8ff85e Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 23 Jul 2023 21:30:57 +0200 Subject: [PATCH 3/5] Implemented basic plugin store and improved plugin system --- .../Models/Misc/OfficialMoonlightPlugin.cs | 6 + Moonlight/App/Perms/Permissions.cs | 7 + Moonlight/App/Plugin/MoonlightPlugin.cs | 4 +- .../Plugin/UI/Servers/ServerPageContext.cs | 1 + .../App/Services/Files/StorageService.cs | 1 + Moonlight/App/Services/MoonlightService.cs | 2 +- .../Services/{ => Plugins}/PluginService.cs | 58 +++++++- .../Services/Plugins/PluginStoreService.cs | 63 ++++++++ Moonlight/Program.cs | 11 +- .../Navigations/AdminSystemNavigation.razor | 5 + .../Shared/Views/Admin/Security/Logs.razor | 2 + .../Shared/Views/Admin/Sys/Plugins.razor | 138 ++++++++++++++++++ Moonlight/Shared/Views/Server/Index.razor | 4 +- Moonlight/Shared/Views/Webspace/Index.razor | 1 + 14 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs rename Moonlight/App/Services/{ => Plugins}/PluginService.cs (50%) create mode 100644 Moonlight/App/Services/Plugins/PluginStoreService.cs create mode 100644 Moonlight/Shared/Views/Admin/Sys/Plugins.razor diff --git a/Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs b/Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs new file mode 100644 index 00000000..2b3d090d --- /dev/null +++ b/Moonlight/App/Models/Misc/OfficialMoonlightPlugin.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Misc; + +public class OfficialMoonlightPlugin +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Perms/Permissions.cs b/Moonlight/App/Perms/Permissions.cs index 9a3fbd78..87b0ec58 100644 --- a/Moonlight/App/Perms/Permissions.cs +++ b/Moonlight/App/Perms/Permissions.cs @@ -15,6 +15,13 @@ public static class Permissions Name = "Admin Statistics", Description = "View statistical information about the moonlight instance" }; + + public static Permission AdminSysPlugins = new() + { + Index = 2, + Name = "Admin system plugins", + Description = "View and install plugins" + }; public static Permission AdminDomains = new() { diff --git a/Moonlight/App/Plugin/MoonlightPlugin.cs b/Moonlight/App/Plugin/MoonlightPlugin.cs index 702dddc7..875c33ee 100644 --- a/Moonlight/App/Plugin/MoonlightPlugin.cs +++ b/Moonlight/App/Plugin/MoonlightPlugin.cs @@ -1,5 +1,4 @@ -using Moonlight.App.Plugin.UI; -using Moonlight.App.Plugin.UI.Servers; +using Moonlight.App.Plugin.UI.Servers; using Moonlight.App.Plugin.UI.Webspaces; namespace Moonlight.App.Plugin; @@ -12,4 +11,5 @@ public abstract class MoonlightPlugin public Func? OnBuildServerPage { get; set; } public Func? OnBuildWebspacePage { get; set; } + public Func? OnBuildServices { get; set; } } \ No newline at end of file diff --git a/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs b/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs index 0905be1d..8179035a 100644 --- a/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs +++ b/Moonlight/App/Plugin/UI/Servers/ServerPageContext.cs @@ -8,4 +8,5 @@ public class ServerPageContext public List Settings { get; set; } = new(); public Server Server { get; set; } public User User { get; set; } + public string[] ImageTags { get; set; } } \ No newline at end of file diff --git a/Moonlight/App/Services/Files/StorageService.cs b/Moonlight/App/Services/Files/StorageService.cs index 9d05c692..9980e23a 100644 --- a/Moonlight/App/Services/Files/StorageService.cs +++ b/Moonlight/App/Services/Files/StorageService.cs @@ -16,6 +16,7 @@ public class StorageService Directory.CreateDirectory(PathBuilder.Dir("storage", "resources")); Directory.CreateDirectory(PathBuilder.Dir("storage", "backups")); Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); + Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); if(IsEmpty(PathBuilder.Dir("storage", "resources"))) { diff --git a/Moonlight/App/Services/MoonlightService.cs b/Moonlight/App/Services/MoonlightService.cs index 650a2b4f..f8699f4a 100644 --- a/Moonlight/App/Services/MoonlightService.cs +++ b/Moonlight/App/Services/MoonlightService.cs @@ -46,7 +46,7 @@ public class MoonlightService try { - var client = new GitHubClient(new ProductHeaderValue("Moonlight")); + var client = new GitHubClient(new ProductHeaderValue("Moonlight-Panel")); var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest { diff --git a/Moonlight/App/Services/PluginService.cs b/Moonlight/App/Services/Plugins/PluginService.cs similarity index 50% rename from Moonlight/App/Services/PluginService.cs rename to Moonlight/App/Services/Plugins/PluginService.cs index fb4f04eb..91e1cfbd 100644 --- a/Moonlight/App/Services/PluginService.cs +++ b/Moonlight/App/Services/Plugins/PluginService.cs @@ -1,25 +1,57 @@ using System.Reflection; -using Moonlight.App.Database.Entities; +using System.Runtime.Loader; using Moonlight.App.Helpers; using Moonlight.App.Plugin; -using Moonlight.App.Plugin.UI; using Moonlight.App.Plugin.UI.Servers; using Moonlight.App.Plugin.UI.Webspaces; -namespace Moonlight.App.Services; +namespace Moonlight.App.Services.Plugins; public class PluginService { - public List Plugins { get; set; } + public List Plugins { get; private set; } + public Dictionary PluginFiles { get; private set; } + + private AssemblyLoadContext LoadContext; public PluginService() { - LoadPlugins(); + LoadContext = new(null, true); + ReloadPlugins().Wait(); } - - private void LoadPlugins() + + private Task UnloadPlugins() { Plugins = new(); + PluginFiles = new(); + + if(LoadContext.Assemblies.Any()) + LoadContext.Unload(); + + return Task.CompletedTask; + } + + public async Task ReloadPlugins() + { + await UnloadPlugins(); + + // Try to update all plugins ending with .dll.cache + foreach (var pluginFile in Directory.EnumerateFiles( + PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins")) + .Where(x => x.EndsWith(".dll.cache"))) + { + try + { + var realPath = pluginFile.Replace(".cache", ""); + File.Copy(pluginFile, realPath, true); + File.Delete(pluginFile); + Logger.Info($"Updated plugin {realPath} on startup"); + } + catch (Exception) + { + // ignored + } + } var pluginType = typeof(MoonlightPlugin); @@ -27,7 +59,7 @@ public class PluginService PathBuilder.Dir(Directory.GetCurrentDirectory(), "storage", "plugins")) .Where(x => x.EndsWith(".dll"))) { - var assembly = Assembly.LoadFile(pluginFile); + var assembly = LoadContext.LoadFromAssemblyPath(pluginFile); foreach (var type in assembly.GetTypes()) { @@ -38,6 +70,7 @@ public class PluginService Logger.Info($"Loaded plugin '{plugin.Name}' ({plugin.Version}) by {plugin.Author}"); Plugins.Add(plugin); + PluginFiles.Add(plugin, pluginFile); } } } @@ -66,4 +99,13 @@ public class PluginService return context; } + + public async Task BuildServices(IServiceCollection serviceCollection) + { + foreach (var plugin in Plugins) + { + if (plugin.OnBuildServices != null) + await plugin.OnBuildServices.Invoke(serviceCollection); + } + } } \ No newline at end of file diff --git a/Moonlight/App/Services/Plugins/PluginStoreService.cs b/Moonlight/App/Services/Plugins/PluginStoreService.cs new file mode 100644 index 00000000..4d885d03 --- /dev/null +++ b/Moonlight/App/Services/Plugins/PluginStoreService.cs @@ -0,0 +1,63 @@ +using System.Text; +using Moonlight.App.Helpers; +using Moonlight.App.Models.Misc; +using Octokit; + +namespace Moonlight.App.Services.Plugins; + +public class PluginStoreService +{ + private readonly GitHubClient Client; + private readonly PluginService PluginService; + + public PluginStoreService(PluginService pluginService) + { + PluginService = pluginService; + Client = new(new ProductHeaderValue("Moonlight-Panel")); + } + + public async Task GetPlugins() + { + var items = await Client.Repository.Content.GetAllContents("Moonlight-Panel", "OfficialPlugins"); + + if (items == null) + { + Logger.Fatal("Unable to read plugin repo contents"); + return Array.Empty(); + } + + return items + .Where(x => x.Type == ContentType.Dir) + .Select(x => new OfficialMoonlightPlugin() + { + Name = x.Name + }) + .ToArray(); + } + + public async Task GetPluginReadme(OfficialMoonlightPlugin plugin) + { + var rawReadme = await Client.Repository.Content + .GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/README.md"); + + if (rawReadme == null) + return "Error"; + + return Encoding.UTF8.GetString(rawReadme); + } + + public async Task InstallPlugin(OfficialMoonlightPlugin plugin, bool updating = false) + { + var rawPlugin = await Client.Repository.Content + .GetRawContent("Moonlight-Panel", "OfficialPlugins", $"{plugin.Name}/{plugin.Name}.dll"); + + if (updating) + { + await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll.cache"), rawPlugin); + return; + } + + await File.WriteAllBytesAsync(PathBuilder.File("storage", "plugins", $"{plugin.Name}.dll"), rawPlugin); + await PluginService.ReloadPlugins(); + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 4f504771..3038850b 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -25,6 +25,7 @@ using Moonlight.App.Services.Interop; using Moonlight.App.Services.Mail; using Moonlight.App.Services.Minecraft; using Moonlight.App.Services.Notifications; +using Moonlight.App.Services.Plugins; using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Statistics; using Moonlight.App.Services.SupportChat; @@ -110,6 +111,9 @@ namespace Moonlight var builder = WebApplication.CreateBuilder(args); + var pluginService = new PluginService(); + await pluginService.BuildServices(builder.Services); + // Switch to logging.net injection // TODO: Enable in production builder.Logging.ClearProviders(); @@ -208,6 +212,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -239,7 +244,8 @@ namespace Moonlight builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + + builder.Services.AddSingleton(pluginService); // Other builder.Services.AddSingleton(); @@ -290,8 +296,7 @@ namespace Moonlight _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); _ = app.Services.GetRequiredService(); - _ = app.Services.GetRequiredService(); - + _ = app.Services.GetRequiredService(); // Discord bot service diff --git a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor index 9733aed1..5dffb252 100644 --- a/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor +++ b/Moonlight/Shared/Components/Navigations/AdminSystemNavigation.razor @@ -34,6 +34,11 @@ Mail +
diff --git a/Moonlight/Shared/Views/Admin/Security/Logs.razor b/Moonlight/Shared/Views/Admin/Security/Logs.razor index 2a7d1958..7dcabcb0 100644 --- a/Moonlight/Shared/Views/Admin/Security/Logs.razor +++ b/Moonlight/Shared/Views/Admin/Security/Logs.razor @@ -40,6 +40,8 @@ { SecurityLogs = SecurityLogRepository .Get() + .ToArray() + .OrderByDescending(x => x.CreatedAt) .ToArray(); return Task.CompletedTask; diff --git a/Moonlight/Shared/Views/Admin/Sys/Plugins.razor b/Moonlight/Shared/Views/Admin/Sys/Plugins.razor new file mode 100644 index 00000000..bafda85d --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Sys/Plugins.razor @@ -0,0 +1,138 @@ +@page "/admin/system/plugins" + +@using Moonlight.Shared.Components.Navigations +@using Moonlight.App.Services.Plugins +@using BlazorTable +@using Moonlight.App.Models.Misc +@using Moonlight.App.Plugin +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop + +@inject PluginStoreService PluginStoreService +@inject SmartTranslateService SmartTranslateService +@inject PluginService PluginService +@inject ToastService ToastService +@inject ModalService ModalService + +@attribute [PermissionRequired(nameof(Permissions.AdminSysPlugins))] + + + +
+
+ + Installed plugins + +
+
+
+ + + + + + + + +
+
+
+
+ +
+
+ + Official plugins + +
+
+ +
+ + + + + + + + + +
+
+
+
+
+ + + +@code +{ + private LazyLoader PluginsLazyLoader; + private OfficialMoonlightPlugin[] PluginList; + private string PluginReadme = ""; + + private async Task LoadOfficialPlugins(LazyLoader lazyLoader) + { + PluginList = await PluginStoreService.GetPlugins(); + } + + private async Task ShowOfficialPluginReadme(OfficialMoonlightPlugin plugin) + { + PluginReadme = await PluginStoreService.GetPluginReadme(plugin); + await InvokeAsync(StateHasChanged); + await ModalService.Show("pluginReadme"); + } + + private async Task InstallOfficialPlugin(OfficialMoonlightPlugin plugin) + { + await PluginStoreService.InstallPlugin(plugin); + await ToastService.Success(SmartTranslateService.Translate("Successfully installed plugin")); + await InvokeAsync(StateHasChanged); + } + + private async Task UpdateOfficialPlugin(OfficialMoonlightPlugin plugin) + { + await PluginStoreService.InstallPlugin(plugin, true); + await ToastService.Success(SmartTranslateService.Translate("Successfully installed plugin. You need to reboot to apply changes")); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Server/Index.razor b/Moonlight/Shared/Views/Server/Index.razor index 3e0b4689..ce14d12b 100644 --- a/Moonlight/Shared/Views/Server/Index.razor +++ b/Moonlight/Shared/Views/Server/Index.razor @@ -11,6 +11,7 @@ @using Moonlight.App.Plugin.UI.Servers @using Moonlight.App.Repositories @using Moonlight.App.Services +@using Moonlight.App.Services.Plugins @using Moonlight.App.Services.Sessions @using Moonlight.Shared.Components.Xterm @using Moonlight.Shared.Views.Server.Settings @@ -240,7 +241,8 @@ Context = new ServerPageContext() { Server = CurrentServer, - User = IdentityService.User + User = IdentityService.User, + ImageTags = Tags }; Context.Tabs.Add(new() diff --git a/Moonlight/Shared/Views/Webspace/Index.razor b/Moonlight/Shared/Views/Webspace/Index.razor index d3e17f36..d15a8e51 100644 --- a/Moonlight/Shared/Views/Webspace/Index.razor +++ b/Moonlight/Shared/Views/Webspace/Index.razor @@ -6,6 +6,7 @@ @using Microsoft.EntityFrameworkCore @using Moonlight.App.Helpers @using Moonlight.App.Plugin.UI.Webspaces +@using Moonlight.App.Services.Plugins @using Moonlight.App.Services.Sessions @inject Repository WebSpaceRepository From fedc9278d43a86a3e6e127827f7607228a2f35b4 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Sun, 23 Jul 2023 22:12:17 +0200 Subject: [PATCH 4/5] Updated dependencies --- Moonlight/Moonlight.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index d8410558..ae6ed630 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -40,12 +40,12 @@ - + - + @@ -90,7 +90,6 @@ - From e2248a84449dd7f471ada0edd2aca3981b5a9855 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Mon, 24 Jul 2023 00:30:18 +0200 Subject: [PATCH 5/5] Fixed domain identity issues --- Moonlight/Moonlight.csproj | 1 + Moonlight/Shared/Views/Domain/View.razor | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index ae6ed630..90a4556b 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -90,6 +90,7 @@ + diff --git a/Moonlight/Shared/Views/Domain/View.razor b/Moonlight/Shared/Views/Domain/View.razor index 5e6604f4..af07ac39 100644 --- a/Moonlight/Shared/Views/Domain/View.razor +++ b/Moonlight/Shared/Views/Domain/View.razor @@ -6,12 +6,14 @@ @using Moonlight.App.Services @using CloudFlare.Client.Enumerators @using Moonlight.App.Services.Interop +@using Moonlight.App.Services.Sessions @inject DomainRepository DomainRepository @inject DomainService DomainService @inject SmartTranslateService SmartTranslateService @inject NavigationManager NavigationManager @inject AlertService AlertService +@inject IdentityService IdentityService @if (Domain == null) @@ -180,9 +182,6 @@ { [Parameter] public int Id { get; set; } - - [CascadingParameter] - public User? User { get; set; } private Domain? Domain; private DnsRecord[] DnsRecords; @@ -205,13 +204,13 @@ if (Domain == null) return; - if (User == null) + if (IdentityService.User == null) { Domain = null; return; } - if (Domain.Owner.Id != User.Id && !User.Admin) + if (Domain.Owner.Id != IdentityService.User.Id && !IdentityService.User.Admin) { Domain = null; return;