using System.Threading.RateLimiting; using Microsoft.AspNetCore.RateLimiting; namespace Backend.Gateway; public static class GatewayServiceCollectionExtensions { public const string CorsPolicyName = "DefaultCorsPolicy"; public static IServiceCollection AddApiGateway( this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection(CorsOptions.SectionName)); services.Configure(configuration.GetSection(RateLimitOptions.SectionName)); var corsSettings = configuration.GetSection(CorsOptions.SectionName).Get() ?? new CorsOptions(); var rateLimitSettings = configuration.GetSection(RateLimitOptions.SectionName).Get() ?? new RateLimitOptions(); services.AddCors(options => options.AddPolicy(CorsPolicyName, policy => { if (corsSettings.AllowedOrigins.Length == 0) { return; } policy.WithOrigins(corsSettings.AllowedOrigins) .AllowAnyHeader() .AllowAnyMethod(); })); services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.GlobalLimiter = CreateGlobalLimiter(rateLimitSettings); }); services.Configure(options => options.LowercaseUrls = true); return services; } // Health checks are exempt so that infrastructure probes are never throttled. private static PartitionedRateLimiter CreateGlobalLimiter(RateLimitOptions settings) => PartitionedRateLimiter.Create(context => { if (context.Request.Path.StartsWithSegments(ApiRoutes.Health)) { return RateLimitPartition.GetNoLimiter("health"); } var partitionKey = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions { PermitLimit = settings.PermitLimit, Window = TimeSpan.FromSeconds(settings.WindowSeconds), QueueLimit = settings.QueueLimit, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, }); }); }