From 3cd6a3cf43890676787f75b58299c0ef54f8feb2 Mon Sep 17 00:00:00 2001 From: Gelu Mihes Date: Tue, 12 May 2026 09:56:43 +0300 Subject: [PATCH] Changes --- api/Program.cs | 2 +- api/appsettings.json | 22 +++++++ .../Settings/RateLimitingSettings.cs | 20 ++++++ startup-helpers/RateLimitingExtensions.cs | 61 ++++++++++--------- 4 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 shared-models/Settings/RateLimitingSettings.cs diff --git a/api/Program.cs b/api/Program.cs index 90fbf48..78c6beb 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -53,7 +53,7 @@ try builder.Services.AddSwaggerWithXmlComments(Assembly.GetExecutingAssembly(), "API"); builder.Services.ConfigureCaddyForwardedHeaders(); builder.Services.AddFrontendCorsFromConfiguration(builder.Configuration); - builder.Services.AddPublicApiRateLimiting(); + builder.Services.AddPublicApiRateLimiting(builder.Configuration); var app = builder.Build(); diff --git a/api/appsettings.json b/api/appsettings.json index 23242c7..43c8c48 100644 --- a/api/appsettings.json +++ b/api/appsettings.json @@ -109,5 +109,27 @@ "CvMatcherApi": { "BaseUrl": "", "InternalApiKey": "" + }, + "RateLimiting": { + "Global": { + "PermitLimit": 120, + "Window": "00:01:00", + "QueueLimit": 0, + "AutoReplenishment": true + }, + "Policies": { + "contact": { + "PermitLimit": 5, + "Window": "00:01:00", + "QueueLimit": 0, + "AutoReplenishment": true + }, + "cv-matcher": { + "PermitLimit": 10, + "Window": "00:10:00", + "QueueLimit": 0, + "AutoReplenishment": true + } + } } } \ No newline at end of file diff --git a/shared-models/Settings/RateLimitingSettings.cs b/shared-models/Settings/RateLimitingSettings.cs new file mode 100644 index 0000000..2f1a730 --- /dev/null +++ b/shared-models/Settings/RateLimitingSettings.cs @@ -0,0 +1,20 @@ +namespace Shared.Models.Settings +{ + public class RateLimitingSettings + { + public RateLimitPolicySettings Global { get; set; } = new(); + public Dictionary Policies { get; set; } = new(); + } + + public class RateLimitPolicySettings + { + public int PermitLimit { get; set; } = 100; + + // Bound from configuration strings like "00:01:00" (1 minute) or "00:10:00" (10 minutes). + public TimeSpan Window { get; set; } = TimeSpan.FromMinutes(1); + + public int QueueLimit { get; set; } = 0; + + public bool AutoReplenishment { get; set; } = true; + } +} diff --git a/startup-helpers/RateLimitingExtensions.cs b/startup-helpers/RateLimitingExtensions.cs index 7524298..03e0a6a 100644 --- a/startup-helpers/RateLimitingExtensions.cs +++ b/startup-helpers/RateLimitingExtensions.cs @@ -1,17 +1,26 @@ using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Shared.Models.Settings; namespace StartupHelpers; public static class RateLimitingExtensions { - public static void AddPublicApiRateLimiting(this IServiceCollection services) + public static void AddPublicApiRateLimiting( + this IServiceCollection services, + IConfiguration configuration, + string sectionName = "RateLimiting") { + var settings = configuration.GetSection(sectionName).Get() + ?? new RateLimitingSettings(); + services.AddRateLimiter(options => { + var global = settings.Global ?? new RateLimitPolicySettings(); options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => { var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; @@ -19,40 +28,32 @@ public static class RateLimitingExtensions partitionKey: ip, factory: _ => new FixedWindowRateLimiterOptions { - PermitLimit = 120, - Window = TimeSpan.FromMinutes(1), - QueueLimit = 0, - AutoReplenishment = true + PermitLimit = global.PermitLimit, + Window = global.Window, + QueueLimit = global.QueueLimit, + AutoReplenishment = global.AutoReplenishment }); }); - options.AddPolicy("contact", httpContext => + foreach (var entry in settings.Policies) { - var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - return RateLimitPartition.GetFixedWindowLimiter( - partitionKey: ip, - factory: _ => new FixedWindowRateLimiterOptions - { - PermitLimit = 5, - Window = TimeSpan.FromMinutes(1), - QueueLimit = 0, - AutoReplenishment = true - }); - }); + var policyName = entry.Key; + var policy = entry.Value ?? new RateLimitPolicySettings(); - options.AddPolicy("cv-matcher", httpContext => - { - var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; - return RateLimitPartition.GetFixedWindowLimiter( - partitionKey: ip, - factory: _ => new FixedWindowRateLimiterOptions - { - PermitLimit = 10, - Window = TimeSpan.FromMinutes(10), - QueueLimit = 0, - AutoReplenishment = true - }); - }); + options.AddPolicy(policyName, httpContext => + { + var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + return RateLimitPartition.GetFixedWindowLimiter( + partitionKey: ip, + factory: _ => new FixedWindowRateLimiterOptions + { + PermitLimit = policy.PermitLimit, + Window = policy.Window, + QueueLimit = policy.QueueLimit, + AutoReplenishment = policy.AutoReplenishment + }); + }); + } options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.OnRejected = async (context, ct) =>