Merge pull request #295 from Moonlight-Panel/AddIpFiltering
Implemented ip filtering to detect datacenter and vpn ips
This commit is contained in:
@@ -304,6 +304,14 @@ public class ConfigV1
|
|||||||
public int BlockIpDuration { get; set; } = 15;
|
public int BlockIpDuration { get; set; } = 15;
|
||||||
|
|
||||||
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
|
[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
|
public class ReCaptchaData
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class StorageService
|
|||||||
await UpdateResources();
|
await UpdateResources();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
if(IsEmpty(PathBuilder.Dir("storage", "resources")))
|
if (IsEmpty(PathBuilder.Dir("storage", "resources")))
|
||||||
{
|
{
|
||||||
Logger.Info("Default resources not found. Copying default resources");
|
Logger.Info("Default resources not found. Copying default resources");
|
||||||
|
|
||||||
@@ -40,64 +40,73 @@ public class StorageService
|
|||||||
|
|
||||||
private async Task UpdateResources()
|
private async Task UpdateResources()
|
||||||
{
|
{
|
||||||
Logger.Info("Checking resources");
|
try
|
||||||
|
|
||||||
var client = new GitHubClient(
|
|
||||||
new ProductHeaderValue("Moonlight-Panel"));
|
|
||||||
|
|
||||||
string user = "Moonlight-Panel";
|
|
||||||
string repo = "Resources";
|
|
||||||
string branch = "main";
|
|
||||||
string resourcesDir = PathBuilder.Dir("storage", "resources");
|
|
||||||
|
|
||||||
async Task CopyDirectory(string dirPath, string localDir)
|
|
||||||
{
|
{
|
||||||
IReadOnlyList<RepositoryContent> contents;
|
Logger.Info("Checking resources");
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(dirPath))
|
var client = new GitHubClient(
|
||||||
contents = await client.Repository.Content.GetAllContents(user, repo);
|
new ProductHeaderValue("Moonlight-Panel"));
|
||||||
else
|
|
||||||
contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
|
|
||||||
|
|
||||||
foreach (var content in contents)
|
var user = "Moonlight-Panel";
|
||||||
|
var repo = "Resources";
|
||||||
|
var resourcesDir = PathBuilder.Dir("storage", "resources");
|
||||||
|
|
||||||
|
async Task CopyDirectory(string dirPath, string localDir)
|
||||||
{
|
{
|
||||||
string localPath = Path.Combine(localDir, content.Name);
|
IReadOnlyList<RepositoryContent> contents;
|
||||||
|
|
||||||
if (content.Type == ContentType.File)
|
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)
|
||||||
{
|
{
|
||||||
if(content.Name.EndsWith(".gitattributes"))
|
string localPath = Path.Combine(localDir, content.Name);
|
||||||
continue;
|
|
||||||
|
|
||||||
if(File.Exists(localPath) && !content.Name.EndsWith(".lang"))
|
if (content.Type == ContentType.File)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (content.Name.EndsWith(".lang") && File.Exists(localPath) &&
|
|
||||||
new FileInfo(localPath).Length == content.Size)
|
|
||||||
{
|
{
|
||||||
Logger.Info($"Skipped language file '{content.Name}'");
|
if (content.Name.EndsWith(".gitattributes"))
|
||||||
continue;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
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)
|
private bool IsEmpty(string path)
|
||||||
{
|
{
|
||||||
return !Directory.EnumerateFileSystemEntries(path).Any();
|
return !Directory.EnumerateFileSystemEntries(path).Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CopyFilesRecursively(string sourcePath, string targetPath)
|
private static void CopyFilesRecursively(string sourcePath, string targetPath)
|
||||||
{
|
{
|
||||||
//Now Create all of the directories
|
//Now Create all of the directories
|
||||||
@@ -107,7 +116,7 @@ public class StorageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Copy all the files & Replaces any files with the same name
|
//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);
|
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
|
||||||
}
|
}
|
||||||
|
|||||||
91
Moonlight/App/Services/IpVerificationService.cs
Normal file
91
Moonlight/App/Services/IpVerificationService.cs
Normal file
@@ -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<bool> IsDatacenterOrVpn(string ip)
|
||||||
|
{
|
||||||
|
if (!ConfigService.Get().Moonlight.Security.BlockDatacenterIps)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(ip))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var datacenterNames = new List<string>()
|
||||||
|
{
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
<PackageReference Include="SSH.NET" Version="2020.0.2" />
|
||||||
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
|
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
|
||||||
<PackageReference Include="UAParser" Version="3.1.47" />
|
<PackageReference Include="UAParser" Version="3.1.47" />
|
||||||
|
<PackageReference Include="WhoisClient.NET" Version="5.0.0" />
|
||||||
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
<PackageReference Include="XtermBlazor" Version="1.8.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<TicketClientService>();
|
builder.Services.AddScoped<TicketClientService>();
|
||||||
builder.Services.AddScoped<TicketAdminService>();
|
builder.Services.AddScoped<TicketAdminService>();
|
||||||
builder.Services.AddScoped<MalwareScanService>();
|
builder.Services.AddScoped<MalwareScanService>();
|
||||||
|
builder.Services.AddSingleton<IpVerificationService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<SessionClientService>();
|
builder.Services.AddScoped<SessionClientService>();
|
||||||
builder.Services.AddSingleton<SessionServerService>();
|
builder.Services.AddSingleton<SessionServerService>();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
@inject DynamicBackgroundService DynamicBackgroundService
|
@inject DynamicBackgroundService DynamicBackgroundService
|
||||||
@inject KeyListenerService KeyListenerService
|
@inject KeyListenerService KeyListenerService
|
||||||
@inject ConfigService ConfigService
|
@inject ConfigService ConfigService
|
||||||
|
@inject IpVerificationService IpVerificationService
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var uri = new Uri(NavigationManager.Uri);
|
var uri = new Uri(NavigationManager.Uri);
|
||||||
@@ -46,9 +47,35 @@
|
|||||||
|
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<SoftErrorBoundary>
|
<SoftErrorBoundary>
|
||||||
@if (!IsIpBanned)
|
@if (UserProcessed)
|
||||||
{
|
{
|
||||||
if (UserProcessed)
|
if (IsIpBanned)
|
||||||
|
{
|
||||||
|
<div class="modal d-block">
|
||||||
|
<div class="modal-dialog modal-dialog-centered mw-900px">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
||||||
|
<h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
|
||||||
|
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (IsIpSuspicious)
|
||||||
|
{
|
||||||
|
<div class="modal d-block">
|
||||||
|
<div class="modal-dialog modal-dialog-centered mw-900px">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
||||||
|
<h2>@(SmartTranslateService.Translate("Your ip his blocked. VPNs and Datacenter IPs are prohibited from accessing this site"))</h2>
|
||||||
|
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Please disable your vpn or proxy and try it again"))</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (uri.LocalPath != "/login" &&
|
if (uri.LocalPath != "/login" &&
|
||||||
uri.LocalPath != "/passwordreset" &&
|
uri.LocalPath != "/passwordreset" &&
|
||||||
@@ -102,19 +129,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="modal d-block">
|
|
||||||
<div class="modal-dialog modal-dialog-centered mw-900px">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
|
||||||
<h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
|
|
||||||
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -122,8 +136,8 @@
|
|||||||
<div class="modal-dialog modal-dialog-centered mw-900px">
|
<div class="modal-dialog modal-dialog-centered mw-900px">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
<div class="pt-2 modal-body py-lg-10 px-lg-10">
|
||||||
<h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
|
<h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
|
||||||
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
|
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,6 +151,7 @@
|
|||||||
{
|
{
|
||||||
private bool UserProcessed = false;
|
private bool UserProcessed = false;
|
||||||
private bool IsIpBanned = false;
|
private bool IsIpBanned = false;
|
||||||
|
private bool IsIpSuspicious = false;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -146,11 +161,6 @@
|
|||||||
{
|
{
|
||||||
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
|
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
|
||||||
|
|
||||||
IsIpBanned = await IpBanService.IsBanned();
|
|
||||||
|
|
||||||
if (IsIpBanned)
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
await Event.On<Object>("ipBan.update", this, async _ =>
|
await Event.On<Object>("ipBan.update", this, async _ =>
|
||||||
{
|
{
|
||||||
IsIpBanned = await IpBanService.IsBanned();
|
IsIpBanned = await IpBanService.IsBanned();
|
||||||
@@ -158,6 +168,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
await IdentityService.Load();
|
await IdentityService.Load();
|
||||||
|
|
||||||
|
IsIpBanned = await IpBanService.IsBanned();
|
||||||
|
IsIpSuspicious = await IpVerificationService.IsDatacenterOrVpn(IdentityService.Ip);
|
||||||
|
|
||||||
UserProcessed = true;
|
UserProcessed = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user