88 Commits
v1b3 ... v1b9

Author SHA1 Message Date
Marcel Baumgartner
efed2a6a5c Merge pull request #188 from Moonlight-Panel/ImproveLogging
Improved logging. Added better error handling for mysql database backup
2023-06-23 00:51:32 +02:00
Marcel Baumgartner
6f138c2c51 Improved logging. Added better error handling for mysql database backup 2023-06-23 00:51:09 +02:00
Marcel Baumgartner
6c43e6a533 Merge pull request #187 from Moonlight-Panel/AddMalwareScan
Added maleware scan
2023-06-22 20:36:58 +02:00
Marcel Baumgartner
0379afd831 Added maleware scan 2023-06-22 20:36:33 +02:00
Marcel Baumgartner
b8bfdb7729 Merge pull request #185 from Moonlight-Panel/SwitchToSerilog
Switched to serilog as logging system
2023-06-21 19:15:53 +02:00
Marcel Baumgartner
72f60ec97c Switched to serilog as logging system 2023-06-21 19:15:30 +02:00
Marcel Baumgartner
1b40250750 Revert "Merge branch 'DiscordBot' into main"
This reverts commit cdf2988cb6, reversing
changes made to 76415b4a0a.
2023-06-20 20:59:49 +02:00
Ole Sziedat
cdf2988cb6 Merge branch 'DiscordBot' into main 2023-06-20 20:52:46 +02:00
Marcel Baumgartner
76415b4a0a Merge pull request #182 from Moonlight-Panel/ImproveFileEditor
Added STRG+S support for editor and better saving
2023-06-20 20:38:17 +02:00
Marcel Baumgartner
52d00baf2b Added STRG+S support for editor and better saving 2023-06-20 20:37:57 +02:00
Marcel Baumgartner
cd41db510e Merge pull request #181 from Moonlight-Panel/AddServerAllocationEditor
Added a basic allocation editor for servers
2023-06-20 19:21:41 +02:00
Marcel Baumgartner
1afd4e8b92 Added a basic allocation editor for servers 2023-06-20 19:18:18 +02:00
Marcel Baumgartner
ef37088c7a Merge pull request #180 from Moonlight-Panel/ImproveUserInterface
Fixed ui bugs, improved plugins page, added new 404 component
2023-06-20 02:56:15 +02:00
Marcel Baumgartner
e71495533b Fixed ui bugs, improved plugins page, added new 404 component 2023-06-20 02:55:50 +02:00
Marcel Baumgartner
e2a6d70f6a Update README.md 2023-06-18 06:23:17 +02:00
Marcel Baumgartner
e95853b09c Update README.md 2023-06-18 03:25:23 +02:00
Marcel Baumgartner
0537ca115e Remove assets from github language statistics 2023-06-18 02:50:08 +02:00
Marcel Baumgartner
432e441972 Change moonlight service to always fetch the changelog 2023-06-18 02:37:07 +02:00
Marcel Baumgartner
1dae5150bd Merge pull request #178 from Moonlight-Panel/RewriteNotificationSystem
Rewrite notification system
2023-06-18 02:32:33 +02:00
Marcel Baumgartner
72c6f636ee Rewritten notification system 2023-06-17 20:47:07 +02:00
Daniel Balk
e54d04277d Merge pull request #177 from Moonlight-Panel/ShowMoonlightAppInSessions
added check for moonlight app for getting the device
2023-06-17 17:52:41 +02:00
Daniel Balk
f559f08e8d added check for moonlight app for getting the device 2023-06-17 17:51:34 +02:00
Daniel Balk
2674fb3fa7 Update Debugging.razor 2023-06-17 14:10:24 +02:00
Marcel Baumgartner
d5d77ae7da Merge pull request #175 from Moonlight-Panel/AddMoonlightSideDnsCheckSsl
Added moonlight side dns check for ssl
2023-06-17 13:39:25 +02:00
Marcel Baumgartner
3ae905038c Added moonlight side dns check for ssl 2023-06-17 13:38:32 +02:00
Marcel Baumgartner
6707d722e0 Merge pull request #174 from Moonlight-Panel/SomeHotfixes
Fixed cleanup, error handling missing and missing gif
2023-06-17 13:00:12 +02:00
Marcel Baumgartner
bc9fecf6d9 Fixed cleanup, error handling missing and missing gif 2023-06-17 12:57:01 +02:00
Marcel Baumgartner
c2cb10f069 Merge pull request #173 from Moonlight-Panel/CleanupHotfix
Fixed cleanup calculation errors
2023-06-17 00:30:16 +02:00
Marcel Baumgartner
cc06d1eb0b Fixed cleanup calculation errors 2023-06-17 00:29:47 +02:00
Marcel Baumgartner
916ff71022 Merge pull request #172 from Moonlight-Panel/FixConsoleIssues
Fixed xterm console issues
2023-06-16 22:57:04 +02:00
Marcel Baumgartner
9e80342e26 Fixed xterm console issues 2023-06-16 22:56:33 +02:00
Marcel Baumgartner
0fb97683bf Merge pull request #171 from Moonlight-Panel/DisableServerDelete
Disabled server delete
2023-06-16 20:30:00 +02:00
Marcel Baumgartner
32415bad54 Merge pull request #170 from Moonlight-Panel/AddModrinthSupport
Add modrinth support
2023-06-16 20:29:45 +02:00
Marcel Baumgartner
d2b0bcc4a3 Disabled server delete 2023-06-16 20:29:23 +02:00
Daniel Balk
2f4f4193d3 Merge pull request #169 from Moonlight-Panel/NotificationDebuggingUi
added simple debugging page
2023-06-16 20:25:39 +02:00
Daniel Balk
7279f05a16 added simple debugging page 2023-06-16 20:24:32 +02:00
Marcel Baumgartner
3962723acb Implemented plugin installer 2023-06-16 20:24:03 +02:00
Marcel Baumgartner
125e72fa58 Switched to new routing for the server manage page 2023-06-16 17:43:48 +02:00
Marcel Baumgartner
880cce060f Added missing server installing db change 2023-06-16 17:33:47 +02:00
Marcel Baumgartner
0e04942111 Merge pull request #168 from Moonlight-Panel/AddServerArchive
Added archive system. Added ml debug menu and related stuff
2023-06-16 17:21:05 +02:00
Marcel Baumgartner
46a88d4638 Added archive system. Added ml debug menu and related stuff 2023-06-16 16:58:58 +02:00
Marcel Baumgartner
c7c39fc511 Merge pull request #167 from Moonlight-Panel/RemoveUselessConsoleLogs
Removed useless console streaming logs
2023-06-13 22:41:46 +02:00
Marcel Baumgartner
3b9bdd1916 Removed useless console streaming logs 2023-06-13 22:35:03 +02:00
Marcel Baumgartner
d267be6d69 Merge pull request #166 from Moonlight-Panel/AddServerFetchBotApi
Added server fetch for single server in bot api
2023-06-13 21:51:20 +02:00
Marcel Baumgartner
18f6a1acdc Added server fetch for single server in bot api 2023-06-13 21:50:33 +02:00
Marcel Baumgartner
2ca41ff18f Merge pull request #165 from Moonlight-Panel/FixSessionListEmailFilter
Fixed session list email filter
2023-06-12 22:12:37 +02:00
Marcel Baumgartner
74c77bc744 Fixed session list email filter 2023-06-12 22:12:05 +02:00
Marcel Baumgartner
1ff8cdd7a9 Merge pull request #164 from Moonlight-Panel/RemovedUnnecessaryReload
Removed unnecessary reload
2023-06-12 00:47:42 +02:00
Marcel Baumgartner
bd320d025a Removed unnecessary reload 2023-06-12 00:47:00 +02:00
Marcel Baumgartner
9a5b004e17 Merge pull request #163 from Moonlight-Panel/AddUserWebsiteDelete
Added webspace delete
2023-06-12 00:10:27 +02:00
Marcel Baumgartner
3aee059860 Added webspace delete 2023-06-12 00:10:03 +02:00
Marcel Baumgartner
3dfa7f66de Merge pull request #162 from Moonlight-Panel/AddUserDomainDelete
Added user domain delete
2023-06-11 22:10:41 +02:00
Marcel Baumgartner
c2949b4773 Added user domain delete 2023-06-11 22:10:28 +02:00
Marcel Baumgartner
c2d0ab4b1b Merge pull request #161 from Moonlight-Panel/FixServerDelete
Fixed server delete
2023-06-11 21:58:56 +02:00
Marcel Baumgartner
de02f0bd74 Fixed server delete 2023-06-11 21:50:45 +02:00
Marcel Baumgartner
e280a95619 Merge pull request #160 from Moonlight-Panel/AddVersioningSystem
Removed old app version copy instruction
2023-06-11 21:11:39 +02:00
Marcel Baumgartner
6f06be9cc6 Removed old app version copy instruction 2023-06-11 21:11:09 +02:00
Marcel Baumgartner
08745a83b4 Merge pull request #159 from Moonlight-Panel/AddVersioningSystem
Add new version and changelog system
2023-06-11 21:06:24 +02:00
Marcel Baumgartner
9a262d1396 Add new version and changelog system 2023-06-11 20:59:20 +02:00
Marcel Baumgartner
0a1b93b8fb Merge pull request #158 from Moonlight-Panel/ImproveStatistics
Added better statistics calculation and active user messurement
2023-06-11 17:57:01 +02:00
Marcel Baumgartner
4b638fc5da Added better statistics calculation and active user messurement 2023-06-11 17:56:45 +02:00
Marcel Baumgartner
d8e34ae891 Merge pull request #157 from Moonlight-Panel/FixSftpWebsitePort
Fixed website sftp port
2023-06-11 16:32:48 +02:00
Marcel Baumgartner
311237e49d Fixed website sftp port 2023-06-11 16:32:28 +02:00
Marcel Baumgartner
6591bbc927 Merge pull request #156 from Moonlight-Panel/AddServerBackgroundImage
Add dynamic background images for servers
2023-06-11 16:28:03 +02:00
Marcel Baumgartner
43c5717d19 Added default background and optimized change methods 2023-06-11 16:26:43 +02:00
Marcel Baumgartner
61d547b2ce Add dynamic background images for servers 2023-06-10 00:00:54 +02:00
Marcel Baumgartner
d7fbe54225 Merge pull request #151 from Moonlight-Panel/AddUptimeCounter
Added uptime service
2023-06-09 15:02:14 +02:00
Marcel Baumgartner
d0004e9fff Added uptime service 2023-06-09 15:01:51 +02:00
Marcel Baumgartner
829596a3e7 Merge pull request #150 from Moonlight-Panel/AddHealthChecks
Add health checks
2023-06-09 14:39:23 +02:00
Marcel Baumgartner
fc319f0f73 Added better error handling and daemon health check 2023-06-09 14:38:30 +02:00
Marcel Baumgartner
bd8ba11410 Merge pull request #149 from Moonlight-Panel/main
Update AddHealthChecks with latest commits
2023-06-09 14:21:28 +02:00
Marcel Baumgartner
0c4fc942b0 Merge pull request #148 from Moonlight-Panel/AddNewDaemonCommunication
Add new daemon communication
2023-06-07 03:34:26 +02:00
Marcel Baumgartner
94b8f07d92 Did some testing. Now able to finish new daemon communication 2023-06-07 03:29:36 +02:00
Marcel Baumgartner
f11eef2734 Merge pull request #147 from Moonlight-Panel/main
Update AddNewDaemonCommunication with latest commits
2023-06-07 02:49:53 +02:00
Marcel Baumgartner
0f8946fe27 Switched to new daemon communication 2023-06-07 02:46:26 +02:00
Marcel Baumgartner
a8cb1392e8 Merge pull request #145 from Moonlight-Panel/ImproveUserExperienceJ2S
Improved user experience for enabling and disabling join2start
2023-06-07 02:39:00 +02:00
Marcel Baumgartner
4241debc3b Improved user experience for enabling and disabling join2start 2023-06-07 02:38:21 +02:00
Marcel Baumgartner
a99959bd2b Merge pull request #144 from Moonlight-Panel/AddDeployNodeOverride
Added smart deploy node override option
2023-06-07 02:23:56 +02:00
Marcel Baumgartner
23644eb93f Added smart deploy node override option 2023-06-07 02:23:30 +02:00
Marcel Baumgartner
f8fcb86ad8 Added base health check and diagnostic system 2023-06-06 22:50:33 +02:00
Marcel Baumgartner
ce0016fa3f Merge pull request #143 from Moonlight-Panel/AddFolderDownloadHandler
Added error handler for folder download
2023-06-05 22:01:40 +02:00
Marcel Baumgartner
15d8f49ce9 Added error handler for folder download 2023-06-05 22:01:21 +02:00
Marcel Baumgartner
98d8e5b755 Merge pull request #142 from Moonlight-Panel/ImproveCpuUsageCalculation
Improved cpu usage calculation
2023-06-05 21:48:15 +02:00
Marcel Baumgartner
bfa1a09aab Improved cpu usage calculation 2023-06-05 21:47:33 +02:00
Marcel Baumgartner
84396c34e6 Merge pull request #140 from Moonlight-Panel/RemoveBundleService
Removed bundle service
2023-06-05 21:34:57 +02:00
Marcel Baumgartner
4fb4a2415b Removed bundle service 2023-06-05 21:34:09 +02:00
0fde9a5005 bot hotfix 2023-05-04 20:40:34 +02:00
Marcel Baumgartner
74541d7f87 Merge pull request #107 from Moonlight-Panel/main
Update to upstream branch
2023-04-29 23:39:46 +02:00
191 changed files with 7400 additions and 2435 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
Moonlight/wwwroot/* linguist-vendored

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ Desktop.ini
storage/ storage/
Moonlight/publish.ps1 Moonlight/publish.ps1
Moonlight/version

View File

@@ -1,5 +1,4 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json; using Newtonsoft.Json;
using RestSharp; using RestSharp;
@@ -14,26 +13,13 @@ public class DaemonApiHelper
Client = new(); Client = new();
} }
private string GetApiUrl(Node node)
{
/* SSL not implemented in moonlight daemon
if(node.Ssl)
return $"https://{node.Fqdn}:{node.MoonlightDaemonPort}/";
else
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";*/
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
}
public async Task<T> Get<T>(Node node, string resource) public async Task<T> Get<T>(Node node, string resource)
{ {
RestRequest request = new(GetApiUrl(node) + resource); var request = await CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json"); request.Method = Method.Get;
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
var response = await Client.GetAsync(request); var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful) if (!response.IsSuccessful)
{ {
@@ -52,4 +38,69 @@ public class DaemonApiHelper
return JsonConvert.DeserializeObject<T>(response.Content!)!; return JsonConvert.DeserializeObject<T>(response.Content!)!;
} }
public async Task Post(Node node, string resource, object body)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Post;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"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)
{
var request = await CreateRequest(node, resource);
request.Method = Method.Delete;
request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new DaemonException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
}
private Task<RestRequest> CreateRequest(Node node, string resource)
{
var url = $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
RestRequest request = new(url + resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
return Task.FromResult(request);
}
} }

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Mount
{
public string Server { get; set; } = "";
public string ServerPath { get; set; } = "";
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Requests;
public class Unmount
{
public string Path { get; set; } = "";
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class Container
{
public string Name { get; set; } = "";
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class ContainerStats
{
public List<Container> Containers { get; set; } = new();
public class Container
{
public string Name { get; set; }
public long Memory { get; set; }
public double Cpu { get; set; }
public long NetworkIn { get; set; }
public long NetworkOut { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuMetrics
{
public string CpuModel { get; set; } = "";
public double CpuUsage { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class CpuStats
{
public double Usage { get; set; }
public int Cores { get; set; }
public string Model { get; set; } = "";
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View File

@@ -1,9 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DiskStats
{
public long FreeBytes { get; set; }
public string DriveFormat { get; set; }
public string Name { get; set; }
public long TotalSize { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class DockerMetrics
{
public Container[] Containers { get; set; } = Array.Empty<Container>();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryMetrics
{
public long Used { get; set; }
public long Total { get; set; }
}

View File

@@ -1,15 +0,0 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class MemoryStats
{
public List<MemoryStick> Sticks { get; set; } = new();
public double Free { get; set; }
public double Used { get; set; }
public double Total { get; set; }
public class MemoryStick
{
public int Size { get; set; }
public string Type { get; set; } = "";
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.ApiClients.Daemon.Resources;
public class SystemMetrics
{
public string OsName { get; set; } = "";
public long Uptime { get; set; }
}

View File

@@ -0,0 +1,58 @@
using Newtonsoft.Json;
using RestSharp;
namespace Moonlight.App.ApiClients.Modrinth;
public class ModrinthApiHelper
{
private readonly RestClient Client;
public ModrinthApiHelper()
{
Client = new();
Client.AddDefaultParameter(
new HeaderParameter("User-Agent", "Moonlight-Panel/Moonlight (admin@endelon-hosting.de)")
);
}
public async Task<T> Get<T>(string resource)
{
var request = CreateRequest(resource);
request.Method = Method.Get;
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
if (response.StatusCode != 0)
{
throw new ModrinthException(
$"An error occured: ({response.StatusCode}) {response.Content}",
(int)response.StatusCode
);
}
else
{
throw new Exception($"An internal error occured: {response.ErrorMessage}");
}
}
return JsonConvert.DeserializeObject<T>(response.Content!)!;
}
private RestRequest CreateRequest(string resource)
{
var url = "https://api.modrinth.com/v2/" + resource;
var request = new RestRequest(url)
{
Timeout = 60 * 15
};
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
return request;
}
}

View File

@@ -0,0 +1,19 @@
namespace Moonlight.App.ApiClients.Modrinth;
public class ModrinthException : Exception
{
public int StatusCode { get; set; }
public ModrinthException()
{
}
public ModrinthException(string message, int statusCode) : base(message)
{
StatusCode = statusCode;
}
public ModrinthException(string message, Exception inner) : base(message, inner)
{
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class Pagination
{
[JsonProperty("hits")] public Project[] Hits { get; set; }
[JsonProperty("offset")] public long Offset { get; set; }
[JsonProperty("limit")] public long Limit { get; set; }
[JsonProperty("total_hits")] public long TotalHits { get; set; }
}

View File

@@ -0,0 +1,48 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class Project
{
[JsonProperty("project_id")] public string ProjectId { get; set; }
[JsonProperty("project_type")] public string ProjectType { get; set; }
[JsonProperty("slug")] public string Slug { get; set; }
[JsonProperty("author")] public string Author { get; set; }
[JsonProperty("title")] public string Title { get; set; }
[JsonProperty("description")] public string Description { get; set; }
[JsonProperty("categories")] public string[] Categories { get; set; }
[JsonProperty("display_categories")] public string[] DisplayCategories { get; set; }
[JsonProperty("versions")] public string[] Versions { get; set; }
[JsonProperty("downloads")] public long Downloads { get; set; }
[JsonProperty("follows")] public long Follows { get; set; }
[JsonProperty("icon_url")] public string IconUrl { get; set; }
[JsonProperty("date_created")] public DateTimeOffset DateCreated { get; set; }
[JsonProperty("date_modified")] public DateTimeOffset DateModified { get; set; }
[JsonProperty("latest_version")] public string LatestVersion { get; set; }
[JsonProperty("license")] public string License { get; set; }
[JsonProperty("client_side")] public string ClientSide { get; set; }
[JsonProperty("server_side")] public string ServerSide { get; set; }
[JsonProperty("gallery")] public Uri[] Gallery { get; set; }
[JsonProperty("featured_gallery")] public Uri FeaturedGallery { get; set; }
[JsonProperty("color")] public long? Color { get; set; }
}

View File

@@ -0,0 +1,57 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class Version
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("version_number")]
public string VersionNumber { get; set; }
[JsonProperty("changelog")]
public string Changelog { get; set; }
[JsonProperty("dependencies")]
public object[] Dependencies { get; set; }
[JsonProperty("game_versions")]
public object[] GameVersions { get; set; }
[JsonProperty("version_type")]
public string VersionType { get; set; }
[JsonProperty("loaders")]
public object[] Loaders { get; set; }
[JsonProperty("featured")]
public bool Featured { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("requested_status")]
public string RequestedStatus { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("project_id")]
public string ProjectId { get; set; }
[JsonProperty("author_id")]
public string AuthorId { get; set; }
[JsonProperty("date_published")]
public DateTime DatePublished { get; set; }
[JsonProperty("downloads")]
public long Downloads { get; set; }
[JsonProperty("changelog_url")]
public object ChangelogUrl { get; set; }
[JsonProperty("files")]
public VersionFile[] Files { get; set; }
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace Moonlight.App.ApiClients.Modrinth.Resources;
public class VersionFile
{
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("primary")]
public bool Primary { get; set; }
[JsonProperty("size")]
public long Size { get; set; }
[JsonProperty("file_type")]
public string FileType { get; set; }
}

View File

@@ -20,4 +20,5 @@ public class Image
public List<DockerImage> DockerImages { get; set; } = new(); public List<DockerImage> DockerImages { get; set; } = new();
public List<ImageVariable> Variables { get; set; } = new(); public List<ImageVariable> Variables { get; set; } = new();
public string TagsJson { get; set; } = ""; public string TagsJson { get; set; } = "";
public string BackgroundImageUrl { get; set; } = "";
} }

View File

@@ -13,6 +13,8 @@ public class Server
public string OverrideStartup { get; set; } = ""; public string OverrideStartup { get; set; } = "";
public bool Installing { get; set; } = false; public bool Installing { get; set; } = false;
public bool Suspended { get; set; } = false; public bool Suspended { get; set; } = false;
public bool IsArchived { get; set; } = false;
public ServerBackup? Archive { get; set; } = null;
public List<ServerVariable> Variables { get; set; } = new(); public List<ServerVariable> Variables { get; set; } = new();
public List<ServerBackup> Backups { get; set; } = new(); public List<ServerBackup> Backups { get; set; } = new();

View File

@@ -43,6 +43,7 @@ public class User
// Date stuff // Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastVisitedAt { get; set; } = DateTime.UtcNow;
// Subscriptions // Subscriptions

View File

@@ -1,6 +1,6 @@
using System.Data.Common; using System.Data.Common;
using Logging.Net;
using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics;
using Moonlight.App.Helpers;
namespace Moonlight.App.Database.Interceptors; namespace Moonlight.App.Database.Interceptors;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddBackgroundImageUrlImage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BackgroundImageUrl",
table: "Images",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BackgroundImageUrl",
table: "Images");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddLastVisitedTimestamp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastVisitedAt",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastVisitedAt",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerAchive : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ArchiveId",
table: "Servers",
type: "int",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsArchived",
table: "Servers",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.CreateIndex(
name: "IX_Servers_ArchiveId",
table: "Servers",
column: "ArchiveId");
migrationBuilder.AddForeignKey(
name: "FK_Servers_ServerBackups_ArchiveId",
table: "Servers",
column: "ArchiveId",
principalTable: "ServerBackups",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Servers_ServerBackups_ArchiveId",
table: "Servers");
migrationBuilder.DropIndex(
name: "IX_Servers_ArchiveId",
table: "Servers");
migrationBuilder.DropColumn(
name: "ArchiveId",
table: "Servers");
migrationBuilder.DropColumn(
name: "IsArchived",
table: "Servers");
}
}
}

View File

@@ -132,6 +132,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Allocations") b.Property<int>("Allocations")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("BackgroundImageUrl")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ConfigFiles") b.Property<string>("ConfigFiles")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -492,6 +496,9 @@ namespace Moonlight.App.Database.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<int?>("ArchiveId")
.HasColumnType("int");
b.Property<int>("Cpu") b.Property<int>("Cpu")
.HasColumnType("int"); .HasColumnType("int");
@@ -507,6 +514,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("Installing") b.Property<bool>("Installing")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsCleanupException") b.Property<bool>("IsCleanupException")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@@ -538,6 +548,8 @@ namespace Moonlight.App.Database.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ArchiveId");
b.HasIndex("ImageId"); b.HasIndex("ImageId");
b.HasIndex("MainAllocationId"); b.HasIndex("MainAllocationId");
@@ -758,6 +770,9 @@ namespace Moonlight.App.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<DateTime>("LastVisitedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Password") b.Property<string>("Password")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -928,6 +943,10 @@ namespace Moonlight.App.Database.Migrations
modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
{ {
b.HasOne("Moonlight.App.Database.Entities.ServerBackup", "Archive")
.WithMany()
.HasForeignKey("ArchiveId");
b.HasOne("Moonlight.App.Database.Entities.Image", "Image") b.HasOne("Moonlight.App.Database.Entities.Image", "Image")
.WithMany() .WithMany()
.HasForeignKey("ImageId") .HasForeignKey("ImageId")
@@ -950,6 +969,8 @@ namespace Moonlight.App.Database.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("Archive");
b.Navigation("Image"); b.Navigation("Image");
b.Navigation("MainAllocation"); b.Navigation("MainAllocation");

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class DaemonHealthCheck : IHealthCheck
{
private readonly Repository<Node> NodeRepository;
private readonly NodeService NodeService;
public DaemonHealthCheck(Repository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var nodes = NodeRepository.Get().ToArray();
var results = new Dictionary<Node, bool>();
var healthCheckData = new Dictionary<string, object>();
foreach (var node in nodes)
{
try
{
await NodeService.GetCpuMetrics(node);
results.Add(node, true);
}
catch (Exception e)
{
results.Add(node, false);
healthCheckData.Add(node.Name, e.ToStringDemystified());
}
}
var offlineNodes = results
.Where(x => !x.Value)
.ToArray();
if (offlineNodes.Length == nodes.Length)
{
return HealthCheckResult.Unhealthy("All node daemons are offline", null, healthCheckData);
}
if (offlineNodes.Length == 0)
{
return HealthCheckResult.Healthy("All node daemons are online");
}
return HealthCheckResult.Degraded($"{offlineNodes.Length} node daemons are offline", null, healthCheckData);
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class DatabaseHealthCheck : IHealthCheck
{
private readonly DataContext DataContext;
public DatabaseHealthCheck(DataContext dataContext)
{
DataContext = dataContext;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = new CancellationToken())
{
try
{
await DataContext.Database.OpenConnectionAsync(cancellationToken);
await DataContext.Database.CloseConnectionAsync();
return HealthCheckResult.Healthy("Database is online");
}
catch (Exception e)
{
return HealthCheckResult.Unhealthy("Database is offline", e);
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Diagnostics.HealthChecks;
public class NodeHealthCheck : IHealthCheck
{
private readonly Repository<Node> NodeRepository;
private readonly NodeService NodeService;
public NodeHealthCheck(Repository<Node> nodeRepository, NodeService nodeService)
{
NodeRepository = nodeRepository;
NodeService = nodeService;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
{
var nodes = NodeRepository.Get().ToArray();
var results = new Dictionary<Node, bool>();
var healthCheckData = new Dictionary<string, object>();
foreach (var node in nodes)
{
try
{
await NodeService.GetStatus(node);
results.Add(node, true);
}
catch (Exception e)
{
results.Add(node, false);
healthCheckData.Add(node.Name, e.ToStringDemystified());
}
}
var offlineNodes = results
.Where(x => !x.Value)
.ToArray();
if (offlineNodes.Length == nodes.Length)
{
return HealthCheckResult.Unhealthy("All nodes are offline", null, healthCheckData);
}
if (offlineNodes.Length == 0)
{
return HealthCheckResult.Healthy("All nodes are online");
}
return HealthCheckResult.Degraded($"{offlineNodes.Length} nodes are offline", null, healthCheckData);
}
}

View File

@@ -1,5 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using Logging.Net; using Moonlight.App.Helpers;
namespace Moonlight.App.Events; namespace Moonlight.App.Events;
@@ -112,4 +112,22 @@ public class EventSystem
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<T> WaitForEvent<T>(string id, object handle, Func<T, bool> filter)
{
var taskCompletionSource = new TaskCompletionSource<T>();
Func<T, Task> action = async data =>
{
if (filter.Invoke(data))
{
taskCompletionSource.SetResult(data);
await Off(id, handle);
}
};
On<T>(id, handle, action);
return taskCompletionSource.Task;
}
} }

View File

@@ -5,6 +5,8 @@ namespace Moonlight.App.Exceptions;
[Serializable] [Serializable]
public class DisplayException : Exception public class DisplayException : Exception
{ {
public bool DoNotTranslate { get; set; } = false;
// //
// For guidelines regarding the creation of new exception types, see // For guidelines regarding the creation of new exception types, see
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
@@ -20,6 +22,11 @@ public class DisplayException : Exception
{ {
} }
public DisplayException(string message, bool doNotTranslate) : base(message)
{
DoNotTranslate = doNotTranslate;
}
public DisplayException(string message, Exception inner) : base(message, inner) public DisplayException(string message, Exception inner) : base(message, inner)
{ {
} }

View File

@@ -0,0 +1,39 @@
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Helpers;
public static class AvgHelper
{
public static StatisticsData[] Calculate(StatisticsData[] data, int splitSize = 40)
{
if (data.Length <= splitSize)
return data;
var result = new List<StatisticsData>();
var i = data.Length / (float)splitSize;
var pc = (int)Math.Round(i);
foreach (var part in data.Chunk(pc))
{
double d = 0;
var res = new StatisticsData();
foreach (var entry in part)
{
d += entry.Value;
}
res.Chart = part.First().Chart;
res.Date = part.First().Date;
if (d == 0)
res.Value = 0;
res.Value = d / part.Length;
result.Add(res);
}
return result.ToArray();
}
}

View File

@@ -1,252 +0,0 @@
using System.Diagnostics;
using Logging.Net;
using Logging.Net.Loggers.SB;
using Moonlight.App.Models.Misc;
using ILogger = Logging.Net.ILogger;
namespace Moonlight.App.Helpers;
public class CacheLogger : ILogger
{
private SBLogger SbLogger = new();
private List<LogEntry> Messages = new();
public LogEntry[] GetMessages()
{
lock (Messages)
{
var result = new LogEntry[Messages.Count];
Messages.CopyTo(result);
return result;
}
}
public void Clear(int messages)
{
lock (Messages)
{
Messages.RemoveRange(0, Math.Min(messages, Messages.Count));
}
}
public void Info(string? s)
{
if (s == null)
return;
lock (Messages)
{
Messages.Add(new()
{
Level = "info",
Message = s
});
}
SbLogger.Info(s);
}
public void Debug(string? s)
{
if (s == null)
return;
lock (Messages)
{
Messages.Add(new()
{
Level = "debug",
Message = s
});
}
SbLogger.Debug(s);
}
public void Warn(string? s)
{
if (s == null)
return;
lock (Messages)
{
Messages.Add(new()
{
Level = "warn",
Message = s
});
}
SbLogger.Warn(s);
}
public void Error(string? s)
{
if (s == null)
return;
lock (Messages)
{
Messages.Add(new()
{
Level = "error",
Message = s
});
}
SbLogger.Error(s);
}
public void Fatal(string? s)
{
if (s == null)
return;
lock (Messages)
{
Messages.Add(new()
{
Level = "fatal",
Message = s
});
}
SbLogger.Fatal(s);
}
public void InfoEx(Exception ex)
{
lock (Messages)
{
Messages.Add(new()
{
Level = "info",
Message = ex.ToStringDemystified()
});
}
SbLogger.InfoEx(ex);
}
public void DebugEx(Exception ex)
{
lock (Messages)
{
Messages.Add(new()
{
Level = "debug",
Message = ex.ToStringDemystified()
});
}
SbLogger.DebugEx(ex);
}
public void WarnEx(Exception ex)
{
lock (Messages)
{
Messages.Add(new()
{
Level = "warn",
Message = ex.ToStringDemystified()
});
}
SbLogger.WarnEx(ex);
}
public void ErrorEx(Exception ex)
{
lock (Messages)
{
Messages.Add(new()
{
Level = "error",
Message = ex.ToStringDemystified()
});
}
SbLogger.ErrorEx(ex);
}
public void FatalEx(Exception ex)
{
lock (Messages)
{
Messages.Add(new()
{
Level = "fatal",
Message = ex.ToStringDemystified()
});
}
SbLogger.FatalEx(ex);
}
public LoggingConfiguration GetErrorConfiguration()
{
return SbLogger.GetErrorConfiguration();
}
public void SetErrorConfiguration(LoggingConfiguration configuration)
{
SbLogger.SetErrorConfiguration(configuration);
}
public LoggingConfiguration GetFatalConfiguration()
{
return SbLogger.GetFatalConfiguration();
}
public void SetFatalConfiguration(LoggingConfiguration configuration)
{
SbLogger.SetFatalConfiguration(configuration);
}
public LoggingConfiguration GetWarnConfiguration()
{
return SbLogger.GetWarnConfiguration();
}
public void SetWarnConfiguration(LoggingConfiguration configuration)
{
SbLogger.SetWarnConfiguration(configuration);
}
public LoggingConfiguration GetInfoConfiguration()
{
return SbLogger.GetInfoConfiguration();
}
public void SetInfoConfiguration(LoggingConfiguration configuration)
{
SbLogger.SetInfoConfiguration(configuration);
}
public LoggingConfiguration GetDebugConfiguration()
{
return SbLogger.GetDebugConfiguration();
}
public void SetDebugConfiguration(LoggingConfiguration configuration)
{
SbLogger.SetDebugConfiguration(configuration);
}
public ILoggingAddition GetAddition()
{
return SbLogger.GetAddition();
}
public void SetAddition(ILoggingAddition addition)
{
SbLogger.SetAddition(addition);
}
public bool LogCallingClass
{
get => SbLogger.LogCallingClass;
set => SbLogger.LogCallingClass = value;
}
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Services; using Moonlight.App.Services;
@@ -82,19 +81,32 @@ public class DatabaseCheckupService
Logger.Info($"Saving it to: {file}"); Logger.Info($"Saving it to: {file}");
Logger.Info("Starting backup..."); Logger.Info("Starting backup...");
var sw = new Stopwatch(); try
sw.Start(); {
var sw = new Stopwatch();
sw.Start();
await using MySqlConnection conn = new MySqlConnection(connectionString); await using MySqlConnection conn = new MySqlConnection(connectionString);
await using MySqlCommand cmd = new MySqlCommand(); await using MySqlCommand cmd = new MySqlCommand();
using MySqlBackup mb = new MySqlBackup(cmd); using MySqlBackup mb = new MySqlBackup(cmd);
cmd.Connection = conn; cmd.Connection = conn;
await conn.OpenAsync(); await conn.OpenAsync();
mb.ExportToFile(file); mb.ExportToFile(file);
await conn.CloseAsync(); await conn.CloseAsync();
sw.Stop(); sw.Stop();
Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s"); Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s");
}
catch (Exception e)
{
Logger.Fatal("-----------------------------------------------");
Logger.Fatal("Unable to create backup for moonlight database");
Logger.Fatal("Moonlight will start anyways in 30 seconds");
Logger.Fatal("-----------------------------------------------");
Logger.Fatal(e);
Thread.Sleep(TimeSpan.FromSeconds(30));
}
} }
} }

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Renci.SshNet;
using Renci.SshNet;
using ConnectionInfo = Renci.SshNet.ConnectionInfo; using ConnectionInfo = Renci.SshNet.ConnectionInfo;
namespace Moonlight.App.Helpers.Files; namespace Moonlight.App.Helpers.Files;

View File

@@ -18,6 +18,18 @@ public static class Formatter
} }
} }
public static string FormatUptime(TimeSpan t)
{
if (t.Days > 0)
{
return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s";
}
else
{
return $"{t.Hours}h {t.Minutes}m {t.Seconds}s";
}
}
private static double Round(this double d, int decimals) private static double Round(this double d, int decimals)
{ {
return Math.Round(d, decimals); return Math.Round(d, decimals);
@@ -116,4 +128,12 @@ public static class Formatter
return (i / (1024D * 1024D)).Round(2) + " GB"; return (i / (1024D * 1024D)).Round(2) + " GB";
} }
} }
public static double BytesToGb(long bytes)
{
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B
double gigabytes = (double)bytes / gbMultiplier;
return gigabytes;
}
} }

View File

@@ -1,6 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Logging.Net;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers;

View File

@@ -0,0 +1,108 @@
using System.Diagnostics;
using System.Reflection;
using Serilog;
namespace Moonlight.App.Helpers;
public static class Logger
{
#region String method calls
public static void Verbose(string message, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Verbose("{Message}", message);
}
public static void Info(string message, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Information("{Message}", message);
}
public static void Debug(string message, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Debug("{Message}", message);
}
public static void Error(string message, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Error("{Message}", message);
}
public static void Warn(string message, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Warning("{Message}", message);
}
public static void Fatal(string message, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Fatal("{Message}", message);
}
#endregion
#region Exception method calls
public static void Verbose(Exception exception, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Verbose(exception, "");
}
public static void Info(Exception exception, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Information(exception, "");
}
public static void Debug(Exception exception, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Debug(exception, "");
}
public static void Error(Exception exception, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Error(exception, "");
}
public static void Warn(Exception exception, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Warning(exception, "");
}
public static void Fatal(Exception exception, string channel = "default")
{
Log.ForContext("SourceContext", GetNameOfCallingClass())
.Fatal(exception, "");
}
#endregion
private static string GetNameOfCallingClass(int skipFrames = 4)
{
string fullName;
Type declaringType;
do
{
MethodBase method = new StackFrame(skipFrames, false).GetMethod();
declaringType = method.DeclaringType;
if (declaringType == null)
{
return method.Name;
}
skipFrames++;
if (declaringType.Name.Contains("<"))
fullName = declaringType.ReflectedType.Name;
else
fullName = declaringType.Name;
}
while (declaringType.Module.Name.Equals("mscorlib.dll", StringComparison.OrdinalIgnoreCase) | fullName.Contains("Logger"));
return fullName;
}
}

View File

@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Logging.Net;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers;

View File

@@ -1,6 +1,4 @@
using Logging.Net; namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers;
public static class ParseHelper public static class ParseHelper
{ {

View File

@@ -1,6 +1,5 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using Logging.Net;
using Moonlight.App.Helpers.Wings.Data; using Moonlight.App.Helpers.Wings.Data;
using Moonlight.App.Helpers.Wings.Enums; using Moonlight.App.Helpers.Wings.Enums;
using Moonlight.App.Helpers.Wings.Events; using Moonlight.App.Helpers.Wings.Events;
@@ -243,6 +242,7 @@ public class WingsConsole : IDisposable
break; break;
} }
} }
catch(JsonReaderException){}
catch (Exception e) catch (Exception e)
{ {
if (!Disconnecting) if (!Disconnecting)

View File

@@ -102,7 +102,7 @@ public class DiscordBotController : Controller
return BadRequest(); return BadRequest();
} }
[HttpGet("{id}/servers/{uuid}")] [HttpGet("{id}/servers/{uuid}/details")]
public async Task<ActionResult<ServerDetails>> GetServerDetails(ulong id, Guid uuid) public async Task<ActionResult<ServerDetails>> GetServerDetails(ulong id, Guid uuid)
{ {
if (!await IsAuth(Request)) if (!await IsAuth(Request))
@@ -124,6 +124,33 @@ public class DiscordBotController : Controller
return await ServerService.GetDetails(server); return await ServerService.GetDetails(server);
} }
[HttpGet("{id}/servers/{uuid}")]
public async Task<ActionResult<ServerDetails>> GetServer(ulong id, Guid uuid)
{
if (!await IsAuth(Request))
return StatusCode(403);
var user = await GetUserFromDiscordId(id);
if (user == null)
return BadRequest();
var server = ServerRepository
.Get()
.Include(x => x.Owner)
.Include(x => x.Image)
.Include(x => x.Node)
.FirstOrDefault(x => x.Owner.Id == user.Id && x.Uuid == uuid);
if (server == null)
return NotFound();
server.Node.Token = "";
server.Node.TokenId = "";
return Ok(server);
}
private Task<User?> GetUserFromDiscordId(ulong discordId) private Task<User?> GetUserFromDiscordId(ulong discordId)
{ {
var user = UserRepository var user = UserRepository

View File

@@ -2,145 +2,166 @@
using System.Text; using System.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification; using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Models.Notifications; using Moonlight.App.Models.Notifications;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Notifications; using Moonlight.App.Services.Notifications;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Moonlight.App.Http.Controllers.Api.Moonlight.Notifications; namespace Moonlight.App.Http.Controllers.Api.Moonlight.Notifications;
public class ListenController : ControllerBase [ApiController]
[Route("api/moonlight/notification/listen")]
public class ListenController : Controller
{ {
internal WebSocket ws; private WebSocket WebSocket;
private bool active = true;
private bool isAuth = false;
private NotificationClient Client; private NotificationClient Client;
private CancellationTokenSource CancellationTokenSource = new();
private User? CurrentUser;
private readonly IdentityService IdentityService;
private readonly NotificationRepository NotificationRepository;
private readonly OneTimeJwtService OneTimeJwtService; private readonly OneTimeJwtService OneTimeJwtService;
private readonly NotificationClientService NotificationClientService;
private readonly NotificationServerService NotificationServerService; private readonly NotificationServerService NotificationServerService;
private readonly Repository<NotificationClient> NotificationClientRepository;
public ListenController(IdentityService identityService, public ListenController(
NotificationRepository notificationRepository,
OneTimeJwtService oneTimeJwtService, OneTimeJwtService oneTimeJwtService,
NotificationClientService notificationClientService, NotificationServerService notificationServerService, Repository<NotificationClient> notificationClientRepository)
NotificationServerService notificationServerService)
{ {
IdentityService = identityService;
NotificationRepository = notificationRepository;
OneTimeJwtService = oneTimeJwtService; OneTimeJwtService = oneTimeJwtService;
NotificationClientService = notificationClientService;
NotificationServerService = notificationServerService; NotificationServerService = notificationServerService;
NotificationClientRepository = notificationClientRepository;
} }
[Route("/api/moonlight/notifications/listen")] [Route("/api/moonlight/notifications/listen")]
public async Task Get() public async Task<ActionResult> Get()
{ {
if (HttpContext.WebSockets.IsWebSocketRequest) if (HttpContext.WebSockets.IsWebSocketRequest)
{ {
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); WebSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
ws = webSocket;
await Echo(); await ProcessWebsocket();
return new EmptyResult();
} }
else else
{ {
HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; return StatusCode(400);
} }
} }
private async Task Echo() private async Task ProcessWebsocket()
{ {
while (active) while (!CancellationTokenSource.Token.IsCancellationRequested && WebSocket.State == WebSocketState.Open)
{ {
byte[] bytes = new byte[1024 * 16]; try
var asg = new ArraySegment<byte>(bytes);
var res = await ws.ReceiveAsync(asg, CancellationToken.None);
var text = Encoding.UTF8.GetString(bytes).Trim('\0');
var obj = JsonConvert.DeserializeObject<BasicWSModel>(text);
if (!string.IsNullOrWhiteSpace(obj.Action))
{ {
await HandleRequest(text, obj.Action); byte[] buffer = new byte[1024 * 16];
} _ = await WebSocket.ReceiveAsync(buffer, CancellationTokenSource.Token);
var text = Encoding.UTF8.GetString(buffer).Trim('\0');
active = ws.State == WebSocketState.Open; var basicWsModel = JsonConvert.DeserializeObject<BasicWSModel>(text) ?? new();
if (!string.IsNullOrWhiteSpace(basicWsModel.Action))
{
await HandleRequest(text, basicWsModel.Action);
}
if (WebSocket.State != WebSocketState.Open)
{
CancellationTokenSource.Cancel();
}
}
catch (WebSocketException e)
{
CancellationTokenSource.Cancel();
}
} }
await NotificationServerService.UnRegisterClient(Client);
} }
private async Task HandleRequest(string text, string action) private async Task HandleRequest(string text, string action)
{ {
if (!isAuth && action == "login") if (CurrentUser == null && action != "login")
await Login(text);
else if (!isAuth)
await ws.SendAsync(Encoding.UTF8.GetBytes("{\"error\": \"Unauthorised\"}"), WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
else switch (action)
{ {
await Send("{\"error\": \"Unauthorised\"}");
}
switch (action)
{
case "login":
await Login(text);
break;
case "received": case "received":
await Received(text); await Received(text);
break; break;
case "read": case "read":
await Read(text); await Read(text);
break; break;
default:
break;
} }
} }
private async Task Send(string text)
{
await WebSocket.SendAsync(
Encoding.UTF8.GetBytes(text),
WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationTokenSource.Token
);
}
private async Task Login(string json) private async Task Login(string json)
{ {
var jwt = JsonConvert.DeserializeObject<Login>(json).token; var loginModel = JsonConvert.DeserializeObject<Login>(json) ?? new();
var dict = await OneTimeJwtService.Validate(jwt); var dict = await OneTimeJwtService.Validate(loginModel.Token);
if (dict == null) if (dict == null)
{ {
string error = "{\"status\":false}"; await Send("{\"status\":false}");
var bytes = Encoding.UTF8.GetBytes(error);
await ws.SendAsync(bytes, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
return; return;
} }
var _clientId = dict["clientId"]; if (!int.TryParse(dict["clientId"], out int clientId))
var clientId = int.Parse(_clientId); {
await Send("{\"status\":false}");
return;
}
var client = NotificationRepository.GetClients().Include(x => x.User).First(x => x.Id == clientId); Client = NotificationClientRepository
.Get()
.Include(x => x.User)
.First(x => x.Id == clientId);
Client = client; CurrentUser = Client.User;
await InitWebsocket();
string success = "{\"status\":true}"; await NotificationServerService.RegisterClient(WebSocket, Client);
var byt = Encoding.UTF8.GetBytes(success);
await ws.SendAsync(byt, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
private async Task InitWebsocket() await Send("{\"status\":true}");
{
NotificationClientService.listenController = this;
NotificationClientService.WebsocketReady(Client);
isAuth = true;
} }
private async Task Received(string json) private async Task Received(string json)
{ {
var id = JsonConvert.DeserializeObject<NotificationById>(json).notification; var id = JsonConvert.DeserializeObject<NotificationById>(json).Notification;
//TODO: Implement ws notification received //TODO: Implement ws notification received
} }
private async Task Read(string json) private async Task Read(string json)
{ {
var id = JsonConvert.DeserializeObject<NotificationById>(json).notification; var model = JsonConvert.DeserializeObject<NotificationById>(json) ?? new();
await NotificationServerService.SendAction(NotificationClientService.User, await NotificationServerService.SendAction(
JsonConvert.SerializeObject(new NotificationById() {Action = "hide", notification = id})); CurrentUser!,
JsonConvert.SerializeObject(
new NotificationById()
{
Action = "hide", Notification = model.Notification
}
)
);
} }
} }

View File

@@ -1,5 +1,5 @@
using Logging.Net; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc; using Moonlight.App.Helpers;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Sessions;

View File

@@ -1,12 +1,6 @@
using System.Text; using Microsoft.AspNetCore.Mvc;
using Logging.Net;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight; namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -14,16 +8,11 @@ namespace Moonlight.App.Http.Controllers.Api.Moonlight;
[Route("api/moonlight/resources")] [Route("api/moonlight/resources")]
public class ResourcesController : Controller public class ResourcesController : Controller
{ {
private readonly SecurityLogService SecurityLogService;
private readonly BucketService BucketService; private readonly BucketService BucketService;
private readonly BundleService BundleService;
public ResourcesController(SecurityLogService securityLogService, public ResourcesController(BucketService bucketService)
BucketService bucketService, BundleService bundleService)
{ {
SecurityLogService = securityLogService;
BucketService = bucketService; BucketService = bucketService;
BundleService = bundleService;
} }
[HttpGet("images/{name}")] [HttpGet("images/{name}")]
@@ -31,10 +20,7 @@ public class ResourcesController : Controller
{ {
if (name.Contains("..")) if (name.Contains(".."))
{ {
await SecurityLogService.Log(SecurityLogType.PathTransversal, x => Logger.Warn($"Detected an attempted path transversal. Path: {name}", "security");
{
x.Add<string>(name);
});
return NotFound(); return NotFound();
} }
@@ -49,15 +35,32 @@ public class ResourcesController : Controller
return NotFound(); return NotFound();
} }
[HttpGet("background/{name}")]
public async Task<ActionResult> GetBackground([FromRoute] string name)
{
if (name.Contains(".."))
{
Logger.Warn($"Detected an attempted path transversal. Path: {name}", "security");
return NotFound();
}
if (System.IO.File.Exists(PathBuilder.File("storage", "resources", "public", "background", name)))
{
var fs = new FileStream(PathBuilder.File("storage", "resources", "public", "background", name), FileMode.Open);
return File(fs, MimeTypes.GetMimeType(name), name);
}
return NotFound();
}
[HttpGet("bucket/{bucket}/{name}")] [HttpGet("bucket/{bucket}/{name}")]
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name) public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
{ {
if (name.Contains("..")) if (name.Contains(".."))
{ {
await SecurityLogService.Log(SecurityLogType.PathTransversal, x => Logger.Warn($"Detected an attempted path transversal. Path: {name}", "security");
{
x.Add<string>(name);
});
return NotFound(); return NotFound();
} }
@@ -77,34 +80,4 @@ public class ResourcesController : Controller
return Problem(); return Problem();
} }
} }
[HttpGet("bundle/js")]
public Task<ActionResult> GetJs()
{
if (BundleService.BundledFinished)
{
return Task.FromResult<ActionResult>(
File(Encoding.ASCII.GetBytes(BundleService.BundledJs), "text/javascript")
);
}
return Task.FromResult<ActionResult>(
NotFound()
);
}
[HttpGet("bundle/css")]
public Task<ActionResult> GetCss()
{
if (BundleService.BundledFinished)
{
return Task.FromResult<ActionResult>(
File(Encoding.ASCII.GetBytes(BundleService.BundledCss), "text/css")
);
}
return Task.FromResult<ActionResult>(
NotFound()
);
}
} }

View File

@@ -1,10 +1,8 @@
using Logging.Net; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Http.Requests.Daemon; using Moonlight.App.Http.Requests.Daemon;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Http.Controllers.Api.Remote; namespace Moonlight.App.Http.Controllers.Api.Remote;

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Moonlight.App.Helpers;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Moonlight.App.LogMigrator; namespace Moonlight.App.LogMigrator;

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Forms;
public class ServerImageDataModel
{
public string OverrideStartup { get; set; }
public int DockerImageIndex { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Database.Entities;
namespace Moonlight.App.Models.Forms;
public class ServerOverviewDataModel
{
[Required(ErrorMessage = "You need to enter a name")]
[MaxLength(32, ErrorMessage = "The name cannot be longer that 32 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "You need to specify a owner")]
public User Owner { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms;
public class ServerResourcesDataModel
{
[Required(ErrorMessage = "You need to specify the cpu cores")]
public int Cpu { get; set; }
[Required(ErrorMessage = "You need to specify the memory")]
public long Memory { get; set; }
[Required(ErrorMessage = "You need to specify the disk")]
public long Disk { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.Net.WebSockets;
using System.Text;
using Moonlight.App.Database.Entities.Notification;
namespace Moonlight.App.Models.Misc;
public class ActiveNotificationClient
{
public WebSocket WebSocket { get; set; }
public NotificationClient Client { get; set; }
public async Task SendAction(string action)
{
await WebSocket.SendAsync(
Encoding.UTF8.GetBytes(action),
WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace Moonlight.App.Models.Misc;
public class HealthCheck
{
public string Status { get; set; }
public TimeSpan TotalDuration { get; set; }
public Dictionary<string, HealthCheckEntry> Entries { get; set; } = new();
public class HealthCheckEntry
{
public Dictionary<string, string> Data { get; set; } = new();
public string Description { get; set; }
public TimeSpan Duration { get; set; }
public string Status { get; set; }
public List<string> Tags { get; set; } = new();
}
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Models.Misc;
public class MalwareScanResult
{
public string Title { get; set; } = "";
public string Description { get; set; } = "";
public string Author { get; set; } = "";
}

View File

@@ -6,5 +6,5 @@ namespace Moonlight.App.Models.Misc;
public class RunningServer public class RunningServer
{ {
public Server Server { get; set; } public Server Server { get; set; }
public ContainerStats.Container Container { get; set; } public Container Container { get; set; }
} }

View File

@@ -1,6 +1,8 @@
namespace Moonlight.App.Models.Notifications; using Newtonsoft.Json;
namespace Moonlight.App.Models.Notifications;
public class Login : BasicWSModel public class Login : BasicWSModel
{ {
public string token { get; set; } [JsonProperty("token")] public string Token { get; set; } = "";
} }

View File

@@ -1,6 +1,9 @@
namespace Moonlight.App.Models.Notifications; using Newtonsoft.Json;
namespace Moonlight.App.Models.Notifications;
public class NotificationById : BasicWSModel public class NotificationById : BasicWSModel
{ {
public int notification { get; set; } [JsonProperty("notification")]
public int Notification { get; set; }
} }

View File

@@ -1,5 +1,4 @@
using System.Text; using System.Text;
using Logging.Net;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;

View File

@@ -1,5 +1,4 @@
using System.Text; using System.Text;
using Logging.Net;
using Moonlight.App.ApiClients.Google.Requests; using Moonlight.App.ApiClients.Google.Requests;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;

View File

@@ -0,0 +1,83 @@
using Moonlight.App.ApiClients.Modrinth;
using Moonlight.App.ApiClients.Modrinth.Resources;
using Moonlight.App.Exceptions;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
using Version = Moonlight.App.ApiClients.Modrinth.Resources.Version;
namespace Moonlight.App.Services.Addon;
public class ServerAddonPluginService
{
private readonly ModrinthApiHelper ModrinthApiHelper;
private readonly ServerService ServerService;
public ServerAddonPluginService(ModrinthApiHelper modrinthApiHelper)
{
ModrinthApiHelper = modrinthApiHelper;
}
public async Task<Project[]> GetPluginsForVersion(string version, string search = "")
{
string resource;
var filter =
"[[\"categories:\'bukkit\'\",\"categories:\'paper\'\",\"categories:\'spigot\'\"],[\"versions:" + version + "\"],[\"project_type:mod\"]]";
if (string.IsNullOrEmpty(search))
resource = "search?limit=21&index=relevance&facets=" + filter;
else
resource = $"search?query={search}&limit=21&index=relevance&facets=" + filter;
var result = await ModrinthApiHelper.Get<Pagination>(resource);
return result.Hits;
}
public async Task InstallPlugin(FileAccess fileAccess, string version, Project project, Action<string>? onStateUpdated = null)
{
// Resolve plugin download
onStateUpdated?.Invoke($"Resolving {project.Slug}");
var filter = "game_versions=[\"" + version + "\"]&loaders=[\"bukkit\", \"paper\", \"spigot\"]";
var versions = await ModrinthApiHelper.Get<Version[]>(
$"project/{project.Slug}/version?" + filter);
if (!versions.Any())
throw new DisplayException("No plugin download for your minecraft version found");
var installVersion = versions.OrderByDescending(x => x.DatePublished).First();
var fileToInstall = installVersion.Files.First();
// Download plugin in a stream cached mode
var httpClient = new HttpClient();
var stream = await httpClient.GetStreamAsync(fileToInstall.Url);
var dataStream = new MemoryStream(1024 * 1024 * 40);
await stream.CopyToAsync(dataStream);
stream.Close();
dataStream.Position = 0;
// Install plugin
await fileAccess.SetDir("/");
try
{
await fileAccess.MkDir("plugins");
}
catch (Exception)
{
// Ignored
}
await fileAccess.SetDir("plugins");
onStateUpdated?.Invoke($"Installing {project.Slug}");
await fileAccess.Upload(fileToInstall.Filename, dataStream);
await dataStream.DisposeAsync();
//TODO: At some point of time, create a dependency resolver
}
}

View File

@@ -1,10 +1,10 @@
using Logging.Net; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using MineStatLib; using MineStatLib;
using Moonlight.App.ApiClients.Daemon.Resources; using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -81,12 +81,35 @@ public class CleanupService
{ {
try try
{ {
var cpuStats = await nodeService.GetCpuStats(node); var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryStats = await nodeService.GetMemoryStats(node); var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory) bool executeCleanup;
if (cpuMetrics.CpuUsage > maxCpu)
{ {
var containerStats = await nodeService.GetContainerStats(node); Logger.Debug($"{node.Name}: CPU Usage is too high");
Logger.Debug($"Usage: {cpuMetrics.CpuUsage}");
Logger.Debug($"Max CPU: {maxCpu}");
executeCleanup = true;
}
else if (Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used) <
minMemory / 1024D)
{
Logger.Debug($"{node.Name}: Memory Usage is too high");
Logger.Debug($"Memory (Total): {Formatter.BytesToGb(memoryMetrics.Total)}");
Logger.Debug($"Memory (Used): {Formatter.BytesToGb(memoryMetrics.Used)}");
Logger.Debug(
$"Memory (Free): {Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used)}");
Logger.Debug($"Min Memory: {minMemory}");
executeCleanup = true;
}
else
executeCleanup = false;
if (executeCleanup)
{
var dockerMetrics = await nodeService.GetDockerMetrics(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>(); var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>(); var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@@ -101,9 +124,9 @@ public class CleanupService
) )
.ToArray(); .ToArray();
var containerMappedToServers = new Dictionary<ContainerStats.Container, Server>(); var containerMappedToServers = new Dictionary<Container, Server>();
foreach (var container in containerStats.Containers) foreach (var container in dockerMetrics.Containers)
{ {
if (Guid.TryParse(container.Name, out Guid uuid)) if (Guid.TryParse(container.Name, out Guid uuid))
{ {
@@ -139,6 +162,7 @@ public class CleanupService
if (players == 0) if (players == 0)
{ {
Logger.Debug($"Restarted {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart); await serverService.SetPowerState(server, PowerSignal.Restart);
ServersCleaned++; ServersCleaned++;
@@ -164,10 +188,12 @@ public class CleanupService
if (handleJ2S) if (handleJ2S)
{ {
Logger.Debug($"Restarted (cleanup) {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart); await serverService.SetPowerState(server, PowerSignal.Restart);
} }
else else
{ {
Logger.Debug($"Stopped {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Stop); await serverService.SetPowerState(server, PowerSignal.Stop);
} }

View File

@@ -1,8 +1,8 @@
using Discord; using Discord;
using Discord.Webhook; using Discord.Webhook;
using Logging.Net;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
namespace Moonlight.App.Services.Background; namespace Moonlight.App.Services.Background;

View File

@@ -0,0 +1,196 @@
using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Background;
public class MalwareScanService
{
private Repository<Server> ServerRepository;
private Repository<Node> NodeRepository;
private NodeService NodeService;
private ServerService ServerService;
private readonly EventSystem Event;
private readonly IServiceScopeFactory ServiceScopeFactory;
public bool IsRunning { get; private set; }
public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
public string Status { get; private set; } = "N/A";
public MalwareScanService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
{
ServiceScopeFactory = serviceScopeFactory;
Event = eventSystem;
ScanResults = new();
}
public Task Start()
{
if (IsRunning)
throw new DisplayException("Malware scan is already running");
Task.Run(Run);
return Task.CompletedTask;
}
private async Task Run()
{
IsRunning = true;
Status = "Clearing last results";
await Event.Emit("malwareScan.status", IsRunning);
lock (ScanResults)
{
ScanResults.Clear();
}
await Event.Emit("malwareScan.result");
using var scope = ServiceScopeFactory.CreateScope();
// Load services from di scope
NodeRepository = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
ServerRepository = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
NodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
ServerService = scope.ServiceProvider.GetRequiredService<ServerService>();
var nodes = NodeRepository.Get().ToArray();
var containers = new List<Container>();
// Fetch and summarize all running containers from all nodes
Logger.Verbose("Fetching and summarizing all running containers from all nodes");
Status = "Fetching and summarizing all running containers from all nodes";
await Event.Emit("malwareScan.status", IsRunning);
foreach (var node in nodes)
{
var metrics = await NodeService.GetDockerMetrics(node);
foreach (var container in metrics.Containers)
{
containers.Add(container);
}
}
var containerServerMapped = new Dictionary<Server, Container>();
// Map all the containers to their corresponding server if existing
Logger.Verbose("Mapping all the containers to their corresponding server if existing");
Status = "Mapping all the containers to their corresponding server if existing";
await Event.Emit("malwareScan.status", IsRunning);
foreach (var container in containers)
{
if (Guid.TryParse(container.Name, out Guid uuid))
{
var server = ServerRepository
.Get()
.FirstOrDefault(x => x.Uuid == uuid);
if(server == null)
continue;
containerServerMapped.Add(server, container);
}
}
// Perform scan
var resultsMapped = new Dictionary<Server, MalwareScanResult[]>();
foreach (var mapping in containerServerMapped)
{
Logger.Verbose($"Scanning server {mapping.Key.Name} for malware");
Status = $"Scanning server {mapping.Key.Name} for malware";
await Event.Emit("malwareScan.status", IsRunning);
var results = await PerformScanOnServer(mapping.Key, mapping.Value);
if (results.Any())
{
resultsMapped.Add(mapping.Key, results);
Logger.Verbose($"{results.Length} findings on server {mapping.Key.Name}");
}
}
Logger.Verbose($"Scan complete. Detected {resultsMapped.Count} servers with findings");
IsRunning = false;
Status = $"Scan complete. Detected {resultsMapped.Count} servers with findings";
await Event.Emit("malwareScan.status", IsRunning);
lock (ScanResults)
{
foreach (var mapping in resultsMapped)
{
ScanResults.Add(mapping.Key, mapping.Value);
}
}
await Event.Emit("malwareScan.result");
}
private async Task<MalwareScanResult[]> PerformScanOnServer(Server server, Container container)
{
var results = new List<MalwareScanResult>();
// TODO: Move scans to an universal format / api
// Define scans here
async Task ScanSelfBot()
{
var access = await ServerService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => x.Name == "tokens.txt"))
{
results.Add(new ()
{
Title = "Found SelfBot",
Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
Author = "Marcel Baumgartner"
});
}
}
async Task ScanFakePlayerPlugins()
{
var access = await ServerService.CreateFileAccess(server, null!);
var fileElements = await access.Ls();
if (fileElements.Any(x => !x.IsFile && x.Name == "plugins")) // Check for plugins folder
{
await access.Cd("plugins");
fileElements = await access.Ls();
foreach (var fileElement in fileElements)
{
if (fileElement.Name.ToLower().Contains("fake"))
{
results.Add(new()
{
Title = "Fake player plugin",
Description = $"Suspicious plugin file: {fileElement.Name}",
Author = "Marcel Baumgartner"
});
}
}
}
}
// Execute scans
await ScanSelfBot();
await ScanFakePlayerPlugins();
return results.ToArray();
}
}

View File

@@ -1,5 +1,4 @@
using System.Text; using System.Text;
using Logging.Net;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
@@ -42,8 +41,6 @@ public class ConfigService : IConfiguration
public void Reload() public void Reload()
{ {
Logger.Info($"Reading config from '{PathBuilder.File("storage", "configs", "config.json")}'");
Configuration = new ConfigurationBuilder().AddJsonStream( Configuration = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes( new MemoryStream(Encoding.ASCII.GetBytes(
File.ReadAllText( File.ReadAllText(
@@ -51,8 +48,6 @@ public class ConfigService : IConfiguration
) )
) )
)).Build(); )).Build();
Logger.Info("Reloaded configuration file");
} }
public IEnumerable<IConfigurationSection> GetChildren() public IEnumerable<IConfigurationSection> GetChildren()

View File

@@ -1,6 +1,5 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net;
namespace Moonlight.App.Services.DiscordBot.Commands; namespace Moonlight.App.Services.DiscordBot.Commands;

View File

@@ -1,6 +1,5 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;

View File

@@ -1,8 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using Discord; using Discord;
using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net; using Moonlight.App.Helpers;
using Moonlight.App.Services.DiscordBot.Commands; using Moonlight.App.Services.DiscordBot.Commands;
using Moonlight.App.Services.DiscordBot.Modules; using Moonlight.App.Services.DiscordBot.Modules;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net; using Moonlight.App.Helpers;
namespace Moonlight.App.Services.DiscordBot.Modules; namespace Moonlight.App.Services.DiscordBot.Modules;

View File

@@ -1,8 +1,8 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;

View File

@@ -5,13 +5,12 @@ using CloudFlare.Client.Api.Result;
using CloudFlare.Client.Api.Zones; using CloudFlare.Client.Api.Zones;
using CloudFlare.Client.Api.Zones.DnsRecord; using CloudFlare.Client.Api.Zones.DnsRecord;
using CloudFlare.Client.Enumerators; using CloudFlare.Client.Enumerators;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.Domains; using Moonlight.App.Repositories.Domains;
using Moonlight.App.Services.LogServices;
using DnsRecord = Moonlight.App.Models.Misc.DnsRecord; using DnsRecord = Moonlight.App.Models.Misc.DnsRecord;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
@@ -21,18 +20,15 @@ public class DomainService
private readonly DomainRepository DomainRepository; private readonly DomainRepository DomainRepository;
private readonly SharedDomainRepository SharedDomainRepository; private readonly SharedDomainRepository SharedDomainRepository;
private readonly CloudFlareClient Client; private readonly CloudFlareClient Client;
private readonly AuditLogService AuditLogService;
private readonly string AccountId; private readonly string AccountId;
public DomainService( public DomainService(
ConfigService configService, ConfigService configService,
DomainRepository domainRepository, DomainRepository domainRepository,
SharedDomainRepository sharedDomainRepository, SharedDomainRepository sharedDomainRepository)
AuditLogService auditLogService)
{ {
DomainRepository = domainRepository; DomainRepository = domainRepository;
SharedDomainRepository = sharedDomainRepository; SharedDomainRepository = sharedDomainRepository;
AuditLogService = auditLogService;
var config = configService var config = configService
.GetSection("Moonlight") .GetSection("Moonlight")
@@ -191,11 +187,7 @@ public class DomainService
})); }));
} }
await AuditLogService.Log(AuditLogType.AddDomainRecord, x => //TODO: AuditLog
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
} }
public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord) public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord)
@@ -225,11 +217,7 @@ public class DomainService
})); }));
} }
await AuditLogService.Log(AuditLogType.UpdateDomainRecord, x => //TODO: AuditLog
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
} }
public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord) public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord)
@@ -240,11 +228,7 @@ public class DomainService
await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id) await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id)
); );
await AuditLogService.Log(AuditLogType.DeleteDomainRecord, x => //TODO: AuditLog
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
} }
private Domain EnsureData(Domain domain) private Domain EnsureData(Domain domain)

