diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 5f9b1b32..4964bbc4 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -304,6 +304,14 @@ public class ConfigV1 public int BlockIpDuration { get; set; } = 15; [JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new(); + + [JsonProperty("BlockDatacenterIps")] + [Description("If this option is enabled, users with an ip from datacenters will not be able to access the panel")] + public bool BlockDatacenterIps { get; set; } = true; + + [JsonProperty("AllowCloudflareIps")] + [Description("Allow cloudflare ips to bypass the datacenter ip check")] + public bool AllowCloudflareIps { get; set; } = false; } public class ReCaptchaData diff --git a/Moonlight/App/Helpers/Wings/WingsConsole.cs b/Moonlight/App/Helpers/Wings/WingsConsole.cs index 9767e540..674aca4a 100644 --- a/Moonlight/App/Helpers/Wings/WingsConsole.cs +++ b/Moonlight/App/Helpers/Wings/WingsConsole.cs @@ -218,6 +218,16 @@ public class WingsConsole : IDisposable break; case "install output": + if (ServerState != ServerState.Installing) + { + // Because wings is sending "install output" events BEFORE + // sending the "install started" event, + // we need to set the install state here + // See https://github.com/pterodactyl/panel/issues/4853 + // for more details + await UpdateServerState(ServerState.Installing); + } + foreach (var line in eventData.Args) { await SaveMessage(line); diff --git a/Moonlight/App/MalwareScans/FakePlayerPluginScan.cs b/Moonlight/App/MalwareScans/FakePlayerPluginScan.cs index 926c281f..8c235fe6 100644 --- a/Moonlight/App/MalwareScans/FakePlayerPluginScan.cs +++ b/Moonlight/App/MalwareScans/FakePlayerPluginScan.cs @@ -9,7 +9,7 @@ public class FakePlayerPluginScan : MalwareScan public override string Name => "Fake player plugin scan"; public override string Description => "This scan is a simple fake player plugin scan provided by moonlight"; - public override async Task Scan(Server server, IServiceProvider serviceProvider) + public override async Task Scan(Server server, IServiceProvider serviceProvider) { var serverService = serviceProvider.GetRequiredService(); var access = await serverService.CreateFileAccess(server, null!); @@ -24,19 +24,16 @@ public class FakePlayerPluginScan : MalwareScan { if (fileElement.Name.ToLower().Contains("fakeplayer")) { - return new[] + return new() { - new MalwareScanResult - { - Title = "Fake player plugin", - Description = $"Suspicious plugin file: {fileElement.Name}", - Author = "Marcel Baumgartner" - } + Title = "Fake player plugin", + Description = $"Suspicious plugin file: {fileElement.Name}", + Author = "Marcel Baumgartner" }; } } } - return Array.Empty(); + return null; } } \ No newline at end of file diff --git a/Moonlight/App/MalwareScans/MinerJarScan.cs b/Moonlight/App/MalwareScans/MinerJarScan.cs index ea8ef3b0..a11ccd4c 100644 --- a/Moonlight/App/MalwareScans/MinerJarScan.cs +++ b/Moonlight/App/MalwareScans/MinerJarScan.cs @@ -9,7 +9,7 @@ public class MinerJarScan : MalwareScan public override string Name => "Miner jar scan"; public override string Description => "This scan is a simple miner jar scan provided by moonlight"; - public override async Task Scan(Server server, IServiceProvider serviceProvider) + public override async Task Scan(Server server, IServiceProvider serviceProvider) { var serverService = serviceProvider.GetRequiredService(); var access = await serverService.CreateFileAccess(server, null!); @@ -23,18 +23,15 @@ public class MinerJarScan : MalwareScan if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile)) { - return new[] + return new() { - new MalwareScanResult - { - Title = "Found Miner", - Description = "Detected suspicious library directory which may contain a script for miners", - Author = "Marcel Baumgartner" - } + Title = "Found Miner", + Description = "Detected suspicious library directory which may contain a script for miners", + Author = "Marcel Baumgartner" }; } } - return Array.Empty(); + return null; } } \ No newline at end of file diff --git a/Moonlight/App/MalwareScans/MinerScan.cs b/Moonlight/App/MalwareScans/MinerScan.cs new file mode 100644 index 00000000..65ea6c4d --- /dev/null +++ b/Moonlight/App/MalwareScans/MinerScan.cs @@ -0,0 +1,35 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Models.Misc; +using Moonlight.App.Services; + +namespace Moonlight.App.MalwareScans; + +public class MinerScan : MalwareScan +{ + public override string Name => "Miner (NEZHA)"; + public override string Description => "Probably a miner"; + public override async Task Scan(Server server, IServiceProvider serviceProvider) + { + var serverService = serviceProvider.GetRequiredService(); + + var access = await serverService.CreateFileAccess(server, null!); + var files = await access.Ls(); + + foreach (var file in files.Where(x => x.IsFile && (x.Name.EndsWith(".sh") || x.Name.EndsWith(".yml")) || x.Name == "bed")) + { + var content = await access.Read(file); + + if (content.ToLower().Contains("nezha")) + { + return new() + { + Title = "Miner", + Description = "Miner start script (NEZHA)", + Author = "Marcel Baumgartner" + }; + } + } + + return null; + } +} \ No newline at end of file diff --git a/Moonlight/App/MalwareScans/SelfBotCodeScan.cs b/Moonlight/App/MalwareScans/SelfBotCodeScan.cs index 3c0a7701..77b6a6a6 100644 --- a/Moonlight/App/MalwareScans/SelfBotCodeScan.cs +++ b/Moonlight/App/MalwareScans/SelfBotCodeScan.cs @@ -9,7 +9,7 @@ public class SelfBotCodeScan : MalwareScan public override string Name => "Selfbot code scan"; public override string Description => "This scan is a simple selfbot code scan provided by moonlight"; - public override async Task Scan(Server server, IServiceProvider serviceProvider) + public override async Task Scan(Server server, IServiceProvider serviceProvider) { var serverService = serviceProvider.GetRequiredService(); var access = await serverService.CreateFileAccess(server, null!); @@ -21,18 +21,15 @@ public class SelfBotCodeScan : MalwareScan if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking { - return new[] + return new MalwareScanResult { - new MalwareScanResult - { - Title = "Potential selfbot", - Description = $"Suspicious script file: {script.Name}", - Author = "Marcel Baumgartner" - } + Title = "Potential selfbot", + Description = $"Suspicious script file: {script.Name}", + Author = "Marcel Baumgartner" }; } } - return Array.Empty(); + return null; } } \ No newline at end of file diff --git a/Moonlight/App/MalwareScans/SelfBotScan.cs b/Moonlight/App/MalwareScans/SelfBotScan.cs index 963017e0..b015b1ba 100644 --- a/Moonlight/App/MalwareScans/SelfBotScan.cs +++ b/Moonlight/App/MalwareScans/SelfBotScan.cs @@ -9,7 +9,7 @@ public class SelfBotScan : MalwareScan public override string Name => "Selfbot Scan"; public override string Description => "This scan is a simple selfbot scan provided by moonlight"; - public override async Task Scan(Server server, IServiceProvider serviceProvider) + public override async Task Scan(Server server, IServiceProvider serviceProvider) { var serverService = serviceProvider.GetRequiredService(); var access = await serverService.CreateFileAccess(server, null!); @@ -17,17 +17,14 @@ public class SelfBotScan : MalwareScan if (fileElements.Any(x => x.Name == "tokens.txt")) { - return new[] + return new MalwareScanResult { - new MalwareScanResult - { - Title = "Found SelfBot", - Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot", - Author = "Marcel Baumgartner" - } + Title = "Found SelfBot", + Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot", + Author = "Marcel Baumgartner" }; } - return Array.Empty(); + return null; } } \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/MalwareScan.cs b/Moonlight/App/Models/Misc/MalwareScan.cs index 9ca18659..9660868c 100644 --- a/Moonlight/App/Models/Misc/MalwareScan.cs +++ b/Moonlight/App/Models/Misc/MalwareScan.cs @@ -6,5 +6,5 @@ public abstract class MalwareScan { public abstract string Name { get; } public abstract string Description { get; } - public abstract Task Scan(Server server, IServiceProvider serviceProvider); + public abstract Task Scan(Server server, IServiceProvider serviceProvider); } \ No newline at end of file diff --git a/Moonlight/App/Services/Background/MalwareBackgroundScanService.cs b/Moonlight/App/Services/Background/MalwareBackgroundScanService.cs index dcf975a5..282fa275 100644 --- a/Moonlight/App/Services/Background/MalwareBackgroundScanService.cs +++ b/Moonlight/App/Services/Background/MalwareBackgroundScanService.cs @@ -15,7 +15,7 @@ public class MalwareBackgroundScanService public bool IsRunning => !ScanTask?.IsCompleted ?? false; public bool ScanAllServers { get; set; } - public readonly Dictionary ScanResults; + public readonly Dictionary ScanResults; public string Status { get; private set; } = "N/A"; private Task? ScanTask; @@ -70,16 +70,16 @@ public class MalwareBackgroundScanService Status = $"[{i} / {servers.Length}] Scanning server {server.Name}"; await Event.Emit("malwareScan.status", IsRunning); - var results = await malwareScanService.Perform(server); + var result = await malwareScanService.Perform(server); - if (results.Any()) + if (result != null) { lock (ScanResults) { - ScanResults.Add(server, results); + ScanResults.Add(server, result); } - await Event.Emit("malwareScan.result"); + await Event.Emit("malwareScan.result", server); } i++; diff --git a/Moonlight/App/Services/Files/StorageService.cs b/Moonlight/App/Services/Files/StorageService.cs index 9980e23a..49fac176 100644 --- a/Moonlight/App/Services/Files/StorageService.cs +++ b/Moonlight/App/Services/Files/StorageService.cs @@ -1,15 +1,11 @@ using Moonlight.App.Helpers; +using Octokit; namespace Moonlight.App.Services.Files; public class StorageService { - public StorageService() - { - EnsureCreated(); - } - - public void EnsureCreated() + public async Task EnsureCreated() { Directory.CreateDirectory(PathBuilder.Dir("storage", "uploads")); Directory.CreateDirectory(PathBuilder.Dir("storage", "configs")); @@ -17,13 +13,16 @@ public class StorageService Directory.CreateDirectory(PathBuilder.Dir("storage", "backups")); Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); - - if(IsEmpty(PathBuilder.Dir("storage", "resources"))) + + await UpdateResources(); + + return; + if (IsEmpty(PathBuilder.Dir("storage", "resources"))) { Logger.Info("Default resources not found. Copying default resources"); - + CopyFilesRecursively( - PathBuilder.Dir("defaultstorage", "resources"), + PathBuilder.Dir("defaultstorage", "resources"), PathBuilder.Dir("storage", "resources") ); } @@ -31,18 +30,83 @@ public class StorageService if (IsEmpty(PathBuilder.Dir("storage", "configs"))) { Logger.Info("Default configs not found. Copying default configs"); - + CopyFilesRecursively( - PathBuilder.Dir("defaultstorage", "configs"), + PathBuilder.Dir("defaultstorage", "configs"), PathBuilder.Dir("storage", "configs") ); } } + private async Task UpdateResources() + { + try + { + Logger.Info("Checking resources"); + + var client = new GitHubClient( + new ProductHeaderValue("Moonlight-Panel")); + + var user = "Moonlight-Panel"; + var repo = "Resources"; + var resourcesDir = PathBuilder.Dir("storage", "resources"); + + async Task CopyDirectory(string dirPath, string localDir) + { + IReadOnlyList contents; + + if (string.IsNullOrEmpty(dirPath)) + contents = await client.Repository.Content.GetAllContents(user, repo); + else + contents = await client.Repository.Content.GetAllContents(user, repo, dirPath); + + foreach (var content in contents) + { + string localPath = Path.Combine(localDir, content.Name); + + if (content.Type == ContentType.File) + { + if (content.Name.EndsWith(".gitattributes")) + continue; + + if (File.Exists(localPath) && !content.Name.EndsWith(".lang")) + continue; + + if (content.Name.EndsWith(".lang") && File.Exists(localPath) && + new FileInfo(localPath).Length == content.Size) + continue; + + var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path); + Directory.CreateDirectory(localDir); // Ensure the directory exists + await File.WriteAllBytesAsync(localPath, fileContent); + + Logger.Debug($"Synced file '{content.Path}'"); + } + else if (content.Type == ContentType.Dir) + { + await CopyDirectory(content.Path, localPath); + } + } + } + + await CopyDirectory("", resourcesDir); + } + catch (RateLimitExceededException) + { + Logger.Warn("Unable to sync resources due to your ip being rate-limited by github"); + } + catch (Exception e) + { + Logger.Warn("Unable to sync resources"); + Logger.Warn(e); + } + } + private bool IsEmpty(string path) { return !Directory.EnumerateFileSystemEntries(path).Any(); } + private static void CopyFilesRecursively(string sourcePath, string targetPath) { //Now Create all of the directories @@ -52,7 +116,7 @@ public class StorageService } //Copy all the files & Replaces any files with the same name - foreach (string newPath in Directory.GetFiles(sourcePath, "*.*",SearchOption.AllDirectories)) + foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories)) { File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); } diff --git a/Moonlight/App/Services/IpVerificationService.cs b/Moonlight/App/Services/IpVerificationService.cs new file mode 100644 index 00000000..af67b523 --- /dev/null +++ b/Moonlight/App/Services/IpVerificationService.cs @@ -0,0 +1,91 @@ +using Moonlight.App.Helpers; +using Whois.NET; + +namespace Moonlight.App.Services; + +public class IpVerificationService +{ + private readonly ConfigService ConfigService; + + public IpVerificationService(ConfigService configService) + { + ConfigService = configService; + } + + public async Task IsDatacenterOrVpn(string ip) + { + if (!ConfigService.Get().Moonlight.Security.BlockDatacenterIps) + return false; + + if (string.IsNullOrEmpty(ip)) + return false; + + var datacenterNames = new List() + { + "amazon", + "aws", + "microsoft", + "azure", + "google", + "google cloud", + "gcp", + "digitalocean", + "linode", + "vultr", + "ovh", + "ovhcloud", + "alibaba", + "oracle", + "ibm cloud", + "bluehost", + "godaddy", + "rackpace", + "hetzner", + "tencent", + "scaleway", + "softlayer", + "dreamhost", + "a2 hosting", + "inmotion hosting", + "red hat openstack", + "kamatera", + "hostgator", + "siteground", + "greengeeks", + "liquidweb", + "joyent", + "aruba", + "interoute", + "fastcomet", + "rosehosting", + "lunarpages", + "fatcow", + "jelastic", + "datacamp" + }; + + if(!ConfigService.Get().Moonlight.Security.AllowCloudflareIps) + datacenterNames.Add("cloudflare"); + + try + { + var response = await WhoisClient.QueryAsync(ip); + var responseText = response.Raw.ToLower(); + + foreach (var name in datacenterNames) + { + if (responseText.Contains(name)) + { + Logger.Debug(name); + return true; + } + } + } + catch (Exception) + { + return false; + } + + return false; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/MalwareScanService.cs b/Moonlight/App/Services/MalwareScanService.cs index 08dbaabf..3674cdd0 100644 --- a/Moonlight/App/Services/MalwareScanService.cs +++ b/Moonlight/App/Services/MalwareScanService.cs @@ -5,7 +5,7 @@ using Moonlight.App.Services.Plugins; namespace Moonlight.App.Services; -public class MalwareScanService //TODO: Make this moddable using plugins +public class MalwareScanService { private readonly PluginService PluginService; private readonly IServiceScopeFactory ServiceScopeFactory; @@ -16,34 +16,29 @@ public class MalwareScanService //TODO: Make this moddable using plugins ServiceScopeFactory = serviceScopeFactory; } - public async Task Perform(Server server) + public async Task Perform(Server server) { var defaultScans = new List { new SelfBotScan(), new MinerJarScan(), new SelfBotCodeScan(), - new FakePlayerPluginScan() + new FakePlayerPluginScan(), + new MinerScan() }; var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray()); - - var results = new List(); + using var scope = ServiceScopeFactory.CreateScope(); foreach (var scan in scans) { var result = await scan.Scan(server, scope.ServiceProvider); - if (result.Any()) - { - foreach (var scanResult in result) - { - results.Add(scanResult); - } - } + if (result != null) + return result; } - return results.ToArray(); + return null; } } \ No newline at end of file diff --git a/Moonlight/App/Services/ServerService.cs b/Moonlight/App/Services/ServerService.cs index 7e9876d3..9054e01b 100644 --- a/Moonlight/App/Services/ServerService.cs +++ b/Moonlight/App/Services/ServerService.cs @@ -113,19 +113,17 @@ public class ServerService if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start || signal == PowerSignal.Restart) { - var results = await new MalwareScanService( + var result = await new MalwareScanService( PluginService, ServiceScopeFactory ).Perform(server); - if (results.Any()) + if (result != null) { - var resultText = string.Join(" ", results.Select(x => x.Title)); - - Logger.Warn($"Found malware on server {server.Uuid}. Results: " + resultText); + Logger.Warn($"Found malware on server {server.Uuid}. Result: " + result.Title); throw new DisplayException( - $"Unable to start server. Found following malware on this server: {resultText}. Please contact the support if you think this detection is a false positive", + $"Unable to start server. Found following malware on this server: {result.Title}. Please contact the support if you think this detection is a false positive", true); } } diff --git a/Moonlight/Dockerfile b/Moonlight/Dockerfile index caf1ab0b..a0ccd337 100644 --- a/Moonlight/Dockerfile +++ b/Moonlight/Dockerfile @@ -22,5 +22,4 @@ COPY --from=publish /app/publish . RUN mkdir -p /app/storage RUN touch /app/storage/donttriggeranyerrors RUN rm -r /app/storage/* -COPY "Moonlight/defaultstorage" "/app/defaultstorage" ENTRYPOINT ["dotnet", "Moonlight.dll"] \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 7055763d..d5d61207 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -55,6 +55,7 @@ + diff --git a/Moonlight/Pages/_Layout.cshtml b/Moonlight/Pages/_Layout.cshtml index 42e0443e..6dd42b6e 100644 --- a/Moonlight/Pages/_Layout.cshtml +++ b/Moonlight/Pages/_Layout.cshtml @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web @using Moonlight.App.Extensions @using Moonlight.App.Repositories @using Moonlight.App.Services @@ -41,7 +41,6 @@ - @@ -49,6 +48,7 @@ + @@ -94,8 +94,8 @@ - - + + @@ -105,12 +105,15 @@ - + - - + + + + + @@ -123,6 +126,8 @@ moonlight.loading.registerXterm(); + + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 05f6d541..e0b769d4 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -42,8 +42,10 @@ namespace Moonlight { public static async Task Main(string[] args) { - // This will also copy all default config files - var configService = new ConfigService(new StorageService()); + var storageService = new StorageService(); + await storageService.EnsureCreated(); + + var configService = new ConfigService(storageService); var shouldUseSentry = configService .Get() .Moonlight.Sentry.Enable; @@ -218,6 +220,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/Moonlight/Properties/launchSettings.json b/Moonlight/Properties/launchSettings.json index 9d662090..6e88d09e 100644 --- a/Moonlight/Properties/launchSettings.json +++ b/Moonlight/Properties/launchSettings.json @@ -9,7 +9,7 @@ "ASPNETCORE_ENVIRONMENT": "Development", "ML_DEBUG": "true" }, - "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", + "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118", "dotnetRunMessages": true }, "Live DB": @@ -21,7 +21,7 @@ "ML_DEBUG": "true", "ML_CONFIG_PATH": "storage\\configs\\live_config.json" }, - "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", + "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118", "dotnetRunMessages": true }, "Dev DB 1": @@ -33,7 +33,7 @@ "ML_DEBUG": "true", "ML_CONFIG_PATH": "storage\\configs\\dev_1_config.json" }, - "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", + "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118", "dotnetRunMessages": true }, "Dev DB 2": @@ -45,7 +45,7 @@ "ML_DEBUG": "true", "ML_CONFIG_PATH": "storage\\configs\\dev_2_config.json" }, - "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118", + "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118", "dotnetRunMessages": true } } diff --git a/Moonlight/Shared/Components/Navigations/AdminDomainsNavigation.razor b/Moonlight/Shared/Components/Navigations/AdminDomainsNavigation.razor new file mode 100644 index 00000000..a84dfa60 --- /dev/null +++ b/Moonlight/Shared/Components/Navigations/AdminDomainsNavigation.razor @@ -0,0 +1,22 @@ + + +@code +{ + [Parameter] + public int Index { get; set; } = 0; +} \ No newline at end of file diff --git a/Moonlight/Shared/Layouts/DefaultLayout.razor b/Moonlight/Shared/Layouts/DefaultLayout.razor index 34903537..8be2076c 100644 --- a/Moonlight/Shared/Layouts/DefaultLayout.razor +++ b/Moonlight/Shared/Layouts/DefaultLayout.razor @@ -187,38 +187,15 @@ else -