129 Commits
v1b1 ... v1b11

Author SHA1 Message Date
Marcel Baumgartner
25b47d8b6c Merge pull request #199 from Moonlight-Panel/AddNewGermanTranslation
Added new german translations
2023-06-29 23:10:21 +02:00
Marcel Baumgartner
85f5b8a7da Added new german translations 2023-06-29 23:09:58 +02:00
Marcel Baumgartner
ab9333f99a Merge pull request #198 from Moonlight-Panel/FixServerDelete
Fixed server delete
2023-06-26 22:51:17 +02:00
Marcel Baumgartner
d60f8fc905 Fixed server delete 2023-06-26 22:50:45 +02:00
Marcel Baumgartner
fe1f4412d8 Merge pull request #197 from Moonlight-Panel/AddedRetry
Added retry class and added it to some api calls that need it
2023-06-26 22:45:56 +02:00
Marcel Baumgartner
f191533410 Added retry class and added it to some api calls that need it 2023-06-26 22:45:33 +02:00
Marcel Baumgartner
a894707536 Merge pull request #196 from Moonlight-Panel/SmallBugFixes
Small bug fixes
2023-06-26 18:05:58 +02:00
Marcel Baumgartner
d2ccc84286 Fixed sidebar text sizing issues 2023-06-26 17:23:25 +02:00
Marcel Baumgartner
f2ec43f2d2 Fixed not enabled installing screen for new servers 2023-06-26 17:19:54 +02:00
Marcel Baumgartner
7feccc8d9f Fixed error loop for fileaccess providers not supporting the launch url 2023-06-26 17:19:32 +02:00
Marcel Baumgartner
a8bd1193ce Merge pull request #195 from Moonlight-Panel/SecurityPatches
Security patches
2023-06-26 00:10:03 +02:00
Marcel Baumgartner
366d1a9205 Merge pull request #194 from Moonlight-Panel/AddStreamerModeAndFixUI
Added streamer mode and fixed security settings ui
2023-06-26 00:07:23 +02:00
Marcel Baumgartner
df9ed95c6b Added streamer mode and fixed security settings ui 2023-06-26 00:06:44 +02:00
Marcel Baumgartner
23a211362e Merge pull request #193 from Moonlight-Panel/EnhanceFileManager
Enhanced winscp button and new file/folder menu
2023-06-25 17:32:57 +02:00
Marcel Baumgartner
cf91d44902 Enhanced winscp button and new file/folder menu 2023-06-25 17:31:36 +02:00
Marcel Baumgartner
35633e21a9 Merge pull request #192 from Moonlight-Panel/EnhanceServerListLayout
Enhanced server list
2023-06-25 00:01:53 +02:00
Marcel Baumgartner
ce8b8f6798 Enhanced server list 2023-06-25 00:01:28 +02:00
Marcel Baumgartner
c28c80ba25 Merge pull request #191 from Moonlight-Panel/FixDnsManager
Fixed dns loading issues. Added udp support
2023-06-24 23:45:49 +02:00
Marcel Baumgartner
da17b1df93 Fixed dns loading issues. Added udp support 2023-06-24 23:45:29 +02:00
Marcel Baumgartner
f9f5865ef9 Prevent user locking when duplicating the email entries 2023-06-24 22:35:38 +02:00
Marcel Baumgartner
389ded9b77 Fixed oauth2 account spoofing using unverified discord accounts for claiming identity 2023-06-24 22:15:04 +02:00
Marcel Baumgartner
faebaa59dd Merge pull request #189 from Moonlight-Panel/AddCustomLayoutServerList
Added custom layout options for the server list
2023-06-24 02:35:21 +02:00
Marcel Baumgartner
6b7dc2ad05 Added custom layout options for the server list 2023-06-24 02:35:01 +02:00
Marcel Baumgartner
e356c9d0c8 Update README.md 2023-06-23 05:06:12 +02:00
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
Marcel Baumgartner
c6cf11626e Merge pull request #139 from Moonlight-Panel/ImproveConsoleStreaming
Improve console streaming
2023-06-04 21:42:13 +02:00
Marcel Baumgartner
233c304b3c Fixed error when closing a failed websocket connection 2023-06-04 21:41:15 +02:00
Marcel Baumgartner
343e527fb6 Added message cache clear for console streaming 2023-06-04 20:56:47 +02:00
Daniel Balk
25da3c233e Merge pull request #138 from Dannyx1604/main
Ein paar kleine Änderungen ;-)
2023-06-03 12:48:43 +02:00
Dannyx
d7fb3382f7 Ein paar kleine Änderungen ;-) 2023-06-03 09:06:22 +02:00
Marcel Baumgartner
88c9f5372d Merge pull request #137 from Moonlight-Panel/AddConsoleStreamingDispose
Add console streaming dispose
2023-06-01 00:44:56 +02:00
Marcel Baumgartner
7128a7f8a7 Add console streaming dispose 2023-06-01 00:44:14 +02:00
Marcel Baumgartner
6e4f1c1dbd Merge pull request #135 from Moonlight-Panel/FixUnarchiveCrash
Fixed decompress issue (hopefully)
2023-05-28 04:33:30 +02:00
Marcel Baumgartner
3527bc1bd5 Fixed decompress issue (hopefully) 2023-05-28 04:33:11 +02:00
Marcel Baumgartner
c0068b58d7 Merge pull request #134 from Moonlight-Panel/AddNewConsoleStreaming
Added new console streaming
2023-05-28 04:27:26 +02:00
Marcel Baumgartner
feec9426b9 Added new console streaming 2023-05-28 04:27:00 +02:00
Marcel Baumgartner
a180cfa31d Merge pull request #133 from Moonlight-Panel/PatchEventSystem
Patched event system
2023-05-28 03:40:43 +02:00
Marcel Baumgartner
b270b48ac1 Patched event system. Storage issues when using the support chat should be fixed 2023-05-28 03:39:36 +02:00
Marcel Baumgartner
a92c34b47f Merge pull request #132 from Moonlight-Panel/AddDiscordLinkSettings
Implemented new discord linking system
2023-05-26 17:14:28 +02:00
Marcel Baumgartner
ac3bdba3e8 Implemented new discord linking system 2023-05-26 17:14:06 +02:00
Marcel Baumgartner
93328b8b88 Merge pull request #131 from Moonlight-Panel/UIFixes
Added new beta server list ui and added days to uptime formatter
2023-05-26 15:21:16 +02:00
Marcel Baumgartner
800f9fbb50 Added new beta server list ui and added days to uptime formatter 2023-05-26 15:18:59 +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
221 changed files with 11359 additions and 2968 deletions