View File

@@ -16,6 +16,11 @@ public class ResourceService
return $"{AppUrl}/api/moonlight/resources/images/{name}"; return $"{AppUrl}/api/moonlight/resources/images/{name}";
} }
public string BackgroundImage(string name)
{
return $"{AppUrl}/api/moonlight/resources/background/{name}";
}
public string Avatar(User user) public string Avatar(User user)
{ {
return $"{AppUrl}/api/moonlight/avatar/{user.Id}"; return $"{AppUrl}/api/moonlight/avatar/{user.Id}";

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Moonlight.App.Helpers;
using Moonlight.App.Helpers;
namespace Moonlight.App.Services.Files; namespace Moonlight.App.Services.Files;

View File

@@ -1,93 +0,0 @@
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Models.Log;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json;
namespace Moonlight.App.Services.LogServices;
public class AuditLogService
{
private readonly AuditLogEntryRepository Repository;
private readonly IHttpContextAccessor HttpContextAccessor;
public AuditLogService(
AuditLogEntryRepository repository,
IHttpContextAccessor httpContextAccessor)
{
Repository = repository;
HttpContextAccessor = httpContextAccessor;
}
public Task Log(AuditLogType type, Action<AuditLogParameters> data)
{
var ip = GetIp();
var al = new AuditLogParameters();
data(al);
var entry = new AuditLogEntry()
{
Ip = ip,
Type = type,
System = false,
JsonData = al.Build()
};
Repository.Add(entry);
return Task.CompletedTask;
}
public Task LogSystem(AuditLogType type, Action<AuditLogParameters> data)
{
var al = new AuditLogParameters();
data(al);
var entry = new AuditLogEntry()
{
Type = type,
System = true,
JsonData = al.Build()
};
Repository.Add(entry);
return Task.CompletedTask;
}
private string GetIp()
{
if (HttpContextAccessor.HttpContext == null)
return "N/A";
if(HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
{
return HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!;
}
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
}
public class AuditLogParameters
{
private List<LogData> Data = new List<LogData>();
public void Add<T>(object? data)
{
if(data == null)
return;
Data.Add(new LogData()
{
Type = typeof(T),
Value = data.ToString()
});
}
internal string Build()
{
return JsonConvert.SerializeObject(Data);
}
}
}

View File

@@ -1,118 +0,0 @@
using System.Diagnostics;
using System.Reflection;
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Models.Log;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json;
namespace Moonlight.App.Services.LogServices;
public class ErrorLogService
{
private readonly ErrorLogEntryRepository Repository;
private readonly IHttpContextAccessor HttpContextAccessor;
public ErrorLogService(ErrorLogEntryRepository repository, IHttpContextAccessor httpContextAccessor)
{
Repository = repository;
HttpContextAccessor = httpContextAccessor;
}
public Task Log(Exception exception, Action<ErrorLogParameters> data)
{
var ip = GetIp();
var al = new ErrorLogParameters();
data(al);
var entry = new ErrorLogEntry()
{
Ip = ip,
System = false,
JsonData = al.Build(),
Class = NameOfCallingClass(),
Stacktrace = exception.ToStringDemystified()
};
Repository.Add(entry);
return Task.CompletedTask;
}
public Task LogSystem(Exception exception, Action<ErrorLogParameters> data)
{
var al = new ErrorLogParameters();
data(al);
var entry = new ErrorLogEntry()
{
System = true,
JsonData = al.Build(),
Class = NameOfCallingClass(),
Stacktrace = exception.ToStringDemystified()
};
Repository.Add(entry);
return Task.CompletedTask;
}
private string NameOfCallingClass(int skipFrames = 4)
{
string fullName;
Type? declaringType;
do
{
MethodBase method = new StackFrame(skipFrames, false).GetMethod()!;
declaringType = method.DeclaringType;
if (declaringType == null)
{
return method.Name;
}
skipFrames++;
if (declaringType.Name.Contains("<"))
fullName = declaringType.ReflectedType!.Name;
else
fullName = declaringType.Name;
}
while (declaringType.Module.Name.Equals("mscorlib.dll", StringComparison.OrdinalIgnoreCase) | fullName.Contains("Log"));
return fullName;
}
private string GetIp()
{
if (HttpContextAccessor.HttpContext == null)
return "N/A";
if(HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
{
return HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!;
}
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
}
public class ErrorLogParameters
{
private List<LogData> Data = new List<LogData>();
public void Add<T>(object? data)
{
if(data == null)
return;
Data.Add(new LogData()
{
Type = typeof(T),
Value = data.ToString()
});
}
internal string Build()
{
return JsonConvert.SerializeObject(Data);
}
}
}

View File

@@ -1,45 +0,0 @@
using Logging.Net;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Services.LogServices;
public class LogService
{
public LogService()
{
Task.Run(ClearLog);
}
private async Task ClearLog()
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(15));
if (GetMessages().Length > 500)
{
if (Logger.UsedLogger is CacheLogger cacheLogger)
{
cacheLogger.Clear(250); //TODO: config
}
else
{
Logger.Warn("Log service cannot access cache. Is Logging.Net using CacheLogger?");
}
}
}
}
public LogEntry[] GetMessages()
{
if (Logger.UsedLogger is CacheLogger cacheLogger)
{
return cacheLogger.GetMessages();
}
Logger.Warn("Log service cannot access cache. Is Logging.Net using CacheLogger?");
return Array.Empty<LogEntry>();
}
}

View File

@@ -1,92 +0,0 @@
using Moonlight.App.Database.Entities.LogsEntries;
using Moonlight.App.Models.Log;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.LogEntries;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json;
namespace Moonlight.App.Services.LogServices;
public class SecurityLogService
{
private readonly SecurityLogEntryRepository Repository;
private readonly IHttpContextAccessor HttpContextAccessor;
public SecurityLogService(SecurityLogEntryRepository repository, IHttpContextAccessor httpContextAccessor)
{
Repository = repository;
HttpContextAccessor = httpContextAccessor;
}
public Task Log(SecurityLogType type, Action<SecurityLogParameters> data)
{
var ip = GetIp();
var al = new SecurityLogParameters();
data(al);
var entry = new SecurityLogEntry()
{
Ip = ip,
Type = type,
System = false,
JsonData = al.Build()
};
Repository.Add(entry);
return Task.CompletedTask;
}
public Task LogSystem(SecurityLogType type, Action<SecurityLogParameters> data)
{
var al = new SecurityLogParameters();
data(al);
var entry = new SecurityLogEntry()
{
Type = type,
System = true,
JsonData = al.Build()
};
Repository.Add(entry);
return Task.CompletedTask;
}
private string GetIp()
{
if (HttpContextAccessor.HttpContext == null)
return "N/A";
if(HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
{
return HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!;
}
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
}
public class SecurityLogParameters
{
private List<LogData> Data = new List<LogData>();
public void Add<T>(object? data)
{
if(data == null)
return;
Data.Add(new LogData()
{
Type = typeof(T),
Value = data.ToString()
});
}
internal string Build()
{
return JsonConvert.SerializeObject(Data);
}
}
}

View File

@@ -1,5 +1,4 @@
using Logging.Net; using MimeKit;
using MimeKit;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;

View File

@@ -1,5 +1,5 @@
using System.Net; using System.Net;
using Logging.Net; using Moonlight.App.Helpers;
namespace Moonlight.App.Services.Mail; namespace Moonlight.App.Services.Mail;

View File

@@ -0,0 +1,98 @@
using Moonlight.App.Helpers;
using Octokit;
using Repository = LibGit2Sharp.Repository;
namespace Moonlight.App.Services;
public class MoonlightService
{
private readonly ConfigService ConfigService;
public readonly DateTime StartTimestamp;
public readonly string AppVersion;
public readonly List<string[]> ChangeLog = new();
public MoonlightService(ConfigService configService)
{
ConfigService = configService;
StartTimestamp = DateTime.UtcNow;
if (File.Exists("version") && !ConfigService.DebugMode)
AppVersion = File.ReadAllText("version");
else if (ConfigService.DebugMode)
{
string repositoryPath = Path.GetFullPath("..");
using var repo = new Repository(repositoryPath);
var commit = repo.Head.Tip;
AppVersion = commit.Sha;
}
else
AppVersion = "unknown";
Task.Run(FetchChangeLog);
}
private async Task FetchChangeLog()
{
if (ConfigService.DebugMode)
{
ChangeLog.Add(new[]
{
"Disabled",
"Fetching changelog from github is disabled in debug mode"
});
return;
}
try
{
var client = new GitHubClient(new ProductHeaderValue("Moonlight"));
var pullRequests = await client.PullRequest.GetAllForRepository("Moonlight-Panel", "Moonlight", new PullRequestRequest
{
State = ItemStateFilter.Closed,
SortDirection = SortDirection.Ascending,
SortProperty = PullRequestSort.Created
});
var groupedPullRequests = new Dictionary<DateTime, List<string>>();
foreach (var pullRequest in pullRequests)
{
if (pullRequest.MergedAt != null)
{
var date = pullRequest.MergedAt.Value.Date;
if (!groupedPullRequests.ContainsKey(date))
{
groupedPullRequests[date] = new List<string>();
}
groupedPullRequests[date].Add(pullRequest.Title);
}
}
int i = 1;
foreach (var group in groupedPullRequests)
{
var pullRequestsList = new List<string>();
var date = group.Key.ToString("dd.MM.yyyy");
pullRequestsList.Add($"Patch {i}, {date}");
foreach (var pullRequest in group.Value)
{
pullRequestsList.Add(pullRequest);
}
ChangeLog.Add(pullRequestsList.ToArray());
i++;
}
}
catch (Exception e)
{
Logger.Warn("Error fetching changelog");
Logger.Warn(e);
}
}
}

View File

@@ -1,4 +1,5 @@
using Moonlight.App.ApiClients.Daemon; using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Daemon.Requests;
using Moonlight.App.ApiClients.Daemon.Resources; using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.ApiClients.Wings.Resources;
@@ -24,35 +25,56 @@ public class NodeService
return await WingsApiHelper.Get<SystemStatus>(node, "api/system"); return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
} }
public async Task<CpuStats> GetCpuStats(Node node) public async Task<CpuMetrics> GetCpuMetrics(Node node)
{ {
return await DaemonApiHelper.Get<CpuStats>(node, "stats/cpu"); return await DaemonApiHelper.Get<CpuMetrics>(node, "metrics/cpu");
} }
public async Task<MemoryStats> GetMemoryStats(Node node) public async Task<MemoryMetrics> GetMemoryMetrics(Node node)
{ {
return await DaemonApiHelper.Get<MemoryStats>(node, "stats/memory"); return await DaemonApiHelper.Get<MemoryMetrics>(node, "metrics/memory");
} }
public async Task<DiskStats> GetDiskStats(Node node) public async Task<DiskMetrics> GetDiskMetrics(Node node)
{ {
return await DaemonApiHelper.Get<DiskStats>(node, "stats/disk"); return await DaemonApiHelper.Get<DiskMetrics>(node, "metrics/disk");
} }
public async Task<ContainerStats> GetContainerStats(Node node) public async Task<SystemMetrics> GetSystemMetrics(Node node)
{ {
return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container"); return await DaemonApiHelper.Get<SystemMetrics>(node, "metrics/system");
}
public async Task<DockerMetrics> GetDockerMetrics(Node node)
{
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
}
public async Task Mount(Node node, string server, string serverPath, string path)
{
await DaemonApiHelper.Post(node, "mount", new Mount()
{
Server = server,
ServerPath = serverPath,
Path = path
});
}
public async Task Unmount(Node node, string path)
{
await DaemonApiHelper.Delete(node, "mount", new Unmount()
{
Path = path
});
} }
public async Task<bool> IsHostUp(Node node) public async Task<bool> IsHostUp(Node node)
{ {
try try
{ {
//TODO: Implement status caching await GetStatus(node);
var data = await GetStatus(node);
if (data != null) return true;
return true;
} }
catch (Exception) catch (Exception)
{ {

View File

@@ -1,43 +0,0 @@
using System.Net.WebSockets;
using System.Text;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Http.Controllers.Api.Moonlight.Notifications;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Services.Notifications;
public class NotificationClientService
{
private readonly NotificationRepository NotificationRepository;
private readonly NotificationServerService NotificationServerService;
internal ListenController listenController;
public NotificationClientService(NotificationRepository notificationRepository, NotificationServerService notificationServerService)
{
NotificationRepository = notificationRepository;
NotificationServerService = notificationServerService;
}
public User User => NotificationClient.User;
public NotificationClient NotificationClient { get; set; }
public async Task SendAction(string action)
{
await listenController.ws.SendAsync(Encoding.UTF8.GetBytes(action), WebSocketMessageType.Text,
WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
public void WebsocketReady(NotificationClient client)
{
NotificationClient = client;
NotificationServerService.AddClient(this);
}
public void WebsocketClosed()
{
NotificationServerService.RemoveClient(this);
}
}

View File

@@ -1,79 +1,113 @@
using Microsoft.EntityFrameworkCore; using System.Net.WebSockets;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification; using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Events;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Notifications; namespace Moonlight.App.Services.Notifications;
public class NotificationServerService public class NotificationServerService
{ {
private UserRepository UserRepository; private readonly List<ActiveNotificationClient> ActiveClients = new();
private NotificationRepository NotificationRepository;
private readonly IServiceScopeFactory ServiceScopeFactory; private readonly IServiceScopeFactory ServiceScopeFactory;
private IServiceScope ServiceScope; private readonly EventSystem Event;
public NotificationServerService(IServiceScopeFactory serviceScopeFactory) public NotificationServerService(IServiceScopeFactory serviceScopeFactory, EventSystem eventSystem)
{ {
ServiceScopeFactory = serviceScopeFactory; ServiceScopeFactory = serviceScopeFactory;
Task.Run(Run); Event = eventSystem;
} }
private Task Run() public Task<ActiveNotificationClient[]> GetActiveClients()
{ {
ServiceScope = ServiceScopeFactory.CreateScope(); lock (ActiveClients)
{
UserRepository = ServiceScope return Task.FromResult(ActiveClients.ToArray());
.ServiceProvider }
.GetRequiredService<UserRepository>();
NotificationRepository = ServiceScope
.ServiceProvider
.GetRequiredService<NotificationRepository>();
return Task.CompletedTask;
} }
private List<NotificationClientService> connectedClients = new(); public Task<ActiveNotificationClient[]> GetUserClients(User user)
public List<NotificationClientService> GetConnectedClients(User user)
{ {
return connectedClients.Where(x => x.User == user).ToList(); lock (ActiveClients)
{
return Task.FromResult(
ActiveClients
.Where(x => x.Client.User.Id == user.Id)
.ToArray()
);
}
} }
public async Task SendAction(User user, string action) public async Task SendAction(User user, string action)
{ {
var clients = NotificationRepository.GetClients().Include(x => x.User).Where(x => x.User == user).ToList(); using var scope = ServiceScopeFactory.CreateScope();
var notificationClientRepository =
scope.ServiceProvider.GetRequiredService<Repository<NotificationClient>>();
var clients = notificationClientRepository
.Get()
.Include(x => x.User)
.Where(x => x.User == user)
.ToList();
foreach (var client in clients) foreach (var client in clients)
{ {
var notificationAction = new NotificationAction() ActiveNotificationClient[] connectedUserClients;
{
Action = action,
NotificationClient = client
};
var connected = connectedClients.Where(x => x.NotificationClient.Id == client.Id).ToList(); lock (ActiveClients)
if (connected.Count > 0)
{ {
var clientService = connected[0]; connectedUserClients = ActiveClients
await clientService.SendAction(action); .Where(x => x.Client.Id == user.Id)
.ToArray();
}
if (connectedUserClients.Length > 0)
{
await connectedUserClients[0].SendAction(action);
} }
else else
{ {
NotificationRepository.AddAction(notificationAction); var notificationAction = new NotificationAction()
{
Action = action,
NotificationClient = client
};
var notificationActionsRepository =
scope.ServiceProvider.GetRequiredService<Repository<NotificationAction>>();
notificationActionsRepository.Add(notificationAction);
} }
} }
} }
public void AddClient(NotificationClientService notificationClientService) public async Task RegisterClient(WebSocket webSocket, NotificationClient notificationClient)
{ {
connectedClients.Add(notificationClientService); var newClient = new ActiveNotificationClient()
{
WebSocket = webSocket,
Client = notificationClient
};
lock (ActiveClients)
{
ActiveClients.Add(newClient);
}
await Event.Emit("notifications.addClient", notificationClient);
} }
public void RemoveClient(NotificationClientService notificationClientService) public async Task UnRegisterClient(NotificationClient client)
{ {
connectedClients.Remove(notificationClientService); lock (ActiveClients)
{
ActiveClients.RemoveAll(x => x.Client == client);
}
await Event.Emit("notifications.removeClient", client);
} }
} }

View File

@@ -4,9 +4,7 @@ using JWT.Builder;
using JWT.Exceptions; using JWT.Exceptions;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.LogServices;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
@@ -14,15 +12,12 @@ public class OneTimeJwtService
{ {
private readonly ConfigService ConfigService; private readonly ConfigService ConfigService;
private readonly RevokeRepository RevokeRepository; private readonly RevokeRepository RevokeRepository;
private readonly SecurityLogService SecurityLogService;
public OneTimeJwtService(ConfigService configService, public OneTimeJwtService(ConfigService configService,
RevokeRepository revokeRepository, RevokeRepository revokeRepository)
SecurityLogService securityLogService)
{ {
ConfigService = configService; ConfigService = configService;
RevokeRepository = revokeRepository; RevokeRepository = revokeRepository;
SecurityLogService = securityLogService;
} }
public string Generate(Action<Dictionary<string, string>> options, TimeSpan? validTime = null) public string Generate(Action<Dictionary<string, string>> options, TimeSpan? validTime = null)
@@ -76,10 +71,7 @@ public class OneTimeJwtService
} }
catch (SignatureVerificationException) catch (SignatureVerificationException)
{ {
await SecurityLogService.LogSystem(SecurityLogType.ManipulatedJwt, x => Logger.Warn($"Detected a manipulated JWT: {token}", "security");
{
x.Add<string>(token);
});
return null; return null;
} }
catch (Exception e) catch (Exception e)

View File

@@ -12,13 +12,13 @@ using Moonlight.App.Helpers.Wings;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services.LogServices;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess; using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
public class ServerService public class ServerService
{ {
private readonly Repository<ServerVariable> ServerVariablesRepository;
private readonly ServerRepository ServerRepository; private readonly ServerRepository ServerRepository;
private readonly UserRepository UserRepository; private readonly UserRepository UserRepository;
private readonly ImageRepository ImageRepository; private readonly ImageRepository ImageRepository;
@@ -28,9 +28,6 @@ public class ServerService
private readonly UserService UserService; private readonly UserService UserService;
private readonly ConfigService ConfigService; private readonly ConfigService ConfigService;
private readonly WingsJwtHelper WingsJwtHelper; private readonly WingsJwtHelper WingsJwtHelper;
private readonly SecurityLogService SecurityLogService;
private readonly AuditLogService AuditLogService;
private readonly ErrorLogService ErrorLogService;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
private readonly EventSystem Event; private readonly EventSystem Event;
@@ -44,13 +41,11 @@ public class ServerService
UserService userService, UserService userService,
ConfigService configService, ConfigService configService,
WingsJwtHelper wingsJwtHelper, WingsJwtHelper wingsJwtHelper,
SecurityLogService securityLogService,
AuditLogService auditLogService,
ErrorLogService errorLogService,
NodeService nodeService, NodeService nodeService,
NodeAllocationRepository nodeAllocationRepository, NodeAllocationRepository nodeAllocationRepository,
DateTimeService dateTimeService, DateTimeService dateTimeService,
EventSystem eventSystem) EventSystem eventSystem,
Repository<ServerVariable> serverVariablesRepository)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
WingsApiHelper = wingsApiHelper; WingsApiHelper = wingsApiHelper;
@@ -60,13 +55,11 @@ public class ServerService
UserService = userService; UserService = userService;
ConfigService = configService; ConfigService = configService;
WingsJwtHelper = wingsJwtHelper; WingsJwtHelper = wingsJwtHelper;
SecurityLogService = securityLogService;
AuditLogService = auditLogService;
ErrorLogService = errorLogService;
NodeService = nodeService; NodeService = nodeService;
NodeAllocationRepository = nodeAllocationRepository; NodeAllocationRepository = nodeAllocationRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
Event = eventSystem; Event = eventSystem;
ServerVariablesRepository = serverVariablesRepository;
} }
private Server EnsureNodeData(Server s) private Server EnsureNodeData(Server s)
@@ -103,11 +96,7 @@ public class ServerService
Action = rawSignal Action = rawSignal
}); });
await AuditLogService.Log(AuditLogType.ChangePowerState, x => //TODO: AuditLog
{
x.Add<Server>(server.Uuid);
x.Add<PowerSignal>(rawSignal);
});
} }
public async Task<ServerBackup> CreateBackup(Server server) public async Task<ServerBackup> CreateBackup(Server server)
@@ -136,12 +125,7 @@ public class ServerService
Ignore = "" Ignore = ""
}); });
await AuditLogService.Log(AuditLogType.CreateBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(backup.Uuid);
});
return backup; return backup;
} }
@@ -178,12 +162,7 @@ public class ServerService
Adapter = "wings" Adapter = "wings"
}); });
await AuditLogService.Log(AuditLogType.RestoreBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(serverBackup.Uuid);
});
} }
public async Task DeleteBackup(Server server, ServerBackup serverBackup) public async Task DeleteBackup(Server server, ServerBackup serverBackup)
@@ -216,13 +195,7 @@ public class ServerService
await Event.Emit("wings.backups.delete", backup); await Event.Emit("wings.backups.delete", backup);
await AuditLogService.Log(AuditLogType.DeleteBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(backup.Uuid);
}
);
} }
public async Task<string> DownloadBackup(Server s, ServerBackup serverBackup) public async Task<string> DownloadBackup(Server s, ServerBackup serverBackup)
@@ -235,12 +208,7 @@ public class ServerService
claims.Add("backup_uuid", serverBackup.Uuid.ToString()); claims.Add("backup_uuid", serverBackup.Uuid.ToString());
}); });
await AuditLogService.Log(AuditLogType.DownloadBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(serverBackup.Uuid);
});
if (server.Node.Ssl) if (server.Node.Ssl)
return $"https://{server.Node.Fqdn}:{server.Node.HttpPort}/download/backup?token={token}"; return $"https://{server.Node.Fqdn}:{server.Node.HttpPort}/download/backup?token={token}";
@@ -342,17 +310,14 @@ public class ServerService
StartOnCompletion = false StartOnCompletion = false
}); });
await AuditLogService.Log(AuditLogType.CreateServer, x => { x.Add<Server>(newServerData.Uuid); }); //TODO: AuditLog
return newServerData; return newServerData;
} }
catch (Exception e) catch (Exception e)
{ {
await ErrorLogService.Log(e, x => Logger.Error("Error creating server on wings");
{ Logger.Error(e);
x.Add<Server>(newServerData.Uuid);
x.Add<Node>(node.Id);
});
ServerRepository.Delete(newServerData); //TODO Remove unsinged table stuff ServerRepository.Delete(newServerData); //TODO Remove unsinged table stuff
@@ -366,7 +331,10 @@ public class ServerService
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null); await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null);
await AuditLogService.Log(AuditLogType.ReinstallServer, x => { x.Add<Server>(server.Uuid); }); server.Installing = true;
ServerRepository.Update(server);
//TODO: AuditLog
} }
public async Task<Server> SftpServerLogin(int serverId, int id, string password) public async Task<Server> SftpServerLogin(int serverId, int id, string password)
@@ -375,7 +343,7 @@ public class ServerService
if (server == null) if (server == null)
{ {
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, x => { x.Add<int>(id); }); Logger.Warn($"Detected an sftp bruteforce attempt. ID: {id} Password: {password}", "security");
throw new Exception("Server not found"); throw new Exception("Server not found");
} }
@@ -401,17 +369,15 @@ public class ServerService
public async Task Delete(Server s) public async Task Delete(Server s)
{ {
throw new DisplayException("Deleting a server is currently a bit buggy. So its disabled for your safety"); throw new DisplayException("Deleting servers is currently disabled");
var server = EnsureNodeData(s); var backups = await GetBackups(s);
var backups = await GetBackups(server);
foreach (var backup in backups) foreach (var backup in backups)
{ {
try try
{ {
await DeleteBackup(server, backup); await DeleteBackup(s, backup);
} }
catch (Exception) catch (Exception)
{ {
@@ -419,9 +385,18 @@ public class ServerService
} }
} }
var server = ServerRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.Node)
.First(x => x.Id == s.Id);
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null); await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
//TODO: Fix empty data models foreach (var variable in server.Variables.ToArray())
{
ServerVariablesRepository.Delete(variable);
}
server.Allocations = new(); server.Allocations = new();
server.MainAllocation = null; server.MainAllocation = null;
@@ -429,7 +404,6 @@ public class ServerService
server.Backups = new(); server.Backups = new();
ServerRepository.Update(server); ServerRepository.Update(server);
ServerRepository.Delete(server); ServerRepository.Delete(server);
} }
@@ -439,4 +413,65 @@ public class ServerService
return await NodeService.IsHostUp(server.Node); return await NodeService.IsHostUp(server.Node);
} }
public async Task ArchiveServer(Server server)
{
if (server.IsArchived)
throw new DisplayException("Unable to archive an already archived server");
// Archive server
var backup = await CreateBackup(server);
server.IsArchived = true;
server.Archive = backup;
ServerRepository.Update(server);
await Event.WaitForEvent<ServerBackup>("wings.backups.create", this, x => backup.Id == x.Id);
// Reset server
var access = await CreateFileAccess(server, null!);
var files = await access.Ls();
foreach (var file in files)
{
try
{
await access.Delete(file);
}
catch (Exception)
{
// ignored
}
}
await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server);
}
public async Task UnArchiveServer(Server s)
{
if (!s.IsArchived)
throw new DisplayException("Unable to unarchive a server which is not archived");
var server = ServerRepository
.Get()
.Include(x => x.Archive)
.First(x => x.Id == s.Id);
if (server.Archive == null)
throw new DisplayException("Archive from server not found");
if (!server.Archive.Created)
throw new DisplayException("Creating the server archive is in progress");
await RestoreBackup(server, server.Archive);
await Event.WaitForEvent<ServerBackup>("wings.backups.restore", this,
x => x.Id == server.Archive.Id);
server.IsArchived = false;
ServerRepository.Update(server);
await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server);
}
} }

