Dynamic rate limit plugin

This commit is contained in:
Kukks
2023-04-05 18:35:32 +02:00
parent f2f18c3634
commit a90b1e7805
9 changed files with 352 additions and 0 deletions

View File

@@ -51,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Tests"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DataErasure", "Plugins\BTCPayServer.Plugins.DataErasure\BTCPayServer.Plugins.DataErasure.csproj", "{034D1487-81C2-4250-A26E-162579C43C18}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.DataErasure", "Plugins\BTCPayServer.Plugins.DataErasure\BTCPayServer.Plugins.DataErasure.csproj", "{034D1487-81C2-4250-A26E-162579C43C18}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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-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.ActiveCfg = Debug|Any CPU
{034D1487-81C2-4250-A26E-162579C43C18}.Altcoins-Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962} {B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
</PropertyGroup>
<!-- -->
<!-- Plugin specific properties -->
<PropertyGroup>
<Product>Dynamic Rate Limit</Product>
<Description>Allows you to override the default rate limiting.</Description>
<Version>1.0.0</Version>
</PropertyGroup>
<!-- Plugin development properties -->
<PropertyGroup>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<PreserveCompilationContext>false</PreserveCompilationContext>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
<ItemDefinitionGroup>
<ProjectReference>
<Properties>StaticWebAssetsEnabled=false</Properties>
<Private>false</Private>
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
</ProjectReference>
</ItemDefinitionGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**" />
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
namespace BTCPayServer.Plugins.DynamicRateLimits
{
public class DynamicRateLimitSettings
{
public string[] RateLimits { get; set; }
}
}

View File

@@ -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<DynamicRateLimitsService>();
applicationBuilder.AddSingleton<IHostedService>(provider => provider.GetRequiredService<DynamicRateLimitsService>());
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("DynamicRateLimitsPlugin/Nav",
"server-nav"));
base.Execute(applicationBuilder);
}
}

View File

@@ -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<string> OriginalLimits { get; private set; }
public DynamicRateLimitsService(ISettingsRepository settingsRepository, RateLimitService rateLimitService)
{
_settingsRepository = settingsRepository;
_rateLimitService = rateLimitService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
OriginalLimits =
((ConcurrentDictionary<string, LimitRequestZone>) _rateLimitService.GetType()
.GetField("_Zones", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(_rateLimitService))
.Values.Select(zone => zone.ToString());
var settings = await _settingsRepository.GetSettingAsync<DynamicRateLimitSettings>();
if (settings?.RateLimits is not null)
{
foreach (var limit in settings.RateLimits)
{
_rateLimitService.SetZone(limit);
}
}
}
public async Task<DynamicRateLimitSettings> Get()
{
return (await _settingsRepository.GetSettingAsync<DynamicRateLimitSettings>())?? 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;
}
}

View File

@@ -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<IActionResult> Update()
{
return View(await _dynamicRateLimitsService.Get());
}
[HttpPost("")]
public async Task<IActionResult> 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());
}
}
}
}

View File

@@ -0,0 +1,117 @@
@using BTCPayServer.Plugins.DynamicRateLimits
@model DynamicRateLimitSettings
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIServer/_Nav";
}
<h2 class="mb-4">Rate limit configuration</h2>
<form method="post">
<div class="row">
<table class="table table-responsive col-12">
<thead>
<tr>
<th>
Rate Limit
</th>
<th class="text-end">
Actions
</th>
</tr>
</thead>
<tbody id="limit-list">
@if (Model.RateLimits is not null)
{
@for (var index = 0; index < Model.RateLimits.Length; index++)
{
<tr data-index="@index">
<td>
<input class="form-control" type="text" asp-for="RateLimits[index]">
</td>
<td class="text-end">
<button class="btn btn-link" type="button" data-remove>Remove</button>
</td>
</tr>
}
}
</tbody>
</table>
</div>
<button type="button" id="add-limit" class="btn btn-outline-secondary mx-2">Add rate limit</button>
<button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button>
<button name="command" type="submit" value="use-defaults" class="btn btn-primary mt-2">Use defaults</button>
<button class="btn btn-link" type="button"data-bs-toggle="collapse" data-bs-target="#defaults">View defaults</button>
<div class="collapse" id="defaults">
@inject DynamicRateLimitsService DynamicRateLimitsService
<div class="card card-body">
<ul>
@foreach (var rateLimit in DynamicRateLimitsService.OriginalLimits)
{
<li>@rateLimit</li>
}
</ul>
</div>
</div>
</form>
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
}
<template id="row">
<tr data-index="-1">
<td>
<input type="text" class="form-control">
</td>
<td class="text-end">
<button class="btn btn-link" type="button" data-remove>Remove</button>
</td>
</tr>
</template>
<script >
document.addEventListener("DOMContentLoaded", ()=>{
setupRemoveBtn();
document.getElementById("add-limit").addEventListener("click", ()=>{
const template = document.querySelector('#row');
const clone = template.content.cloneNode(true);
document.getElementById("limit-list").appendChild(clone);
setIndex();
setupRemoveBtn();
});
function setupRemoveBtn(){
document.querySelectorAll("[data-remove]").forEach(value =>{
value.removeEventListener("click",onRemove )
value.addEventListener("click",onRemove );
});
}
function onRemove(evt){
evt.target.parentElement.parentElement.remove();
setIndex();
}
function setIndex(){
document.querySelectorAll("[data-index]").forEach((value, key) => {
value.setAttribute("data-index", key);
value.querySelector("input").name = `RateLimits[${key}]`;
})
}
});
</script>

View File

@@ -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);
}
<a class="nav-link @(isActive ? "active" : string.Empty)" asp-action="Update" asp-controller="DynamicRatesLimiter">Rate Limits</a>

View File

@@ -0,0 +1,5 @@
@using BTCPayServer.Abstractions.Services
@inject Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BTCPayServer
@addTagHelper *, BTCPayServer.Abstractions