From fcb33cf0b5f3a51cb1a974dee4dfbf24f5ea417c Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Sun, 12 Apr 2026 21:01:43 +0300 Subject: [PATCH 1/8] Add health checks rate limiting feature --- .../HealthChecksRateLimitingMiddleware.cs | 55 +++++++++++++++++++ .../HealthChecksRateLimitingOptions.cs | 13 +++++ .../Manifest.cs | 7 +++ .../Startup.cs | 14 ++++- .../appsettings.json | 10 +++- 5 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs create mode 100644 src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingOptions.cs diff --git a/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs b/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs new file mode 100644 index 0000000..983b632 --- /dev/null +++ b/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Threading.RateLimiting; + +namespace OrchardCoreContrib.HealthChecks; + +public class HealthChecksRateLimitingMiddleware +{ + private readonly RequestDelegate _next; + private readonly HealthChecksOptions _healthChecksOptions; + private readonly SlidingWindowRateLimiter _rateLimiter; + private readonly ILogger _logger; + + public HealthChecksRateLimitingMiddleware( + RequestDelegate next, + IOptions healthChecksOptions, + IOptions healthChecksRateLimitingOptions, + ILogger logger) + { + var healthChecksRateLimitingOptionsValue = healthChecksRateLimitingOptions.Value; + _rateLimiter = new(new SlidingWindowRateLimiterOptions + { + PermitLimit = healthChecksRateLimitingOptionsValue.PermitLimit, + Window = healthChecksRateLimitingOptionsValue.Window, + SegmentsPerWindow = healthChecksRateLimitingOptionsValue.SegmentsPerWindow, + QueueLimit = healthChecksRateLimitingOptionsValue.QueueLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst + }); + _next = next; + _healthChecksOptions = healthChecksOptions.Value; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.Equals(_healthChecksOptions.Url)) + { + var rateLimitLease = _rateLimiter.AttemptAcquire(1); + + if (!rateLimitLease.IsAcquired) + { + _logger.LogWarning("Rate limit exceeded for IP Address {RemoteIP}.", context.Connection.RemoteIpAddress); + + context.Response.StatusCode = StatusCodes.Status429TooManyRequests; + + await context.Response.WriteAsync("Too Many Requests."); + + return; + } + } + + await _next(context); + } +} diff --git a/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingOptions.cs b/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingOptions.cs new file mode 100644 index 0000000..d0ce9bf --- /dev/null +++ b/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingOptions.cs @@ -0,0 +1,13 @@ +namespace OrchardCoreContrib.HealthChecks; + +public class HealthChecksRateLimitingOptions +{ + public int PermitLimit { get; set; } = 5; + + public TimeSpan Window { get; set; } = TimeSpan.FromSeconds(10); + + public int SegmentsPerWindow { get; set; } = 10; + + public int QueueLimit { get; set; } = 0; +} + diff --git a/src/OrchardCoreContrib.HealthChecks/Manifest.cs b/src/OrchardCoreContrib.HealthChecks/Manifest.cs index 681a566..61dbf69 100644 --- a/src/OrchardCoreContrib.HealthChecks/Manifest.cs +++ b/src/OrchardCoreContrib.HealthChecks/Manifest.cs @@ -21,3 +21,10 @@ Description = "Restricts access to health check endpoints by IP address.", Dependencies = [ "OrchardCoreContrib.HealthChecks" ] )] + +[assembly: Feature( + Id = "OrchardCoreContrib.HealthCheck.RateLimiting", + Name = "Health Check Rate Limiting", + Description = "Limits requests to health check endpoints to prevent DOS attacks.", + Dependencies = ["OrchardCoreContrib.HealthChecks"] +)] diff --git a/src/OrchardCoreContrib.HealthChecks/Startup.cs b/src/OrchardCoreContrib.HealthChecks/Startup.cs index 9e14b86..ee9e57a 100644 --- a/src/OrchardCoreContrib.HealthChecks/Startup.cs +++ b/src/OrchardCoreContrib.HealthChecks/Startup.cs @@ -72,7 +72,15 @@ private static async Task WriteResponse(HttpContext context, HealthReport report public class IPRestrictionStartup : StartupBase { public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) - { - app.UseMiddleware(); - } + => app.UseMiddleware(); +} + +[Feature("OrchardCoreContrib.HealthChecks.RateLimiting")] +public class RateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + => services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting")); + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + => app.UseMiddleware(); } diff --git a/src/OrchardCoreContrib.Modules.Web/appsettings.json b/src/OrchardCoreContrib.Modules.Web/appsettings.json index c264852..ebbbaf8 100644 --- a/src/OrchardCoreContrib.Modules.Web/appsettings.json +++ b/src/OrchardCoreContrib.Modules.Web/appsettings.json @@ -45,8 +45,14 @@ "OrchardCoreContrib_HealthChecks": { "Url": "/health", "ShowDetails": true, - "AllowedIPs": [ "127.0.0.1", "::1" ] - }, + "AllowedIPs": [ "127.0.0.1", "::1" ], + "RateLimiting": { + "PermitLimit": 5, + "Window": "00:00:10", + "SegmentsPerWindow": 10, + "QueueLimit": 0 + } + } //"OrchardCoreContrib_Garnet": { // "Host": "127.0.0.1", // "Port": 3278, From 231653aa1b7f3feea16f156b47e1ab62c78268e2 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Sun, 12 Apr 2026 21:08:34 +0300 Subject: [PATCH 2/8] Add unit test --- .../HealthCheckRateLimitingTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs new file mode 100644 index 0000000..8db2e94 --- /dev/null +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs @@ -0,0 +1,31 @@ +using OrchardCoreContrib.HealthChecks.Tests.Tests; +using System.Net; + +namespace OrchardCoreContrib.HealthChecks.Tests; + +public class HealthCheckRateLimitingTests +{ + [Fact] + public async Task ExceedingLimit_Returns429() + { + // Arrange + using var context = new SaasSiteContext(); + + await context.InitializeAsync(); + + // Act + HttpResponseMessage httpResponse = null; + + for (int i = 0; i < 6; i++) + { + httpResponse = await context.Client.GetAsync("health"); + } + + // Assert + Assert.Equal(HttpStatusCode.TooManyRequests, httpResponse.StatusCode); + + var body = await httpResponse.Content.ReadAsStringAsync(); + + Assert.Equal("Too Many Requests.", body); + } +} From 12bbdc3af2bd3f90ef020a8979caa81b9853f8c0 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 13 Apr 2026 01:09:38 +0300 Subject: [PATCH 3/8] Middleware order is matter --- .../Startup.cs | 4 +++ .../HealthChecksMiddlewareOrderTests.cs | 33 +++++++++++++++++++ ...ts.cs => HealthChecksRateLimitingTests.cs} | 6 ++-- .../OrchardCoreStartup.cs | 2 +- 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs rename test/OrchardCoreContrib.HealthChecks.Tests/{HealthCheckRateLimitingTests.cs => HealthChecksRateLimitingTests.cs} (80%) diff --git a/src/OrchardCoreContrib.HealthChecks/Startup.cs b/src/OrchardCoreContrib.HealthChecks/Startup.cs index ee9e57a..4326255 100644 --- a/src/OrchardCoreContrib.HealthChecks/Startup.cs +++ b/src/OrchardCoreContrib.HealthChecks/Startup.cs @@ -71,6 +71,8 @@ private static async Task WriteResponse(HttpContext context, HealthReport report [Feature("OrchardCoreContrib.HealthChecks.IPRestriction")] public class IPRestrictionStartup : StartupBase { + public override int Order => 10; + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => app.UseMiddleware(); } @@ -78,6 +80,8 @@ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder ro [Feature("OrchardCoreContrib.HealthChecks.RateLimiting")] public class RateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase { + public override int Order => 20; + public override void ConfigureServices(IServiceCollection services) => services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting")); diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs new file mode 100644 index 0000000..32e34ff --- /dev/null +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs @@ -0,0 +1,33 @@ +using OrchardCoreContrib.HealthChecks.Tests.Tests; +using System.Net; + +namespace OrchardCoreContrib.HealthChecks.Tests; + +public class HealthChecksMiddlewareOrderTests +{ + [Fact] + public async Task CorrectOrder_BlockedIp_ShouldReturn403_WithoutConsumingLimit() + { + // Arrange + using var context = new SaasSiteContext(); + + await context.InitializeAsync(); + + // Act & Assert + context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "192.168.1.100"); + + var response = await context.Client.GetAsync("health"); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + context.Client.DefaultRequestHeaders.Remove("X-Forwarded-For"); + context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "127.0.0.1"); + + for (int i = 1; i <= 5; i++) + { + var okResponse = await context.Client.GetAsync("health"); + + Assert.Equal(HttpStatusCode.OK, okResponse.StatusCode); + } + } +} diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs similarity index 80% rename from test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs rename to test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs index 8db2e94..7c7f527 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckRateLimitingTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs @@ -3,7 +3,7 @@ namespace OrchardCoreContrib.HealthChecks.Tests; -public class HealthCheckRateLimitingTests +public class HealthChecksRateLimitingTests { [Fact] public async Task ExceedingLimit_Returns429() @@ -13,10 +13,12 @@ public async Task ExceedingLimit_Returns429() await context.InitializeAsync(); + context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "127.0.0.1"); + // Act HttpResponseMessage httpResponse = null; - for (int i = 0; i < 6; i++) + for (int i = 1; i <= 6; i++) { httpResponse = await context.Client.GetAsync("health"); } diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs index 56f842a..5ff39e4 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs @@ -17,7 +17,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddOrchardCms(builder => builder .AddSetupFeatures("OrchardCore.Tenants") - .AddTenantFeatures("OrchardCoreContrib.HealthChecks.IPRestriction") + .AddTenantFeatures("OrchardCoreContrib.HealthChecks.IPRestriction", "OrchardCoreContrib.HealthChecks.RateLimiting") .ConfigureServices(serviceCollection => { serviceCollection.AddScoped(sp => From d4c114248228fb8bb2e103d2a1675cdae532dbf5 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 13 Apr 2026 01:11:47 +0300 Subject: [PATCH 4/8] Tweaks --- ...trictionTests.cs => HealthChecksIPRestrictionTests.cs} | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) rename test/OrchardCoreContrib.HealthChecks.Tests/{IPRestrictionTests.cs => HealthChecksIPRestrictionTests.cs} (69%) diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/IPRestrictionTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs similarity index 69% rename from test/OrchardCoreContrib.HealthChecks.Tests/IPRestrictionTests.cs rename to test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs index 73336f3..e9dd9f9 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/IPRestrictionTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs @@ -3,7 +3,7 @@ namespace OrchardCoreContrib.HealthChecks.Tests; -public class IPRestrictionTests +public class HealthChecksIPRestrictionTests { [Theory] [InlineData("10.0.0.1", HttpStatusCode.Forbidden)] @@ -16,11 +16,9 @@ public async Task HealthCheck_RestrictIP_IfClientIPNotInAllowedIPs(string client await context.InitializeAsync(); // Act - using var request = new HttpRequestMessage(HttpMethod.Get, "health"); + context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", clientIP); - request.Headers.TryAddWithoutValidation("X-Forwarded-For", clientIP); - - var httpResponse = await context.Client.SendAsync(request); + var httpResponse = await context.Client.GetAsync("health"); // Assert Assert.Equal(expectedStatus, httpResponse.StatusCode); From b8a8ec30775c231581047033c9d28fbde783b817 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Tue, 14 Apr 2026 01:27:07 +0300 Subject: [PATCH 5/8] Add Health Checks Blocking Rate Limiting feature --- ...lthChecksBlockingRateLimitingMiddleware.cs | 83 +++++++++++++++++++ ...HealthChecksBlockingRateLimitingOptions.cs | 6 ++ ...=> HealthChecksIPRestrictionMiddleware.cs} | 4 +- .../HealthChecksRateLimitingMiddleware.cs | 17 ++-- .../Manifest.cs | 9 +- .../OrchardCoreContrib.HealthChecks.csproj | 2 +- .../Startup.cs | 17 +++- .../appsettings.json | 7 ++ .../HealthCheckBlockingTests.cs | 66 +++++++++++++++ .../HealthChecksMiddlewareOrderTests.cs | 8 +- .../OrchardCoreStartup.cs | 2 +- 11 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingMiddleware.cs create mode 100644 src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingOptions.cs rename src/OrchardCoreContrib.HealthChecks/{HealthCheckIPRestrictionMiddleware.cs => HealthChecksIPRestrictionMiddleware.cs} (92%) create mode 100644 test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs diff --git a/src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingMiddleware.cs b/src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingMiddleware.cs new file mode 100644 index 0000000..c3817dc --- /dev/null +++ b/src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingMiddleware.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; +using System.Threading.RateLimiting; + +namespace OrchardCoreContrib.HealthChecks; + +public class HealthChecksBlockingRateLimitingMiddleware +{ + private static readonly ConcurrentDictionary _blockedIPs = new(); + + private readonly RequestDelegate _next; + private readonly HealthChecksOptions _healthChecksOptions; + private readonly HealthChecksRateLimitingOptions _healthChecksRateLimitingOptions; + private readonly HealthChecksBlockingRateLimitingOptions _healthChecksBlockingRateLimitingOptions; + private readonly SlidingWindowRateLimiter _rateLimiter; + private readonly ILogger _logger; + + public HealthChecksBlockingRateLimitingMiddleware( + RequestDelegate next, + IOptions healthChecksOptions, + IOptions healthChecksRateLimitingOptions, + IOptions healthChecksBlockingRateLimitingOptions, + ILogger logger) + { + _next = next; + _healthChecksOptions = healthChecksOptions.Value; + _healthChecksRateLimitingOptions = healthChecksRateLimitingOptions.Value; + _healthChecksBlockingRateLimitingOptions = healthChecksBlockingRateLimitingOptions.Value; + _logger = logger; + _rateLimiter = new(new SlidingWindowRateLimiterOptions + { + PermitLimit = _healthChecksRateLimitingOptions.PermitLimit, + Window = _healthChecksRateLimitingOptions.Window, + SegmentsPerWindow = _healthChecksRateLimitingOptions.SegmentsPerWindow, + QueueLimit = _healthChecksRateLimitingOptions.QueueLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst + }); + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.Equals(_healthChecksOptions.Url)) + { + var ip = context.Connection.RemoteIpAddress?.ToString() ?? "Unknown"; + + if (_blockedIPs.TryGetValue(ip, out var blockedUntil)) + { + if (DateTime.UtcNow < blockedUntil) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + + await context.Response.WriteAsync("Blocked due to excessive requests"); + + return; + } + else + { + _blockedIPs.TryRemove(ip, out _); + } + } + + var rateLimitLease = _rateLimiter.AttemptAcquire(1); + + if (!rateLimitLease.IsAcquired) + { + _blockedIPs[ip] = DateTime.UtcNow.Add(_healthChecksBlockingRateLimitingOptions.BlockDuration); + + _logger.LogWarning("Rate limit exceeded for IP Address {RemoteIP}.", context.Connection.RemoteIpAddress); + + context.Response.StatusCode = StatusCodes.Status429TooManyRequests; + + await context.Response.WriteAsync("Too Many Requests."); + + return; + } + } + + await _next(context); + } +} + diff --git a/src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingOptions.cs b/src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingOptions.cs new file mode 100644 index 0000000..1524d12 --- /dev/null +++ b/src/OrchardCoreContrib.HealthChecks/HealthChecksBlockingRateLimitingOptions.cs @@ -0,0 +1,6 @@ +namespace OrchardCoreContrib.HealthChecks; + +public class HealthChecksBlockingRateLimitingOptions : HealthChecksRateLimitingOptions +{ + public TimeSpan BlockDuration { get; set; } = TimeSpan.FromMinutes(1); +} diff --git a/src/OrchardCoreContrib.HealthChecks/HealthCheckIPRestrictionMiddleware.cs b/src/OrchardCoreContrib.HealthChecks/HealthChecksIPRestrictionMiddleware.cs similarity index 92% rename from src/OrchardCoreContrib.HealthChecks/HealthCheckIPRestrictionMiddleware.cs rename to src/OrchardCoreContrib.HealthChecks/HealthChecksIPRestrictionMiddleware.cs index bb9e39b..a10280f 100644 --- a/src/OrchardCoreContrib.HealthChecks/HealthCheckIPRestrictionMiddleware.cs +++ b/src/OrchardCoreContrib.HealthChecks/HealthChecksIPRestrictionMiddleware.cs @@ -6,11 +6,11 @@ namespace OrchardCoreContrib.HealthChecks; -public class HealthCheckIPRestrictionMiddleware( +public class HealthChecksIPRestrictionMiddleware( RequestDelegate next, IShellConfiguration shellConfiguration, IOptions healthChecksOptions, - ILogger logger) + ILogger logger) { private readonly HealthChecksOptions _healthChecksOptions = healthChecksOptions.Value; private readonly HashSet _allowedIPs = diff --git a/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs b/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs index 983b632..f9ebb8c 100644 --- a/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs +++ b/src/OrchardCoreContrib.HealthChecks/HealthChecksRateLimitingMiddleware.cs @@ -9,6 +9,7 @@ public class HealthChecksRateLimitingMiddleware { private readonly RequestDelegate _next; private readonly HealthChecksOptions _healthChecksOptions; + private readonly HealthChecksRateLimitingOptions _healthChecksRateLimitingOptions; private readonly SlidingWindowRateLimiter _rateLimiter; private readonly ILogger _logger; @@ -18,18 +19,18 @@ public HealthChecksRateLimitingMiddleware( IOptions healthChecksRateLimitingOptions, ILogger logger) { - var healthChecksRateLimitingOptionsValue = healthChecksRateLimitingOptions.Value; + _next = next; + _healthChecksOptions = healthChecksOptions.Value; + _healthChecksRateLimitingOptions = healthChecksRateLimitingOptions.Value; + _logger = logger; _rateLimiter = new(new SlidingWindowRateLimiterOptions { - PermitLimit = healthChecksRateLimitingOptionsValue.PermitLimit, - Window = healthChecksRateLimitingOptionsValue.Window, - SegmentsPerWindow = healthChecksRateLimitingOptionsValue.SegmentsPerWindow, - QueueLimit = healthChecksRateLimitingOptionsValue.QueueLimit, + PermitLimit = _healthChecksRateLimitingOptions.PermitLimit, + Window = _healthChecksRateLimitingOptions.Window, + SegmentsPerWindow = _healthChecksRateLimitingOptions.SegmentsPerWindow, + QueueLimit = _healthChecksRateLimitingOptions.QueueLimit, QueueProcessingOrder = QueueProcessingOrder.OldestFirst }); - _next = next; - _healthChecksOptions = healthChecksOptions.Value; - _logger = logger; } public async Task InvokeAsync(HttpContext context) diff --git a/src/OrchardCoreContrib.HealthChecks/Manifest.cs b/src/OrchardCoreContrib.HealthChecks/Manifest.cs index 61dbf69..570b894 100644 --- a/src/OrchardCoreContrib.HealthChecks/Manifest.cs +++ b/src/OrchardCoreContrib.HealthChecks/Manifest.cs @@ -24,7 +24,14 @@ [assembly: Feature( Id = "OrchardCoreContrib.HealthCheck.RateLimiting", - Name = "Health Check Rate Limiting", + Name = "Health Checks Rate Limiting", Description = "Limits requests to health check endpoints to prevent DOS attacks.", Dependencies = ["OrchardCoreContrib.HealthChecks"] )] + +[assembly: Feature( + Id = "OrchardCore.HealthCheck.BlockingRateLimiting", + Name = "Health Checks Blocking Rate Limiting", + Description = "Adds blocking behavior to the health check rate limiter. Clients exceeding the limit are temporarily blocked to prevent DoS attacks.", + Dependencies = new[] { "OrchardCoreContrib.HealthCheck.RateLimiting" } +)] diff --git a/src/OrchardCoreContrib.HealthChecks/OrchardCoreContrib.HealthChecks.csproj b/src/OrchardCoreContrib.HealthChecks/OrchardCoreContrib.HealthChecks.csproj index 7445a8f..7ca1e73 100644 --- a/src/OrchardCoreContrib.HealthChecks/OrchardCoreContrib.HealthChecks.csproj +++ b/src/OrchardCoreContrib.HealthChecks/OrchardCoreContrib.HealthChecks.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/OrchardCoreContrib.HealthChecks/Startup.cs b/src/OrchardCoreContrib.HealthChecks/Startup.cs index 4326255..9307833 100644 --- a/src/OrchardCoreContrib.HealthChecks/Startup.cs +++ b/src/OrchardCoreContrib.HealthChecks/Startup.cs @@ -74,13 +74,13 @@ public class IPRestrictionStartup : StartupBase public override int Order => 10; public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) - => app.UseMiddleware(); + => app.UseMiddleware(); } [Feature("OrchardCoreContrib.HealthChecks.RateLimiting")] public class RateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase { - public override int Order => 20; + public override int Order => 30; public override void ConfigureServices(IServiceCollection services) => services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting")); @@ -88,3 +88,16 @@ public override void ConfigureServices(IServiceCollection services) public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => app.UseMiddleware(); } + +[Feature("OrchardCoreContrib.HealthChecks.BlockingRateLimiting")] +public class HealthCheckBlockingRateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase +{ + public override int Order => 20; + + public override void ConfigureServices(IServiceCollection services) + => services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:BlockingRateLimiting")); + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + => app.UseMiddleware(); +} + diff --git a/src/OrchardCoreContrib.Modules.Web/appsettings.json b/src/OrchardCoreContrib.Modules.Web/appsettings.json index ebbbaf8..8a9e4de 100644 --- a/src/OrchardCoreContrib.Modules.Web/appsettings.json +++ b/src/OrchardCoreContrib.Modules.Web/appsettings.json @@ -51,6 +51,13 @@ "Window": "00:00:10", "SegmentsPerWindow": 10, "QueueLimit": 0 + }, + "BlockingRateLimiting": { + "PermitLimit": 5, + "Window": "00:00:10", + "SegmentsPerWindow": 10, + "QueueLimit": 0, + "BlockDuration": "00:01:00" } } //"OrchardCoreContrib_Garnet": { diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs new file mode 100644 index 0000000..2d4dac6 --- /dev/null +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs @@ -0,0 +1,66 @@ +using OrchardCoreContrib.HealthChecks.Tests.Tests; +using System.Net; + +namespace OrchardCoreContrib.HealthChecks.Tests; + +public class HealthCheckBlockingTests +{ + [Fact] + public async Task ExceedingLimit_ShouldBlockIP_ForConfiguredDuration() + { + // Arrange + using var context = new SaasSiteContext(); + + await context.InitializeAsync(); + + context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "127.0.0.1"); + + HttpResponseMessage response = null; + + // Act + for (int i = 1; i <= 6; i++) + { + response = await context.Client.GetAsync("health"); + } + + // Assert + Assert.Equal(HttpStatusCode.TooManyRequests, response.StatusCode); + + response = await context.Client.GetAsync("health"); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.Contains("Blocked due to excessive requests", body); + } + + [Fact] + public async Task BlockExpires_AfterDuration_AllowsRequestsAgain() + { + // Arrange + using var context = new SaasSiteContext(); + + await context.InitializeAsync(); + + context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "127.0.0.1"); + + // Act + for (int i = 1; i <= 6; i++) + { + await context.Client.GetAsync("health"); + } + + var response = await context.Client.GetAsync("health"); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + + // Wait slightly longer than block duration + await Task.Delay(TimeSpan.FromSeconds(61)); + + response = await context.Client.GetAsync("health"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs index 32e34ff..4284861 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs @@ -16,18 +16,18 @@ public async Task CorrectOrder_BlockedIp_ShouldReturn403_WithoutConsumingLimit() // Act & Assert context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "192.168.1.100"); - var response = await context.Client.GetAsync("health"); + var httpResponse = await context.Client.GetAsync("health"); - Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode); context.Client.DefaultRequestHeaders.Remove("X-Forwarded-For"); context.Client.DefaultRequestHeaders.Add("X-Forwarded-For", "127.0.0.1"); for (int i = 1; i <= 5; i++) { - var okResponse = await context.Client.GetAsync("health"); + httpResponse = await context.Client.GetAsync("health"); - Assert.Equal(HttpStatusCode.OK, okResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); } } } diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs index 5ff39e4..87bdd5b 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs @@ -17,7 +17,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddOrchardCms(builder => builder .AddSetupFeatures("OrchardCore.Tenants") - .AddTenantFeatures("OrchardCoreContrib.HealthChecks.IPRestriction", "OrchardCoreContrib.HealthChecks.RateLimiting") + .AddTenantFeatures("OrchardCoreContrib.HealthChecks.IPRestriction", "OrchardCoreContrib.HealthChecks.RateLimiting", "OrchardCore.HealthCheck.BlockingRateLimiting") .ConfigureServices(serviceCollection => { serviceCollection.AddScoped(sp => From 77fc749480e634b68265d741314f4be041bd1971 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Thu, 16 Apr 2026 20:55:56 +0300 Subject: [PATCH 6/8] Run tests sequential --- .../Manifest.cs | 4 +- .../Startup.cs | 16 +++- .../appsettings.json | 8 +- ...> HealthCheckBlockingRateLimitingTests.cs} | 5 +- .../HealthChecksIPRestrictionTests.cs | 1 + .../HealthChecksMiddlewareOrderTests.cs | 76 ++++++++++++++++++- .../HealthChecksRateLimitingTests.cs | 1 + ...chardCoreContrib.HealthChecks.Tests.csproj | 39 +++++----- .../OrchardCoreStartup.cs | 2 +- 9 files changed, 117 insertions(+), 35 deletions(-) rename test/OrchardCoreContrib.HealthChecks.Tests/{HealthCheckBlockingTests.cs => HealthCheckBlockingRateLimitingTests.cs} (92%) diff --git a/src/OrchardCoreContrib.HealthChecks/Manifest.cs b/src/OrchardCoreContrib.HealthChecks/Manifest.cs index 570b894..c1e13b0 100644 --- a/src/OrchardCoreContrib.HealthChecks/Manifest.cs +++ b/src/OrchardCoreContrib.HealthChecks/Manifest.cs @@ -23,14 +23,14 @@ )] [assembly: Feature( - Id = "OrchardCoreContrib.HealthCheck.RateLimiting", + Id = "OrchardCoreContrib.HealthChecks.RateLimiting", Name = "Health Checks Rate Limiting", Description = "Limits requests to health check endpoints to prevent DOS attacks.", Dependencies = ["OrchardCoreContrib.HealthChecks"] )] [assembly: Feature( - Id = "OrchardCore.HealthCheck.BlockingRateLimiting", + Id = "OrchardCoreContrib.HealthChecks.BlockingRateLimiting", Name = "Health Checks Blocking Rate Limiting", Description = "Adds blocking behavior to the health check rate limiter. Clients exceeding the limit are temporarily blocked to prevent DoS attacks.", Dependencies = new[] { "OrchardCoreContrib.HealthCheck.RateLimiting" } diff --git a/src/OrchardCoreContrib.HealthChecks/Startup.cs b/src/OrchardCoreContrib.HealthChecks/Startup.cs index 9307833..1d9c9c6 100644 --- a/src/OrchardCoreContrib.HealthChecks/Startup.cs +++ b/src/OrchardCoreContrib.HealthChecks/Startup.cs @@ -8,6 +8,7 @@ using OrchardCore.Environment.Shell.Configuration; using OrchardCore.Modules; using OrchardCoreContrib.HealthChecks.Models; +using OrchardCoreContrib.HealthChecks.Services; using System.Net.Mime; using System.Text.Json; @@ -83,19 +84,28 @@ public class RateLimitingStartup(IShellConfiguration shellConfiguration) : Start public override int Order => 30; public override void ConfigureServices(IServiceCollection services) - => services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting")); + { + services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting")); + + services.AddSingleton(); + } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => app.UseMiddleware(); } [Feature("OrchardCoreContrib.HealthChecks.BlockingRateLimiting")] -public class HealthCheckBlockingRateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase +public class BlockingRateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase { public override int Order => 20; public override void ConfigureServices(IServiceCollection services) - => services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:BlockingRateLimiting")); + { + var rateLimitingSection = shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting"); + + services.Configure(rateLimitingSection); + services.Configure(rateLimitingSection); + } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => app.UseMiddleware(); diff --git a/src/OrchardCoreContrib.Modules.Web/appsettings.json b/src/OrchardCoreContrib.Modules.Web/appsettings.json index 8a9e4de..f4d05fb 100644 --- a/src/OrchardCoreContrib.Modules.Web/appsettings.json +++ b/src/OrchardCoreContrib.Modules.Web/appsettings.json @@ -47,17 +47,11 @@ "ShowDetails": true, "AllowedIPs": [ "127.0.0.1", "::1" ], "RateLimiting": { - "PermitLimit": 5, - "Window": "00:00:10", - "SegmentsPerWindow": 10, - "QueueLimit": 0 - }, - "BlockingRateLimiting": { "PermitLimit": 5, "Window": "00:00:10", "SegmentsPerWindow": 10, "QueueLimit": 0, - "BlockDuration": "00:01:00" + "BlockDuration": "00:00:10" } } //"OrchardCoreContrib_Garnet": { diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingRateLimitingTests.cs similarity index 92% rename from test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs rename to test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingRateLimitingTests.cs index 2d4dac6..6fd8fbd 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthCheckBlockingRateLimitingTests.cs @@ -3,7 +3,8 @@ namespace OrchardCoreContrib.HealthChecks.Tests; -public class HealthCheckBlockingTests +[Collection("Sequential")] +public class HealthCheckBlockingRateLimitingTests { [Fact] public async Task ExceedingLimit_ShouldBlockIP_ForConfiguredDuration() @@ -57,7 +58,7 @@ public async Task BlockExpires_AfterDuration_AllowsRequestsAgain() Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); // Wait slightly longer than block duration - await Task.Delay(TimeSpan.FromSeconds(61)); + await Task.Delay(TimeSpan.FromSeconds(11)); response = await context.Client.GetAsync("health"); diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs index e9dd9f9..fbf8f7c 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksIPRestrictionTests.cs @@ -3,6 +3,7 @@ namespace OrchardCoreContrib.HealthChecks.Tests; +[Collection("Sequential")] public class HealthChecksIPRestrictionTests { [Theory] diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs index 4284861..da01031 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs @@ -1,4 +1,11 @@ -using OrchardCoreContrib.HealthChecks.Tests.Tests; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCoreContrib.HealthChecks.Services; +using OrchardCoreContrib.HealthChecks.Tests.Tests; using System.Net; namespace OrchardCoreContrib.HealthChecks.Tests; @@ -30,4 +37,71 @@ public async Task CorrectOrder_BlockedIp_ShouldReturn403_WithoutConsumingLimit() Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); } } + + [Fact] + public void BlockingRateLimiting_ShouldRunBefore_RateLimiting() + { + // Arrange + var shellConfiguration = Mock.Of(); + var blockingStartup = new BlockingRateLimitingStartup(shellConfiguration); + var rateLimitingStartup = new RateLimitingStartup(shellConfiguration); + + // Act + var blockingOrder = blockingStartup.Order; + var rateLimitingOrder = rateLimitingStartup.Order; + + // Assert + Assert.True(blockingOrder < rateLimitingOrder, + $"BlockingRateLimiting order ({blockingOrder}) should be less than RateLimiting order ({rateLimitingOrder})."); + } + + //[Fact] + //public async Task BlockingMiddleware_ShouldRunBefore_RateLimitingMiddleware() + //{ + // var executed = new List(); + + // var builder = WebApplication.CreateBuilder(); + // builder.Services.AddOptions() + // .Configure(o => + // { + // o.PermitLimit = 1; + // o.Window = TimeSpan.FromSeconds(5); + // o.SegmentsPerWindow = 5; + // o.BlockDuration = TimeSpan.FromSeconds(10); + // }); + // builder.Services.AddSingleton(); + + // var app = builder.Build(); + + // // Register blocking first + // app.Use(async (ctx, next) => + // { + // executed.Add("Blocking"); + // //var middleware = new HealthChecksBlockingRateLimitingMiddleware(next, + // // ctx.RequestServices.GetRequiredService>(), + // // ctx.RequestServices.GetRequiredService()); + // //await middleware.InvokeAsync(ctx); + // }); + + // // Register plain limiter second + // app.Use(async (ctx, next) => + // { + // executed.Add("RateLimiting"); + // //var middleware = new HealthChecksRateLimitingMiddleware(next, + // // ctx.RequestServices.GetRequiredService()); + // //await middleware.InvokeAsync(ctx); + // }); + + // app.Map("/health", () => Results.Ok("Healthy")); + + // var client = app.CreateClient(); + // var response = await client.GetAsync("/health"); + + // // Assert pipeline order + // Assert.Equal("Blocking", executed[0]); + // Assert.Equal("RateLimiting", executed[1]); + + // // Also assert response is OK for first request + // Assert.Equal(HttpStatusCode.OK, response.StatusCode); + //} } diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs index 7c7f527..becff0d 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksRateLimitingTests.cs @@ -3,6 +3,7 @@ namespace OrchardCoreContrib.HealthChecks.Tests; +[Collection("Sequential")] public class HealthChecksRateLimitingTests { [Fact] diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreContrib.HealthChecks.Tests.csproj b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreContrib.HealthChecks.Tests.csproj index bc88a2a..417ac2d 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreContrib.HealthChecks.Tests.csproj +++ b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreContrib.HealthChecks.Tests.csproj @@ -1,26 +1,27 @@  - - enable - false - true - + + enable + false + true + - - - - - - - + + + + + + + + - - - - + + + + - - - + + + diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs index 87bdd5b..1c9ca89 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/OrchardCoreStartup.cs @@ -17,7 +17,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddOrchardCms(builder => builder .AddSetupFeatures("OrchardCore.Tenants") - .AddTenantFeatures("OrchardCoreContrib.HealthChecks.IPRestriction", "OrchardCoreContrib.HealthChecks.RateLimiting", "OrchardCore.HealthCheck.BlockingRateLimiting") + .AddTenantFeatures("OrchardCoreContrib.HealthChecks.IPRestriction", "OrchardCoreContrib.HealthChecks.RateLimiting", "OrchardCoreContrib.HealthChecks.BlockingRateLimiting") .ConfigureServices(serviceCollection => { serviceCollection.AddScoped(sp => From f2a448251adc883fe4c948ab2c3e302ee74b644b Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Thu, 16 Apr 2026 21:02:46 +0300 Subject: [PATCH 7/8] Fix build --- src/OrchardCoreContrib.HealthChecks/Startup.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/OrchardCoreContrib.HealthChecks/Startup.cs b/src/OrchardCoreContrib.HealthChecks/Startup.cs index 1d9c9c6..6872829 100644 --- a/src/OrchardCoreContrib.HealthChecks/Startup.cs +++ b/src/OrchardCoreContrib.HealthChecks/Startup.cs @@ -8,7 +8,6 @@ using OrchardCore.Environment.Shell.Configuration; using OrchardCore.Modules; using OrchardCoreContrib.HealthChecks.Models; -using OrchardCoreContrib.HealthChecks.Services; using System.Net.Mime; using System.Text.Json; @@ -86,8 +85,6 @@ public class RateLimitingStartup(IShellConfiguration shellConfiguration) : Start public override void ConfigureServices(IServiceCollection services) { services.Configure(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting")); - - services.AddSingleton(); } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) From 9076fceccf3a0d0fb9b5ff8fb0b0416270d0a361 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Thu, 16 Apr 2026 21:07:51 +0300 Subject: [PATCH 8/8] Remove unnecessary usings --- .../HealthChecksMiddlewareOrderTests.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs index da01031..5689ccd 100644 --- a/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs +++ b/test/OrchardCoreContrib.HealthChecks.Tests/HealthChecksMiddlewareOrderTests.cs @@ -1,10 +1,5 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Moq; +using Moq; using OrchardCore.Environment.Shell.Configuration; -using OrchardCoreContrib.HealthChecks.Services; using OrchardCoreContrib.HealthChecks.Tests.Tests; using System.Net;