View File

@@ -1,129 +0,0 @@
using Logging.Net;
namespace Moonlight.App.Services.Sessions;
public class BundleService
{
public BundleService(ConfigService configService)
{
var url = configService
.GetSection("Moonlight")
.GetValue<string>("AppUrl");
#region JS
JsFiles = new();
JsFiles.AddRange(new[]
{
url + "/_framework/blazor.server.js",
url + "/assets/plugins/global/plugins.bundle.js",
url + "/_content/XtermBlazor/XtermBlazor.min.js",
url + "/_content/BlazorTable/BlazorTable.min.js",
url + "/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js",
url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.js",
"https://www.google.com/recaptcha/api.js",
"https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.min.js",
"https://cdn.jsdelivr.net/npm/xterm-addon-search@0.8.2/lib/xterm-addon-search.min.js",
"https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.5.0/lib/xterm-addon-web-links.min.js",
url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js",
"require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });",
url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js",
url + "/_content/BlazorMonaco/jsInterop.js",
url + "/assets/js/scripts.bundle.js",
url + "/assets/js/moonlight.js",
"moonlight.loading.registerXterm();",
url + "/_content/Blazor-ApexCharts/js/apex-charts.min.js",
url + "/_content/Blazor-ApexCharts/js/blazor-apex-charts.js"
});
#endregion
#region CSS
CssFiles = new();
CssFiles.AddRange(new[]
{
"https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700",
url + "/assets/css/style.bundle.css",
url + "/assets/css/flashbang.css",
url + "/assets/css/snow.css",
url + "/assets/css/utils.css",
url + "/assets/css/blazor.css",
url + "/_content/XtermBlazor/XtermBlazor.css",
url + "/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css",
url + "/_content/Blazor.ContextMenu/blazorContextMenu.min.css",
url + "/assets/plugins/global/plugins.bundle.css"
});
#endregion
CacheId = Guid.NewGuid().ToString();
Task.Run(Bundle);
}
// Javascript
public string BundledJs { get; private set; }
public readonly List<string> JsFiles;
// CSS
public string BundledCss { get; private set; }
public readonly List<string> CssFiles;
// States
public string CacheId { get; private set; }
public bool BundledFinished { get; set; } = false;
private bool IsBundling { get; set; } = false;
private async Task Bundle()
{
if (!IsBundling)
IsBundling = true;
Logger.Info("Bundling js and css files");
BundledJs = "";
BundledCss = "";
BundledJs = await BundleFiles(
JsFiles
);
BundledCss = await BundleFiles(
CssFiles
);
Logger.Info("Successfully bundled");
BundledFinished = true;
}
private async Task<string> BundleFiles(IEnumerable<string> items)
{
var bundled = "";
using HttpClient client = new HttpClient();
foreach (string item in items)
{
// Item is a url, fetch it
if (item.StartsWith("http"))
{
try
{
var jsCode = await client.GetStringAsync(item);
bundled += jsCode + "\n";
}
catch (Exception e)
{
Logger.Warn($"Error fetching '{item}' while bundling");
Logger.Warn(e);
}
}
else // If not, it is probably a manual addition, so add it
bundled += item + "\n";
}
return bundled;
}
}

