Skip to content

Commit 9d02c46

Browse files
author
John Luo
committed
Create request limits middleware
1 parent 556d40e commit 9d02c46

19 files changed

+926
-0
lines changed

AspNetCore.sln

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebviewAppShared", "src\Com
16101610
EndProject
16111611
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestStartupAssembly1", "src\Hosting\test\testassets\TestStartupAssembly1\TestStartupAssembly1.csproj", "{262FF30C-34B4-462D-B5E2-0DABB9196E40}"
16121612
EndProject
1613+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestLimiter", "RequestLimiter", "{74BD81EC-1CE2-42F1-9588-F3812ECF73FD}"
1614+
EndProject
1615+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestLimiter", "src\Middleware\RequestLimiter\src\Microsoft.AspNetCore.RequestLimiter.csproj", "{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}"
1616+
EndProject
1617+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RequestLimiterSample", "src\Middleware\RequestLimiter\sample\RequestLimiterSample.csproj", "{1F7135AE-40D4-4A92-9473-8EB38F56526A}"
1618+
EndProject
16131619
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Spa", "Spa", "{0A064174-8E5C-4F97-B941-A4E302661DF2}"
16141620
EndProject
16151621
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SpaProxy", "SpaProxy", "{5AC2A052-1D4F-4C2F-BCF5-3F07A3E31857}"
@@ -7665,6 +7671,30 @@ Global
76657671
{262FF30C-34B4-462D-B5E2-0DABB9196E40}.Release|x64.Build.0 = Release|Any CPU
76667672
{262FF30C-34B4-462D-B5E2-0DABB9196E40}.Release|x86.ActiveCfg = Release|Any CPU
76677673
{262FF30C-34B4-462D-B5E2-0DABB9196E40}.Release|x86.Build.0 = Release|Any CPU
7674+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
7675+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Debug|Any CPU.Build.0 = Debug|Any CPU
7676+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Debug|x64.ActiveCfg = Debug|Any CPU
7677+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Debug|x64.Build.0 = Debug|Any CPU
7678+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Debug|x86.ActiveCfg = Debug|Any CPU
7679+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Debug|x86.Build.0 = Debug|Any CPU
7680+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Release|Any CPU.ActiveCfg = Release|Any CPU
7681+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Release|Any CPU.Build.0 = Release|Any CPU
7682+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Release|x64.ActiveCfg = Release|Any CPU
7683+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Release|x64.Build.0 = Release|Any CPU
7684+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Release|x86.ActiveCfg = Release|Any CPU
7685+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03}.Release|x86.Build.0 = Release|Any CPU
7686+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
7687+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Debug|Any CPU.Build.0 = Debug|Any CPU
7688+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Debug|x64.ActiveCfg = Debug|Any CPU
7689+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Debug|x64.Build.0 = Debug|Any CPU
7690+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Debug|x86.ActiveCfg = Debug|Any CPU
7691+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Debug|x86.Build.0 = Debug|Any CPU
7692+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Release|Any CPU.ActiveCfg = Release|Any CPU
7693+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Release|Any CPU.Build.0 = Release|Any CPU
7694+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Release|x64.ActiveCfg = Release|Any CPU
7695+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Release|x64.Build.0 = Release|Any CPU
7696+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Release|x86.ActiveCfg = Release|Any CPU
7697+
{1F7135AE-40D4-4A92-9473-8EB38F56526A}.Release|x86.Build.0 = Release|Any CPU
76687698
{0DBACF8E-2EDB-47FC-B998-B76522637B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
76697699
{0DBACF8E-2EDB-47FC-B998-B76522637B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
76707700
{0DBACF8E-2EDB-47FC-B998-B76522637B2E}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -8594,6 +8624,9 @@ Global
85948624
{036C6BDA-7B69-4E8C-A921-822DA5972A56} = {94D0D6F3-8632-41DE-908B-47A787D570FF}
85958625
{64C3BAC8-C4F8-466A-9E84-0400EE54B25A} = {D3B76F4E-A980-45BF-AEA1-EA3175B0B5A1}
85968626
{262FF30C-34B4-462D-B5E2-0DABB9196E40} = {C1409A8F-555A-4A88-B803-C6D3E8B6C3B0}
8627+
{74BD81EC-1CE2-42F1-9588-F3812ECF73FD} = {E5963C9F-20A6-4385-B364-814D2581FADF}
8628+
{B5616C0A-5D0A-4099-B08E-971FE0B4BA03} = {74BD81EC-1CE2-42F1-9588-F3812ECF73FD}
8629+
{1F7135AE-40D4-4A92-9473-8EB38F56526A} = {74BD81EC-1CE2-42F1-9588-F3812ECF73FD}
85978630
{0A064174-8E5C-4F97-B941-A4E302661DF2} = {E5963C9F-20A6-4385-B364-814D2581FADF}
85988631
{5AC2A052-1D4F-4C2F-BCF5-3F07A3E31857} = {0A064174-8E5C-4F97-B941-A4E302661DF2}
85998632
{0DBACF8E-2EDB-47FC-B998-B76522637B2E} = {5AC2A052-1D4F-4C2F-BCF5-3F07A3E31857}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"solution": {
3+
"path": "..\\..\\..\\AspNetCore.sln",
4+
"projects": [
5+
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
6+
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
7+
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
8+
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
9+
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
10+
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
11+
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
12+
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
13+
"src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
14+
"src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
15+
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
16+
"src\\http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
17+
"src\\http\\http\\src\\Microsoft.AspNetCore.Http.csproj",
18+
"src\\Middleware\\RequestLimiter\\sample\\RequestLimiterSample.csproj",
19+
"src\\Middleware\\RequestLimiter\\src\\Microsoft.AspNetCore.RequestLimiter.csproj",
20+
"src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj"
21+
]
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.AspNetCore.RequestLimiter;
7+
8+
namespace RequestLimiterSample
9+
{
10+
[RequestLimit(requestPerSecond: 10)]
11+
public class HomeController : Controller
12+
{
13+
[RequestLimit("rate")]
14+
public IActionResult Index()
15+
{
16+
return View();
17+
}
18+
}
19+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.Extensions.Hosting;
3+
4+
namespace RateLimiterSample
5+
{
6+
public class Program
7+
{
8+
public static void Main(string[] args)
9+
{
10+
CreateHostBuilder(args).Build().Run();
11+
}
12+
13+
public static IHostBuilder CreateHostBuilder(string[] args) =>
14+
Host.CreateDefaultBuilder(args)
15+
.ConfigureWebHost(webBuilder =>
16+
{
17+
webBuilder.UseStartup<Startup>();
18+
});
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
5+
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
6+
<GenerateRazorAssemblyInfo>false</GenerateRazorAssemblyInfo>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Reference Include="Microsoft.AspNetCore" />
11+
<Reference Include="Microsoft.AspNetCore.Mvc" />
12+
<Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
13+
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
14+
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
15+
<Reference Include="Microsoft.AspNetCore.RequestLimiter" />
16+
<Reference Include="Microsoft.AspNetCore.Routing" />
17+
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
18+
<Reference Include="Microsoft.Extensions.Logging.Console" />
19+
<Reference Include="System.Threading.ResourceLimits" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.ResourceLimits;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.RequestLimiter;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
12+
13+
namespace RateLimiterSample
14+
{
15+
public class Startup
16+
{
17+
// This method gets called by the runtime. Use this method to add services to the container.
18+
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
19+
public void ConfigureServices(IServiceCollection services)
20+
{
21+
services.AddControllersWithViews();
22+
services.AddSingleton(new IPAggregatedRateLimiter(2, 2));
23+
services.AddSingleton(new RateLimiter(2, 2));
24+
25+
services.AddRequestLimiter(options =>
26+
{
27+
options.SetDefaultPolicy(new ConcurrencyLimiter(100));
28+
// TODO: Consider a policy builder
29+
// TODO: Support combining/composing policies
30+
options.AddPolicy("concurrency", policy =>
31+
{
32+
// Add instance
33+
policy.AddLimiter(new ConcurrencyLimiter(1));
34+
});
35+
options.AddPolicy("rate", policy =>
36+
{
37+
// Add from DI
38+
policy.AddLimiter<RateLimiter>();
39+
});
40+
});
41+
}
42+
43+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
44+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
45+
{
46+
app.UseRouting();
47+
48+
app.UseRateLimiter();
49+
50+
app.UseEndpoints(endpoints =>
51+
{
52+
endpoints.MapGet("/defaultPolicy", async context =>
53+
{
54+
await Task.Delay(5000);
55+
await context.Response.WriteAsync("Default!");
56+
}).EnforceLimit();
57+
58+
endpoints.MapGet("/instance", async context =>
59+
{
60+
await Task.Delay(5000);
61+
await context.Response.WriteAsync("Hello World!");
62+
}).EnforceLimit(new RateLimiter(2, 2));
63+
64+
endpoints.MapGet("/concurrentPolicy", async context =>
65+
{
66+
await Task.Delay(5000);
67+
await context.Response.WriteAsync("Wrote!");
68+
}).EnforceLimit("concurrency");
69+
70+
endpoints.MapGet("/adhoc", async context =>
71+
{
72+
await Task.Delay(5000);
73+
await context.Response.WriteAsync("Tested!");
74+
}).EnforceLimit(requestPerSecond: 2);
75+
76+
endpoints.MapGet("/ipFromDI", async context =>
77+
{
78+
await Task.Delay(5000);
79+
await context.Response.WriteAsync("IP limited!");
80+
}).EnforceAggregatedLimit<IPAggregatedRateLimiter>();
81+
82+
endpoints.MapGet("/multiple", async context =>
83+
{
84+
await Task.Delay(5000);
85+
await context.Response.WriteAsync("IP limited!");
86+
}).EnforceLimit("concurrency").EnforceLimit("rate");
87+
88+
endpoints.MapControllerRoute(
89+
name: "default",
90+
pattern: "{controller=Home}/{action=Index}/{id?}");
91+
});
92+
}
93+
}
94+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Information",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Information",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
},
9+
"AllowedHosts": "*"
10+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Net;
5+
using System.Threading;
6+
using System.Threading.ResourceLimits;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Http;
9+
10+
namespace Microsoft.AspNetCore.RequestLimiter
11+
{
12+
public class IPAggregatedRateLimiter : AggregatedResourceLimiter<HttpContext>
13+
{
14+
private long _resourceCount;
15+
private readonly long _maxResourceCount;
16+
private readonly long _newResourcePerSecond;
17+
18+
private Timer _renewTimer;
19+
// This is racy
20+
private ConcurrentDictionary<IPAddress, long> _cache = new ConcurrentDictionary<IPAddress, long>();
21+
22+
public IPAggregatedRateLimiter(long resourceCount, long newResourcePerSecond)
23+
{
24+
_resourceCount = resourceCount;
25+
_maxResourceCount = resourceCount;
26+
_newResourcePerSecond = newResourcePerSecond;
27+
28+
// Start timer (5s for demo)
29+
_renewTimer = new Timer(Replenish, this, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
30+
}
31+
32+
public override long EstimatedCount(HttpContext resourceId)
33+
{
34+
if (resourceId.Connection.RemoteIpAddress == null)
35+
{
36+
// Unknown IP?
37+
return 0;
38+
}
39+
40+
return _cache.TryGetValue(resourceId.Connection.RemoteIpAddress, out var count) ? count : 0;
41+
}
42+
43+
public override bool TryAcquire(HttpContext resourceId, long requestedCount, [NotNullWhen(true)] out Resource? resource)
44+
{
45+
resource = Resource.NoopResource;
46+
if (requestedCount > _maxResourceCount)
47+
{
48+
return false;
49+
}
50+
51+
if (resourceId.Connection.RemoteIpAddress == null)
52+
{
53+
return true;
54+
}
55+
56+
var key = resourceId.Connection.RemoteIpAddress;
57+
58+
if (!_cache.TryGetValue(key, out var count))
59+
{
60+
if (_cache.TryAdd(key, requestedCount))
61+
{
62+
return true;
63+
}
64+
}
65+
66+
while (true)
67+
{
68+
var newCount = count + requestedCount;
69+
if (_cache.TryUpdate(key, count + requestedCount, count))
70+
{
71+
if (newCount > _maxResourceCount)
72+
{
73+
return false;
74+
}
75+
76+
return true;
77+
}
78+
if (!_cache.TryGetValue(key, out count))
79+
{
80+
if (_cache.TryAdd(key, requestedCount))
81+
{
82+
return true;
83+
}
84+
}
85+
}
86+
}
87+
88+
public override ValueTask<Resource> AcquireAsync(HttpContext resourceId, long requestedCount, CancellationToken cancellationToken = default)
89+
{
90+
throw new NotImplementedException();
91+
}
92+
93+
private static void Replenish(object? state)
94+
{
95+
// Return if Replenish already running to avoid concurrency.
96+
var limiter = state as IPAggregatedRateLimiter;
97+
98+
if (limiter == null)
99+
{
100+
return;
101+
}
102+
103+
var cache = limiter._cache;
104+
105+
foreach (var entry in cache)
106+
{
107+
if (entry.Value < limiter._newResourcePerSecond)
108+
{
109+
if (cache.TryRemove(entry))
110+
{
111+
continue;
112+
}
113+
}
114+
115+
while (true)
116+
{
117+
if (!cache.TryGetValue(entry.Key, out var newCount))
118+
{
119+
break;
120+
}
121+
if (cache.TryUpdate(entry.Key, Math.Max(0, newCount - limiter._newResourcePerSecond), newCount))
122+
{
123+
break;
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)