Skip to content

Implement minimal RateLimitingMiddleware #41008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2721e4f
First RateLimiting commit
wtgodbe Feb 25, 2022
caa47a6
More
wtgodbe Feb 25, 2022
d649fca
ctrl-s
wtgodbe Feb 28, 2022
6825683
lil bit
wtgodbe Mar 11, 2022
f7e82a5
Small
wtgodbe Mar 25, 2022
c5f2d94
Check-in launchSettings.json files from Middleware (#40695)
wtgodbe Mar 14, 2022
7782861
Merge main
wtgodbe Mar 30, 2022
2c683fc
sln
wtgodbe Mar 30, 2022
301a4ee
More
wtgodbe Mar 31, 2022
8b1e074
More
wtgodbe Mar 31, 2022
7870d62
More
wtgodbe Apr 1, 2022
16bdcf3
Remove stuff
wtgodbe Apr 1, 2022
4f20092
rm
wtgodbe Apr 1, 2022
a417766
Not SharedFx
wtgodbe Apr 1, 2022
0c43a25
Feedback
wtgodbe Apr 1, 2022
017797d
Internal+IVT
wtgodbe Apr 1, 2022
2b406b0
Feedback
wtgodbe Apr 1, 2022
75aa2cf
Feedback chunk 1
wtgodbe Apr 5, 2022
ade42e0
Feedback chunk 2
wtgodbe Apr 5, 2022
d025eb8
Small feedback
wtgodbe Apr 5, 2022
b7affd6
Merge
wtgodbe Apr 13, 2022
ddddbd4
Fix extension methods
wtgodbe Apr 13, 2022
9abcb07
Small fix
wtgodbe Apr 13, 2022
047ab3d
Func
wtgodbe Apr 13, 2022
cea2af6
Config status code
wtgodbe Apr 13, 2022
4761f1e
Merge remote-tracking branch 'upstream/main' into wtgodbe/RateLimidMi…
wtgodbe Apr 15, 2022
ac0ad26
Feedback, add servicecollection extension
wtgodbe Apr 15, 2022
fc92f12
Update API
wtgodbe Apr 19, 2022
43b16a6
Merge from main
wtgodbe Apr 19, 2022
9d3dffb
Some feedback
wtgodbe Apr 19, 2022
385d939
Lil more feedback
wtgodbe Apr 19, 2022
0b9875b
Partially fix test
wtgodbe Apr 19, 2022
9d992ca
Fix/Add tests
wtgodbe Apr 19, 2022
b90f534
Add another test
wtgodbe Apr 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildAfterTargetingPack", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "src\BuildAfterTargetingPack\BuildAfterTargetingPack.csproj", "{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting", "src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj", "{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting.Tests", "src\Middleware\RateLimiting\test\Microsoft.AspNetCore.RateLimiting.Tests.csproj", "{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -10151,6 +10155,38 @@ Global
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.Build.0 = Release|Any CPU
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.ActiveCfg = Release|Any CPU
{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.Build.0 = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|arm64.ActiveCfg = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|arm64.Build.0 = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x64.ActiveCfg = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x64.Build.0 = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x86.ActiveCfg = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x86.Build.0 = Debug|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|Any CPU.Build.0 = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|arm64.ActiveCfg = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|arm64.Build.0 = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x64.ActiveCfg = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x64.Build.0 = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x86.ActiveCfg = Release|Any CPU
{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x86.Build.0 = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|arm64.ActiveCfg = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|arm64.Build.0 = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x64.ActiveCfg = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x64.Build.0 = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x86.ActiveCfg = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x86.Build.0 = Debug|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|Any CPU.Build.0 = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|arm64.ActiveCfg = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|arm64.Build.0 = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x64.ActiveCfg = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x64.Build.0 = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x86.ActiveCfg = Release|Any CPU
{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization.Routing" ProjectPath="$(RepoRoot)src\Middleware\Localization.Routing\src\Microsoft.AspNetCore.Localization.Routing.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization" ProjectPath="$(RepoRoot)src\Middleware\Localization\src\Microsoft.AspNetCore.Localization.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.MiddlewareAnalysis" ProjectPath="$(RepoRoot)src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.RateLimiting" ProjectPath="$(RepoRoot)src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching.Abstractions\src\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCompression" ProjectPath="$(RepoRoot)src\Middleware\ResponseCompression\src\Microsoft.AspNetCore.ResponseCompression.csproj" />
Expand Down
4 changes: 3 additions & 1 deletion src/Middleware/Middleware.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
"src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj",
"src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj",
"src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj",
"src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj",
"src\\Middleware\\RateLimiting\\test\\Microsoft.AspNetCore.RateLimiting.Tests.csproj",
"src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj",
"src\\Middleware\\ResponseCaching\\samples\\ResponseCachingSample\\ResponseCachingSample.csproj",
"src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj",
Expand Down Expand Up @@ -115,4 +117,4 @@
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>ASP.NET Core middleware for enforcing rate limiting in an application</Description>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore</PackageTags>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="System.Threading.RateLimiting" />

<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
9 changes: 9 additions & 0 deletions src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware
Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.RateLimiting.RateLimitingOptions!>! options) -> void
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter<HttpContext>(System.Threading.RateLimiting.PartitionedRateLimiter<Microsoft.AspNetCore.Http.HttpContext!>! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions!
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter<Microsoft.AspNetCore.Http.HttpContext!>?
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate!
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void
148 changes: 148 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Limits the rate of requests allowed in the application, based on limits set by a user-provided <see cref="PartitionedRateLimiter{TResource}"/>.
/// </summary>
public partial class RateLimitingMiddleware
{

private readonly RequestDelegate _next;
private readonly RequestDelegate _onRejected;
private readonly ILogger _logger;
private readonly PartitionedRateLimiter<HttpContext> _limiter;
private RateLimitLease? _lease;

/// <summary>
/// Creates a new <see cref="RateLimitingMiddleware"/>.
/// </summary>
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> used for logging.</param>
/// <param name="options">The options for the middleware.</param>
public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<RateLimitingOptions> options)
{
_next = next ?? throw new ArgumentNullException(nameof(next));

if (options.Value.Limiter == null)
{
throw new ArgumentException("The value of 'options.Limiter' must not be null.", nameof(options));
}

if (options.Value.OnRejected == null)
{
throw new ArgumentException("The value of 'options.OnRejected' must not be null.", nameof(options));
}

if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}

_logger = loggerFactory.CreateLogger<RateLimitingMiddleware>();
_limiter = options.Value.Limiter;
_onRejected = options.Value.OnRejected;
}

// TODO - EventSource?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be valuable to add EventSource logging to this, part of the next pass?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with waiting for the next pass. It might be better to add EventSource logging to the rate limiter implemenations, but that might be too low level to be useful if there are many.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue tracking this yet?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// <summary>
/// Invokes the logic of the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>A <see cref="Task"/> that completes when the request leaves.</returns>
public async Task Invoke(HttpContext context)
{
var acquireLeaseTask = TryAcquireAsync(context);

// Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets.
bool result;

if (acquireLeaseTask.IsCompleted)
{
result = acquireLeaseTask.Result;
}
else
{
result = await acquireLeaseTask;
}

if (result)
{
try
{
await _next(context);
}
finally
{
OnCompletion();
}
}
else
{
RateLimiterLog.RequestRejectedLimitsExceeded(_logger);
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await _onRejected(context);
}
}

private ValueTask<bool> TryAcquireAsync(HttpContext context)
{
// a return value of 'false' indicates that the request is rejected
// a return value of 'true' indicates that the request may proceed

var lease = _limiter.Acquire(context);
if (lease.IsAcquired)
{
_lease = lease;
return ValueTask.FromResult(true);
}

var task = _limiter.WaitAsync(context);
if (task.IsCompletedSuccessfully)
{
lease = task.Result;
if (lease.IsAcquired)
{
_lease = lease;
return ValueTask.FromResult(true);
}

return ValueTask.FromResult(false);
}

return Awaited(task);
}

private void OnCompletion()
{
if (_lease != null)
{
_lease.Dispose();
}
}

private async ValueTask<bool> Awaited(ValueTask<RateLimitLease> task)
{
var lease = await task;

if (lease.IsAcquired)
{
_lease = lease;
return true;
}

return false;
}

private static partial class RateLimiterLog
{
[LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request with a '503 server not available' error", EventName = "RequestRejectedLimitsExceeded")]
internal static partial void RequestRejectedLimitsExceeded(ILogger logger);
}
}
47 changes: 47 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Specifies options for the <see cref="RateLimitingMiddleware"/>.
/// </summary>
public class RateLimitingOptions
{
// TODO - Provide a default?
private PartitionedRateLimiter<HttpContext>? _limiter;

/// <summary>
/// Gets the <see cref="PartitionedRateLimiter{TResource}"/>
/// </summary>
public PartitionedRateLimiter<HttpContext>? Limiter
{
get => _limiter;
}

/// <summary>
/// Adds a new rate limiter.
/// </summary>
/// <param name="limiter">The <see cref="PartitionedRateLimiter{TResource}"/> to be added.</param>
public RateLimitingOptions AddLimiter<HttpContext>(PartitionedRateLimiter<Http.HttpContext> limiter)
{
if (limiter == null)
{
throw new ArgumentNullException(nameof(limiter));
}
_limiter = limiter;
return this;
}

/// <summary>
/// A <see cref="RequestDelegate"/> that handles requests rejected by this middleware.
/// If it doesn't modify the response, an empty 503 response will be written.
/// </summary>
public RequestDelegate OnRejected { get; set; } = context =>
{
return Task.CompletedTask;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.RateLimiting" />
</ItemGroup>
</Project>
Loading