View File

@@ -0,0 +1,38 @@
using Moonlight.App.Services.Files;
namespace Moonlight.App.Services.Sessions;
public class DynamicBackgroundService
{
public EventHandler OnBackgroundImageChanged { get; set; }
public string BackgroundImageUrl { get; private set; }
private string DefaultBackgroundImageUrl;
public DynamicBackgroundService(ResourceService resourceService)
{
DefaultBackgroundImageUrl = resourceService.BackgroundImage("main.jpg");
BackgroundImageUrl = DefaultBackgroundImageUrl;
}
public Task Change(string url)
{
if(BackgroundImageUrl == url) // Prevent unnecessary updates
return Task.CompletedTask;
BackgroundImageUrl = url;
OnBackgroundImageChanged?.Invoke(this, null!);
return Task.CompletedTask;
}
public Task Reset()
{
if(BackgroundImageUrl == DefaultBackgroundImageUrl) // Prevent unnecessary updates
return Task.CompletedTask;
BackgroundImageUrl = DefaultBackgroundImageUrl;
OnBackgroundImageChanged?.Invoke(this, null!);
return Task.CompletedTask;
}
}

View File

@@ -2,12 +2,10 @@
using JWT.Algorithms; using JWT.Algorithms;
using JWT.Builder; using JWT.Builder;
using JWT.Exceptions; using JWT.Exceptions;
using Logging.Net;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.LogServices;
using UAParser; using UAParser;
namespace Moonlight.App.Services.Sessions; namespace Moonlight.App.Services.Sessions;
@@ -16,8 +14,6 @@ public class IdentityService
{ {
private readonly UserRepository UserRepository; private readonly UserRepository UserRepository;
private readonly CookieService CookieService; private readonly CookieService CookieService;
private readonly SecurityLogService SecurityLogService;
private readonly ErrorLogService ErrorLogService;
private readonly IHttpContextAccessor HttpContextAccessor; private readonly IHttpContextAccessor HttpContextAccessor;
private readonly string Secret; private readonly string Secret;
@@ -27,15 +23,11 @@ public class IdentityService
CookieService cookieService, CookieService cookieService,
UserRepository userRepository, UserRepository userRepository,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
ConfigService configService, ConfigService configService)
SecurityLogService securityLogService,
ErrorLogService errorLogService)
{ {
CookieService = cookieService; CookieService = cookieService;
UserRepository = userRepository; UserRepository = userRepository;
HttpContextAccessor = httpContextAccessor; HttpContextAccessor = httpContextAccessor;
SecurityLogService = securityLogService;
ErrorLogService = errorLogService;
Secret = configService Secret = configService
.GetSection("Moonlight") .GetSection("Moonlight")
@@ -90,15 +82,13 @@ public class IdentityService
} }
catch (SignatureVerificationException) catch (SignatureVerificationException)
{ {
await SecurityLogService.Log(SecurityLogType.ManipulatedJwt, x => Logger.Warn($"Detected a manipulated JWT: {token}", "security");
{
x.Add<string>(token);
});
return null; return null;
} }
catch (Exception e) catch (Exception e)
{ {
await ErrorLogService.Log(e, x => {}); Logger.Error("Error reading jwt");
Logger.Error(e);
return null; return null;
} }
@@ -134,7 +124,8 @@ public class IdentityService
} }
catch (Exception e) catch (Exception e)
{ {
await ErrorLogService.Log(e, x => {}); Logger.Error("Unexpected error while processing token");
Logger.Error(e);
return null; return null;
} }
} }
@@ -159,8 +150,17 @@ public class IdentityService
try try
{ {
var userAgent = HttpContextAccessor.HttpContext.Request.Headers.UserAgent.ToString();
if (userAgent.Contains("Moonlight.App"))
{
var version = userAgent.Remove(0, "Moonlight.App/".Length).Split(' ').FirstOrDefault();
return "Moonlight App " + version;
}
var uaParser = Parser.GetDefault(); var uaParser = Parser.GetDefault();
var info = uaParser.Parse(HttpContextAccessor.HttpContext.Request.Headers.UserAgent); var info = uaParser.Parse(userAgent);
return $"{info.OS} - {info.Device}"; return $"{info.OS} - {info.Device}";
} }

