Skip to content

Commit e94c5a3

Browse files
committed
Add Rate-Limiter for RSS Feed
1 parent e40ac8d commit e94c5a3

File tree

3 files changed

+34
-4
lines changed

3 files changed

+34
-4
lines changed

src/LinkDotNet.Blog.Web/Controller/RssFeedController.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
using LinkDotNet.Blog.Infrastructure.Persistence;
1313
using LinkDotNet.Blog.Web.Features;
1414
using Microsoft.AspNetCore.Mvc;
15+
using Microsoft.AspNetCore.RateLimiting;
1516
using Microsoft.Extensions.Options;
1617
using InvalidOperationException = System.InvalidOperationException;
1718

1819
namespace LinkDotNet.Blog.Web.Controller;
1920

2021
[Route("feed.rss")]
22+
[EnableRateLimiting("ip")]
2123
public sealed class RssFeedController : ControllerBase
2224
{
2325
private static readonly XmlWriterSettings Settings = CreateXmlWriterSettings();

src/LinkDotNet.Blog.Web/Program.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System;
2+
using System.Threading.RateLimiting;
13
using System.Threading.Tasks;
24
using Blazored.Toast;
35
using Blazorise;
@@ -8,6 +10,7 @@
810
using LinkDotNet.Blog.Web.RegistrationExtensions;
911
using Microsoft.AspNetCore.Builder;
1012
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
13+
using Microsoft.AspNetCore.Http;
1114
using Microsoft.Extensions.DependencyInjection;
1215
using Microsoft.Extensions.Hosting;
1316

@@ -40,6 +43,17 @@ private static void RegisterServices(WebApplicationBuilder builder)
4043
options.MaximumReceiveMessageSize = 1024 * 1024;
4144
});
4245

46+
builder.Services.AddRateLimiter(options =>
47+
{
48+
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
49+
options.AddPolicy<string>("ip", httpContext =>
50+
51+
RateLimitPartition.GetFixedWindowLimiter(
52+
httpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty,
53+
_ => new FixedWindowRateLimiterOptions { PermitLimit = 15, Window = TimeSpan.FromMinutes(1) })
54+
);
55+
});
56+
4357
builder.Services.AddConfiguration();
4458

4559
builder.Services.AddBlazoredToast();
@@ -91,6 +105,7 @@ private static void ConfigureApp(WebApplication app)
91105
app.UseAuthentication();
92106
app.UseAuthorization();
93107

108+
app.UseRateLimiter();
94109
app.MapControllers();
95110
app.MapBlazorHub();
96111
app.MapFallbackToPage("/_Host");

tests/LinkDotNet.Blog.IntegrationTests/SmokeTests.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,26 @@ public async Task ShouldAllowDotsForFreeTextSearch()
6363
result.IsSuccessStatusCode.ShouldBeTrue();
6464
}
6565

66+
[Fact]
67+
public async Task RssFeedShouldBeRateLimited()
68+
{
69+
const int numberOfRequests = 16;
70+
using var client = factory.CreateClient();
71+
72+
for (var i = 0; i < numberOfRequests - 1; i++)
73+
{
74+
var result = await client.GetAsync("/feed.rss");
75+
result.IsSuccessStatusCode.ShouldBeTrue();
76+
}
77+
78+
var lastResult = await client.GetAsync("/feed.rss");
79+
lastResult.IsSuccessStatusCode.ShouldBeFalse();
80+
}
81+
6682
public void Dispose() => factory?.Dispose();
6783

6884
public async ValueTask DisposeAsync()
6985
{
70-
if (factory is not null)
71-
{
72-
await factory.DisposeAsync();
73-
}
86+
await factory.DisposeAsync();
7487
}
7588
}

0 commit comments

Comments
 (0)