diff --git a/OpenCand.API/Config/RateLimitingConfig.cs b/OpenCand.API/Config/RateLimitingConfig.cs new file mode 100644 index 0000000..0c9cbf0 --- /dev/null +++ b/OpenCand.API/Config/RateLimitingConfig.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.RateLimiting; +using System.Threading.RateLimiting; + +namespace OpenCand.API.Config +{ + public static class RateLimitingConfig + { + public const string DefaultPolicy = "DefaultPolicy"; + public const string CandidatoSearchPolicy = "CandidatoSearchPolicy"; + public const string CpfRevealPolicy = "CpfRevealPolicy"; + + public static void ConfigureRateLimiting(this IServiceCollection services) + { + services.AddRateLimiter(options => + { + options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => + RateLimitPartition.GetFixedWindowLimiter( + partitionKey: GetClientIdentifier(httpContext), + factory: partition => new FixedWindowRateLimiterOptions + { + AutoReplenishment = true, + PermitLimit = 2000, // Global limit per minute + Window = TimeSpan.FromMinutes(1) + })); + + // Default policy: 200 requests per minute with burst of 100 + options.AddFixedWindowLimiter(policyName: DefaultPolicy, options => + { + options.PermitLimit = 200; + options.Window = TimeSpan.FromMinutes(1); + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 100; // Burst capacity + }); + + // Candidato Search policy: 300 requests per minute with burst of 200 + options.AddFixedWindowLimiter(policyName: CandidatoSearchPolicy, options => + { + options.PermitLimit = 300; + options.Window = TimeSpan.FromMinutes(1); + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 200; // Burst capacity + }); + + // CPF Reveal policy: 15 requests per minute without burst + options.AddFixedWindowLimiter(policyName: CpfRevealPolicy, options => + { + options.PermitLimit = 15; + options.Window = TimeSpan.FromMinutes(1); + options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + options.QueueLimit = 0; // No burst + }); + + options.OnRejected = async (context, token) => + { + context.HttpContext.Response.StatusCode = 429; + + var retryAfter = GetRetryAfter(context); + if (retryAfter.HasValue) + { + context.HttpContext.Response.Headers.Add("Retry-After", retryAfter.Value.ToString()); + } + + await context.HttpContext.Response.WriteAsync( + "Rate limit exceeded. Please try again later.", cancellationToken: token); + }; + }); + } + + private static string GetClientIdentifier(HttpContext httpContext) + { + // Get client IP address for partitioning + var clientIp = httpContext.Connection.RemoteIpAddress?.ToString() + ?? httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() + ?? "unknown"; + + // Include the endpoint in the partition key for endpoint-specific limits + var endpoint = httpContext.Request.Path.ToString(); + + return $"{clientIp}:{endpoint}"; + } + + private static int? GetRetryAfter(OnRejectedContext context) + { + if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) + { + return (int)retryAfter.TotalSeconds; + } + return null; + } + } +} diff --git a/OpenCand.API/Controllers/CandidatoController.cs b/OpenCand.API/Controllers/CandidatoController.cs index 53bb3fe..97b4cc7 100644 --- a/OpenCand.API/Controllers/CandidatoController.cs +++ b/OpenCand.API/Controllers/CandidatoController.cs @@ -1,11 +1,14 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; using Microsoft.VisualBasic; +using OpenCand.API.Config; using OpenCand.API.Model; using OpenCand.API.Services; using OpenCand.Core.Models; namespace OpenCand.API.Controllers { + [EnableRateLimiting(RateLimitingConfig.DefaultPolicy)] public class CandidatoController : BaseController { private readonly OpenCandService openCandService; @@ -16,6 +19,7 @@ namespace OpenCand.API.Controllers } [HttpGet("search")] + [EnableRateLimiting(RateLimitingConfig.CandidatoSearchPolicy)] public async Task CandidatoSearch([FromQuery] string q) { return await openCandService.SearchCandidatosAsync(q); @@ -40,6 +44,7 @@ namespace OpenCand.API.Controllers } [HttpGet("{id}/reveal-cpf")] + [EnableRateLimiting(RateLimitingConfig.CpfRevealPolicy)] public async Task GetCandidatoCpfById([FromRoute] Guid id) { var rnd = new Random(); diff --git a/OpenCand.API/Controllers/StatsController.cs b/OpenCand.API/Controllers/StatsController.cs index 5516ac7..daacf4b 100644 --- a/OpenCand.API/Controllers/StatsController.cs +++ b/OpenCand.API/Controllers/StatsController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using OpenCand.API.Config; using OpenCand.API.Services; using OpenCand.Core.Models; namespace OpenCand.API.Controllers { + [EnableRateLimiting(RateLimitingConfig.DefaultPolicy)] public class StatsController : BaseController { private readonly OpenCandService openCandService; diff --git a/OpenCand.API/OpenCand.API.csproj b/OpenCand.API/OpenCand.API.csproj index 9069e74..5de73e4 100644 --- a/OpenCand.API/OpenCand.API.csproj +++ b/OpenCand.API/OpenCand.API.csproj @@ -5,10 +5,9 @@ enable enable - - - + + diff --git a/OpenCand.API/Program.cs b/OpenCand.API/Program.cs index 76930c5..eda9cbd 100644 --- a/OpenCand.API/Program.cs +++ b/OpenCand.API/Program.cs @@ -10,13 +10,14 @@ namespace OpenCand.API { public static void Main(string[] args) { - var builder = WebApplication.CreateBuilder(args); - - // Add services to the container. + var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); SetupServices(builder); + // Configure rate limiting + builder.Services.ConfigureRateLimiting(); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -34,9 +35,10 @@ namespace OpenCand.API { FileProvider = new PhysicalFileProvider(Path.Combine(workingDir, "fotos_cand")), RequestPath = "/assets/fotos" - }); + }); app.UseHttpsRedirection(); - app.UseHttpsRedirection(); + // Use rate limiting middleware + app.UseRateLimiter(); app.UseAuthorization(); diff --git a/OpenCand.ETL/OpenCand.ETL.csproj b/OpenCand.ETL/OpenCand.ETL.csproj index 2af4f60..4b63ea9 100644 --- a/OpenCand.ETL/OpenCand.ETL.csproj +++ b/OpenCand.ETL/OpenCand.ETL.csproj @@ -7,11 +7,11 @@ enable - + - +