139 Commits
v1b1 ... v1b12

Author SHA1 Message Date
Marcel Baumgartner
2fe17473ae Merge pull request #204 from Moonlight-Panel/SwitchToNewConfigSystem
Switched to new config system
2023-07-02 02:19:32 +02:00
Marcel Baumgartner
609cf8cfac Switched to new config system 2023-07-02 02:16:44 +02:00
Marcel Baumgartner
678da30b09 Merge pull request #203 from Moonlight-Panel/LogsAndFixes
Added new log files and log migrators. Fixed some errors with js invokes
2023-07-02 00:21:55 +02:00
Marcel Baumgartner
d19412f4bb Added new log files and log migrators. Fixed some errors with js invokes 2023-07-02 00:21:35 +02:00
Marcel Baumgartner
1665d6e537 Merge pull request #202 from Moonlight-Panel/AddSentrySupport
Added new sentry support
2023-07-01 19:01:00 +02:00
Marcel Baumgartner
fd210f2404 Added new sentry support 2023-07-01 19:00:38 +02:00
Marcel Baumgartner
c33729fb44 Merge pull request #201 from Moonlight-Panel/FixServerList
Fixed server list
2023-06-30 21:55:58 +02:00
Marcel Baumgartner
7983bf3ee4 Fixed server list 2023-06-30 21:55:32 +02:00
Marcel Baumgartner
a09f60aea7 Merge pull request #200 from Moonlight-Panel/HttpTimeoutFixes
Fixed timeout options from assumed seconds to real value miliseconds
2023-06-29 23:28:07 +02:00
Marcel Baumgartner
28b5893c21 Fixed timeout options from assumed seconds to real value miliseconds 2023-06-29 23:27:48 +02:00
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
239 changed files with 12025 additions and 3369 deletions

1
.gitattributes vendored
View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -209,7 +209,7 @@ public class WingsApiHelper
var request = new RestRequest(url) var request = new RestRequest(url)
{ {
Timeout = 60 * 15 Timeout = 300000
}; };
request.AddHeader("Content-Type", "application/json"); request.AddHeader("Content-Type", "application/json");

View File

@@ -0,0 +1,226 @@
namespace Moonlight.App.Configuration;
using System;
using Newtonsoft.Json;
public class ConfigV1
{
[JsonProperty("Moonlight")] public MoonlightData Moonlight { get; set; } = new();
public class MoonlightData
{
[JsonProperty("AppUrl")] public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
[JsonProperty("DiscordBotApi")] public DiscordBotData DiscordBotApi { get; set; } = new();
[JsonProperty("DiscordBot")] public DiscordBotData DiscordBot { get; set; } = new();
[JsonProperty("Domains")] public DomainsData Domains { get; set; } = new();
[JsonProperty("Html")] public HtmlData Html { get; set; } = new();
[JsonProperty("Marketing")] public MarketingData Marketing { get; set; } = new();
[JsonProperty("OAuth2")] public OAuth2Data OAuth2 { get; set; } = new();
[JsonProperty("Security")] public SecurityData Security { get; set; } = new();
[JsonProperty("Mail")] public MailData Mail { get; set; } = new();
[JsonProperty("Cleanup")] public CleanupData Cleanup { get; set; } = new();
[JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new();
[JsonProperty("DiscordNotifications")]
public DiscordNotificationsData DiscordNotifications { get; set; } = new();
[JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new();
[JsonProperty("Rating")] public RatingData Rating { get; set; } = new();
[JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new();
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
}
public class CleanupData
{
[JsonProperty("Cpu")] public long Cpu { get; set; } = 90;
[JsonProperty("Memory")] public long Memory { get; set; } = 8192;
[JsonProperty("Wait")] public long Wait { get; set; } = 15;
[JsonProperty("Uptime")] public long Uptime { get; set; } = 6;
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("MinUptime")] public long MinUptime { get; set; } = 10;
}
public class DatabaseData
{
[JsonProperty("Database")] public string Database { get; set; } = "moonlight_db";
[JsonProperty("Host")] public string Host { get; set; } = "your.database.host";
[JsonProperty("Password")] public string Password { get; set; } = "secret";
[JsonProperty("Port")] public long Port { get; set; } = 3306;
[JsonProperty("Username")] public string Username { get; set; } = "moonlight_user";
}
public class DiscordBotData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Token")] public string Token { get; set; } = "discord token here";
[JsonProperty("PowerActions")] public bool PowerActions { get; set; } = false;
[JsonProperty("SendCommands")] public bool SendCommands { get; set; } = false;
}
public class DiscordNotificationsData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("WebHook")] public string WebHook { get; set; } = "http://your-discord-webhook-url";
}
public class DomainsData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("AccountId")] public string AccountId { get; set; } = "cloudflare acc id";
[JsonProperty("Email")] public string Email { get; set; } = "cloudflare@acc.email";
[JsonProperty("Key")] public string Key { get; set; } = "secret";
}
public class HtmlData
{
[JsonProperty("Headers")] public HeadersData Headers { get; set; } = new();
}
public class HeadersData
{
[JsonProperty("Color")] public string Color { get; set; } = "#4b27e8";
[JsonProperty("Description")] public string Description { get; set; } = "the next generation hosting panel";
[JsonProperty("Keywords")] public string Keywords { get; set; } = "moonlight";
[JsonProperty("Title")] public string Title { get; set; } = "Moonlight - endelon.link";
}
public class MailData
{
[JsonProperty("Email")] public string Email { get; set; } = "username@your.mail.host";
[JsonProperty("Server")] public string Server { get; set; } = "your.mail.host";
[JsonProperty("Password")] public string Password { get; set; } = "secret";
[JsonProperty("Port")] public int Port { get; set; } = 465;
[JsonProperty("Ssl")] public bool Ssl { get; set; } = true;
}
public class MarketingData
{
[JsonProperty("BrandName")] public string BrandName { get; set; } = "Endelon Hosting";
[JsonProperty("Imprint")] public string Imprint { get; set; } = "https://your-site.xyz/imprint";
[JsonProperty("Privacy")] public string Privacy { get; set; } = "https://your-site.xyz/privacy";
[JsonProperty("About")] public string About { get; set; } = "https://your-site.xyz/about";
[JsonProperty("Website")] public string Website { get; set; } = "https://your-site.xyz";
}
public class OAuth2Data
{
[JsonProperty("OverrideUrl")] public string OverrideUrl { get; set; } = "https://only-for-development.cases";
[JsonProperty("EnableOverrideUrl")] public bool EnableOverrideUrl { get; set; } = false;
[JsonProperty("Providers")]
public OAuth2ProviderData[] Providers { get; set; } = Array.Empty<OAuth2ProviderData>();
}
public class OAuth2ProviderData
{
[JsonProperty("Id")] public string Id { get; set; }
[JsonProperty("ClientId")] public string ClientId { get; set; }
[JsonProperty("ClientSecret")] public string ClientSecret { get; set; }
}
public class RatingData
{
[JsonProperty("Enabled")] public bool Enabled { get; set; } = false;
[JsonProperty("Url")] public string Url { get; set; } = "https://link-to-google-or-smth";
[JsonProperty("MinRating")] public int MinRating { get; set; } = 4;
[JsonProperty("DaysSince")] public int DaysSince { get; set; } = 5;
}
public class SecurityData
{
[JsonProperty("Token")] public string Token { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; }
}
public class ReCaptchaData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("SiteKey")] public string SiteKey { get; set; } = "recaptcha site key here";
[JsonProperty("SecretKey")] public string SecretKey { get; set; } = "recaptcha secret here";
}
public class SentryData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Dsn")] public string Dsn { get; set; } = "http://your-sentry-url-here";
}
public class SmartDeployData
{
[JsonProperty("Server")] public SmartDeployServerData Server { get; set; } = new();
}
public class SmartDeployServerData
{
[JsonProperty("EnableOverride")] public bool EnableOverride { get; set; } = false;
[JsonProperty("OverrideNode")] public long OverrideNode { get; set; } = 1;
}
public class StatisticsData
{
[JsonProperty("Enabled")] public bool Enabled { get; set; } = false;
[JsonProperty("Wait")] public long Wait { get; set; } = 15;
}
public class SubscriptionsData
{
[JsonProperty("SellPass")] public SellPassData SellPass { get; set; } = new();
}
public class SellPassData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Url")] public string Url { get; set; } = "https://not-implemented-yet";
}
}

