-
Notifications
You must be signed in to change notification settings - Fork 1.7k
#2138 Introduce ASP.NET rate limiting #2188
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
base: develop
Are you sure you want to change the base?
Changes from 15 commits
c7c4c6b
b6c1577
e6359cb
39a4934
2833359
852fac1
7dbb578
cd3075a
bbe1931
032e7f2
e8987ad
d5e3a44
f6274bb
2c01ab1
e5555ed
fce8f5b
be8d67b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| @RateLimiterSample_HostAddress = http://localhost:5202 | ||
|
|
||
| GET {{RateLimiterSample_HostAddress}}/laura | ||
| Accept: application/json | ||
|
|
||
| ### | ||
|
|
||
| GET {{RateLimiterSample_HostAddress}}/tom | ||
| Accept: application/json | ||
|
|
||
| ### |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| using Microsoft.AspNetCore.RateLimiting; | ||
| using Ocelot.DependencyInjection; | ||
| using Ocelot.Middleware; | ||
| using System.Threading.RateLimiting; | ||
|
|
||
| var builder = WebApplication.CreateBuilder(args); | ||
| builder.Configuration.AddJsonFile("ocelot.json"); | ||
| builder.Services.AddOcelot(); | ||
|
|
||
| builder.Services.AddRateLimiter(op => | ||
| { | ||
| op.AddFixedWindowLimiter(policyName: "fixed", options => | ||
| { | ||
| options.PermitLimit = 2; | ||
| options.Window = TimeSpan.FromSeconds(12); | ||
| options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; | ||
| options.QueueLimit = 0; | ||
| }); | ||
| }); | ||
|
Comment on lines
+10
to
+19
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree! In this case, we haven't proposed anything new.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @raman-m it's basically what i mentioned:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't mix policies, but we can define as many as needed, right?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's global and they can't be modified. |
||
|
|
||
| var app = builder.Build(); | ||
| app.UseHttpsRedirection(); | ||
|
|
||
| await app.UseOcelot(); | ||
|
|
||
| app.Run(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| { | ||
| "$schema": "http://json.schemastore.org/launchsettings.json", | ||
| "iisSettings": { | ||
| "windowsAuthentication": false, | ||
| "anonymousAuthentication": true, | ||
| "iisExpress": { | ||
| "applicationUrl": "http://localhost:12083", | ||
| "sslPort": 44358 | ||
| } | ||
| }, | ||
| "profiles": { | ||
| "http": { | ||
| "commandName": "Project", | ||
| "dotnetRunMessages": true, | ||
| "launchBrowser": true, | ||
| "launchUrl": "weatherforecast", | ||
| "applicationUrl": "http://localhost:5202", | ||
| "environmentVariables": { | ||
| "ASPNETCORE_ENVIRONMENT": "Development" | ||
| } | ||
| }, | ||
| "https": { | ||
| "commandName": "Project", | ||
| "dotnetRunMessages": true, | ||
| "launchBrowser": true, | ||
| "launchUrl": "weatherforecast", | ||
| "applicationUrl": "https://localhost:7116;http://localhost:5202", | ||
| "environmentVariables": { | ||
| "ASPNETCORE_ENVIRONMENT": "Development" | ||
| } | ||
| }, | ||
| "IIS Express": { | ||
| "commandName": "IISExpress", | ||
| "launchBrowser": true, | ||
| "launchUrl": "weatherforecast", | ||
| "environmentVariables": { | ||
| "ASPNETCORE_ENVIRONMENT": "Development" | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "Logging": { | ||
| "LogLevel": { | ||
| "Default": "Information", | ||
| "Microsoft.AspNetCore": "Warning" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "Logging": { | ||
| "LogLevel": { | ||
| "Default": "Information", | ||
| "Microsoft.AspNetCore": "Warning" | ||
| } | ||
| }, | ||
| "AllowedHosts": "*" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| { | ||
| "Routes": [ | ||
| { | ||
| "UpstreamHttpMethod": [ "Get" ], | ||
| "UpstreamPathTemplate": "/laura", | ||
| "DownstreamPathTemplate": "/fact", | ||
| "DownstreamScheme": "https", | ||
| "DownstreamHostAndPorts": [ | ||
| { "Host": "catfact.ninja", "Port": 443 } | ||
| ], | ||
| "Key": "Laura", | ||
| "RateLimitOptions": { | ||
| "EnableRateLimiting": true, | ||
| "Period": "5s", | ||
| "PeriodTimespan": 1, | ||
| "Limit": 1 | ||
| } | ||
| }, | ||
| { | ||
| "UpstreamHttpMethod": [ "Get" ], | ||
| "UpstreamPathTemplate": "/tom", | ||
| "DownstreamPathTemplate": "/fact", | ||
| "DownstreamScheme": "https", | ||
| "DownstreamHostAndPorts": [ | ||
| { "Host": "catfact.ninja", "Port": 443 } | ||
| ], | ||
| "Key": "Tom", | ||
| "RateLimitOptions": { | ||
| "EnableRateLimiting": true, | ||
| "Policy": "fixed" | ||
| } | ||
| } | ||
| ], | ||
| "Aggregates": [ | ||
| { | ||
| "UpstreamPathTemplate": "/", | ||
| "RouteKeys": [ "Tom", "Laura" ] | ||
| } | ||
| ], | ||
| "GlobalConfiguration": { | ||
| "RateLimitOptions": { | ||
| "QuotaExceededMessage": "Customize Tips!", | ||
| "HttpStatusCode": 418 // I'm a teapot | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ public FileRateLimitRule(FileRateLimitRule from) | |
| Limit = from.Limit; | ||
| Period = from.Period; | ||
| PeriodTimespan = from.PeriodTimespan; | ||
| Policy = from.Policy; | ||
| Wait = from.Wait; | ||
| StatusCode = from.StatusCode; | ||
| QuotaMessage = from.QuotaMessage; | ||
|
|
@@ -74,6 +75,14 @@ public FileRateLimitRule(FileRateLimitRule from) | |
| /// <value>A <see cref="string"/> object which value defaults to "Ocelot.RateLimiting", see the <see cref="RateLimitOptions.DefaultCounterPrefix"/> property.</value> | ||
| public string KeyPrefix { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Rate limit policy name. It only takes effect if rate limit middleware type is set to DotNet. | ||
| /// </summary> | ||
| /// <value> | ||
| /// A string of rate limit policy name. | ||
| /// </value> | ||
| public string Policy { get; set; } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to consider removing this property and using |
||
|
|
||
| /// <summary> | ||
| /// Returns a string that represents the current rule in the format, which defaults to empty string if rate limiting is disabled (<see cref="EnableRateLimiting"/> is <see langword="false"/>). | ||
| /// </summary> | ||
|
|
@@ -85,6 +94,10 @@ public override string ToString() | |
| { | ||
| return string.Empty; | ||
| } | ||
| else if (!string.IsNullOrWhiteSpace(Policy)) | ||
| { | ||
| return $"{nameof(Policy)}:{Policy}"; | ||
| } | ||
|
|
||
| char hdrSign = EnableHeaders == false ? '-' : '+'; | ||
| string waitWindow = PeriodTimespan.HasValue ? PeriodTimespan.Value.ToString("F3") + 's' : Wait.IfEmpty(None); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,8 @@ public RateLimitOptions(bool enableRateLimiting) : this() | |
| } | ||
|
|
||
| public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, IList<string> clientWhitelist, bool enableHeaders, | ||
| string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode) | ||
| string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode, | ||
| string policy = null) | ||
| { | ||
| ClientIdHeader = clientIdHeader.IfEmpty(DefaultClientHeader); | ||
| ClientWhitelist = clientWhitelist ?? []; | ||
|
|
@@ -45,8 +46,9 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, IList<st | |
| QuotaMessage = quotaExceededMessage.IfEmpty(DefaultQuotaMessage); | ||
| Rule = rateLimitRule; | ||
| StatusCode = httpStatusCode; | ||
| Policy = policy; | ||
| } | ||
|
|
||
| public RateLimitOptions(FileRateLimitByHeaderRule fromRule) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(fromRule); | ||
|
|
@@ -63,6 +65,7 @@ public RateLimitOptions(FileRateLimitByHeaderRule fromRule) | |
| fromRule.Period.IfEmpty(RateLimitRule.DefaultPeriod), | ||
| fromRule.PeriodTimespan.HasValue ? $"{fromRule.PeriodTimespan.Value}s" : fromRule.Wait, | ||
| fromRule.Limit ?? RateLimitRule.ZeroLimit); | ||
| Policy = fromRule.Policy; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An option of a flawed rule 😄 Consider removing! |
||
| } | ||
|
|
||
| public RateLimitOptions(RateLimitOptions fromOptions) | ||
|
|
@@ -77,6 +80,7 @@ public RateLimitOptions(RateLimitOptions fromOptions) | |
| QuotaMessage = fromOptions.QuotaMessage.IfEmpty(DefaultQuotaMessage); | ||
| KeyPrefix = fromOptions.KeyPrefix.IfEmpty(DefaultCounterPrefix); | ||
| Rule = fromOptions.Rule ?? RateLimitRule.Empty; | ||
| Policy = fromOptions.Policy; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine because of the copy constructor ✔️ |
||
| } | ||
|
|
||
| /// <summary>Gets a Rate Limit rule.</summary> | ||
|
|
@@ -121,4 +125,6 @@ public RateLimitOptions(RateLimitOptions fromOptions) | |
| /// <summary>Enables or disables <c>X-RateLimit-*</c> and <c>Retry-After</c> headers.</summary> | ||
| /// <value>A <see cref="bool"/> value.</value> | ||
| public bool EnableHeaders { get; init; } | ||
|
|
||
| public string Policy { get; init; } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,7 +56,9 @@ public RouteFluentValidator( | |
| .WithMessage("{PropertyName} {PropertyValue} contains scheme"); | ||
| }); | ||
|
|
||
| When(route => route.RateLimitOptions != null && route.RateLimitOptions.EnableRateLimiting != false, () => | ||
| When(route => route.RateLimitOptions != null | ||
| && route.RateLimitOptions.EnableRateLimiting != false | ||
| && string.IsNullOrWhiteSpace(route.RateLimitOptions.Policy), () => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We still need to validate the rule algorithm properties, even if the ASP.NET policy is in place. |
||
| { | ||
| RuleFor(route => route.RateLimitOptions.Limit) | ||
| .Must(limit => !limit.HasValue || (limit.HasValue && limit.Value > 0)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.AspNetCore.Builder; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Ocelot.Cache; | ||
| using Ocelot.Configuration.Creator; | ||
| using Ocelot.Configuration.File; | ||
|
|
@@ -7,6 +10,11 @@ | |
| using Ocelot.RateLimiting; | ||
| using FluentValidation; | ||
|
|
||
| #if NET7_0_OR_GREATER | ||
|
||
| using System.Threading.RateLimiting; | ||
| using Microsoft.AspNetCore.RateLimiting; | ||
| #endif | ||
|
|
||
| namespace Ocelot.DependencyInjection; | ||
|
|
||
| public static class Features | ||
|
|
@@ -30,10 +38,39 @@ public static IServiceCollection AddConfigurationValidators(this IServiceCollect | |
| /// Read The Docs: <see href="https://ocelot.readthedocs.io/en/latest/features/ratelimiting.html">Rate Limiting</see>. | ||
| /// </remarks> | ||
| /// <param name="services">The services collection to add the feature to.</param> | ||
| /// <param name="configurationRoot">Root configuration object.</param> | ||
| /// <returns>The same <see cref="IServiceCollection"/> object.</returns> | ||
| public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services | ||
| .AddSingleton<IRateLimiting, RateLimiting.RateLimiting>() | ||
| .AddSingleton<IRateLimitStorage, MemoryCacheRateLimitStorage>(); | ||
|
|
||
| #if NET7_0_OR_GREATER | ||
| /// <summary> | ||
| /// Ocelot feature: <see href="">AspNet Rate Limiting</see>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Read The Docs: <see href="">Rate Limiting</see>. | ||
| /// </remarks> | ||
| /// <param name="services">The services collection to add the feature to.</param> | ||
| /// <param name="configurationRoot">Root configuration object.</param> | ||
| /// <returns>The same <see cref="IServiceCollection"/> object.</returns> | ||
| public static IServiceCollection AddAspNetRateLimiting(this IServiceCollection services, IConfiguration configurationRoot) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, but I have a strong feeling that this adder should be part of the method on line 43. I'm open to discussions. |
||
| { | ||
| var globalRateLimitOptions = configurationRoot.Get<FileConfiguration>()?.GlobalConfiguration?.RateLimitOptions; | ||
| var rejectStatusCode = globalRateLimitOptions?.HttpStatusCode ?? StatusCodes.Status429TooManyRequests; | ||
| var rejectedMessage = globalRateLimitOptions?.QuotaExceededMessage ?? "API calls quota exceeded!"; | ||
|
Comment on lines
+53
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merging in the wrong place can be risky. |
||
| services.AddRateLimiter(options => | ||
| { | ||
| options.OnRejected = async (rejectedContext, token) => | ||
| { | ||
| rejectedContext.HttpContext.Response.StatusCode = rejectStatusCode; | ||
| await rejectedContext.HttpContext.Response.WriteAsync(rejectedMessage, token); | ||
|
Comment on lines
+60
to
+61
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's amusing that we implement the same approach in Ocelot's middleware. Perhaps adding a virtual method in the middleware to connect to the handler could work? 😉 |
||
| }; | ||
| }); | ||
|
|
||
| return services; | ||
| } | ||
| #endif | ||
|
|
||
| /// <summary> | ||
| /// Ocelot feature: <see href="https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/caching.rst">Request Caching</see>. | ||
|
|
@@ -65,6 +102,6 @@ public static IServiceCollection AddHeaderRouting(this IServiceCollection servic | |
| /// </summary> | ||
| /// <param name="services">The services collection to add the feature to.</param> | ||
| /// <returns>The same <see cref="IServiceCollection"/> object.</returns> | ||
| public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => | ||
| public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => | ||
| services.AddSingleton<IMetadataCreator, DefaultMetadataCreator>(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,7 +99,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo | |
| Services.TryAddSingleton<IDownstreamRouteProviderFactory, DownstreamRouteProviderFactory>(); | ||
| Services.TryAddSingleton<IHttpResponder, HttpContextResponder>(); | ||
| Services.TryAddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>(); | ||
| Services.AddRateLimiting(); // Feature: Rate Limiting | ||
| Services.TryAddSingleton<IRequestMapper, RequestMapper>(); | ||
| Services.TryAddSingleton<IHttpHandlerOptionsCreator, HttpHandlerOptionsCreator>(); | ||
| Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>(); | ||
|
|
@@ -111,7 +110,10 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo | |
| Services.AddOcelotCache(); | ||
| Services.AddOcelotMetadata(); | ||
| Services.AddOcelotMessageInvokerPool(); | ||
|
|
||
| Services.AddRateLimiting(); // Feature: Rate Limiting | ||
| #if NET7_0_OR_GREATER | ||
| Services.AddAspNetRateLimiting(configurationRoot); // Feature: AspNet Rate Limiting | ||
|
||
| #endif | ||
| // Chinese developers should read StackOverflow ignoring Microsoft Learn docs -> http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc | ||
| Services.AddHttpContextAccessor(); | ||
| Services.TryAddSingleton<IRequestScopedDataRepository, HttpDataRepository>(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.