From c3eadf9133d6159841df46635699ddd75b989606 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Mon, 20 Feb 2023 21:12:10 +0100 Subject: [PATCH] Migrated server logic. Added all server endpoints. Migrated some more stuff --- ...30217152643_MigratedSomeModels.Designer.cs | 2 +- .../20230217152643_MigratedSomeModels.cs | 2 +- Moonlight/App/Exceptions/WingsException.cs | 32 ++ Moonlight/App/Helpers/MonacoTypeHelper.cs | 44 ++ Moonlight/App/Helpers/PaperApiHelper.cs | 48 ++ Moonlight/App/Helpers/ParseHelper.cs | 32 ++ Moonlight/App/Helpers/StreamProgressHelper.cs | 40 ++ Moonlight/App/Helpers/WingsApiHelper.cs | 228 ++++++++ Moonlight/App/Helpers/WingsConsoleHelper.cs | 104 ++++ Moonlight/App/Helpers/WingsJwtHelper.cs | 56 ++ Moonlight/App/Helpers/WingsServerConverter.cs | 131 +++++ .../Api/Remote/ActivityController.cs | 14 + .../Api/Remote/ServersController.cs | 202 +++++++ .../Wings/ReportBackupCompleteRequest.cs | 7 + .../Http/Requests/Wings/SftpLoginRequest.cs | 9 + .../Http/Resources/Wings/PaginationResult.cs | 47 ++ .../Http/Resources/Wings/SftpLoginResult.cs | 8 + .../App/Http/Resources/Wings/WingsServer.cs | 154 ++++++ .../Resources/Wings/WingsServerInstall.cs | 8 + Moonlight/App/MessageSystem/MessageSender.cs | 83 +++ .../App/MessageSystem/MessageSubscriber.cs | 9 + .../Models/Files/Accesses/WingsFileAccess.cs | 205 +++++++ .../App/Models/Files/FileManagerObject.cs | 10 + Moonlight/App/Models/Files/IFileAccess.cs | 19 + .../App/Models/Paper/Resources/PaperBuilds.cs | 18 + .../Models/Paper/Resources/PaperVersions.cs | 18 + Moonlight/App/Models/Wings/PowerSignal.cs | 9 + .../Wings/Requests/CreateBackupRequest.cs | 15 + .../Wings/Requests/CreateDirectoryRequest.cs | 12 + .../Wings/Requests/CreateServerRequest.cs | 12 + .../Wings/Requests/DeleteFilesRequest.cs | 11 + .../Wings/Requests/RenameFilesRequest.cs | 20 + .../Wings/Requests/RestoreBackupRequest.cs | 12 + .../Wings/Requests/ServerPowerRequest.cs | 9 + .../Wings/Resources/ListDirectoryRequest.cs | 30 + .../Wings/Resources/ServerDetailsResponse.cs | 48 ++ .../Models/Wings/Resources/SystemStatus.cs | 21 + Moonlight/App/Repositories/ImageRepository.cs | 44 ++ Moonlight/App/Services/MessageService.cs | 11 + Moonlight/App/Services/NodeService.cs | 14 +- Moonlight/App/Services/PaperService.cs | 28 + Moonlight/App/Services/ServerService.cs | 326 +++++++++++ Moonlight/Moonlight.csproj | 6 +- Moonlight/Pages/_Layout.cshtml | 22 +- Moonlight/Program.cs | 13 +- .../FileManagerPartials/FileEditor.razor | 98 ++++ .../FileManagerPartials/FileManager.razor | 513 ++++++++++++++++++ .../ServerControl/ServerBackups.razor | 306 +++++++++++ .../ServerControl/ServerConsole.razor | 97 ++++ .../ServerControl/ServerFiles.razor | 27 + .../ServerControl/ServerNavigation.razor | 208 +++++++ .../ServerControl/ServerNetwork.razor | 41 ++ .../ServerControl/ServerPlugins.razor | 10 + .../ServerControl/ServerSettings.razor | 51 ++ .../Settings/JavascriptVersionSetting.razor | 83 +++ .../Settings/Join2StartSetting.razor | 55 ++ .../Settings/PaperVersionSetting.razor | 182 +++++++ .../Settings/PythonVersionSetting.razor | 84 +++ .../Shared/Components/Xterm/Terminal.razor | 55 ++ Moonlight/Shared/Views/Admin/Nodes/Edit.razor | 199 ++++--- .../Shared/Views/Admin/Nodes/Index.razor | 122 ++++- Moonlight/Shared/Views/Admin/Nodes/New.razor | 15 +- .../Shared/Views/Admin/Nodes/Setup.razor | 159 ++++++ .../Shared/Views/Admin/Servers/Edit.razor | 198 +++++++ .../Shared/Views/Admin/Servers/Index.razor | 69 +++ .../Shared/Views/Admin/Servers/New.razor | 300 ++++++++++ Moonlight/Shared/Views/Index.razor | 20 +- Moonlight/Shared/Views/Server/Index.razor | 215 ++++++++ Moonlight/Shared/Views/Servers.razor | 127 +++++ .../Debug/net6.0/Moonlight.AssemblyInfo.cs | 7 +- ....GeneratedMSBuildEditorConfig.editorconfig | 80 +++ .../net6.0/Moonlight.RazorAssemblyInfo.cs | 7 +- .../obj/Debug/net6.0/Moonlight.assets.cache | Bin 28401 -> 28772 bytes .../Moonlight.csproj.AssemblyReference.cache | Bin 201265 -> 153254 bytes .../obj/Moonlight.csproj.nuget.dgspec.json | 6 +- Moonlight/obj/Moonlight.csproj.nuget.g.props | 3 +- Moonlight/obj/project.assets.json | 119 ++-- Moonlight/obj/project.nuget.cache | 5 +- Moonlight/obj/project.packagespec.json | 2 +- Moonlight/resources/lang/de_de.lang | 74 ++- .../assets/css/{invisiblea.css => utils.css} | 8 + Moonlight/wwwroot/assets/js/monaco.js | 1 - 82 files changed, 5553 insertions(+), 186 deletions(-) rename Moonlight/App/{DatabaseMigrations => Database/Migrations}/20230217152643_MigratedSomeModels.Designer.cs (99%) rename Moonlight/App/{DatabaseMigrations => Database/Migrations}/20230217152643_MigratedSomeModels.cs (98%) create mode 100644 Moonlight/App/Exceptions/WingsException.cs create mode 100644 Moonlight/App/Helpers/MonacoTypeHelper.cs create mode 100644 Moonlight/App/Helpers/PaperApiHelper.cs create mode 100644 Moonlight/App/Helpers/ParseHelper.cs create mode 100644 Moonlight/App/Helpers/StreamProgressHelper.cs create mode 100644 Moonlight/App/Helpers/WingsApiHelper.cs create mode 100644 Moonlight/App/Helpers/WingsConsoleHelper.cs create mode 100644 Moonlight/App/Helpers/WingsJwtHelper.cs create mode 100644 Moonlight/App/Helpers/WingsServerConverter.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs create mode 100644 Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs create mode 100644 Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs create mode 100644 Moonlight/App/Http/Resources/Wings/PaginationResult.cs create mode 100644 Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs create mode 100644 Moonlight/App/Http/Resources/Wings/WingsServer.cs create mode 100644 Moonlight/App/Http/Resources/Wings/WingsServerInstall.cs create mode 100644 Moonlight/App/MessageSystem/MessageSender.cs create mode 100644 Moonlight/App/MessageSystem/MessageSubscriber.cs create mode 100644 Moonlight/App/Models/Files/Accesses/WingsFileAccess.cs create mode 100644 Moonlight/App/Models/Files/FileManagerObject.cs create mode 100644 Moonlight/App/Models/Files/IFileAccess.cs create mode 100644 Moonlight/App/Models/Paper/Resources/PaperBuilds.cs create mode 100644 Moonlight/App/Models/Paper/Resources/PaperVersions.cs create mode 100644 Moonlight/App/Models/Wings/PowerSignal.cs create mode 100644 Moonlight/App/Models/Wings/Requests/CreateBackupRequest.cs create mode 100644 Moonlight/App/Models/Wings/Requests/CreateDirectoryRequest.cs create mode 100644 Moonlight/App/Models/Wings/Requests/CreateServerRequest.cs create mode 100644 Moonlight/App/Models/Wings/Requests/DeleteFilesRequest.cs create mode 100644 Moonlight/App/Models/Wings/Requests/RenameFilesRequest.cs create mode 100644 Moonlight/App/Models/Wings/Requests/RestoreBackupRequest.cs create mode 100644 Moonlight/App/Models/Wings/Requests/ServerPowerRequest.cs create mode 100644 Moonlight/App/Models/Wings/Resources/ListDirectoryRequest.cs create mode 100644 Moonlight/App/Models/Wings/Resources/ServerDetailsResponse.cs create mode 100644 Moonlight/App/Models/Wings/Resources/SystemStatus.cs create mode 100644 Moonlight/App/Repositories/ImageRepository.cs create mode 100644 Moonlight/App/Services/MessageService.cs create mode 100644 Moonlight/App/Services/PaperService.cs create mode 100644 Moonlight/App/Services/ServerService.cs create mode 100644 Moonlight/Shared/Components/FileManagerPartials/FileEditor.razor create mode 100644 Moonlight/Shared/Components/FileManagerPartials/FileManager.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerBackups.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerConsole.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerFiles.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerNavigation.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerNetwork.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerPlugins.razor create mode 100644 Moonlight/Shared/Components/ServerControl/ServerSettings.razor create mode 100644 Moonlight/Shared/Components/ServerControl/Settings/JavascriptVersionSetting.razor create mode 100644 Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor create mode 100644 Moonlight/Shared/Components/ServerControl/Settings/PaperVersionSetting.razor create mode 100644 Moonlight/Shared/Components/ServerControl/Settings/PythonVersionSetting.razor create mode 100644 Moonlight/Shared/Components/Xterm/Terminal.razor create mode 100644 Moonlight/Shared/Views/Admin/Nodes/Setup.razor create mode 100644 Moonlight/Shared/Views/Admin/Servers/Edit.razor create mode 100644 Moonlight/Shared/Views/Admin/Servers/Index.razor create mode 100644 Moonlight/Shared/Views/Admin/Servers/New.razor create mode 100644 Moonlight/Shared/Views/Server/Index.razor create mode 100644 Moonlight/Shared/Views/Servers.razor rename Moonlight/wwwroot/assets/css/{invisiblea.css => utils.css} (56%) delete mode 100644 Moonlight/wwwroot/assets/js/monaco.js diff --git a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.Designer.cs b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.Designer.cs similarity index 99% rename from Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.Designer.cs rename to Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.Designer.cs index c3e9640..bb8b080 100644 --- a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.Designer.cs +++ b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.Designer.cs @@ -8,7 +8,7 @@ using Moonlight.App.Database; #nullable disable -namespace Moonlight.App.DatabaseMigrations +namespace Moonlight.App.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230217152643_MigratedSomeModels")] diff --git a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.cs b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.cs similarity index 98% rename from Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.cs rename to Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.cs index 4c17501..ba2588c 100644 --- a/Moonlight/App/DatabaseMigrations/20230217152643_MigratedSomeModels.cs +++ b/Moonlight/App/Database/Migrations/20230217152643_MigratedSomeModels.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Moonlight.App.DatabaseMigrations +namespace Moonlight.App.Database.Migrations { /// public partial class MigratedSomeModels : Migration diff --git a/Moonlight/App/Exceptions/WingsException.cs b/Moonlight/App/Exceptions/WingsException.cs new file mode 100644 index 0000000..ca9c4b4 --- /dev/null +++ b/Moonlight/App/Exceptions/WingsException.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace Moonlight.App.Exceptions.Wings; + +[Serializable] +public class WingsException : Exception +{ + public int StatusCode { private get; set; } + + public WingsException() + { + } + + public WingsException(string message, int statusCode) : base(message) + { + StatusCode = statusCode; + } + + public WingsException(string message) : base(message) + { + } + + public WingsException(string message, Exception inner) : base(message, inner) + { + } + + protected WingsException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/MonacoTypeHelper.cs b/Moonlight/App/Helpers/MonacoTypeHelper.cs new file mode 100644 index 0000000..8e37c19 --- /dev/null +++ b/Moonlight/App/Helpers/MonacoTypeHelper.cs @@ -0,0 +1,44 @@ +namespace Moonlight.App.Helpers; + +public class MonacoTypeHelper +{ + public static string GetEditorType(string file) + { + var extension = Path.GetExtension(file); + extension = extension.TrimStart("."[0]); + + switch (extension) + { + case "bat": + return "bat"; + case "cs": + return "csharp"; + case "css": + return "css"; + case "html": + return "html"; + case "java": + return "java"; + case "js": + return "javascript"; + case "ini": + return "ini"; + case "json": + return "json"; + case "lua": + return "lua"; + case "php": + return "php"; + case "py": + return "python"; + case "sh": + return "shell"; + case "xml": + return "xml"; + case "yml": + return "yaml"; + default: + return "plaintext"; + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/PaperApiHelper.cs b/Moonlight/App/Helpers/PaperApiHelper.cs new file mode 100644 index 0000000..129e545 --- /dev/null +++ b/Moonlight/App/Helpers/PaperApiHelper.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using RestSharp; + +namespace Moonlight.App.Helpers; + +public class PaperApiHelper +{ + private string ApiUrl { get; set; } + + public PaperApiHelper() + { + ApiUrl = "https://api.papermc.io/v2/projects/"; + } + + public async Task Get(string url) + { + RestClient client = new(); + + string requrl = "NONSET"; + + if (ApiUrl.EndsWith("/")) + requrl = ApiUrl + url; + else + requrl = ApiUrl + "/" + url; + + RestRequest request = new(requrl); + + request.AddHeader("Content-Type", "application/json"); + + var response = await client.GetAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new Exception( + $"An error occured: ({response.StatusCode}) {response.Content}" + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + + return JsonConvert.DeserializeObject(response.Content); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/ParseHelper.cs b/Moonlight/App/Helpers/ParseHelper.cs new file mode 100644 index 0000000..ecc983d --- /dev/null +++ b/Moonlight/App/Helpers/ParseHelper.cs @@ -0,0 +1,32 @@ +namespace Moonlight.App.Helpers; + +public static class ParseHelper +{ + public static int MinecraftToInt(string raw) + { + var versionWithoutPre = raw.Split("-")[0]; + + if (versionWithoutPre.Count(x => x == "."[0]) == 1) + versionWithoutPre += ".0"; + + return int.Parse(versionWithoutPre.Replace(".", "")); + } + + public static string FirstPartStartingWithNumber(string raw) + { + var numbers = "0123456789"; + var res = ""; + var found = false; + + foreach (var p in raw) + { + if (!found) + found = numbers.Contains(p); + + if (found) + res += p; + } + + return res; + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/StreamProgressHelper.cs b/Moonlight/App/Helpers/StreamProgressHelper.cs new file mode 100644 index 0000000..ae669f4 --- /dev/null +++ b/Moonlight/App/Helpers/StreamProgressHelper.cs @@ -0,0 +1,40 @@ +namespace Moonlight.App.Helpers; + +public class StreamProgressHelper : Stream +{ + public Action? Progress { get; set; } + private int lastPercent = -1; + + Stream InnerStream { get; init; } + + public override int Read(byte[] buffer, int offset, int count) + { + var result = InnerStream.ReadAsync(buffer, offset, count).Result; + + int percentComplete = (int)Math.Round((double)(100 * Position) / Length); + + if (lastPercent != percentComplete) + { + Progress?.Invoke(percentComplete); + lastPercent = percentComplete; + } + + return result; + } + public override void Write(byte[] buffer, int offset, int count) + { + InnerStream.WriteAsync(buffer, offset, count); + } + public override bool CanRead => InnerStream.CanRead; + public override bool CanSeek => InnerStream.CanSeek; + public override bool CanWrite => InnerStream.CanWrite; + public override long Length => InnerStream.Length; + public override long Position { get => InnerStream.Position; set => InnerStream.Position = value; } + public StreamProgressHelper(Stream s) + { + this.InnerStream = s; + } + public override void Flush() => InnerStream.Flush(); + public override long Seek(long offset, SeekOrigin origin) => InnerStream.Seek(offset, origin); + public override void SetLength(long value)=> InnerStream.SetLength(value); +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsApiHelper.cs b/Moonlight/App/Helpers/WingsApiHelper.cs new file mode 100644 index 0000000..76ad741 --- /dev/null +++ b/Moonlight/App/Helpers/WingsApiHelper.cs @@ -0,0 +1,228 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Exceptions.Wings; +using Newtonsoft.Json; +using RestSharp; + +namespace Moonlight.App.Helpers; + +public class WingsApiHelper +{ + private readonly RestClient Client; + + public WingsApiHelper() + { + Client = new(); + } + + private string GetApiUrl(Node node) + { + if(node.Ssl) + return $"https://{node.Fqdn}:{node.HttpPort}/"; + else + return $"http://{node.Fqdn}:{node.HttpPort}/"; + //return $"https://{node.Fqdn}:{node.HttpPort}/"; + } + + public async Task Get(Node node, string resource) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + var response = await Client.GetAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + + return JsonConvert.DeserializeObject(response.Content!)!; + } + + public async Task GetRaw(Node node, string resource) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + var response = await Client.GetAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + + return response.Content!; + } + + public async Task Post(Node node, string resource, object? body) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + request.AddParameter("text/plain", + JsonConvert.SerializeObject(body), + ParameterType.RequestBody + ); + + var response = await Client.PostAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + + return JsonConvert.DeserializeObject(response.Content!)!; + } + + public async Task Post(Node node, string resource, object? body) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + if(body != null) + request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody); + + var response = await Client.PostAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + } + + public async Task PostRaw(Node node, string resource, object body) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + request.AddParameter("text/plain", body, ParameterType.RequestBody); + + var response = await Client.PostAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + } + + public async Task Delete(Node node, string resource, object? body) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + if(body != null) + request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody); + + var response = await Client.DeleteAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + } + + public async Task Put(Node node, string resource, object? body) + { + RestRequest request = new(GetApiUrl(node) + resource); + + request.AddHeader("Content-Type", "application/json"); + request.AddHeader("Accept", "application/json"); + request.AddHeader("Authorization", "Bearer " + node.Token); + + request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody); + + var response = await Client.PutAsync(request); + + if (!response.IsSuccessful) + { + if (response.StatusCode != 0) + { + throw new WingsException( + $"An error occured: ({response.StatusCode}) {response.Content}", + (int)response.StatusCode + ); + } + else + { + throw new Exception($"An internal error occured: {response.ErrorMessage}"); + } + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsConsoleHelper.cs b/Moonlight/App/Helpers/WingsConsoleHelper.cs new file mode 100644 index 0000000..720a293 --- /dev/null +++ b/Moonlight/App/Helpers/WingsConsoleHelper.cs @@ -0,0 +1,104 @@ +using System.Security.Cryptography; +using System.Text; +using JWT.Algorithms; +using JWT.Builder; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Repositories.Servers; +using Moonlight.App.Services; + +namespace Moonlight.App.Helpers; + +public class WingsConsoleHelper +{ + private readonly ServerRepository ServerRepository; + private readonly WingsJwtHelper WingsJwtHelper; + private readonly string AppUrl; + + public WingsConsoleHelper( + ServerRepository serverRepository, + ConfigService configService, + WingsJwtHelper wingsJwtHelper) + { + ServerRepository = serverRepository; + WingsJwtHelper = wingsJwtHelper; + + AppUrl = configService.GetSection("Moonlight").GetValue("AppUrl"); + } + + public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server) + { + var serverData = ServerRepository + .Get() + .Include(x => x.Node) + .First(x => x.Id == server.Id); + + var token = GenerateToken(serverData); + + if (serverData.Node.Ssl) + { + await pteroConsole.Connect( + AppUrl, + $"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", + token + ); + } + else + { + await pteroConsole.Connect( + AppUrl, + $"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws", + token + ); + } + } + + public string GenerateToken(Server server) + { + var serverData = ServerRepository + .Get() + .Include(x => x.Node) + .First(x => x.Id == server.Id); + + var userid = 1; + var secret = serverData.Node.Token; + + + using (MD5 md5 = MD5.Create()) + { + var inputBytes = Encoding.ASCII.GetBytes(userid + serverData.Uuid.ToString()); + var outputBytes = md5.ComputeHash(inputBytes); + + var identifier = Convert.ToHexString(outputBytes).ToLower(); + var weirdId = StringHelper.GenerateString(16); + + var token = JwtBuilder.Create() + .AddHeader("jti", identifier) + .WithAlgorithm(new HMACSHA256Algorithm()) + .WithSecret(secret) + .AddClaim("user_id", userid) + .AddClaim("server_uuid", serverData.Uuid.ToString()) + .AddClaim("permissions", new[] + { + "*", + "admin.websocket.errors", + "admin.websocket.install", + "admin.websocket.transfer" + }) + .AddClaim("jti", identifier) + .AddClaim("unique_id", weirdId) + .AddClaim("iat", DateTimeOffset.Now.ToUnixTimeSeconds()) + .AddClaim("nbf", DateTimeOffset.Now.AddSeconds(-10).ToUnixTimeSeconds()) + .AddClaim("exp", DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds()) + .AddClaim("iss", AppUrl) + .AddClaim("aud", new[] + { + serverData.Node.Ssl ? $"https://{serverData.Node.Fqdn}" : $"http://{serverData.Node.Fqdn}" + }) + .MustVerifySignature() + .Encode(); + + return token; + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsJwtHelper.cs b/Moonlight/App/Helpers/WingsJwtHelper.cs new file mode 100644 index 0000000..221abd1 --- /dev/null +++ b/Moonlight/App/Helpers/WingsJwtHelper.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography; +using System.Text; +using JWT.Algorithms; +using JWT.Builder; +using Moonlight.App.Services; + +namespace Moonlight.App.Helpers; + +public class WingsJwtHelper +{ + private readonly ConfigService ConfigService; + private readonly string AppUrl; + + public WingsJwtHelper(ConfigService configService) + { + ConfigService = configService; + + AppUrl = ConfigService.GetSection("Moonlight").GetValue("AppUrl"); + } + + public string Generate(string secret, Action> claimsAction) + { + var userid = 1; + + using MD5 md5 = MD5.Create(); + var inputBytes = Encoding.ASCII.GetBytes(userid + Guid.NewGuid().ToString()); + var outputBytes = md5.ComputeHash(inputBytes); + + var identifier = Convert.ToHexString(outputBytes).ToLower(); + var weirdId = StringHelper.GenerateString(16); + + var builder = JwtBuilder.Create() + .AddHeader("jti", identifier) + .WithAlgorithm(new HMACSHA256Algorithm()) + .WithSecret(secret) + .AddClaim("user_id", userid) + .AddClaim("jti", identifier) + .AddClaim("unique_id", weirdId) + .AddClaim("iat", DateTimeOffset.Now.ToUnixTimeSeconds()) + .AddClaim("nbf", DateTimeOffset.Now.AddSeconds(-10).ToUnixTimeSeconds()) + .AddClaim("exp", DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds()) + .AddClaim("iss", AppUrl) + .MustVerifySignature(); + + var additionalClaims = new Dictionary(); + + claimsAction.Invoke(additionalClaims); + + foreach (var claim in additionalClaims) + { + builder = builder.AddClaim(claim.Key, claim.Value); + } + + return builder.Encode(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/WingsServerConverter.cs b/Moonlight/App/Helpers/WingsServerConverter.cs new file mode 100644 index 0000000..2b4c33d --- /dev/null +++ b/Moonlight/App/Helpers/WingsServerConverter.cs @@ -0,0 +1,131 @@ +using System.Text; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Http.Resources.Wings; +using Moonlight.App.Repositories; +using Moonlight.App.Repositories.Servers; + +namespace Moonlight.App.Helpers; + +public class WingsServerConverter +{ + private readonly ServerRepository ServerRepository; + private readonly ImageRepository ImageRepository; + + public WingsServerConverter(ServerRepository serverRepository, ImageRepository imageRepository) + { + ServerRepository = serverRepository; + ImageRepository = imageRepository; + } + + public WingsServer FromServer(Server s) + { + var server = ServerRepository + .Get() + .Include(x => x.Allocations) + .Include(x => x.Backups) + .Include(x => x.Variables) + .Include(x => x.Image) + .Include(x => x.MainAllocation) + .First(x => x.Id == s.Id); + + var wingsServer = new WingsServer + { + Uuid = server.Uuid + }; + + // Allocations + var def = server.MainAllocation; + + wingsServer.Settings.Allocations.Default.Ip = "0.0.0.0"; + wingsServer.Settings.Allocations.Default.Port = def.Port; + + foreach (var a in server.Allocations) + { + wingsServer.Settings.Allocations.Mappings.Ports.Add(a.Port); + } + + // Build + wingsServer.Settings.Build.Swap = server.Memory * 2; + wingsServer.Settings.Build.Threads = null!; + wingsServer.Settings.Build.Cpu_Limit = server.Cpu; + wingsServer.Settings.Build.Disk_Space = server.Disk; + wingsServer.Settings.Build.Io_Weight = 500; + wingsServer.Settings.Build.Memory_Limit = server.Memory; + wingsServer.Settings.Build.Oom_Disabled = true; + + var image = ImageRepository + .Get() + .Include(x => x.DockerImages) + .First(x => x.Id == server.Image.Id); + + // Container + wingsServer.Settings.Container.Image = image.DockerImages[server.DockerImageIndex].Name; + + // Egg + wingsServer.Settings.Egg.Id = image.Uuid; + + // Settings + wingsServer.Settings.Skip_Egg_Scripts = false; + wingsServer.Settings.Suspended = false; //TODO: Implement + wingsServer.Settings.Invocation = string.IsNullOrEmpty(server.OverrideStartup) ? image.Startup : server.OverrideStartup; + wingsServer.Settings.Uuid = server.Uuid; + + + // Environment + foreach (var v in server.Variables) + { + if (!wingsServer.Settings.Environment.ContainsKey(v.Key)) + { + wingsServer.Settings.Environment.Add(v.Key, v.Value); + } + } + + // Stop + if (image.StopCommand.StartsWith("!")) + { + wingsServer.Process_Configuration.Stop.Type = "stop"; + wingsServer.Process_Configuration.Stop.Value = null!; + } + else + { + wingsServer.Process_Configuration.Stop.Type = "command"; + wingsServer.Process_Configuration.Stop.Value = image.StopCommand; + } + + // Done + + wingsServer.Process_Configuration.Startup.Done = new() { image.StartupDetection }; + wingsServer.Process_Configuration.Startup.Strip_Ansi = false; + wingsServer.Process_Configuration.Startup.User_Interaction = new(); + + // Configs + var configData = new ConfigurationBuilder().AddJsonStream( + new MemoryStream(Encoding.ASCII.GetBytes(image.ConfigFiles!)) + ).Build(); + + foreach (var child in configData.GetChildren()) + { + List replaces = new(); + + foreach (var section in child.GetSection("find").GetChildren()) + { + replaces.Add(new() + { + Match = section.Key, + Replace_With = section.Value + .Replace("{{server.build.default.port}}", def.Port.ToString()) + }); + } + + wingsServer.Process_Configuration.Configs.Add(new() + { + Parser = child.GetValue("parser"), + File = child.Key, + Replace = replaces + }); + } + + return wingsServer; + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs b/Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs new file mode 100644 index 0000000..e1be3ce --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Remote/ActivityController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Moonlight.App.Http.Controllers.Api.Remote; + +[Route("api/remote/activity")] +[ApiController] +public class ActivityController : Controller +{ + [HttpPost] + public ActionResult SaveActivity() + { + return Ok(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs new file mode 100644 index 0000000..9ea1e8f --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Remote/ServersController.cs @@ -0,0 +1,202 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Helpers; +using Moonlight.App.Http.Resources.Wings; +using Moonlight.App.Repositories; +using Moonlight.App.Repositories.Servers; +using Moonlight.App.Services; + +namespace Moonlight.App.Http.Controllers.Api.Remote; + +[Route("api/remote/servers")] +[ApiController] +public class ServersController : Controller +{ + private readonly WingsServerConverter Converter; + private readonly ServerRepository ServerRepository; + private readonly NodeRepository NodeRepository; + private readonly MessageService MessageService; + + public ServersController( + WingsServerConverter converter, + ServerRepository serverRepository, + NodeRepository nodeRepository, + MessageService messageService) + { + Converter = converter; + ServerRepository = serverRepository; + NodeRepository = nodeRepository; + MessageService = messageService; + } + + [HttpGet] + public async Task>> GetServers( + [FromQuery(Name = "page")] int page, + [FromQuery(Name = "per_page")] int perPage) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + var servers = ServerRepository + .Get() + .Include(x => x.Node) + .Where(x => x.Node.Id == node.Id) + .ToArray(); + + List wingsServers = new(); + int totalPages = 1; + + if (servers.Length > 0) + { + var slice = servers.Chunk(perPage).ToArray(); + var part = slice[page]; + + foreach (var server in part) + { + wingsServers.Add(Converter.FromServer(server)); + } + + totalPages = slice.Length - 1; + } + + await MessageService.Emit($"wings.{node.Id}.serverlist", node); + + //Logger.Debug($"[BRIDGE] Node '{node.Name}' is requesting server list page {page} with {perPage} items per page"); + + return PaginationResult.CreatePagination( + wingsServers.ToArray(), + page, + perPage, + totalPages, + servers.Length + ); + } + + + [HttpPost("reset")] + public async Task Reset() + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + await MessageService.Emit($"wings.{node.Id}.statereset", node); + + foreach (var server in ServerRepository + .Get() + .Include(x => x.Node) + .Where(x => x.Node.Id == node.Id) + .ToArray() + ) + { + if (server.Installing) + { + server.Installing = false; + ServerRepository.Update(server); + } + } + + return Ok(); + } + + [HttpGet("{uuid}")] + public async Task> GetServer(Guid uuid) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + var server = ServerRepository.Get().FirstOrDefault(x => x.Uuid == uuid); + + if (server == null) + return NotFound(); + + await MessageService.Emit($"wings.{node.Id}.serverfetch", server); + + return Converter.FromServer(server); + } + + [HttpGet("{uuid}/install")] + public async Task> GetServerInstall(Guid uuid) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + var server = ServerRepository.Get().Include(x => x.Image).FirstOrDefault(x => x.Uuid == uuid); + + if (server == null) + return NotFound(); + + await MessageService.Emit($"wings.{node.Id}.serverinstallfetch", server); + + return new WingsServerInstall() + { + Entrypoint = server.Image.InstallEntrypoint, + Script = server.Image.InstallScript!, + Container_Image = server.Image.InstallDockerImage + }; + } + + [HttpPost("{uuid}/install")] + public async Task SetInstallState(Guid uuid) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + var server = ServerRepository.Get().Include(x => x.Image).FirstOrDefault(x => x.Uuid == uuid); + + if (server == null) + return NotFound(); + + server.Installing = false; + ServerRepository.Update(server); + + await MessageService.Emit($"wings.{node.Id}.serverinstallcomplete", server); + await MessageService.Emit($"server.{server.Uuid}.installcomplete", server); + + return Ok(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs b/Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs new file mode 100644 index 0000000..cacacec --- /dev/null +++ b/Moonlight/App/Http/Requests/Wings/ReportBackupCompleteRequest.cs @@ -0,0 +1,7 @@ +namespace Moonlight.App.Http.Requests.Wings; + +public class ReportBackupCompleteRequest +{ + public bool Successful { get; set; } + public long Size { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs b/Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs new file mode 100644 index 0000000..4057e98 --- /dev/null +++ b/Moonlight/App/Http/Requests/Wings/SftpLoginRequest.cs @@ -0,0 +1,9 @@ +namespace Moonlight.App.Http.Requests.Wings; + +public class SftpLoginRequest +{ + public string Username { get; set; } + public string Password { get; set; } + public string Ip { get; set; } + public string Type { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Resources/Wings/PaginationResult.cs b/Moonlight/App/Http/Resources/Wings/PaginationResult.cs new file mode 100644 index 0000000..610e178 --- /dev/null +++ b/Moonlight/App/Http/Resources/Wings/PaginationResult.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Http.Resources.Wings; + +public class PaginationResult +{ + [JsonProperty("data")] + public List Data { get; set; } + + [JsonProperty("meta")] + public MetaData Meta { get; set; } + + public PaginationResult() + { + Data = new List(); + Meta = new(); + } + + public static PaginationResult CreatePagination(T[] data, int page, int perPage, int totalPages, int totalItems) + { + var res = new PaginationResult(); + + foreach (var i in data) + { + res.Data.Add(i); + } + + res.Meta.Current_Page = page; + res.Meta.Total_Pages = totalPages; + res.Meta.Count = data.Length; + res.Meta.Total = totalItems; + res.Meta.Per_Page = perPage; + res.Meta.Last_Page = totalPages; + + return res; + } + + public class MetaData + { + public int Total { get; set; } + public int Count { get; set; } + public int Per_Page { get; set; } + public int Current_Page { get; set; } + public int Last_Page { get; set; } + public int Total_Pages { get; set; } + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs b/Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs new file mode 100644 index 0000000..54d0ea6 --- /dev/null +++ b/Moonlight/App/Http/Resources/Wings/SftpLoginResult.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Http.Resources.Wings; + +public class SftpLoginResult +{ + public string Server { get; set; } + public string User { get; set; } + public List Permissions { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Resources/Wings/WingsServer.cs b/Moonlight/App/Http/Resources/Wings/WingsServer.cs new file mode 100644 index 0000000..d55a81e --- /dev/null +++ b/Moonlight/App/Http/Resources/Wings/WingsServer.cs @@ -0,0 +1,154 @@ +using System.Text.Json.Serialization; + +namespace Moonlight.App.Http.Resources.Wings; + +public class WingsServer +{ + [JsonPropertyName("uuid")] + public Guid Uuid { get; set; } + + [JsonPropertyName("settings")] public WingsServerSettings Settings { get; set; } = new(); + + [JsonPropertyName("process_configuration")] + public WingsServerProcessConfiguration Process_Configuration { get; set; } = new(); + + public class WingsServerProcessConfiguration + { + [JsonPropertyName("startup")] public WingsServerStartup Startup { get; set; } = new(); + + [JsonPropertyName("stop")] public WingsServerStop Stop { get; set; } = new(); + + [JsonPropertyName("configs")] public List Configs { get; set; } = new(); + } + + public class WingsServerConfig + { + [JsonPropertyName("parser")] + public string Parser { get; set; } + + [JsonPropertyName("file")] + public string File { get; set; } + + [JsonPropertyName("replace")] public List Replace { get; set; } = new(); + } + + public class WingsServerReplace + { + [JsonPropertyName("match")] + public string Match { get; set; } + + [JsonPropertyName("replace_with")] + public string Replace_With { get; set; } + } + + public class WingsServerStartup + { + [JsonPropertyName("done")] public List Done { get; set; } = new(); + + [JsonPropertyName("user_interaction")] public List User_Interaction { get; set; } = new(); + + [JsonPropertyName("strip_ansi")] + public bool Strip_Ansi { get; set; } + } + + public class WingsServerStop + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("value")] + public string Value { get; set; } + } + + public class WingsServerSettings + { + [JsonPropertyName("uuid")] + public Guid Uuid { get; set; } + + [JsonPropertyName("suspended")] + public bool Suspended { get; set; } + + [JsonPropertyName("environment")] public Dictionary Environment { get; set; } = new(); + + [JsonPropertyName("invocation")] + public string Invocation { get; set; } + + [JsonPropertyName("skip_egg_scripts")] + public bool Skip_Egg_Scripts { get; set; } + + [JsonPropertyName("build")] public WingsServerBuild Build { get; set; } = new(); + + [JsonPropertyName("container")] public WingsServerContainer Container { get; set; } = new(); + + [JsonPropertyName("allocations")] public WingsServerAllocations Allocations { get; set; } = new(); + + [JsonPropertyName("mounts")] public List Mounts { get; set; } = new(); + + [JsonPropertyName("egg")] public WingsServerEgg Egg { get; set; } = new(); + } + + public class WingsServerAllocations + { + [JsonPropertyName("default")] public WingsServerDefault Default { get; set; } = new(); + + [JsonPropertyName("mappings")] public WingsServerMappings Mappings { get; set; } = new(); + } + + public class WingsServerDefault + { + [JsonPropertyName("ip")] + public string Ip { get; set; } + + [JsonPropertyName("port")] + public long Port { get; set; } + } + + public class WingsServerMappings + { + [JsonPropertyName("0.0.0.0")] public List Ports { get; set; } = new(); + } + + public class WingsServerBuild + { + [JsonPropertyName("memory_limit")] + public long Memory_Limit { get; set; } + + [JsonPropertyName("swap")] + public long Swap { get; set; } + + [JsonPropertyName("io_weight")] + public long Io_Weight { get; set; } + + [JsonPropertyName("cpu_limit")] + public long Cpu_Limit { get; set; } + + [JsonPropertyName("threads")] + public object Threads { get; set; } + + [JsonPropertyName("disk_space")] + public long Disk_Space { get; set; } + + [JsonPropertyName("oom_disabled")] + public bool Oom_Disabled { get; set; } + } + + public class WingsServerContainer + { + [JsonPropertyName("image")] + public string Image { get; set; } + + [JsonPropertyName("oom_disabled")] + public bool Oom_Disabled { get; set; } + + [JsonPropertyName("requires_rebuild")] + public bool Requires_Rebuild { get; set; } + } + + public class WingsServerEgg + { + [JsonPropertyName("id")] + public Guid Id { get; set; } + + [JsonPropertyName("file_denylist")] public List File_Denylist { get; set; } = new(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Resources/Wings/WingsServerInstall.cs b/Moonlight/App/Http/Resources/Wings/WingsServerInstall.cs new file mode 100644 index 0000000..473338a --- /dev/null +++ b/Moonlight/App/Http/Resources/Wings/WingsServerInstall.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Http.Resources.Wings; + +public class WingsServerInstall +{ + public string Container_Image { get; set; } + public string Entrypoint { get; set; } + public string Script { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/MessageSystem/MessageSender.cs b/Moonlight/App/MessageSystem/MessageSender.cs new file mode 100644 index 0000000..a564eb5 --- /dev/null +++ b/Moonlight/App/MessageSystem/MessageSender.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; +using Logging.Net; + +namespace Moonlight.App.MessageSystem; + +public class MessageSender +{ + private readonly List Subscribers; + + public bool Debug { get; set; } + public TimeSpan TookToLongTime { get; set; } = TimeSpan.FromSeconds(1); + + public MessageSender() + { + Subscribers = new(); + } + + public void Subscribe(string name, object bind, Func method) + { + lock (Subscribers) + { + Subscribers.Add(new () + { + Name = name, + Action = method, + Type = typeof(T), + Bind = bind + }); + } + + if(Debug) + Logger.Debug($"{bind} subscribed to '{name}'"); + } + + public void Unsubscribe(string name, object bind) + { + lock (Subscribers) + { + Subscribers.RemoveAll(x => x.Bind == bind); + } + + if(Debug) + Logger.Debug($"{bind} unsubscribed from '{name}'"); + } + + public Task Emit(string name, object? value, bool disableWarning = false) + { + lock (Subscribers) + { + foreach (var subscriber in Subscribers) + { + if (subscriber.Name == name) + { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + var del = (Delegate)subscriber.Action; + + ((Task)del.DynamicInvoke(value)!).Wait(); + + stopWatch.Stop(); + + if (!disableWarning) + { + if (stopWatch.Elapsed.TotalMilliseconds > TookToLongTime.TotalMilliseconds) + { + Logger.Warn( + $"Subscriber {subscriber.Type.Name} for event '{name}' took long to process. {stopWatch.Elapsed.TotalMilliseconds}ms"); + } + } + + if (Debug) + { + Logger.Debug( + $"Subscriber {subscriber.Type.Name} for event '{name}' took {stopWatch.Elapsed.TotalMilliseconds}ms"); + } + } + } + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/App/MessageSystem/MessageSubscriber.cs b/Moonlight/App/MessageSystem/MessageSubscriber.cs new file mode 100644 index 0000000..b435d81 --- /dev/null +++ b/Moonlight/App/MessageSystem/MessageSubscriber.cs @@ -0,0 +1,9 @@ +namespace Moonlight.App.MessageSystem; + +public class MessageSubscriber +{ + public string Name { get; set; } + public object Action { get; set; } + public Type Type { get; set; } + public object Bind { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Files/Accesses/WingsFileAccess.cs b/Moonlight/App/Models/Files/Accesses/WingsFileAccess.cs new file mode 100644 index 0000000..6d181e2 --- /dev/null +++ b/Moonlight/App/Models/Files/Accesses/WingsFileAccess.cs @@ -0,0 +1,205 @@ +using System.Security.Cryptography; +using System.Text; +using System.Web; +using JWT.Algorithms; +using JWT.Builder; +using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers; +using Moonlight.App.Models.Wings.Requests; +using Moonlight.App.Models.Wings.Resources; +using RestSharp; + +namespace Moonlight.App.Models.Files.Accesses; + +public class WingsFileAccess : IFileAccess +{ + private readonly WingsApiHelper WingsApiHelper; + private readonly WingsJwtHelper WingsJwtHelper; + private readonly Database.Entities.Node Node; + private readonly Server Server; + private readonly User User; + private readonly string AppUrl; + + private string Path { get; set; } = "/"; + + public WingsFileAccess( + WingsApiHelper wingsApiHelper, + Server server, + User user, + WingsJwtHelper wingsJwtHelper, + string appUrl) + { + WingsApiHelper = wingsApiHelper; + Node = server.Node; + Server = server; + User = user; + WingsJwtHelper = wingsJwtHelper; + AppUrl = appUrl; + } + + public async Task GetDirectoryContent() + { + var res = await WingsApiHelper.Get(Node, + $"api/servers/{Server.Uuid}/files/list-directory?directory={Path}"); + + var x = new List(); + + foreach (var response in res) + { + x.Add(new() + { + Name = response.Name, + Size = response.File ? response.Size : 0, + CreatedAt = response.Created.Date, + IsFile = response.File, + UpdatedAt = response.Modified.Date + }); + } + + return x.ToArray(); + } + + public Task ChangeDirectory(string s) + { + var x = System.IO.Path.Combine(Path, s).Replace("\\", "/") + "/"; + x = x.Replace("//", "/"); + Path = x; + + return Task.CompletedTask; + } + + public Task SetDirectory(string s) + { + Path = s; + return Task.CompletedTask; + } + + public Task GoUp() + { + Path = System.IO.Path.GetFullPath(System.IO.Path.Combine(Path, "..")).Replace("\\", "/").Replace("C:", ""); + return Task.CompletedTask; + } + + public async Task ReadFile(FileManagerObject fileManagerObject) + { + return await WingsApiHelper.GetRaw(Node, + $"api/servers/{Server.Uuid}/files/contents?file={Path}{fileManagerObject.Name}"); + } + + public async Task WriteFile(FileManagerObject fileManagerObject, string content) + { + await WingsApiHelper.PostRaw(Node, + $"api/servers/{Server.Uuid}/files/write?file={Path}{fileManagerObject.Name}", content); + } + + public async Task UploadFile(string name, Stream dataStream, Action onProgress) + { + var token = WingsJwtHelper.Generate(Node.Token, + claims => { claims.Add("server_uuid", Server.Uuid.ToString()); }); + + var client = new RestClient(); + var request = new RestRequest(); + + if (Node.Ssl) + request.Resource = $"https://{Node.Fqdn}:{Node.HttpPort}/upload/file?token={token}&directory={Path}"; + else + request.Resource = $"http://{Node.Fqdn}:{Node.HttpPort}/upload/file?token={token}&directory={Path}"; + + request.AddParameter("name", "files"); + request.AddParameter("filename", name); + request.AddHeader("Content-Type", "multipart/form-data"); + request.AddHeader("Origin", AppUrl); + request.AddFile("files", () => + { + return new StreamProgressHelper(dataStream) + { + Progress = i => + { + if (onProgress != null) + onProgress.Invoke(i); + } + }; + }, name); + + await client.ExecutePostAsync(request); + + client.Dispose(); + dataStream.Close(); + } + + public async Task CreateDirectory(string name) + { + await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/create-directory", + new CreateDirectoryRequest() + { + Name = name, + Path = Path + }); + } + + public Task GetCurrentPath() + { + return Task.FromResult(Path); + } + + public Task GetDownloadStream(FileManagerObject managerObject) + { + throw new NotImplementedException(); + } + + public Task GetDownloadUrl(FileManagerObject managerObject) + { + var token = WingsJwtHelper.Generate(Node.Token, claims => + { + claims.Add("server_uuid", Server.Uuid.ToString()); + claims.Add("file_path", HttpUtility.UrlDecode(Path + "/" + managerObject.Name)); + }); + + if (Node.Ssl) + { + return Task.FromResult( + $"https://{Node.Fqdn}:{Node.HttpPort}/download/file?token={token}" + ); + } + else + { + return Task.FromResult( + $"http://{Node.Fqdn}:{Node.HttpPort}/download/file?token={token}" + ); + } + } + + public async Task Delete(FileManagerObject managerObject) + { + await WingsApiHelper.Post(Node, $"api/servers/{Server.Uuid}/files/delete", new DeleteFilesRequest() + { + Root = Path, + Files = new() + { + managerObject.Name + } + }); + } + + public async Task Move(FileManagerObject managerObject, string newPath) + { + await WingsApiHelper.Put(Node, $"api/servers/{Server.Uuid}/files/rename", new RenameFilesRequest() + { + Root = "/", + Files = new[] + { + new RenameFilesData() + { + From = Path + managerObject.Name, + To = newPath + } + } + }); + } + + public Task GetLaunchUrl() + { + return Task.FromResult( + $"sftp://{User.Id}.{StringHelper.IntToStringWithLeadingZeros(Server.Id, 8)}@{Node.Fqdn}:{Node.SftpPort}"); + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Files/FileManagerObject.cs b/Moonlight/App/Models/Files/FileManagerObject.cs new file mode 100644 index 0000000..063d556 --- /dev/null +++ b/Moonlight/App/Models/Files/FileManagerObject.cs @@ -0,0 +1,10 @@ +namespace Moonlight.App.Models.Files; + +public class FileManagerObject +{ + public string Name { get; set; } + public bool IsFile { get; set; } + public long Size { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Files/IFileAccess.cs b/Moonlight/App/Models/Files/IFileAccess.cs new file mode 100644 index 0000000..6792d40 --- /dev/null +++ b/Moonlight/App/Models/Files/IFileAccess.cs @@ -0,0 +1,19 @@ +namespace Moonlight.App.Models.Files; + +public interface IFileAccess +{ + public Task GetDirectoryContent(); + public Task ChangeDirectory(string s); + public Task SetDirectory(string s); + public Task GoUp(); + public Task ReadFile(FileManagerObject fileManagerObject); + public Task WriteFile(FileManagerObject fileManagerObject, string content); + public Task UploadFile(string name, Stream stream, Action progressUpdated); + public Task CreateDirectory(string name); + public Task GetCurrentPath(); + public Task GetDownloadStream(FileManagerObject managerObject); + public Task GetDownloadUrl(FileManagerObject managerObject); + public Task Delete(FileManagerObject managerObject); + public Task Move(FileManagerObject managerObject, string newPath); + public Task GetLaunchUrl(); +} \ No newline at end of file diff --git a/Moonlight/App/Models/Paper/Resources/PaperBuilds.cs b/Moonlight/App/Models/Paper/Resources/PaperBuilds.cs new file mode 100644 index 0000000..63b9cb4 --- /dev/null +++ b/Moonlight/App/Models/Paper/Resources/PaperBuilds.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Paper.Resources; + +public class PaperBuilds +{ + [JsonProperty("project_id")] + public string ProjectId { get; set; } + + [JsonProperty("project_name")] + public string ProjectName { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("builds")] + public List Builds { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Paper/Resources/PaperVersions.cs b/Moonlight/App/Models/Paper/Resources/PaperVersions.cs new file mode 100644 index 0000000..0cd29e5 --- /dev/null +++ b/Moonlight/App/Models/Paper/Resources/PaperVersions.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Paper.Resources; + +public class PaperVersions +{ + [JsonProperty("project_id")] + public string ProjectId { get; set; } + + [JsonProperty("project_name")] + public string ProjectName { get; set; } + + [JsonProperty("version_groups")] + public List VersionGroups { get; set; } + + [JsonProperty("versions")] + public List Versions { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/PowerSignal.cs b/Moonlight/App/Models/Wings/PowerSignal.cs new file mode 100644 index 0000000..921adc8 --- /dev/null +++ b/Moonlight/App/Models/Wings/PowerSignal.cs @@ -0,0 +1,9 @@ +namespace Moonlight.App.Models.Wings; + +public enum PowerSignal +{ + Start, + Stop, + Kill, + Restart +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/CreateBackupRequest.cs b/Moonlight/App/Models/Wings/Requests/CreateBackupRequest.cs new file mode 100644 index 0000000..a0ce5b4 --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/CreateBackupRequest.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class CreateBackupRequest +{ + [JsonProperty("adapter")] + public string Adapter { get; set; } + + [JsonProperty("uuid")] + public Guid Uuid { get; set; } + + [JsonProperty("ignore")] + public string Ignore { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/CreateDirectoryRequest.cs b/Moonlight/App/Models/Wings/Requests/CreateDirectoryRequest.cs new file mode 100644 index 0000000..62c95e3 --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/CreateDirectoryRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class CreateDirectoryRequest +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("path")] + public string Path { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/CreateServerRequest.cs b/Moonlight/App/Models/Wings/Requests/CreateServerRequest.cs new file mode 100644 index 0000000..3288d88 --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/CreateServerRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class CreateServerRequest +{ + [JsonProperty("uuid")] + public Guid Uuid { get; set; } + + [JsonProperty("start_on_completion")] + public bool StartOnCompletion { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/DeleteFilesRequest.cs b/Moonlight/App/Models/Wings/Requests/DeleteFilesRequest.cs new file mode 100644 index 0000000..c64487f --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/DeleteFilesRequest.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class DeleteFilesRequest +{ + [JsonProperty("root")] + public string Root { get; set; } + + [JsonProperty("files")] public List Files { get; set; } = new(); +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/RenameFilesRequest.cs b/Moonlight/App/Models/Wings/Requests/RenameFilesRequest.cs new file mode 100644 index 0000000..8355cff --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/RenameFilesRequest.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class RenameFilesRequest +{ + [JsonProperty("root")] + public string Root { get; set; } + + [JsonProperty("files")] public RenameFilesData[] Files { get; set; } +} + +public class RenameFilesData +{ + [JsonProperty("from")] + public string From { get; set; } + + [JsonProperty("to")] + public string To { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/RestoreBackupRequest.cs b/Moonlight/App/Models/Wings/Requests/RestoreBackupRequest.cs new file mode 100644 index 0000000..0dfa9cb --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/RestoreBackupRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class RestoreBackupRequest +{ + [JsonProperty("adapter")] + public string Adapter { get; set; } + + [JsonProperty("truncate_directory")] public bool TruncateDirectory { get; set; } = false; + [JsonProperty("download_url")] public string DownloadUrl { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Requests/ServerPowerRequest.cs b/Moonlight/App/Models/Wings/Requests/ServerPowerRequest.cs new file mode 100644 index 0000000..597d9e5 --- /dev/null +++ b/Moonlight/App/Models/Wings/Requests/ServerPowerRequest.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Requests; + +public class ServerPowerRequest +{ + [JsonProperty("action")] + public string Action { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Resources/ListDirectoryRequest.cs b/Moonlight/App/Models/Wings/Resources/ListDirectoryRequest.cs new file mode 100644 index 0000000..0a67bfa --- /dev/null +++ b/Moonlight/App/Models/Wings/Resources/ListDirectoryRequest.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Resources; + +public class ListDirectoryRequest +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("created")] + public DateTimeOffset Created { get; set; } + + [JsonProperty("modified")] + public DateTimeOffset Modified { get; set; } + + [JsonProperty("size")] + public long Size { get; set; } + + [JsonProperty("directory")] + public bool Directory { get; set; } + + [JsonProperty("file")] + public bool File { get; set; } + + [JsonProperty("symlink")] + public bool Symlink { get; set; } + + [JsonProperty("mime")] + public string Mime { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Resources/ServerDetailsResponse.cs b/Moonlight/App/Models/Wings/Resources/ServerDetailsResponse.cs new file mode 100644 index 0000000..bc89a29 --- /dev/null +++ b/Moonlight/App/Models/Wings/Resources/ServerDetailsResponse.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Resources; + +public class ServerDetailsResponse +{ + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("is_suspended")] + public bool IsSuspended { get; set; } + + [JsonProperty("utilization")] + public ServerDetailsResponseUtilization Utilization { get; set; } + + public class ServerDetailsResponseUtilization + { + [JsonProperty("memory_bytes")] + public long MemoryBytes { get; set; } + + [JsonProperty("memory_limit_bytes")] + public long MemoryLimitBytes { get; set; } + + [JsonProperty("cpu_absolute")] + public double CpuAbsolute { get; set; } + + [JsonProperty("network")] + public ServerDetailsResponseNetwork Network { get; set; } + + [JsonProperty("uptime")] + public long Uptime { get; set; } + + [JsonProperty("state")] + public string State { get; set; } + + [JsonProperty("disk_bytes")] + public long DiskBytes { get; set; } + } + + public class ServerDetailsResponseNetwork + { + [JsonProperty("rx_bytes")] + public long RxBytes { get; set; } + + [JsonProperty("tx_bytes")] + public long TxBytes { get; set; } + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Wings/Resources/SystemStatus.cs b/Moonlight/App/Models/Wings/Resources/SystemStatus.cs new file mode 100644 index 0000000..26a30de --- /dev/null +++ b/Moonlight/App/Models/Wings/Resources/SystemStatus.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Moonlight.App.Models.Wings.Resources; + +public class SystemStatus +{ + [JsonProperty("architecture")] + public string Architecture { get; set; } + + [JsonProperty("cpu_count")] + public long CpuCount { get; set; } + + [JsonProperty("kernel_version")] + public string KernelVersion { get; set; } + + [JsonProperty("os")] + public string Os { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Repositories/ImageRepository.cs b/Moonlight/App/Repositories/ImageRepository.cs new file mode 100644 index 0000000..e36f790 --- /dev/null +++ b/Moonlight/App/Repositories/ImageRepository.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database; +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Repositories; + +public class ImageRepository : IDisposable +{ + private readonly DataContext DataContext; + + public ImageRepository(DataContext dataContext) + { + DataContext = dataContext; + } + + public DbSet Get() + { + return DataContext.Images; + } + + public Image Add(Image image) + { + var x = DataContext.Images.Add(image); + DataContext.SaveChanges(); + return x.Entity; + } + + public void Update(Image image) + { + DataContext.Images.Update(image); + DataContext.SaveChanges(); + } + + public void Delete(Image image) + { + DataContext.Images.Remove(image); + DataContext.SaveChanges(); + } + + public void Dispose() + { + DataContext.Dispose(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/MessageService.cs b/Moonlight/App/Services/MessageService.cs new file mode 100644 index 0000000..f063d93 --- /dev/null +++ b/Moonlight/App/Services/MessageService.cs @@ -0,0 +1,11 @@ +using Moonlight.App.MessageSystem; + +namespace Moonlight.App.Services; + +public class MessageService : MessageSender +{ + public MessageService() + { + Debug = true; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/NodeService.cs b/Moonlight/App/Services/NodeService.cs index fc309a9..4a0afba 100644 --- a/Moonlight/App/Services/NodeService.cs +++ b/Moonlight/App/Services/NodeService.cs @@ -1,13 +1,23 @@ -using Moonlight.App.Repositories; +using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers; +using Moonlight.App.Models.Wings.Resources; +using Moonlight.App.Repositories; namespace Moonlight.App.Services; public class NodeService { private readonly NodeRepository NodeRepository; + private readonly WingsApiHelper WingsApiHelper; - public NodeService(NodeRepository nodeRepository) + public NodeService(NodeRepository nodeRepository, WingsApiHelper wingsApiHelper) { NodeRepository = nodeRepository; + WingsApiHelper = wingsApiHelper; + } + + public async Task GetStatus(Node node) + { + return await WingsApiHelper.Get(node, "api/system"); } } \ No newline at end of file diff --git a/Moonlight/App/Services/PaperService.cs b/Moonlight/App/Services/PaperService.cs new file mode 100644 index 0000000..57d93d2 --- /dev/null +++ b/Moonlight/App/Services/PaperService.cs @@ -0,0 +1,28 @@ +using Moonlight.App.Helpers; +using Moonlight.App.Models.Paper.Resources; + +namespace Moonlight.App.Services; + +public class PaperService +{ + private readonly PaperApiHelper ApiHelper; + + public PaperService(PaperApiHelper apiHelper) + { + ApiHelper = apiHelper; + } + + public async Task GetVersions() + { + var data = await ApiHelper.Get("paper"); + + return data.Versions.ToArray(); + } + + public async Task GetBuilds(string version) + { + var data = await ApiHelper.Get($"paper/versions/{version}"); + + return data.Builds.ToArray(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/ServerService.cs b/Moonlight/App/Services/ServerService.cs new file mode 100644 index 0000000..c869def --- /dev/null +++ b/Moonlight/App/Services/ServerService.cs @@ -0,0 +1,326 @@ +using System.Security.Cryptography; +using System.Text; +using JWT.Algorithms; +using JWT.Builder; +using Logging.Net; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database; +using Moonlight.App.Database.Entities; +using Moonlight.App.Exceptions; +using Moonlight.App.Helpers; +using Moonlight.App.Models.Files; +using Moonlight.App.Models.Files.Accesses; +using Moonlight.App.Models.Wings; +using Moonlight.App.Models.Wings.Requests; +using Moonlight.App.Models.Wings.Resources; +using Moonlight.App.Repositories; +using Moonlight.App.Repositories.Servers; + +namespace Moonlight.App.Services; + +public class ServerService +{ + private readonly ServerRepository ServerRepository; + private readonly UserRepository UserRepository; + private readonly ImageRepository ImageRepository; + private readonly NodeRepository NodeRepository; + private readonly WingsApiHelper WingsApiHelper; + private readonly MessageService MessageService; + private readonly UserService UserService; + private readonly ConfigService ConfigService; + private readonly WingsJwtHelper WingsJwtHelper; + private readonly string AppUrl; + + public ServerService( + ServerRepository serverRepository, + WingsApiHelper wingsApiHelper, + UserRepository userRepository, + ImageRepository imageRepository, + NodeRepository nodeRepository, + MessageService messageService, + UserService userService, + ConfigService configService, + WingsJwtHelper wingsJwtHelper) + { + ServerRepository = serverRepository; + WingsApiHelper = wingsApiHelper; + UserRepository = userRepository; + ImageRepository = imageRepository; + NodeRepository = nodeRepository; + MessageService = messageService; + UserService = userService; + ConfigService = configService; + WingsJwtHelper = wingsJwtHelper; + + AppUrl = ConfigService.GetSection("Moonlight").GetValue("AppUrl"); + } + + private Server EnsureNodeData(Server s) + { + if (s.Node == null) // Ensure node data is available + { + return ServerRepository + .Get() + .Include(x => x.Node) + .First(x => x.Id == s.Id); + } + else + return s; + } + + public async Task GetDetails(Server s) + { + Server server = EnsureNodeData(s); + + return await WingsApiHelper.Get( + server.Node, + $"api/servers/{server.Uuid}" + ); + } + + public async Task SetPowerState(Server s, PowerSignal signal) + { + Server server = EnsureNodeData(s); + + var rawSignal = signal.ToString().ToLower(); + + await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/power", new ServerPowerRequest() + { + Action = rawSignal + }); + } + + public async Task CreateBackup(Server server) + { + var serverData = ServerRepository // Ensure data + .Get() + .Include(x => x.Node) + .Include(x => x.Backups) + .First(x => x.Id == server.Id); + + var backup = new ServerBackup() + { + Name = $"Created at {DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()}", + Uuid = Guid.NewGuid(), + CreatedAt = DateTime.Now, + Created = false + }; + + serverData.Backups.Add(backup); + ServerRepository.Update(serverData); + + await WingsApiHelper.Post(serverData.Node, $"api/servers/{serverData.Uuid}/backup", new CreateBackupRequest() + { + Adapter = "wings", + Uuid = backup.Uuid, + Ignore = "" + }); + + return backup; + } + + public Task GetBackups(Server server, bool forceReload = false) + { + if (forceReload) //TODO: Find an alternative to avoid cache and the creation of a new db context + { + var serverData = new ServerRepository(new DataContext(ConfigService)) + .Get() + .Include(x => x.Backups) + .First(x => x.Id == server.Id); + + return Task.FromResult(serverData.Backups.ToArray()); + } + else + { + var serverData = ServerRepository + .Get() + .Include(x => x.Backups) + .First(x => x.Id == server.Id); + + return Task.FromResult(serverData.Backups.ToArray()); + } + } + + public async Task RestoreBackup(Server s, ServerBackup serverBackup) + { + Server server = EnsureNodeData(s); + + await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/backup/{serverBackup.Uuid}/restore", + new RestoreBackupRequest() + { + Adapter = "wings" + }); + } + + public async Task DeleteBackup(Server server, ServerBackup serverBackup) + { + var serverData = ServerRepository + .Get() + .Include(x => x.Node) + .Include(x => x.Backups) + .First(x => x.Id == server.Id); + + await WingsApiHelper.Delete(serverData.Node, $"api/servers/{serverData.Uuid}/backup/{serverBackup.Uuid}", + null); + + var backup = serverData.Backups.First(x => x.Uuid == serverBackup.Uuid); + serverData.Backups.Remove(backup); + + ServerRepository.Update(serverData); + + await MessageService.Emit("wings.backups.delete", backup); + } + + public Task DownloadBackup(Server s, ServerBackup serverBackup) + { + Server server = EnsureNodeData(s); + + var token = WingsJwtHelper.Generate(server.Node.Token, claims => + { + claims.Add("server_uuid", server.Uuid.ToString()); + claims.Add("backup_uuid", serverBackup.Uuid.ToString()); + }); + + return Task.FromResult( + $"https://{server.Node.Fqdn}:{server.Node.HttpPort}/download/backup?token={token}" + ); + } + + public Task CreateFileAccess(Server s, User user) // We need the user to create the launch url + { + Server server = EnsureNodeData(s); + + return Task.FromResult( + (IFileAccess)new WingsFileAccess( + WingsApiHelper, + server, + user, + WingsJwtHelper, + AppUrl + ) + ); + } + + public async Task Create(string name, int cpu, long memory, long disk, User u, Image i, Node? n = null, Action? modifyDetails = null) + { + var user = UserRepository + .Get() + .First(x => x.Id == u.Id); + + var image = ImageRepository + .Get() + .Include(x => x.Variables) + .Include(x => x.DockerImages) + .First(x => x.Id == i.Id); + + Node node; + + if (n == null) + { + node = NodeRepository.Get().Include(x => x.Allocations).First(); //TODO: Smart deploy + } + else + { + node = NodeRepository + .Get() + .Include(x => x.Allocations) + .First(x => x.Id == n.Id); + } + + NodeAllocation freeAllo; + + try + { + freeAllo = node.Allocations.First(a => !ServerRepository.Get() + .SelectMany(s => s.Allocations) + .Any(b => b.Id == a.Id)); // Thank you ChatGPT <3 + } + catch (Exception) + { + throw new DisplayException("No allocation found"); + } + + if (freeAllo == null) + throw new DisplayException("No allocation found"); + + var server = new Server() + { + Cpu = cpu, + Memory = memory, + Disk = disk, + Name = name, + Image = image, + Owner = user, + Node = node, + Uuid = Guid.NewGuid(), + MainAllocation = freeAllo, + Allocations = new() + { + freeAllo + }, + Backups = new(), + OverrideStartup = "", + DockerImageIndex = image.DockerImages.FindIndex(x => x.Default) + }; + + foreach (var imageVariable in image.Variables) + { + server.Variables.Add(new() + { + Key = imageVariable.Key, + Value = imageVariable.DefaultValue + }); + } + + if(modifyDetails != null) + modifyDetails.Invoke(server); + + var newServerData = ServerRepository.Add(server); + + try + { + await WingsApiHelper.Post(node, $"api/servers", new CreateServerRequest() + { + Uuid = newServerData.Uuid, + StartOnCompletion = false + }); + + return newServerData; + } + catch (Exception e) + { + Logger.Error("Error creating server on wings. Deleting db model"); + Logger.Error(e); + + ServerRepository.Delete(newServerData); + + throw new Exception("Error creating server on wings"); + } + } + + public async Task Reinstall(Server s) + { + Server server = EnsureNodeData(s); + + await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null); + } + + public async Task SftpServerLogin(int serverId, int id, string password) + { + var server = ServerRepository.Get().FirstOrDefault(x => x.Id == serverId); + + if (server == null) //TODO: Logging + throw new Exception("Server not found"); + + var user = await UserService.SftpLogin(id, password); + + if (server.Owner.Id == user.Id) + { + return server; + } + else + { + throw new Exception("User and owner id do not match"); + } + } +} \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 5bde6d3..5e319e8 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -13,7 +13,8 @@ - + + @@ -56,10 +57,7 @@ - - - diff --git a/Moonlight/Pages/_Layout.cshtml b/Moonlight/Pages/_Layout.cshtml index 9b1e388..cbfcf6e 100644 --- a/Moonlight/Pages/_Layout.cshtml +++ b/Moonlight/Pages/_Layout.cshtml @@ -38,13 +38,14 @@ - + + @@ -80,22 +81,26 @@ - - - - + - - - + + + + + + + + + + @@ -107,6 +112,5 @@ - \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 9ec7b69..1f85523 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -35,6 +35,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // Services builder.Services.AddSingleton(); @@ -47,13 +48,23 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Helpers builder.Services.AddSingleton(); - + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + // Third party services builder.Services.AddBlazorTable(); diff --git a/Moonlight/Shared/Components/FileManagerPartials/FileEditor.razor b/Moonlight/Shared/Components/FileManagerPartials/FileEditor.razor new file mode 100644 index 0000000..c70d3ad --- /dev/null +++ b/Moonlight/Shared/Components/FileManagerPartials/FileEditor.razor @@ -0,0 +1,98 @@ +@using BlazorMonaco +@using Moonlight.App.Services +@using Moonlight.Shared.Components.Partials + +@inject SmartTranslateService TranslationService + +
+ +
+ + + +@code +{ + [Parameter] + public string InitialData { get; set; } + + [Parameter] + public string Language { get; set; } + + // Events + [Parameter] + public Action OnSubmit { get; set; } + + [Parameter] + public Action OnCancel { get; set; } + + // Monaco Editor + private MonacoEditor Editor; + private StandaloneEditorConstructionOptions EditorOptions; + + protected override void OnInitialized() + { + EditorOptions = new() + { + AutomaticLayout = true, + Language = "plaintext", + Value = "Wird geladen", + Theme = "vs-dark", + Contextmenu = false, + Minimap = new() + { + Enabled = false + }, + AutoIndent = true + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + Editor.OnDidInit = new EventCallback(this, async () => + { + EditorOptions.Language = Language; + + var model = await Editor.GetModel(); + await MonacoEditorBase.SetModelLanguage(model, EditorOptions.Language); + await Editor.SetPosition(new Position() + { + Column = 0, + LineNumber = 1 + }); + + await Editor.SetValue(InitialData); + + await Editor.Layout(new Dimension() + { + Height = 500, + Width = 1000 + }); + }); + } + } + + private async Task Submit() + { + var data = await Editor.GetValue(); + await InvokeAsync(() => OnSubmit?.Invoke(data)); + } + + private async Task Cancel() + { + await InvokeAsync(() => OnCancel?.Invoke()); + } +} diff --git a/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor new file mode 100644 index 0000000..f89bc02 --- /dev/null +++ b/Moonlight/Shared/Components/FileManagerPartials/FileManager.razor @@ -0,0 +1,513 @@ +@using Moonlight.App.Helpers +@using BlazorContextMenu +@using Logging.Net +@using Moonlight.App.Models.Files +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop + +@inject AlertService AlertService +@inject ToastService ToastService +@inject NavigationManager NavigationManager +@inject SmartTranslateService TranslationService + +
+@if (Editing == null) +{ +
+
+
+ + + + + + + +
+
+
+ @if (SelectedFiles.Count == 0) + { +
+ + + + +
+ } + else + { +
+
+ + @(SelectedFiles.Count) + + Selected +
+ + +
+ } +
+
+
+
+
+
+ @{ + var vx = "/"; + } + / + + + + + + @{ + var cp = "/"; + var lp = "/"; + var pathParts = CurrentPath.Replace("\\", "/").Split('/', StringSplitOptions.RemoveEmptyEntries); + foreach (var path in pathParts) + { + lp = cp; + @(path) + + + + + + + cp += path + "/"; + } + } +
+
+
+ +
+} +else +{ + if (Loading) + { +
+ + Loading + +
+ } + else + { + + } +} +
+ + + Rename + Move + Archive + Unarchive + Download + Delete + + +@code +{ + [Parameter] + public IFileAccess FileAccess { get; set; } + + // Data + + private List SelectedFiles { get; set; } = new(); + private List Objects { get; set; } = new(); + private string CurrentPath = ""; + + // Search + private string SearchValue = ""; + + private string Search + { + get { return SearchValue; } + set + { + SearchValue = value; + InvokeAsync(StateHasChanged); + } + } + + // States + private bool Loading = false; + + // States - Editor + private FileManagerObject? Editing = null; + private string InitialEditorData = ""; + private string Language = "plaintext"; + + // States - File Upload + private bool Uploading = false; + private int Percent = 0; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await RefreshActive(); + } + } + + private async Task RefreshActive() + { + Loading = true; + await InvokeAsync(StateHasChanged); + + await Refresh(false); + + Loading = false; + await InvokeAsync(StateHasChanged); + } + + private async Task Refresh(bool rerender = true) + { + SelectedFiles.Clear(); + Objects.Clear(); + CurrentPath = await FileAccess.GetCurrentPath(); + + var data = await FileAccess.GetDirectoryContent(); + Objects = data.ToList(); + + if (rerender) + await InvokeAsync(StateHasChanged); + } + + private async Task CdPath(string path) + { + await FileAccess.ChangeDirectory(path); + await RefreshActive(); + } + + private async Task SetPath(string path) + { + await FileAccess.SetDirectory(path); + await RefreshActive(); + } + + private async Task OnContextMenuClick(ItemClickEventArgs e) + { + var data = e.Data as FileManagerObject; + + if (data == null) + return; + + switch (e.MenuItem.Id) + { + case "delete": + await FileAccess.Delete(data); + break; + case "download": + if (data.IsFile) + { + // First we try to download via stream + + try + { + var url = await FileAccess.GetDownloadUrl(data); + NavigationManager.NavigateTo(url, true); + await ToastService.Info(TranslationService.Translate("Starting download")); + } + catch (Exception exception) + { + await ToastService.Error(TranslationService.Translate("Error starting download")); + Logger.Error("Error downloading file"); + Logger.Error(exception.Message); + } + } + break; + case "rename": + var newName = await AlertService.Text(TranslationService.Translate("Rename"), TranslationService.Translate("Enter a new name"), data.Name); + var path = await FileAccess.GetCurrentPath(); + await FileAccess.Move(data, path + "/" + newName); + break; + } + + await Refresh(false); + } + + private async Task OnFileToggle(ChangeEventArgs obj, FileManagerObject o) + { + if ((bool)obj.Value) + { + if (SelectedFiles.Contains(o)) + return; + + SelectedFiles.Add(o); + await InvokeAsync(StateHasChanged); + } + else + { + if (!SelectedFiles.Contains(o)) + return; + + SelectedFiles.Remove(o); + await InvokeAsync(StateHasChanged); + } + } + + private async Task OnAllFileToggle(ChangeEventArgs obj) + { + if ((bool)obj.Value) + { + foreach (var o in Objects) + { + if (SelectedFiles.Contains(o)) + continue; + + SelectedFiles.Add(o); + } + + await InvokeAsync(StateHasChanged); + } + else + { + foreach (var o in Objects) + { + if (!SelectedFiles.Contains(o)) + continue; + + SelectedFiles.Remove(o); + } + + + await InvokeAsync(StateHasChanged); + } + } + + private async Task CreateFolder() + { + var name = await AlertService.Text(TranslationService.Translate("Create a new folder"), TranslationService.Translate("Enter a name"), ""); + + if (string.IsNullOrEmpty(name)) + return; + + await FileAccess.CreateDirectory(name); + await Refresh(); + } + + private async void SaveFile(string data) + { + if (Editing == null) + return; + + await FileAccess.WriteFile(Editing, data); + Editing = null; + await Refresh(); + } + + private async Task OpenFile(FileManagerObject o) + { + Editing = o; + Loading = true; + await InvokeAsync(StateHasChanged); + + InitialEditorData = await FileAccess.ReadFile(Editing); + Language = MonacoTypeHelper.GetEditorType(Editing.Name); + + Loading = false; + await InvokeAsync(StateHasChanged); + } + + private async void CloseFile() + { + Editing = null; + await InvokeAsync(StateHasChanged); + } + + private async void Launch() + { + NavigationManager.NavigateTo(await FileAccess.GetLaunchUrl()); + } + + private async Task OnFileChanged(InputFileChangeEventArgs arg) + { + Uploading = true; + await InvokeAsync(StateHasChanged); + + foreach (var browserFile in arg.GetMultipleFiles()) + { + if (browserFile.Size < 1024 * 1024 * 100) + { + Percent = 0; + + try + { + await FileAccess.UploadFile( + browserFile.Name, + browserFile.OpenReadStream(1024 * 1024 * 100), + async (i) => + { + Percent = i; + + Task.Run(() => { InvokeAsync(StateHasChanged); }); + }); + + await Refresh(); + } + catch (Exception e) + { + await ToastService.Error(TranslationService.Translate("An unknown error occured while uploading a file")); + Logger.Error("Error uploading file"); + Logger.Error(e); + } + } + else + { + await ToastService.Error(TranslationService.Translate("The uploaded file should not be bigger than 100MB")); + } + } + + Uploading = false; + await InvokeAsync(StateHasChanged); + + await ToastService.Success(TranslationService.Translate("File upload complete")); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerBackups.razor b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor new file mode 100644 index 0000000..84cf46e --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerBackups.razor @@ -0,0 +1,306 @@ +@using PteroConsole.NET +@using Moonlight.App.Services +@using Task = System.Threading.Tasks.Task +@using Moonlight.App.Helpers +@using Logging.Net +@using BlazorContextMenu +@using Moonlight.App.Database.Entities +@using Moonlight.App.Services.Interop + +@inject ServerService ServerService +@inject NavigationManager NavigationManager +@inject AlertService AlertService +@inject ToastService ToastService +@inject ClipboardService ClipboardService +@inject MessageService MessageService +@inject SmartTranslateService SmartTranslateService + +@implements IDisposable + + + @if (3 > AllBackups.Length) + { + + } + else + { + + } +
+ + + + + @foreach (var backup in AllBackups) + { + + + + + + + + } + + +
+ + + + Restore + + + Copy url + + + Download + + + Delete + + +
+ +@code +{ + [CascadingParameter] + public PteroConsole Console { get; set; } + + [CascadingParameter] + public Server CurrentServer { get; set; } + + private ServerBackup[]? AllBackups; + + private LazyLoader LazyLoader; + + protected override void OnInitialized() + { + MessageService.Subscribe("wings.backups.create", this, async (backup) => + { + if (AllBackups == null) + return; + + if (AllBackups.Any(x => x.Id == backup.Id)) + { + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(1)); + await ToastService.Success(SmartTranslateService.Translate("Backup successfully created")); + await LazyLoader.Reload(); + }); + } + }); + + MessageService.Subscribe("wings.backups.createfailed", this, async (backup) => + { + if (AllBackups == null) + return; + + if (AllBackups.Any(x => x.Id == backup.Id)) + { + Task.Run(async () => + { + await ToastService.Error(SmartTranslateService.Translate("Backup creation failed")); + await LazyLoader.Reload(); + }); + } + }); + + MessageService.Subscribe("wings.backups.delete", this, async (backup) => + { + if (AllBackups == null) + return; + + if (AllBackups.Any(x => x.Id == backup.Id)) + { + Task.Run(async () => + { + await ToastService.Success(SmartTranslateService.Translate("Backup successfully deleted")); + await LazyLoader.Reload(); + }); + } + }); + + MessageService.Subscribe("wings.backups.restore", this, async (backup) => + { + if (AllBackups == null) + return; + + if (AllBackups.Any(x => x.Id == backup.Id)) + { + Task.Run(async () => { await ToastService.Success(SmartTranslateService.Translate("Backup successfully restored")); }); + } + }); + } + + private async Task Refresh(LazyLoader lazyLoader) + { + await InvokeAsync(StateHasChanged); + + await lazyLoader.SetText(SmartTranslateService.Translate("Loading backups")); + AllBackups = await ServerService.GetBackups(CurrentServer, true); + + await InvokeAsync(StateHasChanged); + } + + private async Task Download(ServerBackup serverBackup) + { + try + { + var url = await ServerService.DownloadBackup(CurrentServer, serverBackup); + + NavigationManager.NavigateTo(url); + await ToastService.Success(SmartTranslateService.Translate("Backup download successfully started")); + } + catch (Exception e) + { + Logger.Warn("Error starting backup download"); + Logger.Warn(e); + await ToastService.Error(SmartTranslateService.Translate("Backup download failed")); + } + } + + private async Task CopyUrl(ServerBackup serverBackup) + { + try + { + var url = await ServerService.DownloadBackup(CurrentServer, serverBackup); + + await ClipboardService.CopyToClipboard(url); + await AlertService.Success( + SmartTranslateService.Translate("Success"), + SmartTranslateService.Translate("Backup URL successfully copied to your clipboard")); + } + catch (Exception e) + { + Logger.Warn("Error copying backup url"); + Logger.Warn(e); + await ToastService.Error(SmartTranslateService.Translate("An unknown error occured while generating backup url")); + } + } + + private async Task Delete(ServerBackup serverBackup) + { + try + { + await ToastService.Info(SmartTranslateService.Translate("Backup deletion started")); + await ServerService.DeleteBackup(CurrentServer, serverBackup); + } + catch (Exception e) + { + Logger.Warn("Error deleting backup"); + Logger.Warn(e); + await ToastService.Error(SmartTranslateService.Translate("An unknown error occured while starting backup deletion")); + } + } + + private async Task Restore(ServerBackup serverBackup) + { + try + { + await ServerService.RestoreBackup(CurrentServer, serverBackup); + + await ToastService.Info(SmartTranslateService.Translate("Backup restore started")); + } + catch (Exception e) + { + Logger.Warn("Error restoring backup"); + Logger.Warn(e); + await ToastService.Error(SmartTranslateService.Translate("An unknown error occured while restoring a backup")); + } + } + + private async Task Create() + { + try + { + await ToastService.Info(SmartTranslateService.Translate("Started backup creation")); + var backup = await ServerService.CreateBackup(CurrentServer); + + /* + + // Modify the backup list so no reload needed. Also the create event will work + var list = AllBackups!.ToList(); + list.Add(backup); + AllBackups = list.ToArray(); + + */ + + await LazyLoader.Reload(); + } + catch (Exception e) + { + Logger.Warn("Error creating backup"); + Logger.Warn(e); + await ToastService.Error(SmartTranslateService.Translate("An unknown error has occured while creating a backup")); + } + } + + private async Task OnContextMenuClick(ItemClickEventArgs args) + { + var backup = (ServerBackup)args.Data; + + switch (args.MenuItem.Id) + { + case "delete": + await Delete(backup); + break; + case "copyurl": + await CopyUrl(backup); + break; + case "restore": + await Restore(backup); + break; + case "download": + await Download(backup); + break; + } + + await Refresh(LazyLoader); + } + + public void Dispose() + { + MessageService.Unsubscribe("wings.backups.create", this); + MessageService.Unsubscribe("wings.backups.createfailed", this); + MessageService.Unsubscribe("wings.backups.restore", this); + MessageService.Unsubscribe("wings.backups.delete", this); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerConsole.razor b/Moonlight/Shared/Components/ServerControl/ServerConsole.razor new file mode 100644 index 0000000..4beda00 --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerConsole.razor @@ -0,0 +1,97 @@ +@using PteroConsole.NET +@using PteroConsole.NET.Enums +@using Task = System.Threading.Tasks.Task +@using Moonlight.App.Helpers +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Logging.Net +@using Moonlight.App.Database.Entities +@using Moonlight.App.Services.Interop +@using Moonlight.Shared.Components.Xterm + +@implements IDisposable + +@inject ClipboardService ClipboardService +@inject AlertService AlertService +@inject SmartTranslateService TranslationService + +
+ + +
+
+ + + + +
+
+
+ +@code +{ + [CascadingParameter] + public PteroConsole Console { get; set; } + + [CascadingParameter] + public Server CurrentServer { get; set; } + + private Terminal? Terminal; + + private string CommandInput = ""; + + protected override void OnInitialized() + { + Console.OnMessage += OnMessage; + } + + private async void OnMessage(object? sender, string e) + { + if (Terminal != null) + { + var s = e; + + s = s.Replace("Pterodactyl Daemon", "Moonlight Daemon"); + s = s.Replace("Checking server disk space usage, this could take a few seconds...", TranslationService.Translate("Checking disk space")); + s = s.Replace("Updating process configuration files...", TranslationService.Translate("Updating config files")); + s = s.Replace("Ensuring file permissions are set correctly, this could take a few seconds...", TranslationService.Translate("Checking file permissions")); + s = s.Replace("Pulling Docker container image, this could take a few minutes to complete...", TranslationService.Translate("Downloading server image")); + s = s.Replace("Finished pulling Docker container image", TranslationService.Translate("Downloaded server image")); + s = s.Replace("container@pterodactyl~", "server@moonlight >"); + + await Terminal.WriteLine(s); + } + } + + public void Dispose() + { + Console.OnMessage -= OnMessage; + Terminal!.Dispose(); + } + + private async Task SendCommand() + { + CommandInput = CommandInput.Replace("\n", ""); + await Console.EnterCommand(CommandInput); + CommandInput = ""; + StateHasChanged(); + } + + private void RunOnFirstRender() + { + lock (Console.MessageCache) + { + foreach (var message in Console.MessageCache.TakeLast(30)) + { + OnMessage(null, message); + } + } + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerFiles.razor b/Moonlight/Shared/Components/ServerControl/ServerFiles.razor new file mode 100644 index 0000000..3fa42fe --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerFiles.razor @@ -0,0 +1,27 @@ +@using Moonlight.Shared.Components.FileManagerPartials +@using Moonlight.App.Services +@using Moonlight.App.Helpers +@using Moonlight.App.Models.Files +@using Moonlight.App.Services.Sessions +@using Moonlight.App.Database.Entities + +@inject ServerService ServerService +@inject IdentityService IdentityService + + + + + +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } + + private IFileAccess FileAccess; + + private async Task Load(LazyLoader arg) + { + var user = await IdentityService.Get(); // User for launch url + FileAccess = await ServerService.CreateFileAccess(CurrentServer, user); + } +} diff --git a/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor new file mode 100644 index 0000000..927cfee --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerNavigation.razor @@ -0,0 +1,208 @@ +@using PteroConsole.NET +@using PteroConsole.NET.Enums +@using Task = System.Threading.Tasks.Task +@using Moonlight.App.Services +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers + +@inject SmartTranslateService TranslationService + +
+
+
+
+
+
+
+
+ +
+
+
+
@(CurrentServer.Name)
+
@(CurrentServer.Cpu <= 100 ? Math.Round(CurrentServer.Cpu / 100f, 2) + $" {TranslationService.Translate("Core")}" : Math.Round(CurrentServer.Cpu / 100f, 2) + $" {TranslationService.Translate("Cores")}") / @(Math.Round(CurrentServer.Memory / 1024f, 2)) GB @(TranslationService.Translate("Memory")) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB @(TranslationService.Translate("Disk")) / @(CurrentServer.Node.Name) - @(CurrentServer.Image.Name)
+
+
+
+
+
+ + + @if (Console.ServerState == ServerState.Stopping) + { + + } + else + { + + } +
+
+
+
+
+
+
+
+
+
+
+ Shared IP: + @($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation.Port}") +
+
+ Server ID: + @(CurrentServer.Id) +
+
+ Status: + + @switch (Console.ServerState) + { + case ServerState.Offline: + Offline + break; + + case ServerState.Starting: + Starting + (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + break; + + case ServerState.Stopping: + Stopping + (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + break; + + case ServerState.Running: + Online + (@(Formatter.FormatUptime(Console.ServerResource.Uptime))) + break; + } + +
+
+ Cpu: + @(Math.Round(Console.ServerResource.CpuAbsolute, 2))% +
+
+ Memory: + @(Formatter.FormatSize(Console.ServerResource.MemoryBytes)) / @(Formatter.FormatSize(Console.ServerResource.MemoryLimitBytes)) +
+
+ Disk: + @(Formatter.FormatSize(Console.ServerResource.DiskBytes)) / @(Math.Round(CurrentServer.Disk / 1024f, 2)) GB +
+
+
+
+ +
+
+ +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } + + [CascadingParameter] + public PteroConsole Console { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + [Parameter] + public int Index { get; set; } = 0; + + //TODO: NodeIpService which loads and caches raw ips for nodes (maybe) + + protected override void OnInitialized() + { + Console.OnServerStateUpdated += async (sender, state) => { await InvokeAsync(StateHasChanged); }; + Console.OnServerResourceUpdated += async (sender, x) => { await InvokeAsync(StateHasChanged); }; + } + + #region Power Actions + + private async Task Start() + { + await Console.SetPowerState("start"); + } + + private async Task Stop() + { + await Console.SetPowerState("stop"); + } + + private async Task Kill() + { + await Console.SetPowerState("kill"); + } + + private async Task Restart() + { + await Console.SetPowerState("restart"); + } + + #endregion +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerNetwork.razor b/Moonlight/Shared/Components/ServerControl/ServerNetwork.razor new file mode 100644 index 0000000..aa66b70 --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerNetwork.razor @@ -0,0 +1,41 @@ +@using Moonlight.App.Repositories +@using Moonlight.Shared.Components.Partials +@using Task = System.Threading.Tasks.Task +@using Logging.Net +@using Moonlight.App.Database.Entities + +@inject NodeRepository NodeRepository + +@foreach (var allocation in CurrentServer.Allocations) +{ +
+
+ +
+
+ + @(CurrentServer.Node.Fqdn):@(allocation.Port) + +
+
+ @if (allocation.Id == CurrentServer.MainAllocation.Id) + { + + } + else + { + + } +
+
+} + +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerPlugins.razor b/Moonlight/Shared/Components/ServerControl/ServerPlugins.razor new file mode 100644 index 0000000..3168bf9 --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerPlugins.razor @@ -0,0 +1,10 @@ +
+
+
+

Plugins

+
+ This feature is currently not available +
+
+
+
\ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/ServerSettings.razor b/Moonlight/Shared/Components/ServerControl/ServerSettings.razor new file mode 100644 index 0000000..77457bc --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/ServerSettings.razor @@ -0,0 +1,51 @@ +@using PteroConsole.NET +@using Moonlight.App.Database.Entities +@using Moonlight.Shared.Components.ServerControl.Settings + +
+ @if (Tags.Contains("paperversion")) + { + + } + + @if (Tags.Contains("pythonversion")) + { + + } + +@{ + /* + * @if (Tags.Contains("pythonfile")) + { + + } + + @if (Tags.Contains("javascriptfile")) + { + + } + */ +} + + @if (Tags.Contains("javascriptversion")) + { + + } + + @if (Tags.Contains("join2start")) + { + + } +
+ +@code +{ + [CascadingParameter] + public PteroConsole Console { get; set; } + + [CascadingParameter] + public Server CurrentServer { get; set; } + + [CascadingParameter] + public string[] Tags { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/Settings/JavascriptVersionSetting.razor b/Moonlight/Shared/Components/ServerControl/Settings/JavascriptVersionSetting.razor new file mode 100644 index 0000000..d456de2 --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/Settings/JavascriptVersionSetting.razor @@ -0,0 +1,83 @@ +@using Moonlight.App.Services +@using Moonlight.App.Helpers +@using Moonlight.App.Repositories +@using Moonlight.App.Repositories.Servers +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Database.Entities + +@inject ServerRepository ServerRepository +@inject ImageRepository ImageRepository +@inject SmartTranslateService TranslationService + +
+
+ + + + + +
+
+ +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } + + private string[] Images; + private string Image; + + private LazyLoader LazyLoader; + + private async Task Load(LazyLoader lazyLoader) + { + //TODO: Check if this is a redundant call + var serverImage = ImageRepository + .Get() + .Include(x => x.DockerImages) + .First(x => x.Id == CurrentServer.Image.Id); + + Image = ParseHelper.FirstPartStartingWithNumber(serverImage.DockerImages.First(x => x.Id == CurrentServer.DockerImageIndex).Name); + + var res = new List(); + foreach (var image in serverImage.DockerImages) + { + res.Add(ParseHelper.FirstPartStartingWithNumber(image.Name)); + } + Images = res.ToArray(); + + await InvokeAsync(StateHasChanged); + } + + private async Task Save() + { + var serverImage = ImageRepository + .Get() + .Include(x => x.DockerImages) + .First(x => x.Id == CurrentServer.Image.Id); + + var allImages = serverImage.DockerImages; + var imageToUse = allImages.First(x => x.Name.EndsWith(Image)); + CurrentServer.DockerImageIndex = allImages.IndexOf(imageToUse); + + ServerRepository.Update(CurrentServer); + + await LazyLoader.Reload(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor b/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor new file mode 100644 index 0000000..bf02355 --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/Settings/Join2StartSetting.razor @@ -0,0 +1,55 @@ +@using Moonlight.App.Services +@using Task = System.Threading.Tasks.Task +@using Moonlight.Shared.Components.Partials +@using Moonlight.App.Helpers +@using Moonlight.App.Repositories +@using Moonlight.App.Repositories.Servers +@using Logging.Net +@using Moonlight.App.Database.Entities + +@inject ServerRepository ServerRepository +@inject SmartTranslateService TranslationService + +
+
+ +
+ + +
+ +
+
+
+ +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } + + private bool Value; + + private LazyLoader Loader; + + private async Task Load(LazyLoader lazyLoader) + { + Value = CurrentServer.Variables.First(x => x.Key == "J2S").Value == "1"; + + await InvokeAsync(StateHasChanged); + } + + private async Task Save() + { + CurrentServer.Variables.First(x => x.Key == "J2S").Value = Value ? "1" : "0"; + + ServerRepository.Update(CurrentServer); + + await Loader.Reload(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/Settings/PaperVersionSetting.razor b/Moonlight/Shared/Components/ServerControl/Settings/PaperVersionSetting.razor new file mode 100644 index 0000000..c8611ba --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/Settings/PaperVersionSetting.razor @@ -0,0 +1,182 @@ +@using Moonlight.App.Services +@using Moonlight.Shared.Components.Partials +@using Task = System.Threading.Tasks.Task +@using Logging.Net +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Database.Entities +@using Moonlight.App.Repositories +@using Moonlight.App.Repositories.Servers + +@inject ServerService ServerService +@inject ServerRepository ServerRepository +@inject ImageRepository ImageRepository +@inject PaperService PaperService +@inject SmartTranslateService TranslationService + +
+
+ + + + + + + +
+
+ +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } + + private string[] Versions; + private string Version; + + private string[] Builds; + private string Build; + + // Form + + private string InputVersion + { + get { return Version; } + set + { + Version = value; + RefreshBuilds(); + Build = Builds.First(); + InvokeAsync(StateHasChanged); + } + } + + private string InputBuild + { + get { return Build; } + set { Build = value; } + } + + private async Task RefreshVersions() + { + Versions = (await PaperService.GetVersions()).Reverse().ToArray(); + } + + private async Task RefreshBuilds() + { + Builds = (await PaperService.GetBuilds(Version)).Reverse().ToArray(); + } + + private async Task Load(LazyLoader lazyLoader) + { + var vars = CurrentServer.Variables; + + await RefreshVersions(); + + Version = vars.First(x => x.Key == "MINECRAFT_VERSION").Value; + Build = vars.First(x => x.Key == "BUILD_NUMBER").Value; + + if (string.IsNullOrEmpty(Version)) + Version = "latest"; + + if (string.IsNullOrEmpty(Build)) + Version = "latest"; + + if (Version == "latest") // Live migration + { + Version = Versions.First(); + + CurrentServer.Variables.First(x => x.Key == "MINECRAFT_VERSION").Value = Version; + ServerRepository.Update(CurrentServer); + } + + await RefreshBuilds(); + + if (Build == "latest") // Live migration + { + Build = Builds.First(); + + CurrentServer.Variables.First(x => x.Key == "BUILD_NUMBER").Value = Build; + ServerRepository.Update(CurrentServer); + } + + await InvokeAsync(StateHasChanged); + } + + private async Task Save() + { + CurrentServer.Variables.First(x => x.Key == "MINECRAFT_VERSION").Value = Version; + CurrentServer.Variables.First(x => x.Key == "BUILD_NUMBER").Value = Build; + + ServerRepository.Update(CurrentServer); + + var versionWithoutPre = Version.Split("-")[0]; + + if (versionWithoutPre.Count(x => x == "."[0]) == 1) + versionWithoutPre += ".0"; + + var version = int.Parse(versionWithoutPre.Replace(".", "")); + + var serverImage = ImageRepository + .Get() + .Include(x => x.DockerImages) + .First(x => x.Id == CurrentServer.Image.Id); + + var dockerImages = serverImage.DockerImages; + + var dockerImageToUpdate = dockerImages.Last(); + + if (version < 1130) + { + dockerImageToUpdate = dockerImages.First(x => x.Name.Contains("8")); + } + + if (version >= 1130) + { + dockerImageToUpdate = dockerImages.First(x => x.Name.Contains("11")); + } + + if (version >= 1170) + { + dockerImageToUpdate = dockerImages.First(x => x.Name.Contains("16")); + } + + if (version >= 1190) + { + dockerImageToUpdate = dockerImages.First(x => x.Name.Contains("17")); + } + + CurrentServer.DockerImageIndex = dockerImages.IndexOf(dockerImageToUpdate); + + ServerRepository.Update(CurrentServer); + + await ServerService.Reinstall(CurrentServer); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/ServerControl/Settings/PythonVersionSetting.razor b/Moonlight/Shared/Components/ServerControl/Settings/PythonVersionSetting.razor new file mode 100644 index 0000000..2cf8374 --- /dev/null +++ b/Moonlight/Shared/Components/ServerControl/Settings/PythonVersionSetting.razor @@ -0,0 +1,84 @@ +@using Moonlight.App.Services +@using Task = System.Threading.Tasks.Task +@using Moonlight.Shared.Components.Partials +@using Moonlight.App.Helpers +@using Moonlight.App.Repositories +@using Moonlight.App.Repositories.Servers +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Database.Entities + +@inject ServerRepository ServerRepository +@inject ImageRepository ImageRepository +@inject SmartTranslateService TranslationService + +
+
+ + + + + +
+
+ +@code +{ + [CascadingParameter] + public Server CurrentServer { get; set; } + + private string[] Images; + private string Image; + + private LazyLoader LazyLoader; + + private async Task Load(LazyLoader lazyLoader) + { + var serverImage = ImageRepository + .Get() + .Include(x => x.DockerImages) + .First(x => x.Id == CurrentServer.Image.Id); + + Image = ParseHelper.FirstPartStartingWithNumber(serverImage.DockerImages.First(x => x.Id == CurrentServer.DockerImageIndex).Name); + + var res = new List(); + foreach (var image in serverImage.DockerImages) + { + res.Add(ParseHelper.FirstPartStartingWithNumber(image.Name)); + } + Images = res.ToArray(); + + await InvokeAsync(StateHasChanged); + } + + private async Task Save() + { + var serverImage = ImageRepository + .Get() + .Include(x => x.DockerImages) + .First(x => x.Id == CurrentServer.Image.Id); + + var allImages = serverImage.DockerImages; + var imageToUse = allImages.First(x => x.Name.EndsWith(Image)); + CurrentServer.DockerImageIndex = allImages.IndexOf(imageToUse); + + ServerRepository.Update(CurrentServer); + + await LazyLoader.Reload(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Xterm/Terminal.razor b/Moonlight/Shared/Components/Xterm/Terminal.razor new file mode 100644 index 0000000..6a1c693 --- /dev/null +++ b/Moonlight/Shared/Components/Xterm/Terminal.razor @@ -0,0 +1,55 @@ +@using XtermBlazor + +@implements IDisposable + + + + +@code +{ + private Xterm Xterm; + + [Parameter] + public Action RunOnFirstRender { get; set; } + + private TerminalOptions TerminalOptions = new TerminalOptions + { + CursorBlink = false, + CursorStyle = CursorStyle.Underline, + CursorWidth = 1, + DisableStdin = true, + FontFamily = "monospace" + }; + + public async Task WriteLine(string message) + { + try + { + await Xterm.WriteLine(message); + } + catch (Exception) + { + } + } + + public async void Dispose() + { + await Xterm.DisposeAsync(); + } + + private async void OnFirstRender() + { + try + { + await Xterm.InvokeAddonFunctionVoidAsync("xterm-addon-fit", "fit"); + RunOnFirstRender.Invoke(); + } + catch (Exception) + { + } + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Nodes/Edit.razor b/Moonlight/Shared/Views/Admin/Nodes/Edit.razor index 81f932f..b958349 100644 --- a/Moonlight/Shared/Views/Admin/Nodes/Edit.razor +++ b/Moonlight/Shared/Views/Admin/Nodes/Edit.razor @@ -2,13 +2,15 @@ @using Moonlight.App.Repositories @using Moonlight.App.Database.Entities @using Moonlight.App.Services +@using Microsoft.EntityFrameworkCore +@using BlazorTable @inject NodeRepository NodeRepository @inject SmartTranslateService SmartTranslateService @inject NavigationManager NavigationManager - + @if (Node == null) {
@@ -17,75 +19,112 @@ } else { -
-
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-