Skip to content
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

#2138 Introduce ASP.NET rate limiting #2188

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
6 changes: 6 additions & 0 deletions samples/Ocelot.Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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 =>
yjorayev marked this conversation as resolved.
Show resolved Hide resolved
{
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
}
}
}
9 changes: 8 additions & 1 deletion src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class RateLimitOptionsBuilder
private string _rateLimitCounterPrefix;
private RateLimitRule _rateLimitRule;
private int _httpStatusCode;
private string _rateLimitPolicyName;

public RateLimitOptionsBuilder WithEnableRateLimiting(bool enableRateLimiting)
{
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public RateLimitOptions Create(FileRateLimitRule fileRateLimitRule, FileGlobalCo
.WithRateLimitRule(new RateLimitRule(fileRateLimitRule.Period,
fileRateLimitRule.PeriodTimespan,
fileRateLimitRule.Limit))
.WithRateLimitPolicyName(fileRateLimitRule.Policy)
.Build();
}

Expand Down
24 changes: 21 additions & 3 deletions src/Ocelot/Configuration/File/FileRateLimitRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public FileRateLimitRule(FileRateLimitRule from)
Limit = from.Limit;
Period = from.Period;
PeriodTimespan = from.PeriodTimespan;
Policy = from.Policy;
}

/// <summary>
Expand Down Expand Up @@ -56,6 +57,14 @@ public FileRateLimitRule(FileRateLimitRule from)
/// </value>
public long Limit { 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; }

/// <inheritdoc/>
public override string ToString()
{
Expand All @@ -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();
}
}
Expand Down
47 changes: 25 additions & 22 deletions src/Ocelot/Configuration/RateLimitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class RateLimitOptions
private readonly Func<List<string>> _getClientWhitelist;

public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func<List<string>> 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;
Expand All @@ -18,73 +18,76 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func<Lis
RateLimitCounterPrefix = rateLimitCounterPrefix;
RateLimitRule = rateLimitRule;
HttpStatusCode = httpStatusCode;
}

/// <summary>
/// Gets a Rate Limit rule.
Policy = rateLimitPolicy;
}

/// <summary>
/// Gets a Rate Limit rule.
/// </summary>
/// <value>
/// A <see cref="Configuration.RateLimitRule"/> object that represents the rule.
/// </value>
public RateLimitRule RateLimitRule { get; }

public RateLimitRule RateLimitRule { get; }
/// <summary>
/// Gets the list of white listed clients.
/// </summary>
/// <value>
/// A <see cref="List{T}"/> (where T is <see cref="string"/>) collection with white listed clients.
/// </value>
public List<string> ClientWhitelist => _getClientWhitelist();

public List<string> ClientWhitelist => _getClientWhitelist();
/// <summary>
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId.
/// </summary>
/// <value>
/// A string value with the HTTP header.
/// </value>
public string ClientIdHeader { get; }

public string ClientIdHeader { get; }
/// <summary>
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests).
/// </summary>
/// <value>
/// An integer value with the HTTP Status code.
/// <para>Default value: 429 (Too Many Requests).</para>
/// An integer value with the HTTP Status code.
/// <para>Default value: 429 (Too Many Requests).</para>
/// </value>
public int HttpStatusCode { get; }

public int HttpStatusCode { get; }
/// <summary>
/// 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.
/// <para>If none specified the default will be: "API calls quota exceeded! maximum admitted {0} per {1}".</para>
/// </summary>
/// <value>
/// A string value with a formatter for the QuotaExceeded response message.
/// <para>Default will be: "API calls quota exceeded! maximum admitted {0} per {1}".</para>
/// </value>
public string QuotaExceededMessage { get; }

public string QuotaExceededMessage { get; }
/// <summary>
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key.
/// </summary>
/// <value>
/// A string value with the counter prefix.
/// </value>
public string RateLimitCounterPrefix { get; }

public string RateLimitCounterPrefix { get; }
/// <summary>
/// Enables endpoint rate limiting based URL path and HTTP verb.
/// </summary>
/// <value>
/// A boolean value for enabling endpoint rate limiting based URL path and HTTP verb.
/// </value>
public bool EnableRateLimiting { get; }

public bool EnableRateLimiting { get; }
/// <summary>
/// Disables <c>X-Rate-Limit</c> and <c>Retry-After</c> headers.
/// </summary>
/// <value>
/// A boolean value for disabling <c>X-Rate-Limit</c> and <c>Retry-After</c> headers.
/// </value>
public bool DisableRateLimitHeaders { get; }

public string Policy { get; }
}
}
Loading