View File

@@ -0,0 +1,38 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Sessions;
public class KeyListenerService
{
private readonly IJSRuntime _jsRuntime;
private DotNetObjectReference<KeyListenerService> _objRef;
public event EventHandler<string> KeyPressed;
public KeyListenerService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task Initialize()
{
_objRef = DotNetObjectReference.Create(this);
await _jsRuntime.InvokeVoidAsync("moonlight.keyListener.register", _objRef);
}
[JSInvokable]
public void OnKeyPress(string key)
{
KeyPressed?.Invoke(this, key);
}
public async ValueTask DisposeAsync()
{
try
{
await _jsRuntime.InvokeVoidAsync("moonlight.keyListener.unregister", _objRef);
_objRef.Dispose();
}
catch (Exception) { /* ignored */}
}
}

View File

@@ -9,6 +9,7 @@ namespace Moonlight.App.Services.Sessions;
public class SessionService public class SessionService
{ {
private readonly SessionRepository SessionRepository; private readonly SessionRepository SessionRepository;
private Repository<User> UserRepository;
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
private readonly NavigationManager NavigationManager; private readonly NavigationManager NavigationManager;
private readonly AlertService AlertService; private readonly AlertService AlertService;
@@ -21,13 +22,15 @@ public class SessionService
IdentityService identityService, IdentityService identityService,
NavigationManager navigationManager, NavigationManager navigationManager,
AlertService alertService, AlertService alertService,
DateTimeService dateTimeService) DateTimeService dateTimeService,
Repository<User> userRepository)
{ {
SessionRepository = sessionRepository; SessionRepository = sessionRepository;
IdentityService = identityService; IdentityService = identityService;
NavigationManager = navigationManager; NavigationManager = navigationManager;
AlertService = alertService; AlertService = alertService;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
UserRepository = userRepository;
} }
public async Task Register() public async Task Register()
@@ -46,6 +49,12 @@ public class SessionService
}; };
SessionRepository.Add(OwnSession); SessionRepository.Add(OwnSession);
if (user != null) // Track last session init of user as last visited timestamp
{
user.LastVisitedAt = DateTimeService.GetCurrent();
UserRepository.Update(user);
}
} }
public void Refresh() public void Refresh()

