108 Commits
v1b4 ... 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
f8fcb86ad8 Added base health check and diagnostic system 2023-06-06 22:50:33 +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
212 changed files with 11354 additions and 3373 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

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

View File

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

View File

@@ -0,0 +1,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

@@ -111,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)

View File

@@ -18,6 +18,18 @@ public static class Formatter
} }
} }
public static string FormatUptime(TimeSpan t)
{
if (t.Days > 0)
{
return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s";
}
else
{
return $"{t.Hours}h {t.Minutes}m {t.Seconds}s";
}
}
private static double Round(this double d, int decimals) private static double Round(this double d, int decimals)
{ {
return Math.Round(d, decimals); return Math.Round(d, decimals);

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

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

View File

@@ -20,7 +20,7 @@ public class WingsConsoleHelper
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl"); AppUrl = configService.Get().Moonlight.AppUrl;
} }
public async Task ConnectWings(WingsConsole console, Server server) public async Task ConnectWings(WingsConsole console, Server server)

View File

@@ -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

@@ -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,5 +1,5 @@
using Logging.Net; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc; using Moonlight.App.Helpers;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Sessions;

View File

@@ -1,12 +1,6 @@
using System.Text; using Microsoft.AspNetCore.Mvc;
using Logging.Net;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Services;
using Moonlight.App.Services.Files; using Moonlight.App.Services.Files;
using Moonlight.App.Services.LogServices;
using Moonlight.App.Services.Sessions;
namespace Moonlight.App.Http.Controllers.Api.Moonlight; namespace Moonlight.App.Http.Controllers.Api.Moonlight;
@@ -14,13 +8,10 @@ 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;
public ResourcesController(SecurityLogService securityLogService, public ResourcesController(BucketService bucketService)
BucketService bucketService)
{ {
SecurityLogService = securityLogService;
BucketService = bucketService; BucketService = bucketService;
} }
@@ -29,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();
} }
@@ -47,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();
} }

View File

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

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Moonlight.App.Helpers;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Moonlight.App.LogMigrator; namespace Moonlight.App.LogMigrator;
@@ -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

@@ -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

@@ -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;
@@ -87,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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
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;
@@ -44,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);
} }
@@ -64,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>();
@@ -85,7 +84,30 @@ public class CleanupService
var cpuMetrics = await nodeService.GetCpuMetrics(node); var cpuMetrics = await nodeService.GetCpuMetrics(node);
var memoryMetrics = await nodeService.GetMemoryMetrics(node); var memoryMetrics = await nodeService.GetMemoryMetrics(node);
if (cpuMetrics.CpuUsage > maxCpu || (Formatter.BytesToGb(memoryMetrics.Total) - (Formatter.BytesToGb(memoryMetrics.Used))) < minMemory) bool executeCleanup;
if (cpuMetrics.CpuUsage > maxCpu)
{
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 dockerMetrics = await nodeService.GetDockerMetrics(node);
@@ -140,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++;
@@ -165,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)

View File

