Skip to content

Commit fcb33cf

Browse files
committed
Add health checks rate limiting feature
1 parent aa1de96 commit fcb33cf

5 files changed

Lines changed: 94 additions & 5 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
using System.Threading.RateLimiting;
5+
6+
namespace OrchardCoreContrib.HealthChecks;
7+
8+
public class HealthChecksRateLimitingMiddleware
9+
{
10+
private readonly RequestDelegate _next;
11+
private readonly HealthChecksOptions _healthChecksOptions;
12+
private readonly SlidingWindowRateLimiter _rateLimiter;
13+
private readonly ILogger _logger;
14+
15+
public HealthChecksRateLimitingMiddleware(
16+
RequestDelegate next,
17+
IOptions<HealthChecksOptions> healthChecksOptions,
18+
IOptions<HealthChecksRateLimitingOptions> healthChecksRateLimitingOptions,
19+
ILogger<HealthChecksRateLimitingMiddleware> logger)
20+
{
21+
var healthChecksRateLimitingOptionsValue = healthChecksRateLimitingOptions.Value;
22+
_rateLimiter = new(new SlidingWindowRateLimiterOptions
23+
{
24+
PermitLimit = healthChecksRateLimitingOptionsValue.PermitLimit,
25+
Window = healthChecksRateLimitingOptionsValue.Window,
26+
SegmentsPerWindow = healthChecksRateLimitingOptionsValue.SegmentsPerWindow,
27+
QueueLimit = healthChecksRateLimitingOptionsValue.QueueLimit,
28+
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
29+
});
30+
_next = next;
31+
_healthChecksOptions = healthChecksOptions.Value;
32+
_logger = logger;
33+
}
34+
35+
public async Task InvokeAsync(HttpContext context)
36+
{
37+
if (context.Request.Path.Equals(_healthChecksOptions.Url))
38+
{
39+
var rateLimitLease = _rateLimiter.AttemptAcquire(1);
40+
41+
if (!rateLimitLease.IsAcquired)
42+
{
43+
_logger.LogWarning("Rate limit exceeded for IP Address {RemoteIP}.", context.Connection.RemoteIpAddress);
44+
45+
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
46+
47+
await context.Response.WriteAsync("Too Many Requests.");
48+
49+
return;
50+
}
51+
}
52+
53+
await _next(context);
54+
}
55+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace OrchardCoreContrib.HealthChecks;
2+
3+
public class HealthChecksRateLimitingOptions
4+
{
5+
public int PermitLimit { get; set; } = 5;
6+
7+
public TimeSpan Window { get; set; } = TimeSpan.FromSeconds(10);
8+
9+
public int SegmentsPerWindow { get; set; } = 10;
10+
11+
public int QueueLimit { get; set; } = 0;
12+
}
13+

src/OrchardCoreContrib.HealthChecks/Manifest.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,10 @@
2121
Description = "Restricts access to health check endpoints by IP address.",
2222
Dependencies = [ "OrchardCoreContrib.HealthChecks" ]
2323
)]
24+
25+
[assembly: Feature(
26+
Id = "OrchardCoreContrib.HealthCheck.RateLimiting",
27+
Name = "Health Check Rate Limiting",
28+
Description = "Limits requests to health check endpoints to prevent DOS attacks.",
29+
Dependencies = ["OrchardCoreContrib.HealthChecks"]
30+
)]

src/OrchardCoreContrib.HealthChecks/Startup.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,15 @@ private static async Task WriteResponse(HttpContext context, HealthReport report
7272
public class IPRestrictionStartup : StartupBase
7373
{
7474
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
75-
{
76-
app.UseMiddleware<HealthCheckIPRestrictionMiddleware>();
77-
}
75+
=> app.UseMiddleware<HealthCheckIPRestrictionMiddleware>();
76+
}
77+
78+
[Feature("OrchardCoreContrib.HealthChecks.RateLimiting")]
79+
public class RateLimitingStartup(IShellConfiguration shellConfiguration) : StartupBase
80+
{
81+
public override void ConfigureServices(IServiceCollection services)
82+
=> services.Configure<HealthChecksRateLimitingOptions>(shellConfiguration.GetSection($"{Constants.ConfigurationKey}:RateLimiting"));
83+
84+
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
85+
=> app.UseMiddleware<HealthChecksRateLimitingMiddleware>();
7886
}

src/OrchardCoreContrib.Modules.Web/appsettings.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,14 @@
4545
"OrchardCoreContrib_HealthChecks": {
4646
"Url": "/health",
4747
"ShowDetails": true,
48-
"AllowedIPs": [ "127.0.0.1", "::1" ]
49-
},
48+
"AllowedIPs": [ "127.0.0.1", "::1" ],
49+
"RateLimiting": {
50+
"PermitLimit": 5,
51+
"Window": "00:00:10",
52+
"SegmentsPerWindow": 10,
53+
"QueueLimit": 0
54+
}
55+
}
5056
//"OrchardCoreContrib_Garnet": {
5157
// "Host": "127.0.0.1",
5258
// "Port": 3278,

0 commit comments

Comments
 (0)