View File

@@ -9,21 +9,36 @@ public class SmartDeployService
private readonly Repository<CloudPanel> CloudPanelRepository; private readonly Repository<CloudPanel> CloudPanelRepository;
private readonly WebSpaceService WebSpaceService; private readonly WebSpaceService WebSpaceService;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly ConfigService ConfigService;
public SmartDeployService( public SmartDeployService(
NodeRepository nodeRepository, NodeRepository nodeRepository,
NodeService nodeService, NodeService nodeService,
WebSpaceService webSpaceService, WebSpaceService webSpaceService,
Repository<CloudPanel> cloudPanelRepository) Repository<CloudPanel> cloudPanelRepository,
ConfigService configService)
{ {
NodeRepository = nodeRepository; NodeRepository = nodeRepository;
NodeService = nodeService; NodeService = nodeService;
WebSpaceService = webSpaceService; WebSpaceService = webSpaceService;
CloudPanelRepository = cloudPanelRepository; CloudPanelRepository = cloudPanelRepository;
ConfigService = configService;
} }
public async Task<Node?> GetNode() public async Task<Node?> GetNode()
{ {
var config = ConfigService
.GetSection("Moonlight")
.GetSection("SmartDeploy")
.GetSection("Server");
if (config.GetValue<bool>("EnableOverride"))
{
var nodeId = config.GetValue<int>("OverrideNode");
return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId);
}
var data = new Dictionary<Node, double>(); var data = new Dictionary<Node, double>();
foreach (var node in NodeRepository.Get().ToArray()) foreach (var node in NodeRepository.Get().ToArray())
@@ -59,17 +74,17 @@ public class SmartDeployService
try try
{ {
var cpuStats = await NodeService.GetCpuStats(node); var cpuMetrics = await NodeService.GetCpuMetrics(node);
var memoryStats = await NodeService.GetMemoryStats(node); var memoryMetrics = await NodeService.GetMemoryMetrics(node);
var diskStats = await NodeService.GetDiskStats(node); var diskMetrics = await NodeService.GetDiskMetrics(node);
var cpuWeight = 0.5; // Weight of CPU usage in the final score var cpuWeight = 0.5; // Weight of CPU usage in the final score
var memoryWeight = 0.3; // Weight of memory usage in the final score var memoryWeight = 0.3; // Weight of memory usage in the final score
var diskSpaceWeight = 0.2; // Weight of free disk space in the final score var diskSpaceWeight = 0.2; // Weight of free disk space in the final score
var cpuScore = (1 - cpuStats.Usage) * cpuWeight; // CPU score is based on the inverse of CPU usage var cpuScore = (1 - cpuMetrics.CpuUsage) * cpuWeight; // CPU score is based on the inverse of CPU usage
var memoryScore = (1 - (memoryStats.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory var memoryScore = (1 - (memoryMetrics.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory
var diskSpaceScore = (double) diskStats.FreeBytes / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB var diskSpaceScore = (double) (diskMetrics.Total - diskMetrics.Used) / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB
var finalScore = cpuScore + memoryScore + diskSpaceScore; var finalScore = cpuScore + memoryScore + diskSpaceScore;

View File

@@ -78,7 +78,7 @@ public class SmartTranslateService
} }
catch (Exception ex) catch (Exception ex)
{ {
Logging.Net.Logger.Error(ex); Logger.Error(ex);
return key; return key;
} }
} }

View File

@@ -1,6 +1,5 @@
using Logging.Net; using Moonlight.App.Database.Entities;
using Moonlight.App.Database; using Moonlight.App.Helpers;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Sessions;
@@ -66,8 +65,6 @@ public class StatisticsCaptureService
AddEntry("databasesCount", databasesRepo.Get().Count()); AddEntry("databasesCount", databasesRepo.Get().Count());
AddEntry("sessionsCount", sessionService.GetAll().Length); AddEntry("sessionsCount", sessionService.GetAll().Length);
} }
Logger.Log("Statistics are weird");
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -7,20 +7,33 @@ namespace Moonlight.App.Services.Statistics;
public class StatisticsViewService public class StatisticsViewService
{ {
private readonly StatisticsRepository StatisticsRepository; private readonly StatisticsRepository StatisticsRepository;
private readonly Repository<User> UserRepository;
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService) public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService, Repository<User> userRepository)
{ {
StatisticsRepository = statisticsRepository; StatisticsRepository = statisticsRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
UserRepository = userRepository;
} }
public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan) public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan)
{ {
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan); var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
var objs = StatisticsRepository.Get().Where(x => x.Date > startDate && x.Chart == chart); var objs = StatisticsRepository
.Get()
.Where(x => x.Date > startDate && x.Chart == chart);
return objs.ToArray(); return objs.ToArray();
} }
public int GetActiveUsers(StatisticsTimeSpan timeSpan)
{
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
return UserRepository
.Get()
.Count(x => x.LastVisitedAt > startDate);
}
} }

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Forms;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;