1
.gitattributes vendored
View File

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

1
.gitignore vendored
View File

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

View File

@@ -1,5 +1,4 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Newtonsoft.Json;
using RestSharp;
@@ -14,26 +13,13 @@ public class DaemonApiHelper
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)
{
RestRequest request = new(GetApiUrl(node) + resource);
var request = await CreateRequest(node, resource);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", node.Token);
var response = await Client.GetAsync(request);
request.Method = Method.Get;
var response = await Client.ExecuteAsync(request);
if (!response.IsSuccessful)
{
@@ -52,4 +38,69 @@ public class DaemonApiHelper
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<ImageVariable> Variables { get; set; } = new();
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 bool Installing { 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<ServerBackup> Backups { get; set; } = new();

View File

@@ -23,7 +23,9 @@ public class User
public string State { get; set; } = "";
public string Country { get; set; } = "";
public string ServerListLayoutJson { get; set; } = "";
// States
public UserStatus Status { get; set; } = UserStatus.Unverified;
@@ -31,6 +33,7 @@ public class User
public bool SupportPending { get; set; } = false;
public bool HasRated { get; set; } = false;
public int Rating { get; set; } = 0;
public bool StreamerMode { get; set; } = false;
// Security
public bool TotpEnabled { get; set; } = false;
@@ -43,6 +46,7 @@ public class User
// Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastVisitedAt { get; set; } = DateTime.UtcNow;
// Subscriptions

View File

@@ -1,6 +1,6 @@
using System.Data.Common;
using Logging.Net;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Moonlight.App.Helpers;
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");
}
}
}

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 AddedServerListLayoutToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ServerListLayoutJson",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ServerListLayoutJson",
table: "Users");
}
}
}

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 AddedStreamerMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "StreamerMode",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "StreamerMode",
table: "Users");
}
}
}

View File