View File

@@ -52,14 +52,14 @@ public class DataContext : DbContext
if (!optionsBuilder.IsConfigured) if (!optionsBuilder.IsConfigured)
{ {
var config = ConfigService var config = ConfigService
.GetSection("Moonlight") .Get()
.GetSection("Database"); .Moonlight.Database;
var connectionString = $"host={config.GetValue<string>("Host")};" + var connectionString = $"host={config.Host};" +
$"port={config.GetValue<int>("Port")};" + $"port={config.Port};" +
$"database={config.GetValue<string>("Database")};" + $"database={config.Database};" +
$"uid={config.GetValue<string>("Username")};" + $"uid={config.Username};" +
$"pwd={config.GetValue<string>("Password")}"; $"pwd={config.Password}";
optionsBuilder.UseMySql( optionsBuilder.UseMySql(
connectionString, connectionString,

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
using System.Diagnostics; using System.Diagnostics;
using Logging.Net; using Moonlight.App.Helpers;
namespace Moonlight.App.Events; namespace Moonlight.App.Events;
public class EventSystem public class EventSystem
{ {
private Dictionary<int, object> Storage = new();
private List<Subscriber> Subscribers = new(); private List<Subscriber> Subscribers = new();
private readonly bool Debug = false; private readonly bool Debug = false;
@@ -33,16 +32,8 @@ public class EventSystem
return Task.CompletedTask; 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; Subscriber[] subscribers;
lock (Subscribers) lock (Subscribers)
@@ -58,23 +49,6 @@ public class EventSystem
{ {
tasks.Add(new Task(() => 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(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
@@ -115,7 +89,6 @@ public class EventSystem
Task.Run(() => Task.Run(() =>
{ {
Task.WaitAll(tasks.ToArray()); Task.WaitAll(tasks.ToArray());
Storage.Remove(hashCode);
if(Debug) if(Debug)
Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage"); Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage");
@@ -139,4 +112,22 @@ public class EventSystem
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<T> WaitForEvent<T>(string id, object handle, Func<T, bool> filter)
{
var taskCompletionSource = new TaskCompletionSource<T>();
Func<T, Task> action = async data =>
{
if (filter.Invoke(data))
{
taskCompletionSource.SetResult(data);
await Off(id, handle);
}
};
On<T>(id, handle, action);
return taskCompletionSource.Task;
}
} }

View File

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

View File

@@ -0,0 +1,30 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Extensions;
public static class JSRuntimeExtensions
{
public static async Task InvokeVoidSafeAsync(this IJSRuntime jsRuntime, string method, params object[] args)
{
try
{
await jsRuntime.InvokeVoidAsync(method, args);
}
catch (Exception)
{
// ignored
}
}
public static void InvokeVoidSafe(this IJSRuntime jsRuntime, string method, params object[] args)
{
try
{
jsRuntime.InvokeVoidAsync(method, args);
}
catch (Exception)
{
// ignored
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Services; using Moonlight.App.Services;
@@ -67,21 +66,21 @@ public class DatabaseCheckupService
var configService = new ConfigService(new StorageService()); var configService = new ConfigService(new StorageService());
var dateTimeService = new DateTimeService(); var dateTimeService = new DateTimeService();
var config = configService var config = configService.Get().Moonlight.Database;
.GetSection("Moonlight")
.GetSection("Database");
var connectionString = $"host={config.GetValue<string>("Host")};" + var connectionString = $"host={config.Host};" +
$"port={config.GetValue<int>("Port")};" + $"port={config.Port};" +
$"database={config.GetValue<string>("Database")};" + $"database={config.Database};" +
$"uid={config.GetValue<string>("Username")};" + $"uid={config.Username};" +
$"pwd={config.GetValue<string>("Password")}"; $"pwd={config.Password}";
string file = PathBuilder.File("storage", "backups", $"{dateTimeService.GetCurrentUnix()}-mysql.sql"); string file = PathBuilder.File("storage", "backups", $"{dateTimeService.GetCurrentUnix()}-mysql.sql");
Logger.Info($"Saving it to: {file}"); Logger.Info($"Saving it to: {file}");
Logger.Info("Starting backup..."); Logger.Info("Starting backup...");
try
{
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
@@ -97,4 +96,15 @@ public class DatabaseCheckupService
sw.Stop(); sw.Stop();
Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s"); Logger.Info($"Done. {sw.Elapsed.TotalSeconds}s");
} }
catch (Exception e)
{
Logger.Fatal("-----------------------------------------------");
Logger.Fatal("Unable to create backup for moonlight database");
Logger.Fatal("Moonlight will start anyways in 30 seconds");
Logger.Fatal("-----------------------------------------------");
Logger.Fatal(e);
Thread.Sleep(TimeSpan.FromSeconds(30));
}
}
} }

View File

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

View File

@@ -3,6 +3,7 @@ using Moonlight.App.ApiClients.Wings;
using Moonlight.App.ApiClients.Wings.Requests; using Moonlight.App.ApiClients.Wings.Requests;
using Moonlight.App.ApiClients.Wings.Resources; using Moonlight.App.ApiClients.Wings.Resources;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers.Wings;
using Moonlight.App.Services; using Moonlight.App.Services;
using RestSharp; using RestSharp;
@@ -110,7 +111,7 @@ public class WingsFileAccess : FileAccess
request.AddParameter("name", "files"); request.AddParameter("name", "files");
request.AddParameter("filename", name); request.AddParameter("filename", name);
request.AddHeader("Content-Type", "multipart/form-data"); request.AddHeader("Content-Type", "multipart/form-data");
request.AddHeader("Origin", ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")); request.AddHeader("Origin", ConfigService.Get().Moonlight.AppUrl);
request.AddFile("files", () => request.AddFile("files", () =>
{ {
return new StreamProgressHelper(dataStream) return new StreamProgressHelper(dataStream)
@@ -210,6 +211,8 @@ public class WingsFileAccess : FileAccess
} }
public override async Task Decompress(FileData fileData) public override async Task Decompress(FileData fileData)
{
try
{ {
var req = new DecompressFile() var req = new DecompressFile()
{ {
@@ -219,6 +222,18 @@ public class WingsFileAccess : FileAccess
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() public override Task<string> GetLaunchUrl()
{ {

View File

@@ -8,8 +8,27 @@ public static class Formatter
{ {
TimeSpan t = TimeSpan.FromMilliseconds(uptime); TimeSpan t = TimeSpan.FromMilliseconds(uptime);
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"; 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) private static double Round(this double d, int decimals)
{ {
@@ -109,4 +128,12 @@ public static class Formatter
return (i / (1024D * 1024D)).Round(2) + " GB"; return (i / (1024D * 1024D)).Round(2) + " GB";
} }
} }
public static double BytesToGb(long bytes)
{
const double gbMultiplier = 1024 * 1024 * 1024; // 1 GB = 1024 MB * 1024 KB * 1024 B
double gigabytes = (double)bytes / gbMultiplier;
return gigabytes;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ using JWT.Algorithms;
using JWT.Builder; using JWT.Builder;
using Moonlight.App.Services; using Moonlight.App.Services;
namespace Moonlight.App.Helpers; namespace Moonlight.App.Helpers.Wings;
public class WingsJwtHelper public class WingsJwtHelper
{ {
@@ -15,7 +15,7 @@ public class WingsJwtHelper
{ {
ConfigService = configService; ConfigService = configService;
AppUrl = ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl"); AppUrl = ConfigService.Get().Moonlight.AppUrl;
} }
public string Generate(string secret, Action<Dictionary<string, string>> claimsAction) public string Generate(string secret, Action<Dictionary<string, string>> claimsAction)

View File

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

View File

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

View File

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

View File

@@ -1,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;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight; namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -11,12 +12,41 @@ public class OAuth2Controller : Controller
private readonly UserService UserService; private readonly UserService UserService;
private readonly OAuth2Service OAuth2Service; private readonly OAuth2Service OAuth2Service;
private readonly DateTimeService DateTimeService; 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; UserService = userService;
OAuth2Service = oAuth2Service; OAuth2Service = oAuth2Service;
DateTimeService = dateTimeService; 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}")] [HttpGet("{id}")]
@@ -24,6 +54,18 @@ public class OAuth2Controller : Controller
{ {
try 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); var user = await OAuth2Service.HandleCode(id, code);
Response.Cookies.Append("token", await UserService.GenerateToken(user), new() Response.Cookies.Append("token", await UserService.GenerateToken(user), new()

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Moonlight.App.Helpers;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Moonlight.App.LogMigrator; namespace Moonlight.App.LogMigrator;
@@ -28,19 +27,39 @@ public class LogMigrator : ILogger
switch (logLevel) switch (logLevel)
{ {
case LogLevel.Critical: case LogLevel.Critical:
Logger.Fatal($"[{Name}] {formatter(state, exception)}"); Logger.Fatal(formatter(state, exception));
if(exception != null)
Logger.Fatal(exception);
break; break;
case LogLevel.Warning: case LogLevel.Warning:
Logger.Warn($"[{Name}] {formatter(state, exception)}"); Logger.Warn(formatter(state, exception));
if(exception != null)
Logger.Warn(exception);
break; break;
case LogLevel.Debug: case LogLevel.Debug:
Logger.Debug($"[{Name}] {formatter(state, exception)}"); Logger.Debug(formatter(state, exception));
if(exception != null)
Logger.Debug(exception);
break; break;
case LogLevel.Error: case LogLevel.Error:
Logger.Error($"[{Name}] {formatter(state, exception)}"); Logger.Error(formatter(state, exception));
if(exception != null)
Logger.Error(exception);
break; break;
case LogLevel.Information: case LogLevel.Information:
Logger.Info($"[{Name}] {formatter(state, exception)}"); Logger.Info(formatter(state, exception));
if(exception != null)
Logger.Info(exception);
break; break;
} }
} }

View File

@@ -0,0 +1,71 @@
using Moonlight.App.Helpers;
using Sentry;
using Sentry.Extensibility;
namespace Moonlight.App.LogMigrator;
public class SentryDiagnosticsLogger : IDiagnosticLogger
{
private readonly SentryLevel Level;
public SentryDiagnosticsLogger(SentryLevel level)
{
Level = level;
}
public bool IsEnabled(SentryLevel level)
{
if ((int)level >= (int)Level)
{
return true;
}
return false;
}
public void Log(SentryLevel logLevel, string message, Exception? exception = null, params object?[] args)
{
switch (logLevel)
{
case SentryLevel.Debug:
Logger.Debug(string.Format(message, args));
if(exception != null)
Logger.Debug(exception);
break;
case SentryLevel.Info:
Logger.Info(string.Format(message, args));
if(exception != null)
Logger.Info(exception);
break;
case SentryLevel.Warning:
Logger.Warn(string.Format(message, args));
if(exception != null)
Logger.Warn(exception);
break;
case SentryLevel.Error:
Logger.Error(string.Format(message, args));
if(exception != null)
Logger.Error(exception);
break;
case SentryLevel.Fatal:
Logger.Fatal(string.Format(message, args));
if(exception != null)
Logger.Fatal(exception);
break;
}
}
}

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

View File

@@ -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 class Login : BasicWSModel
{ {
public string token { get; set; } [JsonProperty("token")] public string Token { get; set; } = "";
} }

View File

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

View File

@@ -9,7 +9,9 @@ public abstract class OAuth2Provider
public string Url { get; set; } public string Url { get; set; }
public IServiceScopeFactory ServiceScopeFactory { get; set; } public IServiceScopeFactory ServiceScopeFactory { get; set; }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public bool CanBeLinked { get; set; } = false;
public abstract Task<string> GetUrl(); public abstract Task<string> GetUrl();
public abstract Task<User> HandleCode(string code); 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 System.Text;
using Logging.Net;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
@@ -12,6 +11,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class DiscordOAuth2Provider : OAuth2Provider public class DiscordOAuth2Provider : OAuth2Provider
{ {
public DiscordOAuth2Provider()
{
CanBeLinked = true;
}
public override Task<string> GetUrl() public override Task<string> GetUrl()
{ {
string url = $"https://discord.com/api/oauth2/authorize?client_id={Config.ClientId}" + 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 email = getData.GetValue<string>("email");
var id = getData.GetValue<ulong>("id"); 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 // Handle data
@@ -119,4 +130,74 @@ public class DiscordOAuth2Provider : OAuth2Provider
return user; 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 System.Text;
using Logging.Net;
using Moonlight.App.ApiClients.Google.Requests; using Moonlight.App.ApiClients.Google.Requests;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services; using Moonlight.App.Services;
using RestSharp; using RestSharp;
@@ -13,6 +11,11 @@ namespace Moonlight.App.OAuth2.Providers;
public class GoogleOAuth2Provider : OAuth2Provider public class GoogleOAuth2Provider : OAuth2Provider
{ {
public GoogleOAuth2Provider()
{
CanBeLinked = false;
}
public override Task<string> GetUrl() public override Task<string> GetUrl()
{ {
var endpoint = Url + "/api/moonlight/oauth2/google"; var endpoint = Url + "/api/moonlight/oauth2/google";
@@ -127,4 +130,9 @@ public class GoogleOAuth2Provider : OAuth2Provider
return user; 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 MineStatLib;
using Moonlight.App.ApiClients.Daemon.Resources; using Moonlight.App.ApiClients.Daemon.Resources;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -43,15 +43,15 @@ public class CleanupService
CompletedAt = DateTimeService.GetCurrent(); CompletedAt = DateTimeService.GetCurrent();
IsRunning = false; IsRunning = false;
var config = ConfigService.GetSection("Moonlight").GetSection("Cleanup"); var config = ConfigService.Get().Moonlight.Cleanup;
if (!config.GetValue<bool>("Enable") || ConfigService.DebugMode) if (!config.Enable || ConfigService.DebugMode)
{ {
Logger.Info("Disabling cleanup service"); Logger.Info("Disabling cleanup service");
return; return;
} }
Timer = new(TimeSpan.FromMinutes(config.GetValue<int>("Wait"))); Timer = new(TimeSpan.FromMinutes(config.Wait));
Task.Run(Run); Task.Run(Run);
} }
@@ -63,12 +63,12 @@ public class CleanupService
IsRunning = true; IsRunning = true;
using var scope = ServiceScopeFactory.CreateScope(); using var scope = ServiceScopeFactory.CreateScope();
var config = ConfigService.GetSection("Moonlight").GetSection("Cleanup"); var config = ConfigService.Get().Moonlight.Cleanup;
var maxCpu = config.GetValue<int>("Cpu"); var maxCpu = config.Cpu;
var minMemory = config.GetValue<int>("Memory"); var minMemory = config.Memory;
var maxUptime = config.GetValue<int>("Uptime"); var maxUptime = config.Uptime;
var minUptime = config.GetValue<int>("MinUptime"); var minUptime = config.MinUptime;
var nodeRepository = scope.ServiceProvider.GetRequiredService<NodeRepository>(); var nodeRepository = scope.ServiceProvider.GetRequiredService<NodeRepository>();
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>(); var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
@@ -81,12 +81,35 @@ public class CleanupService
{ {
try try
{ {
var cpuStats = await nodeService.GetCpuStats(node); var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryStats = await nodeService.GetMemoryStats(node); var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuStats.Usage > maxCpu || memoryStats.Free < minMemory) bool executeCleanup;
if (cpuMetrics.CpuUsage > maxCpu)
{ {
var containerStats = await nodeService.GetContainerStats(node); Logger.Debug($"{node.Name}: CPU Usage is too high");
Logger.Debug($"Usage: {cpuMetrics.CpuUsage}");
Logger.Debug($"Max CPU: {maxCpu}");
executeCleanup = true;
}
else if (Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used) <
minMemory / 1024D)
{
Logger.Debug($"{node.Name}: Memory Usage is too high");
Logger.Debug($"Memory (Total): {Formatter.BytesToGb(memoryMetrics.Total)}");
Logger.Debug($"Memory (Used): {Formatter.BytesToGb(memoryMetrics.Used)}");
Logger.Debug(
$"Memory (Free): {Formatter.BytesToGb(memoryMetrics.Total) - Formatter.BytesToGb(memoryMetrics.Used)}");
Logger.Debug($"Min Memory: {minMemory}");
executeCleanup = true;
}
else
executeCleanup = false;
if (executeCleanup)
{
var dockerMetrics = await nodeService.GetDockerMetrics(node);
var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>(); var serverRepository = scope.ServiceProvider.GetRequiredService<ServerRepository>();
var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>(); var imageRepository = scope.ServiceProvider.GetRequiredService<ImageRepository>();
@@ -101,9 +124,9 @@ public class CleanupService
) )
.ToArray(); .ToArray();
var containerMappedToServers = new Dictionary<ContainerStats.Container, Server>(); var containerMappedToServers = new Dictionary<Container, Server>();
foreach (var container in containerStats.Containers) foreach (var container in dockerMetrics.Containers)
{ {
if (Guid.TryParse(container.Name, out Guid uuid)) if (Guid.TryParse(container.Name, out Guid uuid))
{ {
@@ -139,6 +162,7 @@ public class CleanupService
if (players == 0) if (players == 0)
{ {
Logger.Debug($"Restarted {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart); await serverService.SetPowerState(server, PowerSignal.Restart);
ServersCleaned++; ServersCleaned++;
@@ -164,10 +188,12 @@ public class CleanupService
if (handleJ2S) if (handleJ2S)
{ {
Logger.Debug($"Restarted (cleanup) {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Restart); await serverService.SetPowerState(server, PowerSignal.Restart);
} }
else else
{ {
Logger.Debug($"Stopped {server.Name} ({server.Uuid}) on node {node.Name}");
await serverService.SetPowerState(server, PowerSignal.Stop); await serverService.SetPowerState(server, PowerSignal.Stop);
} }

View File

@@ -1,8 +1,8 @@
using Discord; using Discord;
using Discord.Webhook; using Discord.Webhook;
using Logging.Net;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Events; using Moonlight.App.Events;
using Moonlight.App.Helpers;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
namespace Moonlight.App.Services.Background; namespace Moonlight.App.Services.Background;
@@ -22,14 +22,14 @@ public class DiscordNotificationService
Event = eventSystem; Event = eventSystem;
ResourceService = resourceService; ResourceService = resourceService;
var config = configService.GetSection("Moonlight").GetSection("DiscordNotifications"); var config = configService.Get().Moonlight.DiscordNotifications;
if (config.GetValue<bool>("Enable")) if (config.Enable)
{ {
Logger.Info("Discord notifications enabled"); Logger.Info("Discord notifications enabled");
Client = new(config.GetValue<string>("WebHook")); Client = new(config.WebHook);
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl"); AppUrl = configService.Get().Moonlight.AppUrl;
Event.On<User>("supportChat.new", this, OnNewSupportChat); Event.On<User>("supportChat.new", this, OnNewSupportChat);
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage); Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage);

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,16 +1,14 @@
using System.Text; using Moonlight.App.Configuration;
using Logging.Net;
using Microsoft.Extensions.Primitives;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
using Newtonsoft.Json;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
public class ConfigService : IConfiguration public class ConfigService
{ {
private readonly StorageService StorageService; private readonly StorageService StorageService;
private ConfigV1 Configuration;
private IConfiguration Configuration;
public bool DebugMode { get; private set; } = false; public bool DebugMode { get; private set; } = false;
public bool SqlDebugMode { get; private set; } = false; public bool SqlDebugMode { get; private set; } = false;
@@ -42,37 +40,22 @@ public class ConfigService : IConfiguration
public void Reload() public void Reload()
{ {
Logger.Info($"Reading config from '{PathBuilder.File("storage", "configs", "config.json")}'"); var path = PathBuilder.File("storage", "configs", "config.json");
Configuration = new ConfigurationBuilder().AddJsonStream( if (!File.Exists(path))
new MemoryStream(Encoding.ASCII.GetBytes(
File.ReadAllText(
PathBuilder.File("storage", "configs", "config.json")
)
)
)).Build();
Logger.Info("Reloaded configuration file");
}
public IEnumerable<IConfigurationSection> GetChildren()
{ {
return Configuration.GetChildren(); File.WriteAllText(path, "{}");
} }
public IChangeToken GetReloadToken() Configuration = JsonConvert.DeserializeObject<ConfigV1>(
{ File.ReadAllText(path)
return Configuration.GetReloadToken(); ) ?? new ConfigV1();
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration));
} }
public IConfigurationSection GetSection(string key) public ConfigV1 Get()
{ {
return Configuration.GetSection(key); return Configuration;
}
public string this[string key]
{
get => Configuration[key];
set => Configuration[key] = value;
} }
} }

View File

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

View File

@@ -1,6 +1,5 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
@@ -30,7 +29,7 @@ public class ServerListCommand : BaseModule
{ {
embed = dcs.EmbedBuilderModule.StandardEmbed("Sorry ;( \n Please first create and/or link a Account to Discord! \n Press the Button to register/log in.", Color.Red, command.User); embed = dcs.EmbedBuilderModule.StandardEmbed("Sorry ;( \n Please first create and/or link a Account to Discord! \n Press the Button to register/log in.", Color.Red, command.User);
components = new ComponentBuilder(); components = new ComponentBuilder();
components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.GetSection("Moonlight").GetValue<String>("AppUrl")); components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.Get().Moonlight.AppUrl);
await command.RespondAsync(embed: embed.Build(), components: components.Build(), ephemeral: true); await command.RespondAsync(embed: embed.Build(), components: components.Build(), ephemeral: true);
return; return;
@@ -58,7 +57,7 @@ public class ServerListCommand : BaseModule
components.WithButton("Panel", components.WithButton("Panel",
emote: Emote.Parse("<a:Earth:1092814004113657927>"), emote: Emote.Parse("<a:Earth:1092814004113657927>"),
style: ButtonStyle.Link, style: ButtonStyle.Link,
url: $"{ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")}"); url: $"{ConfigService.Get().Moonlight.AppUrl}");
if (servers.Count > 25) if (servers.Count > 25)
{ {

View File

@@ -1,8 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using Discord; using Discord;
using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net; using Moonlight.App.Helpers;
using Moonlight.App.Services.DiscordBot.Commands; using Moonlight.App.Services.DiscordBot.Commands;
using Moonlight.App.Services.DiscordBot.Modules; using Moonlight.App.Services.DiscordBot.Modules;
@@ -45,10 +44,10 @@ public DiscordBotService(
ServiceScope = ServiceScopeFactory.CreateScope(); ServiceScope = ServiceScopeFactory.CreateScope();
var discordConfig = ConfigService var discordConfig = ConfigService
.GetSection("Moonlight") .Get()
.GetSection("DiscordBot"); .Moonlight.DiscordBot;
if (!discordConfig.GetValue<bool>("Enable")) if (!discordConfig.Enable)
return; return;
Client.Log += Log; Client.Log += Log;
@@ -68,7 +67,7 @@ public DiscordBotService(
await ActivityStatusModule.UpdateActivityStatusList(); await ActivityStatusModule.UpdateActivityStatusList();
await Client.LoginAsync(TokenType.Bot, discordConfig.GetValue<string>("Token")); await Client.LoginAsync(TokenType.Bot, discordConfig.Token);
await Client.StartAsync(); await Client.StartAsync();
await Task.Delay(-1); await Task.Delay(-1);

View File

@@ -87,8 +87,8 @@ public class EmbedBuilderModule : BaseModule
int[] randomNumbers = new int[] { 1, 3, 8, 11, 20 }; int[] randomNumbers = new int[] { 1, 3, 8, 11, 20 };
if (randomNumbers.Contains(random.Next(1, 24))) if (randomNumbers.Contains(random.Next(1, 24)))
return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username + " - The Rick version").WithUrl(ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")).WithIconUrl("https://cdn.discordapp.com/attachments/750696464014901268/1092783310129860618/rick.gif"); return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username + " - The Rick version").WithUrl(ConfigService.Get().Moonlight.AppUrl).WithIconUrl("https://cdn.discordapp.com/attachments/750696464014901268/1092783310129860618/rick.gif");
return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username).WithUrl(ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")).WithIconUrl(Client.CurrentUser.GetAvatarUrl()); return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username).WithUrl(ConfigService.Get().Moonlight.AppUrl).WithIconUrl(Client.CurrentUser.GetAvatarUrl());
} }
} }

View File

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

View File

@@ -1,8 +1,8 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.ApiClients.Wings; using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Helpers;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
@@ -72,7 +72,7 @@ public class ServerListComponentHandlerModule : BaseModule
// stopping // stopping
// offline // offline
// installing // installing
if (!ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("PowerActions") && costomId[1] is "Start" or "Restart" or "Stop" or "Kill" or "Update") if (!ConfigService.Get().Moonlight.DiscordBot.PowerActions && costomId[1] is "Start" or "Restart" or "Stop" or "Kill" or "Update")
{ {
embed = dcs.EmbedBuilderModule.StandardEmbed($"This feature is disabled for Security reasons! \n If you believe this is a error please contact the Administrators from this panel.", Color.Red, component.User); embed = dcs.EmbedBuilderModule.StandardEmbed($"This feature is disabled for Security reasons! \n If you believe this is a error please contact the Administrators from this panel.", Color.Red, component.User);
await component.RespondAsync(embed: embed.Build(), ephemeral: true); await component.RespondAsync(embed: embed.Build(), ephemeral: true);
@@ -80,7 +80,7 @@ public class ServerListComponentHandlerModule : BaseModule
return; return;
} }
if (!ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("SendCommands") && costomId[1] is "SendCommand") if (!ConfigService.Get().Moonlight.DiscordBot.SendCommands && costomId[1] is "SendCommand")
{ {
embed = dcs.EmbedBuilderModule.StandardEmbed($"This feature is disabled for Security reasons! \n If you believe this is a error please contact the Administrators from this panel.", Color.Red, component.User); embed = dcs.EmbedBuilderModule.StandardEmbed($"This feature is disabled for Security reasons! \n If you believe this is a error please contact the Administrators from this panel.", Color.Red, component.User);
await component.RespondAsync(embed: embed.Build(), ephemeral: true); await component.RespondAsync(embed: embed.Build(), ephemeral: true);
@@ -302,7 +302,7 @@ public class ServerListComponentHandlerModule : BaseModule
{ {
embed = dcs.EmbedBuilderModule.StandardEmbed("Sorry ;( \n Please first create and/or link a Account to Discord! \n Press the Button to register/log in.", Color.Red, component.User); embed = dcs.EmbedBuilderModule.StandardEmbed("Sorry ;( \n Please first create and/or link a Account to Discord! \n Press the Button to register/log in.", Color.Red, component.User);
components = new ComponentBuilder(); components = new ComponentBuilder();
components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.GetSection("Moonlight").GetValue<String>("AppUrl")); components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.Get().Moonlight.AppUrl);
await component.RespondAsync(embed: embed.Build(), components: components.Build(), ephemeral: true); await component.RespondAsync(embed: embed.Build(), components: components.Build(), ephemeral: true);
return; return;
@@ -332,7 +332,7 @@ public class ServerListComponentHandlerModule : BaseModule
components.WithButton("Panel", components.WithButton("Panel",
emote: Emote.Parse("<a:Earth:1092814004113657927>"), emote: Emote.Parse("<a:Earth:1092814004113657927>"),
style: ButtonStyle.Link, style: ButtonStyle.Link,
url: $"{ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")}"); url: $"{ConfigService.Get().Moonlight.AppUrl}");
components.WithButton("Previous-page", components.WithButton("Previous-page",
emote: Emote.Parse("<:ArrowLeft:1101547474180649030>"), emote: Emote.Parse("<:ArrowLeft:1101547474180649030>"),
@@ -378,7 +378,7 @@ public class ServerListComponentHandlerModule : BaseModule
var components = new ComponentBuilder(); var components = new ComponentBuilder();
if (ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("PowerActions")) if (ConfigService.Get().Moonlight.DiscordBot.PowerActions)
{ {
components.WithButton("Start", style: ButtonStyle.Success, customId: $"Sm.Start.{server.Id}", disabled: false); components.WithButton("Start", style: ButtonStyle.Success, customId: $"Sm.Start.{server.Id}", disabled: false);
components.WithButton("Restart", style: ButtonStyle.Primary, customId: $"Sm.Restart.{server.Id}", disabled: false); components.WithButton("Restart", style: ButtonStyle.Primary, customId: $"Sm.Restart.{server.Id}", disabled: false);
@@ -389,14 +389,14 @@ public class ServerListComponentHandlerModule : BaseModule
components.WithButton("Way2Server", components.WithButton("Way2Server",
emote: Emote.Parse("<a:Earth:1092814004113657927>"), emote: Emote.Parse("<a:Earth:1092814004113657927>"),
style: ButtonStyle.Link, style: ButtonStyle.Link,
url: $"{ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")}/server/{server.Uuid}"); url: $"{ConfigService.Get().Moonlight.AppUrl}/server/{server.Uuid}");
components.WithButton("Update", components.WithButton("Update",
emote: Emote.Parse("<:refresh:1101547898803605605>"), emote: Emote.Parse("<:refresh:1101547898803605605>"),
style: ButtonStyle.Secondary, style: ButtonStyle.Secondary,
customId: $"Sm.Update.{server.Id}"); customId: $"Sm.Update.{server.Id}");
if (ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("SendCommands")) if (ConfigService.Get().Moonlight.DiscordBot.SendCommands)
{ {
components.WithButton("SendCommand", components.WithButton("SendCommand",
emote: Emote.Parse("<:Console:1101547358157819944>"), emote: Emote.Parse("<:Console:1101547358157819944>"),

View File

@@ -1,18 +1,19 @@
using CloudFlare.Client; using CloudFlare.Client;
using CloudFlare.Client.Api.Authentication; using CloudFlare.Client.Api.Authentication;
using CloudFlare.Client.Api.Display;
using CloudFlare.Client.Api.Parameters.Data; using CloudFlare.Client.Api.Parameters.Data;
using CloudFlare.Client.Api.Result; using CloudFlare.Client.Api.Result;
using CloudFlare.Client.Api.Zones; using CloudFlare.Client.Api.Zones;
using CloudFlare.Client.Api.Zones.DnsRecord; using CloudFlare.Client.Api.Zones.DnsRecord;
using CloudFlare.Client.Enumerators; using CloudFlare.Client.Enumerators;
using Logging.Net;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.Domains; using Moonlight.App.Repositories.Domains;
using Moonlight.App.Services.LogServices;
using DnsRecord = Moonlight.App.Models.Misc.DnsRecord; using DnsRecord = Moonlight.App.Models.Misc.DnsRecord;
using MatchType = CloudFlare.Client.Enumerators.MatchType;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
@@ -21,29 +22,26 @@ public class DomainService
private readonly DomainRepository DomainRepository; private readonly DomainRepository DomainRepository;
private readonly SharedDomainRepository SharedDomainRepository; private readonly SharedDomainRepository SharedDomainRepository;
private readonly CloudFlareClient Client; private readonly CloudFlareClient Client;
private readonly AuditLogService AuditLogService;
private readonly string AccountId; private readonly string AccountId;
public DomainService( public DomainService(
ConfigService configService, ConfigService configService,
DomainRepository domainRepository, DomainRepository domainRepository,
SharedDomainRepository sharedDomainRepository, SharedDomainRepository sharedDomainRepository)
AuditLogService auditLogService)
{ {
DomainRepository = domainRepository; DomainRepository = domainRepository;
SharedDomainRepository = sharedDomainRepository; SharedDomainRepository = sharedDomainRepository;
AuditLogService = auditLogService;
var config = configService var config = configService
.GetSection("Moonlight") .Get()
.GetSection("Domains"); .Moonlight.Domains;
AccountId = config.GetValue<string>("AccountId"); AccountId = config.AccountId;
Client = new( Client = new(
new ApiKeyAuthentication( new ApiKeyAuthentication(
config.GetValue<string>("Email"), config.Email,
config.GetValue<string>("Key") config.Key
) )
); );
} }
@@ -97,9 +95,36 @@ public class DomainService
{ {
var domain = EnsureData(d); var domain = EnsureData(d);
var records = GetData( var records = new List<CloudFlare.Client.Api.Zones.DnsRecord.DnsRecord>();
await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId)
// 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 rname = $"{domain.Name}.{domain.SharedDomain.Name}";
var dname = $".{rname}"; var dname = $".{rname}";
@@ -149,7 +174,11 @@ public class DomainService
if (dnsRecord.Type == DnsRecordType.Srv) if (dnsRecord.Type == DnsRecordType.Srv)
{ {
var parts = dnsRecord.Name.Split("."); 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(" "); var valueParts = dnsRecord.Content.Split(" ");
@@ -191,11 +220,7 @@ public class DomainService
})); }));
} }
await AuditLogService.Log(AuditLogType.AddDomainRecord, x => //TODO: AuditLog
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
} }
public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord) public async Task UpdateDnsRecord(Domain d, DnsRecord dnsRecord)
@@ -225,11 +250,7 @@ public class DomainService
})); }));
} }
await AuditLogService.Log(AuditLogType.UpdateDomainRecord, x => //TODO: AuditLog
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
} }
public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord) public async Task DeleteDnsRecord(Domain d, DnsRecord dnsRecord)
@@ -240,11 +261,7 @@ public class DomainService
await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id) await Client.Zones.DnsRecords.DeleteAsync(domain.SharedDomain.CloudflareId, dnsRecord.Id)
); );
await AuditLogService.Log(AuditLogType.DeleteDomainRecord, x => //TODO: AuditLog
{
x.Add<Domain>(d.Id);
x.Add<DnsRecord>(dnsRecord.Name);
});
} }
private Domain EnsureData(Domain domain) private Domain EnsureData(Domain domain)

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