View File

@@ -1,7 +1,4 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Repositories;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Sessions;
using OtpNet; using OtpNet;
@@ -11,16 +8,13 @@ public class TotpService
{ {
private readonly IdentityService IdentityService; private readonly IdentityService IdentityService;
private readonly UserRepository UserRepository; private readonly UserRepository UserRepository;
private readonly AuditLogService AuditLogService;
public TotpService( public TotpService(
IdentityService identityService, IdentityService identityService,
UserRepository userRepository, UserRepository userRepository)
AuditLogService auditLogService)
{ {
IdentityService = identityService; IdentityService = identityService;
UserRepository = userRepository; UserRepository = userRepository;
AuditLogService = auditLogService;
} }
public Task<bool> Verify(string secret, string code) public Task<bool> Verify(string secret, string code)
@@ -52,10 +46,7 @@ public class TotpService
UserRepository.Update(user); UserRepository.Update(user);
await AuditLogService.Log(AuditLogType.EnableTotp, x => //TODO: AuditLog
{
x.Add<User>(user.Email);
});
} }
public async Task EnforceTotpLogin() public async Task EnforceTotpLogin()
@@ -74,10 +65,7 @@ public class TotpService
UserRepository.Update(user); UserRepository.Update(user);
await AuditLogService.Log(AuditLogType.DisableTotp,x => //TODO: AuditLog
{
x.Add<User>(user.Email);
});
} }
private string GenerateSecret() private string GenerateSecret()

Some files were not shown because too many files have changed in this diff Show More