@@ -132,6 +132,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Allocations")
.HasColumnType("int");
b.Property<string>("BackgroundImageUrl")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ConfigFiles")
.IsRequired()
.HasColumnType("longtext");
@@ -492,6 +496,9 @@ namespace Moonlight.App.Database.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("ArchiveId")
.HasColumnType("int");
b.Property<int>("Cpu")
.HasColumnType("int");
@@ -507,6 +514,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("Installing")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsCleanupException")
.HasColumnType("tinyint(1)");
@@ -538,6 +548,8 @@ namespace Moonlight.App.Database.Migrations
b.HasKey("Id");
b.HasIndex("ArchiveId");
b.HasIndex("ImageId");
b.HasIndex("MainAllocationId");
@@ -758,6 +770,9 @@ namespace Moonlight.App.Database.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("LastVisitedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
@@ -765,6 +780,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<string>("ServerListLayoutJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("State")
.IsRequired()
.HasColumnType("longtext");
@@ -772,6 +791,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Status")
.HasColumnType("int");
b.Property<bool>("StreamerMode")
.HasColumnType("tinyint(1)");
b.Property<int>("SubscriptionDuration")
.HasColumnType("int");
@@ -928,6 +950,10 @@ namespace Moonlight.App.Database.Migrations
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")
.WithMany()
.HasForeignKey("ImageId")
@@ -950,6 +976,8 @@ namespace Moonlight.App.Database.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
b.Navigation("Image");
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,11 +1,10 @@
using System.Diagnostics;
using Logging.Net;
using Moonlight.App.Helpers;
namespace Moonlight.App.Events;
public class EventSystem
{
private Dictionary<int, object> Storage = new();
private List<Subscriber> Subscribers = new();
private readonly bool Debug = false;
@@ -33,16 +32,8 @@ public class EventSystem
return Task.CompletedTask;
}
public Task Emit(string id, object? d = null)
public Task Emit(string id, object? data = null)
{
int hashCode = -1;
if (d != null)
{
hashCode = d.GetHashCode();
Storage.TryAdd(hashCode, d);
}
Subscriber[] subscribers;
lock (Subscribers)
@@ -58,23 +49,6 @@ public class EventSystem
{
tasks.Add(new Task(() =>
{
int storageId = hashCode + 0; // To create a copy of the hash code
object? data = null;
if (storageId != -1)
{
if (Storage.TryGetValue(storageId, out var value))
{
data = value;
}
else
{
Logger.Warn($"Object with the hash '{storageId}' was not present in the storage");
return;
}
}
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -115,8 +89,7 @@ public class EventSystem
Task.Run(() =>
{
Task.WaitAll(tasks.ToArray());
Storage.Remove(hashCode);
if(Debug)
Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage");
});
@@ -139,4 +112,22 @@ public class EventSystem
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]
public class DisplayException : Exception
{
public bool DoNotTranslate { get; set; } = false;
//
// 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
@@ -19,6 +21,11 @@ public class DisplayException : Exception
public DisplayException(string message) : base(message)
{
}
public DisplayException(string message, bool doNotTranslate) : base(message)
{
DoNotTranslate = doNotTranslate;
}
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 Logging.Net;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database;
using Moonlight.App.Services;
@@ -82,19 +81,32 @@ public class DatabaseCheckupService
Logger.Info($"Saving it to: {file}");
Logger.Info("Starting backup...");
var sw = new Stopwatch();
sw.Start();
try
{
var sw = new Stopwatch();
sw.Start();
await using MySqlConnection conn = new MySqlConnection(connectionString);
await using MySqlCommand cmd = new MySqlCommand();
using MySqlBackup mb = new MySqlBackup(cmd);
await using MySqlConnection conn = new MySqlConnection(connectionString);
await using MySqlCommand cmd = new MySqlCommand();
using MySqlBackup mb = new MySqlBackup(cmd);
cmd.Connection = conn;
await conn.OpenAsync();
mb.ExportToFile(file);
await conn.CloseAsync();
cmd.Connection = conn;
await conn.OpenAsync();
mb.ExportToFile(file);
await conn.CloseAsync();
sw.Stop();
Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s");
sw.Stop();
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;
namespace Moonlight.App.Helpers.Files;

View File

@@ -3,6 +3,7 @@ using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Requests;
using Moonlight.App.ApiClients.Wings.Resources;
using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Services;
using RestSharp;
@@ -211,13 +212,27 @@ public class WingsFileAccess : FileAccess
public override async Task Decompress(FileData fileData)
{
var req = new DecompressFile()
try
{
Root = CurrentPath,
File = fileData.Name
};
var req = new DecompressFile()
{
Root = CurrentPath,
File = fileData.Name
};
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req);
await WingsApiHelper.Post(Server.Node, $"api/servers/{Server.Uuid}/files/decompress", req);
}
catch (Exception e)
{
if (e.Message.ToLower().Contains("canceled"))
{
// ignore, maybe do smth better here, like showing a waiting thing or so
}
else
{
throw;
}
}
}
public override Task<string> GetLaunchUrl()

View File

@@ -8,7 +8,26 @@ public static class Formatter
{
TimeSpan t = TimeSpan.FromMilliseconds(uptime);
return $"{t.Hours}h {t.Minutes}m {t.Seconds}s";
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";
}
}
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)
@@ -109,4 +128,12 @@ public static class Formatter
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.Runtime.InteropServices;
using Logging.Net;
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 Logging.Net;
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
{

View File

@@ -0,0 +1,66 @@
namespace Moonlight.App.Helpers;
public class Retry
{
private List<Type> RetryExceptionTypes;
private List<Func<Exception, bool>> RetryFilters;
private int RetryTimes = 1;
public Retry()
{
RetryExceptionTypes = new();
RetryFilters = new();
}
public Retry Times(int times)
{
RetryTimes = times;
return this;
}
public Retry At(Func<Exception, bool> filter)
{
RetryFilters.Add(filter);
return this;
}
public Retry At<T>()
{
RetryExceptionTypes.Add(typeof(T));
return this;
}
public async Task Call(Func<Task> method)
{
int triesLeft = RetryTimes;
do
{
try
{
await method.Invoke();
return;
}
catch (Exception e)
{
if(triesLeft < 1) // Throw if no tries left
throw;
if (RetryExceptionTypes.Any(x => x.FullName == e.GetType().FullName))
{
triesLeft--;
continue;
}
if (RetryFilters.Any(x => x.Invoke(e)))
{
triesLeft--;
continue;
}
// Throw if not filtered -> unknown/unhandled
throw;
}
} while (triesLeft >= 0);
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Data;
public class ConsoleMessage
{
public string Content { get; set; } = "";
public bool IsInternal { get; set; } = false;
}

View File

@@ -0,0 +1,36 @@
using Newtonsoft.Json;
namespace Moonlight.App.Helpers.Wings.Data;
public class ServerResource
{
[JsonProperty("memory_bytes")]
public long MemoryBytes { get; set; }
[JsonProperty("memory_limit_bytes")]
public long MemoryLimitBytes { get; set; }
[JsonProperty("cpu_absolute")]
public float CpuAbsolute { get; set; }
[JsonProperty("network")]
public NetworkData Network { get; set; }
[JsonProperty("uptime")]
public double Uptime { get; set; }
[JsonProperty("state")]
public string State { get; set; }
[JsonProperty("disk_bytes")]
public long DiskBytes { get; set; }
public class NetworkData
{
[JsonProperty("rx_bytes")]
public long RxBytes { get; set; }
[JsonProperty("tx_bytes")]
public long TxBytes { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace Moonlight.App.Helpers.Wings.Enums;
public enum ConsoleState
{
Disconnected,
Connecting,
Connected
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.App.Helpers.Wings.Enums;
public enum ServerState
{
Starting,
Running,
Stopping,
Offline,
Installing
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Events;
public class BaseEvent
{
public string Event { get; set; } = "";
public string[] Args { get; set; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Helpers.Wings.Events;
public class SendTokenEvent
{
public string Event { get; set; } = "auth";
public List<string> Args = new();
}

View File

@@ -0,0 +1,389 @@
using System.Net.WebSockets;
using System.Text;
using Moonlight.App.Helpers.Wings.Data;
using Moonlight.App.Helpers.Wings.Enums;
using Moonlight.App.Helpers.Wings.Events;
using Newtonsoft.Json;
using ConsoleMessage = Moonlight.App.Helpers.Wings.Data.ConsoleMessage;
namespace Moonlight.App.Helpers.Wings;
public class WingsConsole : IDisposable
{
private ClientWebSocket WebSocket;
public List<ConsoleMessage> Messages;
private Task? ConsoleTask;
private string Socket = "";
private string Origin = "";
private string Token = "";
private bool Disconnecting;
public ConsoleState ConsoleState { get; private set; }
public ServerState ServerState { get; private set; }
public ServerResource Resource { get; private set; }
public EventHandler<ConsoleState> OnConsoleStateUpdated { get; set; }
public EventHandler<ServerState> OnServerStateUpdated { get; set; }
public EventHandler<ServerResource> OnResourceUpdated { get; set; }
public EventHandler<ConsoleMessage> OnMessage { get; set; }
public Func<WingsConsole, Task<string>> OnRequestNewToken { get; set; }
public WingsConsole()
{
ConsoleState = ConsoleState.Disconnected;
ServerState = ServerState.Offline;
Messages = new();
Resource = new()
{
Network = new()
{
RxBytes = 0,
TxBytes = 0
},
State = "offline",
Uptime = 0,
CpuAbsolute = 0,
DiskBytes = 0,
MemoryBytes = 0,
MemoryLimitBytes = 0
};
}
public Task Connect(string origin, string socket, string token)
{
Disconnecting = false;
WebSocket = new();
ConsoleState = ConsoleState.Disconnected;
ServerState = ServerState.Offline;
Messages = new();
Resource = new()
{
Network = new()
{
RxBytes = 0,
TxBytes = 0
},
State = "offline",
Uptime = 0,
CpuAbsolute = 0,
DiskBytes = 0,
MemoryBytes = 0,
MemoryLimitBytes = 0
};
Socket = socket;
Origin = origin;
Token = token;
WebSocket.Options.SetRequestHeader("Origin", Origin);
WebSocket.Options.SetRequestHeader("Authorization", "Bearer " + Token);
ConsoleTask = Task.Run(async () =>
{
try
{
await Work();
}
catch (Exception e)
{
Logger.Warn("Error connecting to wings console");
Logger.Warn(e);
}
});
return Task.CompletedTask;
}
private async Task Work()
{
await UpdateConsoleState(ConsoleState.Connecting);
await WebSocket.ConnectAsync(
new Uri(Socket),
CancellationToken.None
);
if (WebSocket.State != WebSocketState.Connecting && WebSocket.State != WebSocketState.Open)
{
await SaveMessage("Unable to connect to websocket", true);
await UpdateConsoleState(ConsoleState.Disconnected);
return;
}
await UpdateConsoleState(ConsoleState.Connected);
await Send(new SendTokenEvent()
{
Args = { Token }
});
while (WebSocket.State == WebSocketState.Open)
{
try
{
var raw = await ReceiveRaw();
if(string.IsNullOrEmpty(raw))
continue;
var eventData = JsonConvert.DeserializeObject<BaseEvent>(raw);
if (eventData == null)
{
await SaveMessage("Unable to parse event", true);
continue;
}
switch (eventData.Event)
{
case "jwt error":
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
WebSocket.Dispose();
}
await UpdateServerState(ServerState.Offline);
await UpdateConsoleState(ConsoleState.Disconnected);
await SaveMessage("Received a jwt error. Disconnected", true);
break;
case "token expired":
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
WebSocket.Dispose();
}
await UpdateServerState(ServerState.Offline);
await UpdateConsoleState(ConsoleState.Disconnected);
await SaveMessage("Token expired", true);
break;
case "token expiring":
await SaveMessage("Token will expire soon. Generating a new one", true);
Token = await OnRequestNewToken.Invoke(this);
await Send(new SendTokenEvent()
{
Args = { Token }
});
break;
case "auth success":
// Send intents
await SendRaw("{\"event\":\"send logs\",\"args\":[null]}");
await SendRaw("{\"event\":\"send stats\",\"args\":[null]}");
break;
case "stats":
var stats = JsonConvert.DeserializeObject<ServerResource>(eventData.Args[0]);
if (stats == null)
break;
var serverState = ParseServerState(stats.State);
if (ServerState != serverState)
await UpdateServerState(serverState);
await UpdateResource(stats);
break;
case "status":
var serverStateParsed = ParseServerState(eventData.Args[0]);
if (ServerState != serverStateParsed)
await UpdateServerState(serverStateParsed);
break;
case "console output":
foreach (var line in eventData.Args)
{
await SaveMessage(line);
}
break;
case "install output":
foreach (var line in eventData.Args)
{
await SaveMessage(line);
}
break;
case "daemon message":
foreach (var line in eventData.Args)
{
await SaveMessage(line);
}
break;
case "install started":
await UpdateServerState(ServerState.Installing);
break;
case "install completed":
await UpdateServerState(ServerState.Offline);
break;
}
}
catch(JsonReaderException){}
catch (Exception e)
{
if (!Disconnecting)
{
Logger.Warn("Error while performing websocket actions");
Logger.Warn(e);
await SaveMessage("A unknown error occured while processing websocket", true);
}
}
}
}
private Task UpdateConsoleState(ConsoleState consoleState)
{
ConsoleState = consoleState;
OnConsoleStateUpdated?.Invoke(this, consoleState);
return Task.CompletedTask;
}
private Task UpdateServerState(ServerState serverState)
{
ServerState = serverState;
OnServerStateUpdated?.Invoke(this, serverState);
return Task.CompletedTask;
}
private Task UpdateResource(ServerResource resource)
{
Resource = resource;
OnResourceUpdated?.Invoke(this, Resource);
return Task.CompletedTask;
}
private Task SaveMessage(string content, bool internalMessage = false)
{
var msg = new ConsoleMessage()
{
Content = content,
IsInternal = internalMessage
};
lock (Messages)
{
Messages.Add(msg);
}
OnMessage?.Invoke(this, msg);
return Task.CompletedTask;
}
private ServerState ParseServerState(string raw)
{
switch (raw)
{
case "offline":
return ServerState.Offline;
case "starting":
return ServerState.Starting;
case "running":
return ServerState.Running;
case "stopping":
return ServerState.Stopping;
case "installing":
return ServerState.Installing;
default:
return ServerState.Offline;
}
}
public async Task EnterCommand(string content)
{
if (ConsoleState == ConsoleState.Connected)
{
await SendRaw("{\"event\":\"send command\",\"args\":[\"" + content + "\"]}");
}
}
public async Task SetPowerState(string state)
{
if (ConsoleState == ConsoleState.Connected)
{
await SendRaw("{\"event\":\"set state\",\"args\":[\"" + state + "\"]}");
}
}
private async Task Send(object data)
{
await SendRaw(JsonConvert.SerializeObject(data));
}
private async Task SendRaw(string data)
{
if (WebSocket.State == WebSocketState.Open)
{
byte[] byteContentBuffer = Encoding.UTF8.GetBytes(data);
await WebSocket.SendAsync(new ArraySegment<byte>(byteContentBuffer), WebSocketMessageType.Text, true,
CancellationToken.None);
}
}
private async Task<string> ReceiveRaw()
{
ArraySegment<byte> receivedBytes = new ArraySegment<byte>(new byte[1024]);
WebSocketReceiveResult result = await WebSocket.ReceiveAsync(receivedBytes, CancellationToken.None);
return Encoding.UTF8.GetString(receivedBytes.Array!, 0, result.Count);
}
public async Task Disconnect()
{
Disconnecting = true;
Messages.Clear();
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
await WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None);
WebSocket.Dispose();
}
if(ConsoleTask != null && ConsoleTask.IsCompleted)
ConsoleTask.Dispose();
}
public void Dispose()
{
Disconnecting = true;
Messages.Clear();
if (WebSocket != null)
{
if (WebSocket.State == WebSocketState.Connecting || WebSocket.State == WebSocketState.Open)
WebSocket.CloseAsync(WebSocketCloseStatus.Empty, null, CancellationToken.None).Wait();
WebSocket.Dispose();
}
if(ConsoleTask != null && ConsoleTask.IsCompleted)
ConsoleTask.Dispose();
}
}

View File

@@ -7,37 +7,34 @@ using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers.Wings;
public class WingsConsoleHelper
{
private readonly ServerRepository ServerRepository;
private readonly WingsJwtHelper WingsJwtHelper;
private readonly string AppUrl;
public WingsConsoleHelper(
ServerRepository serverRepository,
ConfigService configService,
WingsJwtHelper wingsJwtHelper)
ConfigService configService)
{
ServerRepository = serverRepository;
WingsJwtHelper = wingsJwtHelper;
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
}
public async Task ConnectWings(PteroConsole.NET.PteroConsole pteroConsole, Server server)
public async Task ConnectWings(WingsConsole console, Server server)
{
var serverData = ServerRepository
.Get()
.Include(x => x.Node)
.First(x => x.Id == server.Id);
var token = GenerateToken(serverData);
var token = await GenerateToken(serverData);
if (serverData.Node.Ssl)
{
await pteroConsole.Connect(
await console.Connect(
AppUrl,
$"wss://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
token
@@ -45,7 +42,7 @@ public class WingsConsoleHelper
}
else
{
await pteroConsole.Connect(
await console.Connect(
AppUrl,
$"ws://{serverData.Node.Fqdn}:{serverData.Node.HttpPort}/api/servers/{serverData.Uuid}/ws",
token
@@ -53,20 +50,20 @@ public class WingsConsoleHelper
}
}
public string GenerateToken(Server server)
public async Task<string> GenerateToken(Server server)
{
var serverData = ServerRepository
.Get()
.Include(x => x.Node)
.First(x => x.Id == server.Id);
var userid = 1;
var secret = serverData.Node.Token;
using (MD5 md5 = MD5.Create())
{
var inputBytes = Encoding.ASCII.GetBytes(userid + serverData.Uuid.ToString());
var inputBytes = Encoding.ASCII.GetBytes(userid + server.Uuid.ToString());
var outputBytes = md5.ComputeHash(inputBytes);
var identifier = Convert.ToHexString(outputBytes).ToLower();
@@ -77,7 +74,7 @@ public class WingsConsoleHelper
.WithAlgorithm(new HMACSHA256Algorithm())
.WithSecret(secret)
.AddClaim("user_id", userid)
.AddClaim("server_uuid", serverData.Uuid.ToString())
.AddClaim("server_uuid", server.Uuid.ToString())
.AddClaim("permissions", new[]
{
"*",

View File

@@ -4,7 +4,7 @@ using JWT.Algorithms;
using JWT.Builder;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers.Wings;
public class WingsJwtHelper
{

View File

@@ -5,7 +5,7 @@ using Moonlight.App.Http.Resources.Wings;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
namespace Moonlight.App.Helpers;
namespace Moonlight.App.Helpers.Wings;
public class WingsServerConverter
{

View File

@@ -102,7 +102,7 @@ public class DiscordBotController : Controller
return BadRequest();
}
[HttpGet("{id}/servers/{uuid}")]
[HttpGet("{id}/servers/{uuid}/details")]
public async Task<ActionResult<ServerDetails>> GetServerDetails(ulong id, Guid uuid)
{
if (!await IsAuth(Request))
@@ -123,6 +123,33 @@ public class DiscordBotController : Controller
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)
{

View File

@@ -2,145 +2,166 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Notification;
using Moonlight.App.Models.Notifications;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
using Moonlight.App.Services.Notifications;
using Moonlight.App.Services.Sessions;
using Newtonsoft.Json;
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 bool active = true;
private bool isAuth = false;
private WebSocket WebSocket;
private NotificationClient Client;
private readonly IdentityService IdentityService;
private readonly NotificationRepository NotificationRepository;
private readonly OneTimeJwtService OneTimeJwtService;
private readonly NotificationClientService NotificationClientService;
private readonly NotificationServerService NotificationServerService;
private CancellationTokenSource CancellationTokenSource = new();
public ListenController(IdentityService identityService,
NotificationRepository notificationRepository,
OneTimeJwtService oneTimeJwtService,
NotificationClientService notificationClientService,
NotificationServerService notificationServerService)
private User? CurrentUser;
private readonly OneTimeJwtService OneTimeJwtService;
private readonly NotificationServerService NotificationServerService;
private readonly Repository<NotificationClient> NotificationClientRepository;
public ListenController(
OneTimeJwtService oneTimeJwtService,
NotificationServerService notificationServerService, Repository<NotificationClient> notificationClientRepository)
{
IdentityService = identityService;
NotificationRepository = notificationRepository;
OneTimeJwtService = oneTimeJwtService;
NotificationClientService = notificationClientService;
NotificationServerService = notificationServerService;
NotificationClientRepository = notificationClientRepository;
}
[Route("/api/moonlight/notifications/listen")]
public async Task Get()
public async Task<ActionResult> Get()
{
if (HttpContext.WebSockets.IsWebSocketRequest)
{
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
ws = webSocket;
await Echo();
WebSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
await ProcessWebsocket();
return new EmptyResult();
}
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];
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))
try
{
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');
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();
}
active = ws.State == WebSocketState.Open;
}
await NotificationServerService.UnRegisterClient(Client);
}
private async Task HandleRequest(string text, string action)
{
if (!isAuth && 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)
if (CurrentUser == null && action != "login")
{
await Send("{\"error\": \"Unauthorised\"}");
}
switch (action)
{
case "login":
await Login(text);
break;
case "received":
await Received(text);
break;
case "read":
await Read(text);
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)
{
var jwt = JsonConvert.DeserializeObject<Login>(json).token;
var dict = await OneTimeJwtService.Validate(jwt);
var loginModel = JsonConvert.DeserializeObject<Login>(json) ?? new();
var dict = await OneTimeJwtService.Validate(loginModel.Token);
if (dict == null)
{
string error = "{\"status\":false}";
var bytes = Encoding.UTF8.GetBytes(error);
await ws.SendAsync(bytes, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
await Send("{\"status\":false}");
return;
}
var _clientId = dict["clientId"];
var clientId = int.Parse(_clientId);
if (!int.TryParse(dict["clientId"], out int 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;
await InitWebsocket();
string success = "{\"status\":true}";
var byt = Encoding.UTF8.GetBytes(success);
await ws.SendAsync(byt, WebSocketMessageType.Text, WebSocketMessageFlags.EndOfMessage, CancellationToken.None);
}
CurrentUser = Client.User;
private async Task InitWebsocket()
{
NotificationClientService.listenController = this;
NotificationClientService.WebsocketReady(Client);
await NotificationServerService.RegisterClient(WebSocket, Client);
isAuth = true;
await Send("{\"status\":true}");
}
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
}
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,
JsonConvert.SerializeObject(new NotificationById() {Action = "hide", notification = id}));
await NotificationServerService.SendAction(
CurrentUser!,
JsonConvert.SerializeObject(
new NotificationById()
{
Action = "hide", Notification = model.Notification
}
)
);
}
}

View File

@@ -1,6 +1,7 @@
using Logging.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers;
using Moonlight.App.Services;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -11,12 +12,41 @@ public class OAuth2Controller : Controller
private readonly UserService UserService;
private readonly OAuth2Service OAuth2Service;
private readonly DateTimeService DateTimeService;
private readonly IdentityService IdentityService;
public OAuth2Controller(UserService userService, OAuth2Service oAuth2Service, DateTimeService dateTimeService)
public OAuth2Controller(
UserService userService,
OAuth2Service oAuth2Service,
DateTimeService dateTimeService,
IdentityService identityService)
{
UserService = userService;
OAuth2Service = oAuth2Service;
DateTimeService = dateTimeService;
IdentityService = identityService;
}
[HttpGet("{id}/start")]
public async Task<ActionResult> Start([FromRoute] string id)
{
try
{
if (OAuth2Service.Providers.ContainsKey(id))
{
return Redirect(await OAuth2Service.GetUrl(id));
}
Logger.Warn($"Someone tried to start an oauth2 flow using the id '{id}' which is not registered");
return Redirect("/");
}
catch (Exception e)
{
Logger.Warn($"Error starting oauth2 flow for id: {id}");
Logger.Warn(e);
return Redirect("/");
}
}
[HttpGet("{id}")]
@@ -24,6 +54,18 @@ public class OAuth2Controller : Controller
{
try
{
var currentUser = await IdentityService.Get();
if (currentUser != null)
{
if (await OAuth2Service.CanBeLinked(id))
{
await OAuth2Service.LinkToUser(id, currentUser, code);
return Redirect("/profile");
}
}
var user = await OAuth2Service.HandleCode(id, code);
Response.Cookies.Append("token", await UserService.GenerateToken(user), new()

View File

@@ -1,12 +1,6 @@
using System.Text;
using Logging.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
using Moonlight.App.Services.Files;
using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -14,16 +8,11 @@ namespace Moonlight.App.Http.Controllers.Api.Moonlight;
[Route("api/moonlight/resources")]
public class ResourcesController : Controller
{
private readonly SecurityLogService SecurityLogService;
private readonly BucketService BucketService;
private readonly BundleService BundleService;
public ResourcesController(SecurityLogService securityLogService,
BucketService bucketService, BundleService bundleService)
public ResourcesController(BucketService bucketService)
{
SecurityLogService = securityLogService;
BucketService = bucketService;
BundleService = bundleService;
}
[HttpGet("images/{name}")]
@@ -31,10 +20,7 @@ public class ResourcesController : Controller
{
if (name.Contains(".."))
{
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
{
x.Add<string>(name);
});
Logger.Warn($"Detected an attempted path transversal. Path: {name}", "security");
return NotFound();
}
@@ -48,16 +34,33 @@ public class ResourcesController : Controller
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}")]
public async Task<ActionResult> GetBucket([FromRoute] string bucket, [FromRoute] string name)
{
if (name.Contains(".."))
{
await SecurityLogService.Log(SecurityLogType.PathTransversal, x =>
{
x.Add<string>(name);
});
Logger.Warn($"Detected an attempted path transversal. Path: {name}", "security");
return NotFound();
}
@@ -77,34 +80,4 @@ public class ResourcesController : Controller
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.Events;
using Moonlight.App.Http.Requests.Daemon;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
namespace Moonlight.App.Http.Controllers.Api.Remote;

View File

@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Http.Resources.Wings;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;

View File

@@ -1,5 +1,4 @@
using Logging.Net;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Moonlight.App.Helpers;
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,6 @@
namespace Moonlight.App.Models.Forms;
public class UserPreferencesDataModel
{
public bool StreamerMode { get; set; } = false;
}

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 Server Server { get; set; }
public ContainerStats.Container Container { get; set; }
public Container Container { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Models.Misc;
public class ServerGroup
{
public string Name { get; set; } = "";
public List<string> Servers { get; set; } = new();
}

View File

@@ -1,6 +1,8 @@
namespace Moonlight.App.Models.Notifications;
using Newtonsoft.Json;
namespace Moonlight.App.Models.Notifications;
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 int notification { get; set; }
[JsonProperty("notification")]
public int Notification { get; set; }
}

View File

@@ -9,7 +9,9 @@ public abstract class OAuth2Provider
public string Url { get; set; }
public IServiceScopeFactory ServiceScopeFactory { get; set; }
public string DisplayName { get; set; }
public bool CanBeLinked { get; set; } = false;
public abstract Task<string> GetUrl();
public abstract Task<User> HandleCode(string code);
public abstract Task LinkToUser(User user, string code);
}

View File

@@ -1,5 +1,4 @@
using System.Text;
using Logging.Net;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
@@ -12,6 +11,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class DiscordOAuth2Provider : OAuth2Provider
{
public DiscordOAuth2Provider()
{
CanBeLinked = true;
}
public override Task<string> GetUrl()
{
string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" +
@@ -82,6 +86,13 @@ public class DiscordOAuth2Provider : OAuth2Provider
var email = getData.GetValue<string>("email");
var id = getData.GetValue<ulong>("id");
var verified = getData.GetValue<bool>("verified");
if (!verified)
{
Logger.Warn("A user tried to use an unverified discord account to login", "security");
throw new DisplayException("You can only use verified discord accounts for oauth signin");
}
// Handle data
@@ -119,4 +130,74 @@ public class DiscordOAuth2Provider : OAuth2Provider
return user;
}
}
public override async Task LinkToUser(User user, string code)
{
// Endpoints
var endpoint = Url + "/api/moonlight/oauth2/discord";
var discordUserDataEndpoint = "https://discordapp.com/api/users/@me";
var discordEndpoint = "https://discordapp.com/api/oauth2/token";
// Generate access token
using var client = new RestClient();
var request = new RestRequest(discordEndpoint);
request.AddParameter("client_id", Config.ClientId);
request.AddParameter("client_secret", Config.ClientSecret);
request.AddParameter("grant_type", "authorization_code");
request.AddParameter("code", code);
request.AddParameter("redirect_uri", endpoint);
var response = await client.ExecutePostAsync(request);
if (!response.IsSuccessful)
{
Logger.Warn("Error verifying oauth2 code");
Logger.Warn(response.ErrorMessage);
throw new DisplayException("An error occured while verifying oauth2 code");
}
// parse response
var data = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes(response.Content!))
).Build();
var accessToken = data.GetValue<string>("access_token");
// Now, we will call the discord api with our access token to get the data we need
var getRequest = new RestRequest(discordUserDataEndpoint);
getRequest.AddHeader("Authorization", $"Bearer {accessToken}");
var getResponse = await client.ExecuteGetAsync(getRequest);
if (!getResponse.IsSuccessful)
{
Logger.Warn("An unexpected error occured while fetching user data from remote api");
Logger.Warn(getResponse.ErrorMessage);
throw new DisplayException("An unexpected error occured while fetching user data from remote api");
}
// Parse response
var getData = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes(getResponse.Content!))
).Build();
var id = getData.GetValue<ulong>("id");
// Handle data
using var scope = ServiceScopeFactory.CreateScope();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
user.DiscordId = id;
userRepo.Update(user);
}
}

View File

@@ -1,10 +1,8 @@
using System.Text;
using Logging.Net;
using Moonlight.App.ApiClients.Google.Requests;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Services;
using RestSharp;
@@ -13,6 +11,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class GoogleOAuth2Provider : OAuth2Provider
{
public GoogleOAuth2Provider()
{
CanBeLinked = false;
}
public override Task<string> GetUrl()
{
var endpoint = Url + "/api/moonlight/oauth2/google";
@@ -127,4 +130,9 @@ public class GoogleOAuth2Provider : OAuth2Provider
return user;
}
}
public override Task LinkToUser(User user, string code)
{
throw new NotImplementedException();
}
}

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 Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json;
@@ -81,12 +81,35 @@ public class CleanupService
{
try
{
var cpuStats = await nodeService.GetCpuStats(node);
var memoryStats = await nodeService.GetMemoryStats(node);
var cpuMetrics = await nodeService.GetCpuMetrics(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 imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@@ -101,9 +124,9 @@ public class CleanupService
)
.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))
{
@@ -139,6 +162,7 @@ public class CleanupService
if (players == 0)
{
Logger.Debug($"Restarted {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart);
ServersCleaned++;
@@ -164,10 +188,12 @@ public class CleanupService
if (handleJ2S)
{
Logger.Debug($"Restarted (cleanup) {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart);
}
else
{
Logger.Debug($"Stopped {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Stop);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,19 @@
using CloudFlare.Client;
using CloudFlare.Client.Api.Authentication;
using CloudFlare.Client.Api.Display;
using CloudFlare.Client.Api.Parameters.Data;
using CloudFlare.Client.Api.Result;
using CloudFlare.Client.Api.Zones;
using CloudFlare.Client.Api.Zones.DnsRecord;
using CloudFlare.Client.Enumerators;
using Logging.Net;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.Domains;
using Moonlight.App.Services.LogServices;
using DnsRecord = Moonlight.App.Models.Misc.DnsRecord;
using MatchType = CloudFlare.Client.Enumerators.MatchType;
namespace Moonlight.App.Services;
@@ -21,18 +22,15 @@ public class DomainService
private readonly DomainRepository DomainRepository;
private readonly SharedDomainRepository SharedDomainRepository;
private readonly CloudFlareClient Client;
private readonly AuditLogService AuditLogService;
private readonly string AccountId;
public DomainService(
ConfigService configService,
DomainRepository domainRepository,
SharedDomainRepository sharedDomainRepository,
AuditLogService auditLogService)
SharedDomainRepository sharedDomainRepository)
{
DomainRepository = domainRepository;
SharedDomainRepository = sharedDomainRepository;
AuditLogService = auditLogService;
var config = configService
.GetSection("Moonlight")
@@ -52,7 +50,7 @@ public class DomainService
{
if (DomainRepository.Get().Where(x => x.SharedDomain.Id == sharedDomain.Id).Any(x => x.Name == domain))
throw new DisplayException("A domain with this name does already exist for this shared domain");
var res = DomainRepository.Add(new()
{
Name = domain,
@@ -66,7 +64,7 @@ public class DomainService
public Task Delete(Domain domain)
{
DomainRepository.Delete(domain);
return Task.CompletedTask;
}
@@ -97,9 +95,36 @@ public class DomainService
{
var domain = EnsureData(d);
var records = GetData(
await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId)
);
var records = new List<CloudFlare.Client.Api.Zones.DnsRecord.DnsRecord>();
// Load paginated
// TODO: Find an alternative option. This way to load the records is NOT optimal
// and can result in long loading time when there are many dns records.
// As cloudflare does not offer a way to search dns records which starts
// with a specific string we are not able to filter it using the api (client)
var initialResponse = await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId);
records.AddRange(GetData(initialResponse));
// Check if there are more pages
while (initialResponse.ResultInfo.Page < initialResponse.ResultInfo.TotalPage)
{
// Get the next page of data
var nextPageResponse = await Client.Zones.DnsRecords.GetAsync(
domain.SharedDomain.CloudflareId,
displayOptions: new()
{
Page = initialResponse.ResultInfo.Page + 1
}
);
var nextPageRecords = GetData(nextPageResponse);
// Append the records from the next page to the existing records
records.AddRange(nextPageRecords);
// Update the initial response to the next page response
initialResponse = nextPageResponse;
}
var rname = $"{domain.Name}.{domain.SharedDomain.Name}";
var dname = $".{rname}";
@@ -149,7 +174,11 @@ public class DomainService
if (dnsRecord.Type == DnsRecordType.Srv)
{
var parts = dnsRecord.Name.Split(".");
Enum.TryParse(parts[1], out Protocol protocol);
Protocol protocol = Protocol.Tcp;
if (parts[1].Contains("udp"))
protocol = Protocol.Udp;
var valueParts = dnsRecord.Content.Split(" ");
@@ -191,11 +220,7 @@ public class DomainService
}));
}
await AuditLogService.Log(AuditLogType.AddDomainRecord, x =>
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
//TODO: AuditLog
}
public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord)
@@ -224,12 +249,8 @@ public class DomainService
Type = dnsRecord.Type
}));
}
await AuditLogService.Log(AuditLogType.UpdateDomainRecord, x =>
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
//TODO: AuditLog
}
public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord)
@@ -239,12 +260,8 @@ public class DomainService
GetData(
await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id)
);
await AuditLogService.Log(AuditLogType.DeleteDomainRecord, x =>
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
//TODO: AuditLog
}
private Domain EnsureData(Domain domain)

View File

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

View File

@@ -1,5 +1,4 @@
using Logging.Net;
using Moonlight.App.Helpers;
using Moonlight.App.Helpers;
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);
}
}
}

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