diff --git a/samples/Ocelot.Samples.sln b/samples/Ocelot.Samples.sln
index 93aeba9c6..70606a044 100644
--- a/samples/Ocelot.Samples.sln
+++ b/samples/Ocelot.Samples.sln
@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabri
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Web", "Web\Ocelot.Samples.Web.csproj", "{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.RateLimiter", "RateLimiter\Ocelot.Samples.RateLimiter.csproj", "{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -93,6 +95,10 @@ Global
{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj b/samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj
new file mode 100644
index 000000000..c49a854ca
--- /dev/null
+++ b/samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/samples/RateLimiter/Ocelot.Samples.RateLimiter.http b/samples/RateLimiter/Ocelot.Samples.RateLimiter.http
new file mode 100644
index 000000000..f35158ae9
--- /dev/null
+++ b/samples/RateLimiter/Ocelot.Samples.RateLimiter.http
@@ -0,0 +1,11 @@
+@RateLimiterSample_HostAddress = http://localhost:5202
+
+GET {{RateLimiterSample_HostAddress}}/laura
+Accept: application/json
+
+###
+
+GET {{RateLimiterSample_HostAddress}}/tom
+Accept: application/json
+
+###
diff --git a/samples/RateLimiter/Program.cs b/samples/RateLimiter/Program.cs
new file mode 100644
index 000000000..99c78bd49
--- /dev/null
+++ b/samples/RateLimiter/Program.cs
@@ -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;
+ });
+});
+
+var app = builder.Build();
+app.UseHttpsRedirection();
+
+await app.UseOcelot();
+
+app.Run();
diff --git a/samples/RateLimiter/Properties/launchSettings.json b/samples/RateLimiter/Properties/launchSettings.json
new file mode 100644
index 000000000..9a5cdd083
--- /dev/null
+++ b/samples/RateLimiter/Properties/launchSettings.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/samples/RateLimiter/appsettings.Development.json b/samples/RateLimiter/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/samples/RateLimiter/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/RateLimiter/appsettings.json b/samples/RateLimiter/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/samples/RateLimiter/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/RateLimiter/ocelot.json b/samples/RateLimiter/ocelot.json
new file mode 100644
index 000000000..64b2c4f04
--- /dev/null
+++ b/samples/RateLimiter/ocelot.json
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs
index 67e39af8b..f9cbda965 100644
--- a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs
+++ b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs
@@ -10,6 +10,7 @@ public class RateLimitOptionsBuilder
private string _rateLimitCounterPrefix;
private RateLimitRule _rateLimitRule;
private int _httpStatusCode;
+ private string _rateLimitPolicyName;
public RateLimitOptionsBuilder WithEnableRateLimiting(bool enableRateLimiting)
{
@@ -59,11 +60,17 @@ public RateLimitOptionsBuilder WithHttpStatusCode(int httpStatusCode)
return this;
}
+ public RateLimitOptionsBuilder WithRateLimitPolicyName(string policyName)
+ {
+ _rateLimitPolicyName = policyName;
+ return this;
+ }
+
public RateLimitOptions Build()
{
return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _getClientWhitelist,
_disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix,
- _rateLimitRule, _httpStatusCode);
+ _rateLimitRule, _httpStatusCode, _rateLimitPolicyName);
}
}
}
diff --git a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs
index 7bbd7263a..cc78b3909 100644
--- a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs
+++ b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs
@@ -20,6 +20,7 @@ public RateLimitOptions Create(FileRateLimitRule fileRateLimitRule, FileGlobalCo
.WithRateLimitRule(new RateLimitRule(fileRateLimitRule.Period,
fileRateLimitRule.PeriodTimespan,
fileRateLimitRule.Limit))
+ .WithRateLimitPolicyName(fileRateLimitRule.Policy)
.Build();
}
diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
index ffbc0c994..7a79b9995 100644
--- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs
+++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
@@ -14,6 +14,7 @@ public FileRateLimitRule(FileRateLimitRule from)
Limit = from.Limit;
Period = from.Period;
PeriodTimespan = from.PeriodTimespan;
+ Policy = from.Policy;
}
///
@@ -56,6 +57,14 @@ public FileRateLimitRule(FileRateLimitRule from)
///
public long Limit { get; set; }
+ ///
+ /// Rate limit policy name. It only takes effect if rate limit middleware type is set to DotNet.
+ ///
+ ///
+ /// A string of rate limit policy name.
+ ///
+ public string Policy { get; set; }
+
///
public override string ToString()
{
@@ -65,11 +74,20 @@ public override string ToString()
}
var sb = new StringBuilder();
- sb.Append(
+
+ if (!string.IsNullOrWhiteSpace(Policy))
+ {
+ sb.Append($"{nameof(Policy)}:{Policy}");
+ }
+ else
+ {
+ sb.Append(
$"{nameof(Period)}:{Period},{nameof(PeriodTimespan)}:{PeriodTimespan:F},{nameof(Limit)}:{Limit},{nameof(ClientWhitelist)}:[");
- sb.AppendJoin(',', ClientWhitelist);
- sb.Append(']');
+ sb.AppendJoin(',', ClientWhitelist);
+ sb.Append(']');
+ }
+
return sb.ToString();
}
}
diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs
index 3ff91e44b..243d1574b 100644
--- a/src/Ocelot/Configuration/RateLimitOptions.cs
+++ b/src/Ocelot/Configuration/RateLimitOptions.cs
@@ -8,7 +8,7 @@ public class RateLimitOptions
private readonly Func> _getClientWhitelist;
public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func> getClientWhitelist, bool disableRateLimitHeaders,
- string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
+ string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode, string rateLimitPolicy = null)
{
EnableRateLimiting = enableRateLimiting;
ClientIdHeader = clientIdHeader;
@@ -18,67 +18,68 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func
- /// Gets a Rate Limit rule.
+ Policy = rateLimitPolicy;
+ }
+
+ ///
+ /// Gets a Rate Limit rule.
///
///
/// A object that represents the rule.
///
- public RateLimitRule RateLimitRule { get; }
-
+ public RateLimitRule RateLimitRule { get; }
+
///
/// Gets the list of white listed clients.
///
///
/// A (where T is ) collection with white listed clients.
///
- public List ClientWhitelist => _getClientWhitelist();
-
+ public List ClientWhitelist => _getClientWhitelist();
+
///
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId.
///
///
/// A string value with the HTTP header.
///
- public string ClientIdHeader { get; }
-
+ public string ClientIdHeader { get; }
+
///
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests).
///
///
- /// An integer value with the HTTP Status code.
- /// Default value: 429 (Too Many Requests).
+ /// An integer value with the HTTP Status code.
+ /// Default value: 429 (Too Many Requests).
///
- public int HttpStatusCode { get; }
-
+ public int HttpStatusCode { get; }
+
///
- /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
+ /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
/// If none specified the default will be: "API calls quota exceeded! maximum admitted {0} per {1}".
///
///
/// A string value with a formatter for the QuotaExceeded response message.
/// Default will be: "API calls quota exceeded! maximum admitted {0} per {1}".
///
- public string QuotaExceededMessage { get; }
-
+ public string QuotaExceededMessage { get; }
+
///
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key.
///
///
/// A string value with the counter prefix.
///
- public string RateLimitCounterPrefix { get; }
-
+ public string RateLimitCounterPrefix { get; }
+
///
/// Enables endpoint rate limiting based URL path and HTTP verb.
///
///
/// A boolean value for enabling endpoint rate limiting based URL path and HTTP verb.
///
- public bool EnableRateLimiting { get; }
-
+ public bool EnableRateLimiting { get; }
+
///
/// Disables X-Rate-Limit and Retry-After headers.
///
@@ -86,5 +87,7 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, FuncX-Rate-Limit and Retry-After headers.
///
public bool DisableRateLimitHeaders { get; }
+
+ public string Policy { get; }
}
}
diff --git a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
index fbcbd57d2..e241627e0 100644
--- a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
@@ -1,7 +1,7 @@
-using FluentValidation;
+using FluentValidation;
using Microsoft.AspNetCore.Authentication;
-using Ocelot.Configuration.File;
-using Ocelot.Configuration.Creator;
+using Ocelot.Configuration.File;
+using Ocelot.Configuration.Creator;
namespace Ocelot.Configuration.Validator
{
@@ -56,13 +56,16 @@ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemePr
When(route => route.RateLimitOptions.EnableRateLimiting, () =>
{
- RuleFor(route => route.RateLimitOptions.Period)
+ When(IsOcelotRateLimiter, () =>
+ {
+ RuleFor(route => route.RateLimitOptions.Period)
.NotEmpty()
.WithMessage("RateLimitOptions.Period is empty");
- RuleFor(route => route.RateLimitOptions)
- .Must(IsValidPeriod)
- .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period");
+ RuleFor(route => route.RateLimitOptions)
+ .Must(IsValidPeriod)
+ .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period");
+ });
});
RuleFor(route => route.AuthenticationOptions)
@@ -85,28 +88,33 @@ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemePr
{
RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$");
});
-
- When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () =>
- {
- RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$");
- });
+
+ When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () =>
+ {
+ RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$");
+ });
}
private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken)
{
- if (string.IsNullOrEmpty(options.AuthenticationProviderKey)
+ if (string.IsNullOrEmpty(options.AuthenticationProviderKey)
&& options.AuthenticationProviderKeys.Length == 0)
{
return true;
}
var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
- var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList();
+ var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList();
var primary = options.AuthenticationProviderKey;
- return !string.IsNullOrEmpty(primary) && supportedSchemes.Contains(primary)
+ return !string.IsNullOrEmpty(primary) && supportedSchemes.Contains(primary)
|| (string.IsNullOrEmpty(primary) && options.AuthenticationProviderKeys.All(supportedSchemes.Contains));
}
+ private static bool IsOcelotRateLimiter(FileRoute fileRoute)
+ {
+ return string.IsNullOrWhiteSpace(fileRoute.RateLimitOptions.Policy);
+ }
+
private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
{
if (string.IsNullOrEmpty(rateLimitOptions.Period))
@@ -120,7 +128,7 @@ private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
var minutesRegEx = new Regex("^[0-9]+m");
var hoursRegEx = new Regex("^[0-9]+h");
var daysRegEx = new Regex("^[0-9]+d");
-
+
return secondsRegEx.Match(period).Success
|| minutesRegEx.Match(period).Success
|| hoursRegEx.Match(period).Success
diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs
index 1ea558932..aadfe949c 100644
--- a/src/Ocelot/DependencyInjection/Features.cs
+++ b/src/Ocelot/DependencyInjection/Features.cs
@@ -1,10 +1,18 @@
-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;
using Ocelot.DownstreamRouteFinder.HeaderMatcher;
using Ocelot.RateLimiting;
+#if NET7_0_OR_GREATER
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.RateLimiting;
+#endif
+
namespace Ocelot.DependencyInjection;
public static class Features
@@ -16,10 +24,39 @@ public static class Features
/// Read The Docs: Rate Limiting.
///
/// The services collection to add the feature to.
+ /// Root configuration object.
/// The same object.
public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services
.AddSingleton()
.AddSingleton();
+
+#if NET7_0_OR_GREATER
+ ///
+ /// Ocelot feature: AspNet Rate Limiting.
+ ///
+ ///
+ /// Read The Docs: Rate Limiting.
+ ///
+ /// The services collection to add the feature to.
+ /// Root configuration object.
+ /// The same object.
+ public static IServiceCollection AddAspNetRateLimiting(this IServiceCollection services, IConfiguration configurationRoot)
+ {
+ var globalRateLimitOptions = configurationRoot.Get()?.GlobalConfiguration?.RateLimitOptions;
+ var rejectStatusCode = globalRateLimitOptions?.HttpStatusCode ?? StatusCodes.Status429TooManyRequests;
+ var rejectedMessage = globalRateLimitOptions?.QuotaExceededMessage ?? "API calls quota exceeded!";
+ services.AddRateLimiter(options =>
+ {
+ options.OnRejected = async (rejectedContext, token) =>
+ {
+ rejectedContext.HttpContext.Response.StatusCode = rejectStatusCode;
+ await rejectedContext.HttpContext.Response.WriteAsync(rejectedMessage, token);
+ };
+ });
+
+ return services;
+ }
+#endif
///
/// Ocelot feature: Request Caching.
@@ -50,6 +87,6 @@ public static IServiceCollection AddHeaderRouting(this IServiceCollection servic
///
/// The services collection to add the feature to.
/// The same object.
- public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) =>
+ public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) =>
services.AddSingleton();
}
diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs
index 642697744..505ff064e 100644
--- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs
+++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs
@@ -104,6 +104,9 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.TryAddSingleton();
Services.TryAddSingleton();
Services.AddRateLimiting(); // Feature: Rate Limiting
+#if NET7_0_OR_GREATER
+ Services.AddAspNetRateLimiting(configurationRoot); // Feature: AspNet Rate Limiting
+#endif
Services.TryAddSingleton();
Services.TryAddSingleton();
Services.TryAddSingleton();
diff --git a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs
index 55d784ca7..40d4c2cc1 100644
--- a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs
+++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs
@@ -1,4 +1,7 @@
-using Microsoft.AspNetCore.Http;
+#if NET7_0_OR_GREATER
+using Microsoft.AspNetCore.RateLimiting;
+#endif
+using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Ocelot.Configuration;
using Ocelot.Logging;
@@ -39,6 +42,18 @@ public async Task Invoke(HttpContext httpContext)
return;
}
+ #if NET7_0_OR_GREATER
+ if (!string.IsNullOrWhiteSpace(options.Policy))
+ {
+ //add EnableRateLimiting attribute to endpoint, so that .Net rate limiter can pick it up and do its thing
+ var metadata = new EndpointMetadataCollection(new EnableRateLimitingAttribute(options.Policy));
+ var endpoint = new Endpoint(null, metadata, "tempEndpoint");
+ httpContext.SetEndpoint(endpoint);
+ await _next.Invoke(httpContext);
+ return;
+ }
+ #endif
+
// compute identity from request
var identity = SetIdentity(httpContext, options);
diff --git a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs
index 68268cb40..84960b1d7 100644
--- a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs
+++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs
@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Ocelot.Middleware;
namespace Ocelot.RateLimiting.Middleware;
@@ -6,6 +8,22 @@ public static class RateLimitingMiddlewareExtensions
{
public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
{
- return builder.UseMiddleware();
+ builder.UseMiddleware();
+
+ //use AspNet rate limiter
+#if NET7_0_OR_GREATER
+ builder.UseWhen(UseAspNetRateLimiter, rateLimitedApp =>
+ {
+ rateLimitedApp.UseRateLimiter();
+ });
+#endif
+
+ return builder;
+ }
+
+ private static bool UseAspNetRateLimiter(HttpContext httpContext)
+ {
+ var downstreamRoute = httpContext.Items.DownstreamRoute();
+ return !string.IsNullOrWhiteSpace(downstreamRoute?.RateLimitOptions?.Policy);
}
}
diff --git a/test/Ocelot.AcceptanceTests/RateLimiting/AspNetRateLimitingTests.cs b/test/Ocelot.AcceptanceTests/RateLimiting/AspNetRateLimitingTests.cs
new file mode 100644
index 000000000..628fae452
--- /dev/null
+++ b/test/Ocelot.AcceptanceTests/RateLimiting/AspNetRateLimitingTests.cs
@@ -0,0 +1,108 @@
+#if NET7_0_OR_GREATER
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.DependencyInjection;
+using System.Threading.RateLimiting;
+#endif
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Ocelot.Configuration.File;
+using Ocelot.DependencyInjection;
+
+namespace Ocelot.AcceptanceTests.RateLimiting
+{
+ public class AspNetRateLimitingTests: RateLimitingSteps
+ {
+ private const string _rateLimitPolicyName = "RateLimitPolicy";
+ private const int _rateLimitLimit = 3;
+ private const int _rateLimitWindow = 1;
+ private const string _quotaExceededMessage = "woah!";
+ private readonly ServiceHandler _serviceHandler = new ();
+
+ public override void Dispose()
+ {
+ _serviceHandler.Dispose();
+ base.Dispose();
+ }
+
+#if NET7_0_OR_GREATER
+ [Fact]
+ [Trait("Feat", "2138")]
+ public void Should_RateLimit()
+ {
+ var port = PortFinder.GetRandomPort();
+ var route = GivenRoute(port, _rateLimitPolicyName);
+ var configuration = GivenConfigurationWithRateLimitOptions(route);
+
+ var ocelotServices = GivenOcelotServices();
+
+ this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/"))
+ .And(x => GivenThereIsAConfiguration(configuration))
+ .And(x => GivenOcelotIsRunningWithServices(ocelotServices))
+ .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/", 1))
+ .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.OK))
+ .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/", 2))
+ .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.OK))
+ .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/", 1))
+ .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.TooManyRequests))
+ .Then(x => x.ThenTheResponseBodyShouldBe(_quotaExceededMessage))
+ .And(x => GivenIWait((1000 * _rateLimitWindow) + 100))
+ .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/", 1))
+ .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.OK))
+ .BDDfy();
+ }
+
+ private FileRoute GivenRoute(int port, string rateLimitPolicyName) => new()
+ {
+ DownstreamHostAndPorts = new() { new("localhost", port) },
+ DownstreamPathTemplate = "/",
+ DownstreamScheme = Uri.UriSchemeHttp,
+ UpstreamHttpMethod = new() { HttpMethods.Get },
+ UpstreamPathTemplate = "/",
+ RateLimitOptions = new FileRateLimitRule()
+ {
+ EnableRateLimiting = true,
+ Policy = rateLimitPolicyName,
+ },
+ };
+
+ private static FileConfiguration GivenConfigurationWithRateLimitOptions(params FileRoute[] routes)
+ {
+ var config = GivenConfiguration(routes);
+ config.GlobalConfiguration = new()
+ {
+ RateLimitOptions = new()
+ {
+ QuotaExceededMessage = _quotaExceededMessage,
+ HttpStatusCode = (int)HttpStatusCode.TooManyRequests,
+ },
+ };
+ return config;
+ }
+
+ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath)
+ {
+ _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context =>
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.OK;
+ context.Response.WriteAsync("test response");
+ return Task.CompletedTask;
+ });
+ }
+
+ private Action GivenOcelotServices() => services =>
+ {
+ services.AddOcelot();
+ services.AddRateLimiter(op =>
+ {
+ op.AddFixedWindowLimiter(_rateLimitPolicyName, options =>
+ {
+ options.PermitLimit = _rateLimitLimit;
+ options.Window = TimeSpan.FromSeconds(_rateLimitWindow);
+ options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
+ options.QueueLimit = 0;
+ });
+ });
+ };
+#endif
+ }
+}
diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs
index e514877d2..3013f708f 100644
--- a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs
+++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs
@@ -1,7 +1,7 @@
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.Creator;
-using Ocelot.Configuration.File;
+using Ocelot.Configuration.File;
namespace Ocelot.UnitTests.Configuration
{
@@ -19,7 +19,7 @@ public RateLimitOptionsCreatorTests()
}
[Fact]
- public void should_create_rate_limit_options()
+ public void should_create_rate_limit_options_ocelot()
{
var fileRoute = new FileRoute
{
@@ -55,8 +55,52 @@ public void should_create_rate_limit_options()
fileRoute.RateLimitOptions.PeriodTimespan,
fileRoute.RateLimitOptions.Limit))
.Build();
-
- _enabled = false;
+
+ _enabled = false;
+
+ this.Given(x => x.GivenTheFollowingFileRoute(fileRoute))
+ .And(x => x.GivenTheFollowingFileGlobalConfig(fileGlobalConfig))
+ .And(x => x.GivenRateLimitingIsEnabled())
+ .When(x => x.WhenICreate())
+ .Then(x => x.ThenTheFollowingIsReturned(expected))
+ .BDDfy();
+ }
+
+ [Fact]
+ [Trait("Feat", "2138")]
+ public void should_create_rate_limit_options_aspnet()
+ {
+ var fileRoute = new FileRoute
+ {
+ RateLimitOptions = new FileRateLimitRule
+ {
+ Policy = "test",
+ EnableRateLimiting = true,
+ },
+ };
+ var fileGlobalConfig = new FileGlobalConfiguration
+ {
+ RateLimitOptions = new FileRateLimitOptions
+ {
+ ClientIdHeader = "ClientIdHeader",
+ DisableRateLimitHeaders = true,
+ QuotaExceededMessage = "QuotaExceededMessage",
+ HttpStatusCode = 200,
+ },
+ };
+ var expected = new RateLimitOptionsBuilder()
+ .WithClientIdHeader("ClientIdHeader")
+ .WithClientWhiteList(() => new List())
+ .WithDisableRateLimitHeaders(true)
+ .WithEnableRateLimiting(true)
+ .WithHttpStatusCode(200)
+ .WithQuotaExceededMessage("QuotaExceededMessage")
+ .WithRateLimitCounterPrefix("ocelot")
+ .WithRateLimitRule(new RateLimitRule(null, 0, 0))
+ .WithRateLimitPolicyName("test")
+ .Build();
+
+ _enabled = false;
this.Given(x => x.GivenTheFollowingFileRoute(fileRoute))
.And(x => x.GivenTheFollowingFileGlobalConfig(fileGlobalConfig))
@@ -87,7 +131,7 @@ private void WhenICreate()
}
private void ThenTheFollowingIsReturned(RateLimitOptions expected)
- {
+ {
_enabled.ShouldBeTrue();
_result.ClientIdHeader.ShouldBe(expected.ClientIdHeader);
_result.ClientWhitelist.ShouldBe(expected.ClientWhitelist);
diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs
index 1b2bfadb3..257c054dd 100644
--- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs
+++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs
@@ -1,9 +1,10 @@
-using FluentValidation.Results;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Http;
-using Ocelot.Configuration.File;
-using Ocelot.Configuration.Validator;
-using System.Reflection;
+using FluentValidation.Results;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Ocelot.Configuration;
+using Ocelot.Configuration.File;
+using Ocelot.Configuration.Validator;
+using System.Reflection;
namespace Ocelot.UnitTests.Configuration.Validation
{
@@ -191,31 +192,31 @@ public void should_not_be_valid_if_enable_rate_limiting_true_and_period_has_valu
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"))
.BDDfy();
- }
-
- [Theory]
- [InlineData(null, false)]
- [InlineData("", false)]
- [InlineData("1s", true)]
- [InlineData("2m", true)]
- [InlineData("3h", true)]
- [InlineData("4d", true)]
- [InlineData("123", false)]
- [InlineData("-123", false)]
- [InlineData("bad", false)]
- [InlineData(" 3s ", true)]
- [InlineData(" -3s ", false)]
- public void IsValidPeriod_ReflectionLifeHack_BranchesAreCovered(string period, bool expected)
- {
- // Arrange
- var method = _validator.GetType().GetMethod("IsValidPeriod", BindingFlags.NonPublic | BindingFlags.Static);
- var argument = new FileRateLimitRule { Period = period };
-
- // Act
- bool actual = (bool)method.Invoke(_validator, new object[] { argument });
-
- // Assert
- Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(null, false)]
+ [InlineData("", false)]
+ [InlineData("1s", true)]
+ [InlineData("2m", true)]
+ [InlineData("3h", true)]
+ [InlineData("4d", true)]
+ [InlineData("123", false)]
+ [InlineData("-123", false)]
+ [InlineData("bad", false)]
+ [InlineData(" 3s ", true)]
+ [InlineData(" -3s ", false)]
+ public void IsValidPeriod_ReflectionLifeHack_BranchesAreCovered(string period, bool expected)
+ {
+ // Arrange
+ var method = _validator.GetType().GetMethod("IsValidPeriod", BindingFlags.NonPublic | BindingFlags.Static);
+ var argument = new FileRateLimitRule { Period = period };
+
+ // Act
+ bool actual = (bool)method.Invoke(_validator, new object[] { argument });
+
+ // Assert
+ Assert.Equal(expected, actual);
}
[Fact]
diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs
index 741af9d54..4c2deb5fd 100644
--- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs
+++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs
@@ -1,9 +1,9 @@
-using Ocelot.Configuration;
-using Ocelot.Configuration.Builder;
-using Ocelot.Configuration.Creator;
-using Ocelot.DownstreamRouteFinder.Finder;
-using Ocelot.LoadBalancer.LoadBalancers;
-using Ocelot.Responses;
+using Ocelot.Configuration;
+using Ocelot.Configuration.Builder;
+using Ocelot.Configuration.Creator;
+using Ocelot.DownstreamRouteFinder.Finder;
+using Ocelot.LoadBalancer.LoadBalancers;
+using Ocelot.Responses;
namespace Ocelot.UnitTests.DownstreamRouteFinder
{
@@ -16,7 +16,7 @@ public class DownstreamRouteCreatorTests : UnitTest
private Response _result;
private string _upstreamHost;
private string _upstreamUrlPath;
- private string _upstreamHttpMethod;
+ private string _upstreamHttpMethod;
private Dictionary _upstreamHeaders;
private IInternalConfiguration _configuration;
private readonly Mock _qosOptionsCreator;
@@ -32,7 +32,7 @@ public DownstreamRouteCreatorTests()
_qosOptionsCreator
.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>()))
.Returns(_qoSOptions);
- _creator = new DownstreamRouteCreator(_qosOptionsCreator.Object);
+ _creator = new DownstreamRouteCreator(_qosOptionsCreator.Object);
_upstreamQuery = string.Empty;
}
@@ -58,11 +58,13 @@ public void should_create_downstream_route()
}
[Fact]
+ [Trait("Feat", "2138")]
public void should_create_downstream_route_with_rate_limit_options()
{
var rateLimitOptions = new RateLimitOptionsBuilder()
.WithEnableRateLimiting(true)
.WithClientIdHeader("test")
+ .WithRateLimitPolicyName("test")
.Build();
var downstreamRoute = new DownstreamRouteBuilder()
@@ -370,7 +372,7 @@ private void GivenTheConfiguration(IInternalConfiguration config)
{
_upstreamHost = "doesnt matter";
_upstreamUrlPath = "/auth/test";
- _upstreamHttpMethod = "GET";
+ _upstreamHttpMethod = "GET";
_upstreamHeaders = new Dictionary();
_configuration = config;
}
diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs
index 809961a1d..759c307b0 100644
--- a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs
+++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs
@@ -1,4 +1,7 @@
-using Microsoft.AspNetCore.Http;
+#if NET7_0_OR_GREATER
+using Microsoft.AspNetCore.RateLimiting;
+#endif
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
@@ -58,7 +61,8 @@ public async Task Should_call_middleware_and_ratelimiting()
quotaExceededMessage: "Exceeding!",
rateLimitCounterPrefix: string.Empty,
new RateLimitRule("1s", 100.0D, limit),
- (int)HttpStatusCode.TooManyRequests))
+ (int)HttpStatusCode.TooManyRequests,
+ string.Empty))
.WithUpstreamHttpMethod(new() { "Get" })
.WithUpstreamPathTemplate(upstreamTemplate)
.Build();
@@ -100,7 +104,8 @@ public async Task Should_call_middleware_withWhitelistClient()
quotaExceededMessage: "Exceeding!",
rateLimitCounterPrefix: string.Empty,
new RateLimitRule("1s", 100.0D, 3),
- (int)HttpStatusCode.TooManyRequests))
+ (int)HttpStatusCode.TooManyRequests,
+ string.Empty))
.WithUpstreamHttpMethod(new() { "Get" })
.Build())
.WithUpstreamHttpMethod(new() { "Get" })
@@ -132,7 +137,8 @@ public async Task MiddlewareInvoke_PeriodTimespanValueIsGreaterThanPeriod_Status
quotaExceededMessage: "Exceeding!",
rateLimitCounterPrefix: string.Empty,
new RateLimitRule("1s", 30.0D, limit), // bug scenario
- (int)HttpStatusCode.TooManyRequests))
+ (int)HttpStatusCode.TooManyRequests,
+ string.Empty))
.WithUpstreamHttpMethod(new() { "Get" })
.WithUpstreamPathTemplate(upstreamTemplate)
.Build();
@@ -143,7 +149,9 @@ public async Task MiddlewareInvoke_PeriodTimespanValueIsGreaterThanPeriod_Status
var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route);
// Act, Assert: 100 requests must be successful
- var contexts = await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); // make 100 requests, but not exceed the limit
+ var contexts =
+ await WhenICallTheMiddlewareMultipleTimes(limit,
+ downstreamRouteHolder); // make 100 requests, but not exceed the limit
_downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());
contexts.ForEach(ctx =>
{
@@ -163,7 +171,54 @@ public async Task MiddlewareInvoke_PeriodTimespanValueIsGreaterThanPeriod_Status
contexts[0].Items.Errors().Single().HttpStatusCode.ShouldBe((int)HttpStatusCode.TooManyRequests);
}
- private async Task> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ downstreamRoute)
+ #if NET7_0_OR_GREATER
+ [Fact]
+ [Trait("Feat", "2138")]
+ public async Task Should_add_EnableRateLimittingAttribute_When_AspNetRateLimiting()
+ {
+ // Arrange
+ const long limit = 3L;
+ var upstreamTemplate = new UpstreamPathTemplateBuilder()
+ .Build();
+ var downstreamRoute = new DownstreamRouteBuilder()
+ .WithEnableRateLimiting(true)
+ .WithRateLimitOptions(new(
+ enableRateLimiting: true,
+ clientIdHeader: null,
+ getClientWhitelist: null,
+ disableRateLimitHeaders: false,
+ quotaExceededMessage: null,
+ rateLimitCounterPrefix: null,
+ null,
+ (int)HttpStatusCode.TooManyRequests,
+ "testPolicy"))
+ .WithUpstreamHttpMethod(new() { "Get" })
+ .WithUpstreamPathTemplate(upstreamTemplate)
+ .Build();
+ var route = new RouteBuilder()
+ .WithDownstreamRoute(downstreamRoute)
+ .WithUpstreamHttpMethod(new() { "Get" })
+ .Build();
+ var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route);
+
+ // Act, Assert
+ var contexts = await WhenICallTheMiddlewareMultipleTimes(limit+1, downstreamRouteHolder);
+ _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());
+
+ contexts.ForEach(ctx =>
+ {
+ var endpoint = ctx.GetEndpoint();
+ endpoint.ShouldNotBeNull();
+
+ var rateLimitAttribute = endpoint.Metadata.GetMetadata();
+ rateLimitAttribute.PolicyName.ShouldBe("testPolicy");
+ });
+
+ }
+#endif
+
+ private async Task> WhenICallTheMiddlewareMultipleTimes(long times,
+ _DownstreamRouteHolder_ downstreamRoute)
{
var contexts = new List();
_downstreamResponses.Clear();