diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs new file mode 100644 index 0000000..5d87e46 --- /dev/null +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs @@ -0,0 +1,16 @@ +namespace WebRateLimitAuth.Models; + +public class MyRateLimitOptions +{ + public const string MyRateLimit = "MyRateLimit"; + + public int permitLimit { get; set; } = 100; + public int window { get; set; } = 10; + public int replenishmentPeriod { get; set; } = 2; + public int queueLimit { get; set; } = 100; + public int segmentsPerWindow { get; set; } = 8; + public int tokenLimit { get; set; } = 10; + public int tokenLimit2 { get; set; } = 20; + public int tokensPerPeriod { get; set; } = 4; + public bool autoReplenishment { get; set; } = false; +} diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml index 055a107..c476ede 100644 --- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml @@ -31,6 +31,12 @@ + + diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs new file mode 100644 index 0000000..a91b1a8 --- /dev/null +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs @@ -0,0 +1,447 @@ +#define FIRST // FIRST ADMIN FIXED SLIDING CONCUR TOKEN FIXED2 JWT +#if NEVER +#elif FIXED +// +using Microsoft.AspNetCore.RateLimiting; +using System.Threading.RateLimiting; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + +app.UseRateLimiter(new RateLimiterOptions() + .AddFixedWindowLimiter(policyName: "fixed", + new FixedWindowRateLimiterOptions(permitLimit: 4, + window: TimeSpan.FromSeconds(12), + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: 2))); + +app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}")) + .RequireRateLimiting("fixed"); + +app.Run(); +// +#elif FIXED2 +// +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.RateLimiting; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.Configure( + builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit)); +var app = builder.Build(); + +static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + +var myOptions = new MyRateLimitOptions(); +app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions); +var fixedPolicy = "fixed"; + +app.UseRateLimiter(new RateLimiterOptions() + .AddFixedWindowLimiter(policyName: fixedPolicy, + new FixedWindowRateLimiterOptions(permitLimit: myOptions.permitLimit, + window: TimeSpan.FromSeconds(myOptions.window), + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit))); + +app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}")) + .RequireRateLimiting(fixedPolicy); + +app.Run(); +// +#elif SLIDING +// +using Microsoft.AspNetCore.RateLimiting; +using System.Threading.RateLimiting; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + +var myOptions = new MyRateLimitOptions(); +app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions); +var slidingPolicy = "sliding"; + +app.UseRateLimiter(new RateLimiterOptions() + .AddSlidingWindowLimiter(policyName: slidingPolicy, + new SlidingWindowRateLimiterOptions(permitLimit: myOptions.permitLimit, + window: TimeSpan.FromSeconds(myOptions.window), + segmentsPerWindow: myOptions.segmentsPerWindow, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit))); + +app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}")) + .RequireRateLimiting(slidingPolicy); + +app.Run(); +// +#elif CONCUR +// Quicktest 10 users, 9 seconds -> 982 requests, 900 errors +// +using Microsoft.AspNetCore.RateLimiting; +using System.Threading.RateLimiting; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + +var concurrencyPolicy = "Concurrency"; +var myOptions = new MyRateLimitOptions(); +app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions); + +app.UseRateLimiter(new RateLimiterOptions() + .AddConcurrencyLimiter(policyName: concurrencyPolicy, + new ConcurrencyLimiterOptions(permitLimit: myOptions.permitLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit))); + +app.MapGet("/", async () => +{ + await Task.Delay(500); + return Results.Ok($"Concurrency Limiter {GetTicks()}"); + +}).RequireRateLimiting(concurrencyPolicy); + +app.Run(); +// +#elif TOKEN +// Quicktest 20 users, 20 seconds -> 8965 requests 2,250 errors +// +using Microsoft.AspNetCore.RateLimiting; +using System.Threading.RateLimiting; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + +var tokenPolicy = "token"; +var myOptions = new MyRateLimitOptions(); +app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions); + +app.UseRateLimiter(new RateLimiterOptions() + .AddTokenBucketLimiter(policyName: tokenPolicy, + new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + replenishmentPeriod: TimeSpan.FromSeconds(2), + tokensPerPeriod: myOptions.tokensPerPeriod, + autoReplenishment: myOptions.autoReplenishment))); + +app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}")) + .RequireRateLimiting(tokenPolicy); + +app.Run(); +// +#elif FIRST +// +using System.Globalization; +using System.Net; +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using WebRateLimitAuth; +using WebRateLimitAuth.Data; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? + throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + +builder.Services.Configure( + builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit)); + +builder.Services.AddRazorPages(); +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseMigrationsEndPoint(); +} +else +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +// +app.UseAuthentication(); +app.UseAuthorization(); + +var userPolicyName = "user"; +var helloPolicy = "hello"; +var myOptions = new MyRateLimitOptions(); +var myConfigSection = app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit); +myConfigSection.Bind(myOptions); + + +var options = new RateLimiterOptions() +{ + OnRejected = (context, cancellationToken) => + { + if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) + { + context.HttpContext.Response.Headers.RetryAfter = + ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo); + } + + context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; + context?.HttpContext?.RequestServices?.GetService()? + .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") + .LogWarning($"OnRejected: {GetUserEndPoint(context.HttpContext)}"); + + return new ValueTask(); + } +} + .AddPolicy(helloPolicy) + .AddPolicy(userPolicyName, context => + { + if (context.User?.Identity?.IsAuthenticated is not true) + { + var username = "anonymous user"; + + return RateLimitPartition.CreateSlidingWindowLimiter(username, + key => new SlidingWindowRateLimiterOptions( + permitLimit: myOptions.permitLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + window: TimeSpan.FromSeconds(myOptions.window), + segmentsPerWindow: myOptions.segmentsPerWindow + )); + } + else + { + return RateLimitPartition.CreateNoLimiter(string.Empty); + } + }); + +options.GlobalLimiter = PartitionedRateLimiter.Create(context => +{ + IPAddress? remoteIPaddress = context?.Connection?.RemoteIpAddress; + + if (!IPAddress.IsLoopback(remoteIPaddress!)) + { + return RateLimitPartition.CreateTokenBucketLimiter + (remoteIPaddress!, key => + new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod), + tokensPerPeriod: myOptions.tokensPerPeriod, + autoReplenishment: myOptions.autoReplenishment)); + } + else + { + return RateLimitPartition.CreateNoLimiter(IPAddress.Loopback); + } +}); + +app.UseRateLimiter(options); + +app.MapRazorPages().RequireRateLimiting(userPolicyName); +app.MapDefaultControllerRoute(); + +static string GetUserEndPoint(HttpContext context) => + $"User {context.User?.Identity?.Name ?? "Anonymous"} endpoint: {context.Request.Path}" + + $" {context.Connection.RemoteIpAddress}"; +static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000"); + +app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}") + .RequireRateLimiting(userPolicyName); + +app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}") + .RequireRateLimiting(helloPolicy); + +app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}"); + +app.Run(); +// +// +#elif ADMIN +// +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Primitives; +using WebRateLimitAuth.Data; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddDefaultIdentity(options => + options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + +builder.Services.AddRazorPages(); +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseMigrationsEndPoint(); +} +else +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +// +var getPolicyName = "get"; +var postPolicyName = "post"; +var myOptions = new MyRateLimitOptions(); +app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions); + +app.UseRateLimiter(new RateLimiterOptions() + .AddConcurrencyLimiter(policyName: getPolicyName, + new ConcurrencyLimiterOptions(permitLimit: myOptions.permitLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit)) + .AddPolicy(policyName: postPolicyName, partitioner: httpContext => + { + string userName = httpContext?.User?.Identity?.Name ?? string.Empty; + + if (!StringValues.IsNullOrEmpty(userName)) + { + return RateLimitPartition.CreateTokenBucketLimiter(userName, key => + new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod), + tokensPerPeriod: myOptions.tokensPerPeriod, + autoReplenishment: myOptions.autoReplenishment)); + } + else + { + return RateLimitPartition.CreateTokenBucketLimiter("Anon", key => + new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod), + tokensPerPeriod: myOptions.tokensPerPeriod, + autoReplenishment: true)); + } + })); +// + +static string GetUserEndPointMethod(HttpContext context) => + $"Hello {context.User?.Identity?.Name ?? "Anonymous"} " + + $"Endpoint:{context.Request.Path} Method: {context.Request.Method}"; + +app.MapGet("/test", (HttpContext context) => $"{GetUserEndPointMethod(context)}") + .RequireRateLimiting(getPolicyName); + +app.MapRazorPages().RequireRateLimiting(getPolicyName) + .RequireRateLimiting(postPolicyName); + +app.MapDefaultControllerRoute(); + +app.Run(); +// +#elif JWT +// +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.Primitives; +using WebRateLimitAuth.Models; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(); +builder.Services.AddAuthentication("Bearer").AddJwtBearer(); + +var app = builder.Build(); + +app.UseAuthorization(); + +var jwtPolicyName = "jwt"; +var myOptions = new MyRateLimitOptions(); +app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions); + +app.UseRateLimiter(new RateLimiterOptions() + .AddPolicy(policyName: jwtPolicyName, partitioner: httpContext => + { + var accessToken = httpContext?.Features?.Get()? + .AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString() + ?? string.Empty; + if (!StringValues.IsNullOrEmpty(accessToken)) + { + return RateLimitPartition.CreateTokenBucketLimiter( accessToken, key => + new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod), + tokensPerPeriod: myOptions.tokensPerPeriod, + autoReplenishment: myOptions.autoReplenishment)); + } + else + { + return RateLimitPartition.CreateTokenBucketLimiter("Anon", key => + new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: myOptions.queueLimit, + replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod), + tokensPerPeriod: myOptions.tokensPerPeriod, + autoReplenishment: true)); + } + })); + +app.MapGet("/", () => "Hello, World!"); + +app.MapGet("/jwt", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}") + .RequireRateLimiting(jwtPolicyName) + .RequireAuthorization(); + +app.MapPost("/post", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}") + .RequireRateLimiting(jwtPolicyName) + .RequireAuthorization(); + +app.Run(); + +static string GetUserEndPointMethod(HttpContext context) => + $"Hello {context.User?.Identity?.Name ?? "Anonymous"} " + + $"Endpoint:{context.Request.Path} Method: {context.Request.Method}"; + +// +#endif diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/SampleRateLimiterPolicy.cs b/fundamentals/middleware/rate-limit/WebRateLimitAuth/SampleRateLimiterPolicy.cs new file mode 100644 index 0000000..e027265 --- /dev/null +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/SampleRateLimiterPolicy.cs @@ -0,0 +1,39 @@ +using System.Threading.RateLimiting; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.Extensions.Options; +using WebRateLimitAuth.Models; + +namespace WebRateLimitAuth; + +public class SampleRateLimiterPolicy : IRateLimiterPolicy +{ + private Func? _onRejected; + private readonly MyRateLimitOptions _options; + + public SampleRateLimiterPolicy(ILogger logger, + IOptions options) + { + _onRejected = (context, token) => + { + context.HttpContext.Response.StatusCode = 429; + logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}"); + return ValueTask.CompletedTask; + }; + _options = options.Value; + } + + public Func? + OnRejected { get => _onRejected; } + + public RateLimitPartition GetPartition(HttpContext httpContext) + { + return RateLimitPartition.CreateSlidingWindowLimiter(string.Empty, + key => new SlidingWindowRateLimiterOptions( + permitLimit: _options.permitLimit, + queueProcessingOrder: QueueProcessingOrder.OldestFirst, + queueLimit: _options.queueLimit, + window: TimeSpan.FromSeconds(_options.window), + segmentsPerWindow: _options.segmentsPerWindow)); + + } +} diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj b/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj index 0419be6..8b00507 100644 --- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj @@ -8,12 +8,16 @@ - - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json index 770d3e9..d3279b1 100644 --- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json @@ -5,5 +5,29 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "MyRateLimit": { + "permitLimit": "4", + "window": "3", + "replenishmentPeriod": "1", + "queueLimit": "2", + "segmentsPerWindow": "2", + "tokenLimit": 4, + "tokenLimit2": 6, + "tokensPerPeriod": 4, + "autoReplenishment": true + }, + "Authentication": { + "Schemes": { + "Bearer": { + "ValidAudiences": [ + "http://localhost:58245", + "https://localhost:44387", + "http://localhost:5266", + "https://localhost:7266" + ], + "ValidIssuer": "dotnet-user-jwts" + } + } } } diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json index 768ff94..4ff58e9 100644 --- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json +++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json @@ -8,5 +8,16 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "MyRateLimit": { + "permitLimit": "10", + "window": "2", + "replenishmentPeriod": "1", + "queueLimit": "10", + "segmentsPerWindow": "4", + "tokenLimit": 8, + "tokenLimit2": 12, + "tokensPerPeriod": 4, + "autoReplenishment": true + } }