Refactor: Update rate limiting to account for reverse proxy deployments

This commit is contained in:
kurt-mcrae
2025-05-04 21:08:12 +10:00
parent 4a8bd9307b
commit 6ec69ee491
8 changed files with 41 additions and 32 deletions
-2
View File
@@ -1,13 +1,11 @@
using backend.Models.NswFuelApi;
using backend.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/brands")]
public class BrandController : ControllerBase
{
@@ -1,13 +1,11 @@
using backend.Models.NswFuelApi;
using backend.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/fuel-types")]
public class FuelTypeController : ControllerBase
{
-2
View File
@@ -3,13 +3,11 @@ using backend.Models.Utilities;
using backend.Services;
using backend.Validators;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/prices")]
public class PriceController : ControllerBase
{
@@ -1,13 +1,11 @@
using backend.Models.Trends;
using backend.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/trends/daily-averages")]
public class PriceTrendsController : ControllerBase
{
-2
View File
@@ -3,13 +3,11 @@ using backend.Models.Utilities;
using backend.Services;
using backend.Validators;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/stations")]
public class StationController : ControllerBase
{
@@ -3,13 +3,11 @@ using backend.Models.Utilities;
using backend.Services;
using backend.Validators;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/stations-with-prices")]
public class StationWithPricesController : ControllerBase
{
@@ -1,13 +1,11 @@
using backend.Models.SuburbBoundariesApi;
using backend.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Swashbuckle.AspNetCore.Annotations;
namespace backend.Controllers;
[ApiController]
[EnableRateLimiting("sliding")]
[Route("api/suburb-boundaries")]
public class SuburbBoundaryController : ControllerBase
{
@@ -6,7 +6,6 @@ using backend.Services.Init;
using backend.Validators;
using Hangfire;
using Hangfire.PostgreSql;
using Microsoft.AspNetCore.RateLimiting;
using ServiceStack.Data;
using ServiceStack.OrmLite;
@@ -36,26 +35,50 @@ public static class ServiceCollectionExtensions
options.AddSimpleConsole(c =>
c.TimestampFormat = "[dd-MM-yyyy HH:mm:ss] "))
.AddHttpClient()
.AddRateLimiter(rateLimiterOptions =>
rateLimiterOptions.AddSlidingWindowLimiter("sliding", options =>
.AddRateLimiter(options =>
{
// apply to all endpoints
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
//TODO: this is something we should grab from config
options.PermitLimit = 100;
options.Window = TimeSpan.FromSeconds(60);
options.SegmentsPerWindow = 60;
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
//just show the rate limit message immediately, don't queue up any requests
options.QueueLimit = 0;
}).OnRejected = async (context, token) =>
// grab the first IP in X-Forwarded-For, else fall back
var forwarded = context.Request.Headers.TryGetValue("X-Forwarded-For", out var vals)
? vals.FirstOrDefault()?.Split(',')[0]
: null;
var clientIp = !string.IsNullOrEmpty(forwarded)
? forwarded
: context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetSlidingWindowLimiter(
clientIp,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromSeconds(60),
SegmentsPerWindow = 60,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0,
AutoReplenishment = true
}
);
});
options.OnRejected = async (context, ct) =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>()
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("RateLimiter");
logger.LogWarning("Client rate limit exceeded for {ConnectionRemoteIpAddress}",
context.HttpContext.Connection.RemoteIpAddress);
context.HttpContext.Response.StatusCode = 429;
await context.HttpContext.Response.WriteAsync("Rate limit exceeded, please try again in one minute",
token);
})
var ip = context.HttpContext.Request.Headers["X-Forwarded-For"]
.FirstOrDefault()?.Split(',')[0]
?? context.HttpContext.Connection.RemoteIpAddress?.ToString();
logger.LogWarning("Client rate limit exceeded for {ClientIp}", ip);
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response
.WriteAsync("Rate limit exceeded, please try again in one minute", ct);
};
})
.AddHangfire(config =>
config.UsePostgreSqlStorage(c =>
c.UseNpgsqlConnection(connectionString)))