mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 19:03:22 +00:00
85 lines
3.6 KiB
C#
85 lines
3.6 KiB
C#
|
|
using System.Net;
|
||
|
|
using System.Threading.RateLimiting;
|
||
|
|
using Data.SeaHavenIndustries;
|
||
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
||
|
|
using Microsoft.AspNetCore.RateLimiting;
|
||
|
|
|
||
|
|
namespace Api.SeaHavenIndustries.Infrastructure;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Per-client limits on the anonymous password reset endpoints, plus the
|
||
|
|
/// forwarded-header trust that makes "per client" mean the caller and not the proxy.
|
||
|
|
/// </summary>
|
||
|
|
public static class PasswordResetRateLimiting
|
||
|
|
{
|
||
|
|
public const string ForgetPasswordPolicy = "password-reset-request";
|
||
|
|
public const string VerificationCodePolicy = "password-reset-verify";
|
||
|
|
public const string ResetPasswordPolicy = "password-reset-confirm";
|
||
|
|
|
||
|
|
public const int PermitLimit = 10;
|
||
|
|
public static readonly TimeSpan Window = TimeSpan.FromMinutes(15);
|
||
|
|
|
||
|
|
public const string RejectedMessage = "Too many requests. Please try again later.";
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The API runs on Elastic Beanstalk behind an application load balancer and the
|
||
|
|
/// instance's nginx, so every request reaches Kestrel from loopback. X-Forwarded-For
|
||
|
|
/// is read right to left through loopback and private (VPC) hops only; the first
|
||
|
|
/// public address is the client. Entries a caller writes further left are ignored.
|
||
|
|
/// </summary>
|
||
|
|
public static IServiceCollection AddPasswordResetRateLimiting(this IServiceCollection services)
|
||
|
|
{
|
||
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
||
|
|
{
|
||
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
|
||
|
|
options.ForwardLimit = null;
|
||
|
|
options.KnownNetworks.Clear();
|
||
|
|
options.KnownProxies.Clear();
|
||
|
|
options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("127.0.0.0"), 8));
|
||
|
|
options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("10.0.0.0"), 8));
|
||
|
|
options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
|
||
|
|
options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("192.168.0.0"), 16));
|
||
|
|
options.KnownProxies.Add(IPAddress.IPv6Loopback);
|
||
|
|
});
|
||
|
|
|
||
|
|
services.AddRateLimiter(options =>
|
||
|
|
{
|
||
|
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||
|
|
options.OnRejected = async (context, cancellationToken) =>
|
||
|
|
{
|
||
|
|
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||
|
|
await context.HttpContext.Response.WriteAsJsonAsync(
|
||
|
|
new Response { Status = "Error", Message = RejectedMessage },
|
||
|
|
cancellationToken);
|
||
|
|
};
|
||
|
|
|
||
|
|
AddPerClientPolicy(options, ForgetPasswordPolicy);
|
||
|
|
AddPerClientPolicy(options, VerificationCodePolicy);
|
||
|
|
AddPerClientPolicy(options, ResetPasswordPolicy);
|
||
|
|
});
|
||
|
|
|
||
|
|
return services;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static string ClientPartitionKey(HttpContext context)
|
||
|
|
{
|
||
|
|
var address = context.Connection.RemoteIpAddress;
|
||
|
|
if (address == null)
|
||
|
|
return "unknown";
|
||
|
|
return (address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address).ToString();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void AddPerClientPolicy(RateLimiterOptions options, string policyName)
|
||
|
|
{
|
||
|
|
options.AddPolicy(policyName, context => RateLimitPartition.GetFixedWindowLimiter(
|
||
|
|
ClientPartitionKey(context),
|
||
|
|
_ => new FixedWindowRateLimiterOptions
|
||
|
|
{
|
||
|
|
PermitLimit = PermitLimit,
|
||
|
|
Window = Window,
|
||
|
|
QueueLimit = 0,
|
||
|
|
AutoReplenishment = true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|