Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions samples/Ocelot.Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Configuratio
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Metadata", "Metadata\Ocelot.Samples.Metadata.csproj", "{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}"
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
Expand Down Expand Up @@ -99,6 +101,10 @@ Global
{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.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
Expand Down
13 changes: 13 additions & 0 deletions samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj
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>
11 changes: 11 additions & 0 deletions samples/RateLimiter/Ocelot.Samples.RateLimiter.http
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

###
26 changes: 26 additions & 0 deletions samples/RateLimiter/Program.cs
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;
});
});

var app = builder.Build();
app.UseHttpsRedirection();

await app.UseOcelot();

app.Run();
41 changes: 41 additions & 0 deletions samples/RateLimiter/Properties/launchSettings.json
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"
}
}
}
}
8 changes: 8 additions & 0 deletions samples/RateLimiter/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/RateLimiter/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
46 changes: 46 additions & 0 deletions samples/RateLimiter/ocelot.json
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
Expand Up @@ -81,6 +81,7 @@ protected virtual RateLimitOptions MergeHeaderRules(FileRateLimitByHeaderRule ru
rule.Wait = rule.Wait.IfEmpty(globalRule.Wait.IfEmpty(RateLimitRule.ZeroWait));

rule.Limit ??= globalRule.Limit ?? RateLimitRule.ZeroLimit;
rule.Policy = rule.Policy.IfEmpty(globalRule.Policy);
return new(rule);
}
}
13 changes: 13 additions & 0 deletions src/Ocelot/Configuration/File/FileRateLimitRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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; }

/// <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>
Expand All @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions src/Ocelot/Configuration/RateLimitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? [];
Expand All @@ -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);
Expand All @@ -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;
}

public RateLimitOptions(RateLimitOptions fromOptions)
Expand All @@ -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;
}

/// <summary>Gets a Rate Limit rule.</summary>
Expand Down Expand Up @@ -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; }
}
4 changes: 3 additions & 1 deletion src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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), () =>
{
RuleFor(route => route.RateLimitOptions.Limit)
.Must(limit => !limit.HasValue || (limit.HasValue && limit.Value > 0))
Expand Down
29 changes: 29 additions & 0 deletions src/Ocelot/DependencyInjection/Features.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using FluentValidation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Cache;
using Ocelot.Configuration.Creator;
Expand Down Expand Up @@ -35,6 +38,32 @@
public static IServiceCollection AddOcelotRateLimiting(this IServiceCollection services) => services
.AddSingleton<IRateLimiting, RateLimiting.RateLimiting>()
.AddSingleton<IRateLimitStorage, MemoryCacheRateLimitStorage>();

/// <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)
{
var globalRateLimitOptions = configurationRoot.Get<FileConfiguration>()?.GlobalConfiguration?.RateLimitOptions;
var rejectStatusCode = globalRateLimitOptions?.HttpStatusCode ?? StatusCodes.Status429TooManyRequests;

Check warning on line 54 in src/Ocelot/DependencyInjection/Features.cs

View workflow job for this annotation

GitHub Actions / build-cake

'FileRateLimitByHeaderRule.HttpStatusCode' is obsolete: 'Use StatusCode instead of HttpStatusCode! Note that HttpStatusCode will be removed in version 25.0!'
var rejectedMessage = globalRateLimitOptions?.QuotaExceededMessage ?? "API calls quota exceeded!";

Check warning on line 55 in src/Ocelot/DependencyInjection/Features.cs

View workflow job for this annotation

GitHub Actions / build-cake

'FileRateLimitByHeaderRule.QuotaExceededMessage' is obsolete: 'Use QuotaMessage instead of QuotaExceededMessage! Note that QuotaExceededMessage will be removed in version 25.0!'
services.AddRateLimiter(options =>
{
options.OnRejected = async (rejectedContext, token) =>
{
rejectedContext.HttpContext.Response.StatusCode = rejectStatusCode;
await rejectedContext.HttpContext.Response.WriteAsync(rejectedMessage, token);
};
});

return services;
}

/// <summary>
/// Ocelot feature: <see href="https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/caching.rst">Request Caching</see>.
Expand Down
1 change: 1 addition & 0 deletions src/Ocelot/DependencyInjection/OcelotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.AddOcelotMessageInvokerPool();
Services.AddOcelotMetadata();
Services.AddOcelotRateLimiting();
Services.AddAspNetRateLimiting(configurationRoot); // Feature: AspNet Rate Limiting

// Add ASP.NET services
var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly;
Expand Down
10 changes: 10 additions & 0 deletions src/Ocelot/RateLimiting/RateLimitingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Ocelot.Configuration;
Expand Down Expand Up @@ -39,6 +40,15 @@ public Task Invoke(HttpContext context)
return _next.Invoke(context);
}

if (!options.Policy.IsEmpty())
{
// 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");
context.SetEndpoint(endpoint);
return _next.Invoke(context);
}

var identity = Identify(context, options, downstreamRoute);
if (IsWhitelisted(identity, options))
{
Expand Down
26 changes: 26 additions & 0 deletions src/Ocelot/RateLimiting/RateLimitingMiddlewareExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Ocelot.Middleware;

namespace Ocelot.RateLimiting.Middleware;

public static class RateLimitingMiddlewareExtensions
{
public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
{
builder.UseMiddleware<RateLimitingMiddleware>();

//use AspNet rate limiter
builder.UseWhen(UseAspNetRateLimiter, rateLimitedApp =>
{
rateLimitedApp.UseRateLimiter();
});
return builder;
}

private static bool UseAspNetRateLimiter(HttpContext httpContext)
{
var downstreamRoute = httpContext.Items.DownstreamRoute();
return !string.IsNullOrWhiteSpace(downstreamRoute?.RateLimitOptions?.Policy);
}
}
Loading
Loading