diff --git a/BTCPayServerPlugins.sln b/BTCPayServerPlugins.sln index 9d72191..eba2f75 100644 --- a/BTCPayServerPlugins.sln +++ b/BTCPayServerPlugins.sln @@ -51,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DataErasure", "Plugins\BTCPayServer.Plugins.DataErasure\BTCPayServer.Plugins.DataErasure.csproj", "{034D1487-81C2-4250-A26E-162579C43C18}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DynamicRateLimits", "Plugins\BTCPayServer.Plugins.DynamicRateLimits\BTCPayServer.Plugins.DynamicRateLimits.csproj", "{C6033B0A-1070-4908-8A4E-F7B32C5007DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -243,6 +245,14 @@ Global {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU {034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Release|Any CPU.Build.0 = Release|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU + {C6033B0A-1070-4908-8A4E-F7B32C5007DB}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj new file mode 100644 index 0000000..1fbd453 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + 10 + + + + + Dynamic Rate Limit + Allows you to override the default rate limiting. + 1.0.0 + + + + true + false + true + + + + + + StaticWebAssetsEnabled=false + false + runtime;native;build;buildTransitive;contentFiles + + + + + + + + + diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitSettings.cs b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitSettings.cs new file mode 100644 index 0000000..7db0816 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitSettings.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Plugins.DynamicRateLimits +{ + public class DynamicRateLimitSettings + { + public string[] RateLimits { get; set; } + } +} diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs new file mode 100644 index 0000000..64ff2a9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs @@ -0,0 +1,23 @@ +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace BTCPayServer.Plugins.DynamicRateLimits; + +public class DynamicRateLimitsPlugin : BaseBTCPayServerPlugin +{ + public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } = + { + new() { Identifier = nameof(BTCPayServer), Condition = ">=1.8.0" } + }; + public override void Execute(IServiceCollection applicationBuilder) + { + applicationBuilder.AddSingleton(); + applicationBuilder.AddSingleton(provider => provider.GetRequiredService()); + applicationBuilder.AddSingleton(new UIExtension("DynamicRateLimitsPlugin/Nav", + "server-nav")); + base.Execute(applicationBuilder); + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsService.cs b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsService.cs new file mode 100644 index 0000000..f44bd57 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsService.cs @@ -0,0 +1,81 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Contracts; +using Microsoft.Extensions.Hosting; +using NicolasDorier.RateLimits; + +namespace BTCPayServer.Plugins.DynamicRateLimits; + +public class DynamicRateLimitsService : IHostedService +{ + private readonly ISettingsRepository _settingsRepository; + private readonly RateLimitService _rateLimitService; + public IEnumerable OriginalLimits { get; private set; } + + public DynamicRateLimitsService(ISettingsRepository settingsRepository, RateLimitService rateLimitService) + { + _settingsRepository = settingsRepository; + _rateLimitService = rateLimitService; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + OriginalLimits = + ((ConcurrentDictionary) _rateLimitService.GetType() + .GetField("_Zones", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(_rateLimitService)) + .Values.Select(zone => zone.ToString()); + + var settings = await _settingsRepository.GetSettingAsync(); + if (settings?.RateLimits is not null) + { + foreach (var limit in settings.RateLimits) + { + _rateLimitService.SetZone(limit); + } + } + } + + public async Task Get() + { + + return (await _settingsRepository.GetSettingAsync())?? new DynamicRateLimitSettings(); + } + + public async Task UseDefaults() + { + foreach (var originalLimit in OriginalLimits) + { + _rateLimitService.SetZone(originalLimit); + } + + await _settingsRepository.UpdateSetting(new DynamicRateLimitSettings()); + } + + public async Task Update(string[] limits) + { + foreach (var originalLimit in OriginalLimits) + { + _rateLimitService.SetZone(originalLimit); + } + foreach (var limit in limits) + { + _rateLimitService.SetZone(limit); + } + + await _settingsRepository.UpdateSetting(new DynamicRateLimitSettings() + { + RateLimits = limits + }); + } + + + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs new file mode 100644 index 0000000..b855da9 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs @@ -0,0 +1,67 @@ +using System; +using System.Security.Cryptography; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Extensions; +using BTCPayServer.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NicolasDorier.RateLimits; +using Org.BouncyCastle.Security.Certificates; + +namespace BTCPayServer.Plugins.DynamicRateLimits +{ + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] + [Route("~/plugins/dynamicrateslimiter")] + public class DynamicRatesLimiterController : Controller + { + private readonly DynamicRateLimitsService _dynamicRateLimitsService; + + public DynamicRatesLimiterController(DynamicRateLimitsService dynamicRateLimitsService) + { + _dynamicRateLimitsService = dynamicRateLimitsService; + } + + [HttpGet("")] + public async Task Update() + { + return View(await _dynamicRateLimitsService.Get()); + } + + + [HttpPost("")] + public async Task Update(DynamicRateLimitSettings vm, string command) + { + switch (command) + { + case "save": + if (vm.RateLimits is not null) + { + for (int i = 0; i < vm.RateLimits.Length; i++) + { + if (!LimitRequestZone.TryParse(vm.RateLimits[i], out var zone)) + { + vm.AddModelError(s => s.RateLimits[i], "Invalid rate limit", this); + } + } + } + + if (!ModelState.IsValid) + { + return View(vm); + } + + await _dynamicRateLimitsService.Update(vm.RateLimits); + TempData["SuccessMessage"] = "Dynamic rate limits modified"; + return RedirectToAction(nameof(Update)); + case "use-defaults": + await _dynamicRateLimitsService.UseDefaults(); + TempData["SuccessMessage"] = "Dynamic rate limits modified"; + return RedirectToAction(nameof(Update)); + default: + return View(await _dynamicRateLimitsService.Get()); + } + } + } +} \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/Views/DynamicRatesLimiter/Update.cshtml b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/Views/DynamicRatesLimiter/Update.cshtml new file mode 100644 index 0000000..e6dea6e --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/Views/DynamicRatesLimiter/Update.cshtml @@ -0,0 +1,117 @@ +@using BTCPayServer.Plugins.DynamicRateLimits +@model DynamicRateLimitSettings + +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData["NavPartialName"] = "../UIServer/_Nav"; +} + +Rate limit configuration + + + + + + + + + Rate Limit + + + Actions + + + + + @if (Model.RateLimits is not null) + { + @for (var index = 0; index < Model.RateLimits.Length; index++) + { + + + + + + Remove + + + } + } + + + + + + + Add rate limit + Save + Use defaults + + View defaults + + @inject DynamicRateLimitsService DynamicRateLimitsService + + + @foreach (var rateLimit in DynamicRateLimitsService.OriginalLimits) + { + @rateLimit + } + + + + + + +@section PageFootContent { + +} + + + + + + + Remove + + + + + \ No newline at end of file diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/Views/Shared/DynamicRateLimitsPlugin/Nav.cshtml b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/Views/Shared/DynamicRateLimitsPlugin/Nav.cshtml new file mode 100644 index 0000000..c3a7eee --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/Views/Shared/DynamicRateLimitsPlugin/Nav.cshtml @@ -0,0 +1,7 @@ +@using BTCPayServer.Plugins.DynamicRateLimits +@{ + var isActive = ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null && + nameof(DynamicRatesLimiterController).StartsWith(controller?.ToString(), StringComparison.InvariantCultureIgnoreCase); +} + +Rate Limits diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/_ViewImports.cshtml b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/_ViewImports.cshtml new file mode 100644 index 0000000..d897d63 --- /dev/null +++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using BTCPayServer.Abstractions.Services +@inject Safe Safe +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, BTCPayServer +@addTagHelper *, BTCPayServer.Abstractions \ No newline at end of file