Skip to content

Commit

Permalink
Merge pull request #2664 from area363/transaction-results-rate-limit
Browse files Browse the repository at this point in the history
Add rate-limit to transactionresults query
  • Loading branch information
area363 authored Dec 19, 2024
2 parents 30e8814 + d60b66a commit fd7e093
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 2 deletions.
6 changes: 6 additions & 0 deletions NineChronicles.Headless.Executable/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@
"Endpoint": "*:/graphql/stagetransaction",
"Period": "60s",
"Limit": 12
},
{
"Endpoint": "*:/graphql/transactionresults",
"Period": "60s",
"Limit": 60
}
],
"QuotaExceededResponse": {
Expand All @@ -117,6 +122,7 @@
"StatusCode": 429
},
"IpBanThresholdCount": 5,
"TransactionResultsBanThresholdCount": 100,
"IpBanMinute" : 60,
"IpBanResponse": {
"Content": "{ \"message\": \"Your Ip has been banned.\" }",
Expand Down
62 changes: 62 additions & 0 deletions NineChronicles.Headless/Middleware/CustomRateLimitMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class CustomRateLimitMiddleware : RateLimitMiddleware<CustomIpRateLimitPr
private readonly IRateLimitConfiguration _config;
private readonly IOptions<CustomIpRateLimitOptions> _options;
private readonly string _whitelistedIp;
private readonly int _banCount;
private readonly System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler _tokenHandler = new();
private readonly Microsoft.IdentityModel.Tokens.TokenValidationParameters _validationParams;

Expand All @@ -36,6 +37,7 @@ public CustomRateLimitMiddleware(RequestDelegate next,
var issuer = jwtConfig["Issuer"] ?? "";
var key = jwtConfig["Key"] ?? "";
_whitelistedIp = configuration.GetSection("IpRateLimiting:IpWhitelist")?.Get<string[]>()?.FirstOrDefault() ?? "127.0.0.1";
_banCount = configuration.GetValue<int>("IpRateLimiting:TransactionResultsBanThresholdCount", 100);
_validationParams = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
Expand Down Expand Up @@ -71,6 +73,18 @@ public override async Task<ClientRequestIdentity> ResolveIdentityAsync(HttpConte
{
identity.Path = "/graphql/stagetransaction";
}
else if (body.Contains("transactionResults"))
{
identity.Path = "/graphql/transactionresults";

// Check for txIds count
var txIdsCount = CountTxIds(body);
if (txIdsCount > _banCount)
{
_logger.Information($"[IP-RATE-LIMITER] Banning IP {identity.ClientIp} due to excessive txIds count: {txIdsCount}");
IpBanMiddleware.BanIp(identity.ClientIp);
}
}
}

// Check for JWT secret key in headers
Expand Down Expand Up @@ -110,5 +124,53 @@ public override async Task<ClientRequestIdentity> ResolveIdentityAsync(HttpConte

return (headerValues[0], headerValues[1]);
}

private int CountTxIds(string body)
{
try
{
var json = System.Text.Json.JsonDocument.Parse(body);

// Check for txIds in query variables first
if (json.RootElement.TryGetProperty("variables", out var variables) &&
variables.TryGetProperty("txIds", out var txIdsElement) &&
txIdsElement.ValueKind == System.Text.Json.JsonValueKind.Array)
{
// Count from variables
return txIdsElement.GetArrayLength();
}

// Fallback to check the query string if variables are not set
if (json.RootElement.TryGetProperty("query", out var queryElement))
{
var query = queryElement.GetString();
if (!string.IsNullOrWhiteSpace(query))
{
// Extract txIds from the query string using regex
var txIdMatches = System.Text.RegularExpressions.Regex.Matches(
query, @"transactionResults\s*\(\s*txIds\s*:\s*\[(?<txIds>[^\]]*)\]"
);

if (txIdMatches.Count > 0)
{
// Extract the inner contents of txIds
var txIdList = txIdMatches[0].Groups["txIds"].Value;

// Count txIds using commas
var txIds = txIdList.Split(',', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);

return txIds.Length;
}
}
}
}
catch (System.Exception ex)
{
_logger.Warning("[IP-RATE-LIMITER] Error parsing request body: {Message}", ex.Message);
}

// Return 0 if txIds not found
return 0;
}
}
}
16 changes: 14 additions & 2 deletions NineChronicles.Headless/Middleware/HttpCaptureMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,39 @@
using Microsoft.AspNetCore.Http;
using Serilog;
using ILogger = Serilog.ILogger;
using Microsoft.Extensions.Configuration;

namespace NineChronicles.Headless.Middleware
{
public class HttpCaptureMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly bool _enableIpRateLimiting;

public HttpCaptureMiddleware(RequestDelegate next)
public HttpCaptureMiddleware(RequestDelegate next, Microsoft.Extensions.Configuration.IConfiguration configuration)
{
_next = next;
_logger = Log.Logger.ForContext<HttpCaptureMiddleware>();
_enableIpRateLimiting = configuration.GetValue<bool>("IpRateLimiting:EnableEndpointRateLimiting");
}

public async Task InvokeAsync(HttpContext context)
{
var remoteIp = context.Connection.RemoteIpAddress!.ToString();

// Conditionally skip IP banning if endpoint rate-limiting is disabled
if (_enableIpRateLimiting && IpBanMiddleware.IsIpBanned(remoteIp))
{
_logger.Information($"[GRAPHQL-REQUEST-CAPTURE] Skipping logging for banned IP: {remoteIp}");
await _next(context);
return;
}

// Prevent to harm HTTP/2 communication.
if (context.Request.Protocol == "HTTP/1.1")
{
context.Request.EnableBuffering();
var remoteIp = context.Connection.RemoteIpAddress;
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
context.Items["RequestBody"] = body;
_logger.Information("[GRAPHQL-REQUEST-CAPTURE] IP: {IP} Method: {Method} Endpoint: {Path} {Body}",
Expand Down
10 changes: 10 additions & 0 deletions NineChronicles.Headless/Middleware/IpBanMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ public static void UnbanIp(string ip)
}
}

public static bool IsIpBanned(string ip)
{
if (_bannedIps.ContainsKey(ip))
{
return true;
}

return false;
}

public Task InvokeAsync(HttpContext context)
{
var remoteIp = context.Connection.RemoteIpAddress!.ToString();
Expand Down

0 comments on commit fd7e093

Please sign in to comment.