Skip to content

Commit

Permalink
Merge pull request #290 from rGunti/feature/rate-limit
Browse files Browse the repository at this point in the history
Implemented basic rate limiting
  • Loading branch information
rGunti authored Jun 15, 2024
2 parents d09e192 + d9cb521 commit 470cda9
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
[*.cs]
# csharpier exceptions
dotnet_diagnostic.IDE0055.severity = none

# StyleCop.Analyzers
dotnet_diagnostic.SA1000.severity = none
dotnet_diagnostic.SA1009.severity = none
dotnet_diagnostic.SA1111.severity = none
dotnet_diagnostic.SA1118.severity = none
dotnet_diagnostic.SA1137.severity = none
dotnet_diagnostic.SA1413.severity = none
dotnet_diagnostic.SA1500.severity = none
dotnet_diagnostic.SA1501.severity = none
dotnet_diagnostic.SA1502.severity = none
dotnet_diagnostic.SA1504.severity = none
dotnet_diagnostic.SA1515.severity = none
dotnet_diagnostic.SA1516.severity = none

# for csharpier <= 0.21.0
dotnet_diagnostic.SA1127.severity = none
dotnet_diagnostic.SA1128.severity = none

dotnet_diagnostic.SA1001.severity = none
dotnet_diagnostic.SA1002.severity = none
dotnet_diagnostic.SA1003.severity = none
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ internal class InternalLogRecord
{
public ObjectId Id { get; set; }
public DateTime Timestamp { get; set; }
public string UtcTimestamp { get; set; }
public string UtcTimestamp { get; set; } = string.Empty;
public LogLevel Level { get; set; }
public string MessageTemplate { get; set; }
public string RenderedMessage { get; set; }
public string MessageTemplate { get; set; } = string.Empty;
public string RenderedMessage { get; set; } = string.Empty;
public string? Exception { get; set; }
public Dictionary<string, object> Properties { get; set; }
public Dictionary<string, object> Properties { get; set; } = new();

public LogRecord ToLogRecord(bool includeProperties = false)
{
Expand Down
6 changes: 4 additions & 2 deletions src/FloppyBot.WebApi.Agent/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using FloppyBot.HealthCheck.Receiver;
using FloppyBot.Version;
using FloppyBot.WebApi.Agent.Hubs;
using FloppyBot.WebApi.Agent.Utils;
using FloppyBot.WebApi.Auth;
using FloppyBot.WebApi.Base.ExceptionHandler;
using FloppyBot.WebApi.V1Compatibility;
Expand Down Expand Up @@ -152,13 +153,14 @@ await context.Response.WriteAsync(
.AddV1Compatibility()
.AddSingleton<StreamSourceListener>()
.AddSingleton<LogService>()
.AddMemoryCache();
.AddMemoryCache()
.ConfigureRateLimiter(builder.Configuration);

// *** CONFIGURE ************************************************************************
var app = builder.Build();

// - Routing
app.UseRouting();
app.UseRouting().UseRateLimiter();

// - Swagger only in development
if (app.Environment.IsDevelopment())
Expand Down
30 changes: 30 additions & 0 deletions src/FloppyBot.WebApi.Agent/Utils/HttpContextHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Net;
using System.Net.Sockets;

namespace FloppyBot.WebApi.Agent.Utils;

public static class HttpContextHelpers
{
private const string HEADER_FORWARDED_FOR = "X-Forwarded-For";
private const string HEADER_REAL_IP = "X-Real-IP";

public static IPAddress? GetRemoteHostIpFromHeaders(this HttpContext httpContext)
{
return httpContext
.Request.Headers.GetCommaSeparatedValues(HEADER_REAL_IP)
.Concat(httpContext.Request.Headers.GetCommaSeparatedValues(HEADER_FORWARDED_FOR))
.Select(ip => IPAddress.TryParse(ip, out var address) ? address : null)
.FirstOrDefault(
ip =>
ip?.AddressFamily is AddressFamily.InterNetwork or AddressFamily.InterNetworkV6,
httpContext.Connection.RemoteIpAddress
);
}

public static ILogger GetLogger(this HttpContext httpContext, string categoryName)
{
return httpContext
.RequestServices.GetRequiredService<ILoggerFactory>()
.CreateLogger(categoryName);
}
}
99 changes: 99 additions & 0 deletions src/FloppyBot.WebApi.Agent/Utils/Limiters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;

namespace FloppyBot.WebApi.Agent.Utils;

internal static class Limiters
{
private const string KEY_GLOBAL = "global";
private const string KEY_AUTH = "auth";

private const string LOGGER_CATEGORY = "RateLimiter";

private const string SECTION_DEFAULT = "RateLimiter:Default";
private const string SECTION_AUTH = "RateLimiter:Authenticated";

internal static IServiceCollection ConfigureRateLimiter(
this IServiceCollection services,
IConfiguration config
)
{
return services
.Configure<TokenBucketRateLimiterOptions>(
KEY_GLOBAL,
o => config.GetRequiredSection(SECTION_DEFAULT).Bind(o)
)
.Configure<TokenBucketRateLimiterOptions>(
KEY_AUTH,
o => config.GetRequiredSection(SECTION_AUTH).Bind(o)
)
.AddRateLimiter(rl =>
{
rl.OnRejected = RateLimiter_OnRejected;
rl.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
rl.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
RateLimiter_Build
);
});
}

private static RateLimitPartition<string> RateLimiter_Build(HttpContext httpContext)
{
var logger = httpContext.GetLogger(LOGGER_CATEGORY);

string? accessToken = httpContext.Request.Headers.Authorization.ToString().HashString();
string? remoteIp = httpContext.GetRemoteHostIpFromHeaders()?.ToString();

var partitionKey = accessToken ?? remoteIp ?? KEY_GLOBAL;
logger.LogTrace("Building rate limiter for partition={PartitionKey}", partitionKey);
return RateLimitPartition.GetTokenBucketLimiter(
accessToken ?? remoteIp ?? KEY_GLOBAL,
_ =>
httpContext
.RequestServices.GetRequiredService<
IOptionsFactory<TokenBucketRateLimiterOptions>
>()
.Create(!string.IsNullOrWhiteSpace(accessToken) ? KEY_AUTH : KEY_GLOBAL)
);
}

private static async ValueTask RateLimiter_OnRejected(
OnRejectedContext context,
CancellationToken cancellationToken
)
{
var response = context.HttpContext.Response;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
// Add a Retry-After header to the response
response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString(
NumberFormatInfo.InvariantInfo
);
}

response.StatusCode = StatusCodes.Status429TooManyRequests;

context
.HttpContext.GetLogger(LOGGER_CATEGORY)
.LogWarning(
"Request rejected from addr={UserRequestAddress} to path={UserRequestPath}",
context.HttpContext.GetRemoteHostIpFromHeaders(),
context.HttpContext.Request.Path
);
await response.WriteAsync(
"Whoo there, calm down, mate! You're exceeding the speed limit here, gonna call the cops next time.",
cancellationToken
);
}

private static string? HashString(this string? s)
{
return string.IsNullOrWhiteSpace(s)
? null
: Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(s)));
}
}
4 changes: 3 additions & 1 deletion src/FloppyBot.WebApi.Agent/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"Override": {
"FloppyBot.HealthCheck.Core.HealthCheckProducerCronJob": "Debug",
"FloppyBot.HealthCheck.Receiver.HealthCheckReceiver": "Debug",
"Microsoft.AspNetCore.Mvc": "Information"
"Microsoft.AspNetCore.Mvc": "Information",
"Microsoft.AspNetCore.Server.Kestrel.Connections": "Information",
"Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets": "Information"
}
}
},
Expand Down
12 changes: 12 additions & 0 deletions src/FloppyBot.WebApi.Agent/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,17 @@
"Authority": "TO_BE_DEFINED",
"Audience": "TO_BE_DEFINED"
},
"RateLimiter": {
"Default": {
"TokenLimit": 10,
"TokensPerPeriod": 10,
"ReplenishmentPeriod": "0:00:10"
},
"Authenticated": {
"TokenLimit": 100,
"TokensPerPeriod": 10,
"ReplenishmentPeriod": "0:00:10"
}
},
"InstanceName": "1"
}
2 changes: 1 addition & 1 deletion src/FloppyBot.WebApi.V2/Dtos/CommandConfigurationDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public CommandConfiguration ToEntity()
{
return new CommandConfiguration
{
Id = Id,
Id = Id!,
ChannelId = ChannelId,
CommandName = CommandName,
RequiredPrivilegeLevel = RequiredPrivilegeLevel,
Expand Down

0 comments on commit 470cda9

Please sign in to comment.