@@ -8,7 +8,7 @@ public class ResourceService
public ResourceService(ConfigService configService) public ResourceService(ConfigService configService)
{ {
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl"); AppUrl = configService.Get().Moonlight.AppUrl;
} }
public string Image(string name) public string Image(string name)
@@ -16,6 +16,11 @@ public class ResourceService
return $"{AppUrl}/api/moonlight/resources/images/{name}"; return $"{AppUrl}/api/moonlight/resources/images/{name}";
} }
public string BackgroundImage(string name)
{
return $"{AppUrl}/api/moonlight/resources/background/{name}";
}
public string Avatar(User user) public string Avatar(User user)
{ {
return $"{AppUrl}/api/moonlight/avatar/{user.Id}"; return $"{AppUrl}/api/moonlight/avatar/{user.Id}";

View File

@@ -1,5 +1,4 @@
using Logging.Net; using Moonlight.App.Helpers;
using Moonlight.App.Helpers;
namespace Moonlight.App.Services.Files; namespace Moonlight.App.Services.Files;
@@ -16,6 +15,7 @@ public class StorageService
Directory.CreateDirectory(PathBuilder.Dir("storage", "configs")); Directory.CreateDirectory(PathBuilder.Dir("storage", "configs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources")); Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups")); Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
if(IsEmpty(PathBuilder.Dir("storage", "resources"))) if(IsEmpty(PathBuilder.Dir("storage", "resources")))
{ {

View File

@@ -25,16 +25,15 @@ public class ReCaptchaService
ConfigService = configService; ConfigService = configService;
var recaptchaConfig = ConfigService var recaptchaConfig = ConfigService
.GetSection("Moonlight") .Get()
.GetSection("Security") .Moonlight.Security.ReCaptcha;
.GetSection("ReCaptcha");
Enable = recaptchaConfig.GetValue<bool>("Enable"); Enable = recaptchaConfig.Enable;
if (Enable) if (Enable)
{ {
SiteKey = recaptchaConfig.GetValue<string>("SiteKey"); SiteKey = recaptchaConfig.SiteKey;
SecretKey = recaptchaConfig.GetValue<string>("SecretKey"); SecretKey = recaptchaConfig.SecretKey;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using Logging.Net; using MimeKit;
using MimeKit;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
@@ -18,14 +17,14 @@ public class MailService
public MailService(ConfigService configService) public MailService(ConfigService configService)
{ {
var mailConfig = configService var mailConfig = configService
.GetSection("Moonlight") .Get()
.GetSection("Mail"); .Moonlight.Mail;
Server = mailConfig.GetValue<string>("Server"); Server = mailConfig.Server;
Password = mailConfig.GetValue<string>("Password"); Password = mailConfig.Password;
Email = mailConfig.GetValue<string>("Email"); Email = mailConfig.Email;
Port = mailConfig.GetValue<int>("Port"); Port = mailConfig.Port;
Ssl = mailConfig.GetValue<bool>("Ssl"); Ssl = mailConfig.Ssl;
} }
public async Task SendMail( public async Task SendMail(

View File

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

View File

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

View File

@@ -72,7 +72,7 @@ public class NodeService
{ {
try try
{ {
await GetSystemMetrics(node); await GetStatus(node);
return true; return true;
} }

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using Moonlight.App.Database.Entities; using Mappy.Net;
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.Models.Misc;
@@ -24,14 +25,17 @@ public class OAuth2Service
ConfigService = configService; ConfigService = configService;
ServiceScopeFactory = serviceScopeFactory; ServiceScopeFactory = serviceScopeFactory;
var config = ConfigService.GetSection("Moonlight").GetSection("OAuth2"); var config = ConfigService
.Get()
.Moonlight.OAuth2;
Configs = config.GetSection("Providers").Get<OAuth2ProviderConfig[]>() Configs = config.Providers
?? Array.Empty<OAuth2ProviderConfig>(); .Select(Mapper.Map<OAuth2ProviderConfig>)
.ToArray();
OverrideUrl = config.GetValue<string>("OverrideUrl"); OverrideUrl = config.OverrideUrl;
EnableOverrideUrl = config.GetValue<bool>("EnableOverrideUrl"); EnableOverrideUrl = config.EnableOverrideUrl;
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl"); AppUrl = configService.Get().Moonlight.AppUrl;
// Register additional providers here // Register additional providers here
RegisterOAuth2<DiscordOAuth2Provider>("discord"); RegisterOAuth2<DiscordOAuth2Provider>("discord");

View File

@@ -4,9 +4,7 @@ using JWT.Builder;
using JWT.Exceptions; using JWT.Exceptions;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.LogServices;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
@@ -14,15 +12,12 @@ public class OneTimeJwtService
{ {
private readonly ConfigService ConfigService; private readonly ConfigService ConfigService;
private readonly RevokeRepository RevokeRepository; private readonly RevokeRepository RevokeRepository;
private readonly SecurityLogService SecurityLogService;
public OneTimeJwtService(ConfigService configService, public OneTimeJwtService(ConfigService configService,
RevokeRepository revokeRepository, RevokeRepository revokeRepository)
SecurityLogService securityLogService)
{ {
ConfigService = configService; ConfigService = configService;
RevokeRepository = revokeRepository; RevokeRepository = revokeRepository;
SecurityLogService = securityLogService;
} }
public string Generate(Action<Dictionary<string, string>> options, TimeSpan? validTime = null) public string Generate(Action<Dictionary<string, string>> options, TimeSpan? validTime = null)
@@ -30,10 +25,9 @@ public class OneTimeJwtService
var opt = new Dictionary<string, string>(); var opt = new Dictionary<string, string>();
options.Invoke(opt); options.Invoke(opt);
string secret = ConfigService var secret = ConfigService
.GetSection("Moonlight") .Get()
.GetSection("Security") .Moonlight.Security.Token;
.GetValue<string>("Token");
var id = StringHelper.GenerateString(16); var id = StringHelper.GenerateString(16);
@@ -60,10 +54,9 @@ public class OneTimeJwtService
public async Task<Dictionary<string, string>?> Validate(string token) public async Task<Dictionary<string, string>?> Validate(string token)
{ {
string secret = ConfigService var secret = ConfigService
.GetSection("Moonlight") .Get()
.GetSection("Security") .Moonlight.Security.Token;
.GetValue<string>("Token");
string json; string json;
@@ -76,10 +69,7 @@ public class OneTimeJwtService
} }
catch (SignatureVerificationException) catch (SignatureVerificationException)
{ {
await SecurityLogService.LogSystem(SecurityLogType.ManipulatedJwt, x => Logger.Warn($"Detected a manipulated JWT: {token}", "security");
{
x.Add<string>(token);
});
return null; return null;
} }
catch (Exception e) catch (Exception e)

View File

@@ -26,12 +26,12 @@ public class RatingService
Event = eventSystem; Event = eventSystem;
UserRepository = userRepository; UserRepository = userRepository;
var config = configService.GetSection("Moonlight").GetSection("Rating"); var config = configService.Get().Moonlight.Rating;
Enabled = config.GetValue<bool>("Enabled"); Enabled = config.Enabled;
Url = config.GetValue<string>("Url"); Url = config.Url;
MinRating = config.GetValue<int>("MinRating"); MinRating = config.MinRating;
DaysSince = config.GetValue<int>("DaysSince"); DaysSince = config.DaysSince;
} }
public async Task<bool> ShouldRate() public async Task<bool> ShouldRate()

View File

@@ -12,13 +12,13 @@ using Moonlight.App.Helpers.Wings;
using Moonlight.App.Models.Misc; using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Repositories.Servers; using Moonlight.App.Repositories.Servers;
using Moonlight.App.Services.LogServices;
using FileAccess = Moonlight.App.Helpers.Files.FileAccess; using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
namespace Moonlight.App.Services; namespace Moonlight.App.Services;
public class ServerService public class ServerService
{ {
private readonly Repository<ServerVariable> ServerVariablesRepository;
private readonly ServerRepository ServerRepository; private readonly ServerRepository ServerRepository;
private readonly UserRepository UserRepository; private readonly UserRepository UserRepository;
private readonly ImageRepository ImageRepository; private readonly ImageRepository ImageRepository;
@@ -28,9 +28,6 @@ public class ServerService
private readonly UserService UserService; private readonly UserService UserService;
private readonly ConfigService ConfigService; private readonly ConfigService ConfigService;
private readonly WingsJwtHelper WingsJwtHelper; private readonly WingsJwtHelper WingsJwtHelper;
private readonly SecurityLogService SecurityLogService;
private readonly AuditLogService AuditLogService;
private readonly ErrorLogService ErrorLogService;
private readonly NodeService NodeService; private readonly NodeService NodeService;
private readonly DateTimeService DateTimeService; private readonly DateTimeService DateTimeService;
private readonly EventSystem Event; private readonly EventSystem Event;
@@ -44,13 +41,11 @@ public class ServerService
UserService userService, UserService userService,
ConfigService configService, ConfigService configService,
WingsJwtHelper wingsJwtHelper, WingsJwtHelper wingsJwtHelper,
SecurityLogService securityLogService,
AuditLogService auditLogService,
ErrorLogService errorLogService,
NodeService nodeService, NodeService nodeService,
NodeAllocationRepository nodeAllocationRepository, NodeAllocationRepository nodeAllocationRepository,
DateTimeService dateTimeService, DateTimeService dateTimeService,
EventSystem eventSystem) EventSystem eventSystem,
Repository<ServerVariable> serverVariablesRepository)
{ {
ServerRepository = serverRepository; ServerRepository = serverRepository;
WingsApiHelper = wingsApiHelper; WingsApiHelper = wingsApiHelper;
@@ -60,13 +55,11 @@ public class ServerService
UserService = userService; UserService = userService;
ConfigService = configService; ConfigService = configService;
WingsJwtHelper = wingsJwtHelper; WingsJwtHelper = wingsJwtHelper;
SecurityLogService = securityLogService;
AuditLogService = auditLogService;
ErrorLogService = errorLogService;
NodeService = nodeService; NodeService = nodeService;
NodeAllocationRepository = nodeAllocationRepository; NodeAllocationRepository = nodeAllocationRepository;
DateTimeService = dateTimeService; DateTimeService = dateTimeService;
Event = eventSystem; Event = eventSystem;
ServerVariablesRepository = serverVariablesRepository;
} }
private Server EnsureNodeData(Server s) private Server EnsureNodeData(Server s)
@@ -86,10 +79,20 @@ public class ServerService
{ {
Server server = EnsureNodeData(s); Server server = EnsureNodeData(s);
return await WingsApiHelper.Get<ServerDetails>( ServerDetails result = null!;
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
result = await WingsApiHelper.Get<ServerDetails>(
server.Node, server.Node,
$"api/servers/{server.Uuid}" $"api/servers/{server.Uuid}"
); );
});
return result;
} }
public async Task SetPowerState(Server s, PowerSignal signal) public async Task SetPowerState(Server s, PowerSignal signal)
@@ -103,11 +106,7 @@ public class ServerService
Action = rawSignal Action = rawSignal
}); });
await AuditLogService.Log(AuditLogType.ChangePowerState, x => //TODO: AuditLog
{
x.Add<Server>(server.Uuid);
x.Add<PowerSignal>(rawSignal);
});
} }
public async Task<ServerBackup> CreateBackup(Server server) public async Task<ServerBackup> CreateBackup(Server server)
@@ -120,7 +119,8 @@ public class ServerService
var backup = new ServerBackup() var backup = new ServerBackup()
{ {
Name = $"Created at {DateTimeService.GetCurrent().ToShortDateString()} {DateTimeService.GetCurrent().ToShortTimeString()}", Name =
$"Created at {DateTimeService.GetCurrent().ToShortDateString()} {DateTimeService.GetCurrent().ToShortTimeString()}",
Uuid = Guid.NewGuid(), Uuid = Guid.NewGuid(),
CreatedAt = DateTimeService.GetCurrent(), CreatedAt = DateTimeService.GetCurrent(),
Created = false Created = false
@@ -136,12 +136,7 @@ public class ServerService
Ignore = "" Ignore = ""
}); });
await AuditLogService.Log(AuditLogType.CreateBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(backup.Uuid);
});
return backup; return backup;
} }
@@ -178,12 +173,7 @@ public class ServerService
Adapter = "wings" Adapter = "wings"
}); });
await AuditLogService.Log(AuditLogType.RestoreBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(serverBackup.Uuid);
});
} }
public async Task DeleteBackup(Server server, ServerBackup serverBackup) public async Task DeleteBackup(Server server, ServerBackup serverBackup)
@@ -196,8 +186,15 @@ public class ServerService
try try
{ {
await WingsApiHelper.Delete(serverData.Node, $"api/servers/{serverData.Uuid}/backup/{serverBackup.Uuid}", await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
await WingsApiHelper.Delete(serverData.Node,
$"api/servers/{serverData.Uuid}/backup/{serverBackup.Uuid}",
null); null);
});
} }
catch (WingsException e) catch (WingsException e)
{ {
@@ -216,13 +213,7 @@ public class ServerService
await Event.Emit("wings.backups.delete", backup); await Event.Emit("wings.backups.delete", backup);
await AuditLogService.Log(AuditLogType.DeleteBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(backup.Uuid);
}
);
} }
public async Task<string> DownloadBackup(Server s, ServerBackup serverBackup) public async Task<string> DownloadBackup(Server s, ServerBackup serverBackup)
@@ -235,12 +226,7 @@ public class ServerService
claims.Add("backup_uuid", serverBackup.Uuid.ToString()); claims.Add("backup_uuid", serverBackup.Uuid.ToString());
}); });
await AuditLogService.Log(AuditLogType.DownloadBackup, //TODO: AuditLog
x =>
{
x.Add<Server>(server.Uuid);
x.Add<ServerBackup>(serverBackup.Uuid);
});
if (server.Node.Ssl) if (server.Node.Ssl)
return $"https://{server.Node.Fqdn}:{server.Node.HttpPort}/download/backup?token={token}"; return $"https://{server.Node.Fqdn}:{server.Node.HttpPort}/download/backup?token={token}";
@@ -289,7 +275,8 @@ public class ServerService
freeAllocations = NodeAllocationRepository freeAllocations = NodeAllocationRepository
.Get() .Get()
.FromSqlRaw($"SELECT * FROM `NodeAllocations` WHERE ServerId IS NULL AND NodeId={node.Id} LIMIT {allocations}") .FromSqlRaw(
$"SELECT * FROM `NodeAllocations` WHERE ServerId IS NULL AND NodeId={node.Id} LIMIT {allocations}")
.ToArray(); .ToArray();
} }
catch (Exception) catch (Exception)
@@ -317,7 +304,8 @@ public class ServerService
Allocations = freeAllocations.ToList(), Allocations = freeAllocations.ToList(),
Backups = new(), Backups = new(),
OverrideStartup = "", OverrideStartup = "",
DockerImageIndex = image.DockerImages.FindIndex(x => x.Default) DockerImageIndex = image.DockerImages.FindIndex(x => x.Default),
Installing = true
}; };
foreach (var imageVariable in image.Variables) foreach (var imageVariable in image.Variables)
@@ -335,24 +323,27 @@ public class ServerService
var newServerData = ServerRepository.Add(server); var newServerData = ServerRepository.Add(server);
try try
{
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{ {
await WingsApiHelper.Post(node, $"api/servers", new CreateServer() await WingsApiHelper.Post(node, $"api/servers", new CreateServer()
{ {
Uuid = newServerData.Uuid, Uuid = newServerData.Uuid,
StartOnCompletion = false StartOnCompletion = false
}); });
});
await AuditLogService.Log(AuditLogType.CreateServer, x => { x.Add<Server>(newServerData.Uuid); }); //TODO: AuditLog
return newServerData; return newServerData;
} }
catch (Exception e) catch (Exception e)
{ {
await ErrorLogService.Log(e, x => Logger.Error("Error creating server on wings");
{ Logger.Error(e);
x.Add<Server>(newServerData.Uuid);
x.Add<Node>(node.Id);
});
ServerRepository.Delete(newServerData); //TODO Remove unsinged table stuff ServerRepository.Delete(newServerData); //TODO Remove unsinged table stuff
@@ -366,7 +357,10 @@ public class ServerService
await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null); await WingsApiHelper.Post(server.Node, $"api/servers/{server.Uuid}/reinstall", null);
await AuditLogService.Log(AuditLogType.ReinstallServer, x => { x.Add<Server>(server.Uuid); }); server.Installing = true;
ServerRepository.Update(server);
//TODO: AuditLog
} }
public async Task<Server> SftpServerLogin(int serverId, int id, string password) public async Task<Server> SftpServerLogin(int serverId, int id, string password)
@@ -375,7 +369,7 @@ public class ServerService
if (server == null) if (server == null)
{ {
await SecurityLogService.LogSystem(SecurityLogType.SftpBruteForce, x => { x.Add<int>(id); }); Logger.Warn($"Detected an sftp bruteforce attempt. ID: {id} Password: {password}", "security");
throw new Exception("Server not found"); throw new Exception("Server not found");
} }
@@ -401,17 +395,13 @@ public class ServerService
public async Task Delete(Server s) public async Task Delete(Server s)
{ {
throw new DisplayException("Deleting a server is currently a bit buggy. So its disabled for your safety"); var backups = await GetBackups(s);
var server = EnsureNodeData(s);
var backups = await GetBackups(server);
foreach (var backup in backups) foreach (var backup in backups)
{ {
try try
{ {
await DeleteBackup(server, backup); await DeleteBackup(s, backup);
} }
catch (Exception) catch (Exception)
{ {
@@ -419,9 +409,32 @@ public class ServerService
} }
} }
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null); var server = ServerRepository
.Get()
.Include(x => x.Variables)
.Include(x => x.Node)
.First(x => x.Id == s.Id);
//TODO: Fix empty data models try
{
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
});
}
catch (WingsException e)
{
if (e.StatusCode != 404)
throw;
}
foreach (var variable in server.Variables.ToArray())
{
ServerVariablesRepository.Delete(variable);
}
server.Allocations = new(); server.Allocations = new();
server.MainAllocation = null; server.MainAllocation = null;
@@ -429,7 +442,6 @@ public class ServerService
server.Backups = new(); server.Backups = new();
ServerRepository.Update(server); ServerRepository.Update(server);
ServerRepository.Delete(server); ServerRepository.Delete(server);
} }
@@ -439,4 +451,65 @@ public class ServerService
return await NodeService.IsHostUp(server.Node); return await NodeService.IsHostUp(server.Node);
} }
public async Task ArchiveServer(Server server)
{
if (server.IsArchived)
throw new DisplayException("Unable to archive an already archived server");
// Archive server
var backup = await CreateBackup(server);
server.IsArchived = true;
server.Archive = backup;
ServerRepository.Update(server);
await Event.WaitForEvent<ServerBackup>("wings.backups.create", this, x => backup.Id == x.Id);
// Reset server
var access = await CreateFileAccess(server, null!);
var files = await access.Ls();
foreach (var file in files)
{
try
{
await access.Delete(file);
}
catch (Exception)
{
// ignored
}
}
await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server);
}
public async Task UnArchiveServer(Server s)
{
if (!s.IsArchived)
throw new DisplayException("Unable to unarchive a server which is not archived");
var server = ServerRepository
.Get()
.Include(x => x.Archive)
.First(x => x.Id == s.Id);
if (server.Archive == null)
throw new DisplayException("Archive from server not found");
if (!server.Archive.Created)
throw new DisplayException("Creating the server archive is in progress");
await RestoreBackup(server, server.Archive);
await Event.WaitForEvent<ServerBackup>("wings.backups.restore", this,
x => x.Id == server.Archive.Id);
server.IsArchived = false;
ServerRepository.Update(server);
await Event.Emit($"server.{server.Uuid}.archiveStatusChanged", server);
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,13 +28,12 @@ public class SmartDeployService
public async Task<Node?> GetNode() public async Task<Node?> GetNode()
{ {
var config = ConfigService var config = ConfigService
.GetSection("Moonlight") .Get()
.GetSection("SmartDeploy") .Moonlight.SmartDeploy.Server;
.GetSection("Server");
if (config.GetValue<bool>("EnableOverride")) if (config.EnableOverride)
{ {
var nodeId = config.GetValue<int>("OverrideNode"); var nodeId = config.OverrideNode;
return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId); return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId);
} }

View File

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

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