mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 06:22:50 +08:00
Fix Ratelimit every day, every minute, every hour logic bugs
This commit is contained in:
parent
02162dd7a6
commit
0b09644d7e
@ -28,6 +28,12 @@ namespace Ocelot.Configuration.Validator
|
|||||||
|
|
||||||
result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration);
|
result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration);
|
||||||
|
|
||||||
|
if (result.IsError)
|
||||||
|
{
|
||||||
|
return new OkResponse<ConfigurationValidationResult>(result);
|
||||||
|
}
|
||||||
|
result = CheckForReRoutesRateLimitOptions(configuration);
|
||||||
|
|
||||||
if (result.IsError)
|
if (result.IsError)
|
||||||
{
|
{
|
||||||
return new OkResponse<ConfigurationValidationResult>(result);
|
return new OkResponse<ConfigurationValidationResult>(result);
|
||||||
@ -111,5 +117,35 @@ namespace Ocelot.Configuration.Validator
|
|||||||
|
|
||||||
return new ConfigurationValidationResult(true, errors);
|
return new ConfigurationValidationResult(true, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ConfigurationValidationResult CheckForReRoutesRateLimitOptions(FileConfiguration configuration)
|
||||||
|
{
|
||||||
|
var errors = new List<Error>();
|
||||||
|
|
||||||
|
foreach (var reRoute in configuration.ReRoutes)
|
||||||
|
{
|
||||||
|
if (reRoute.RateLimitOptions.EnableRateLimiting)
|
||||||
|
{
|
||||||
|
if (!IsValidPeriod(reRoute))
|
||||||
|
{
|
||||||
|
errors.Add(new RateLimitOptionsValidationError($"{reRoute.RateLimitOptions.Period} not contains scheme"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
return new ConfigurationValidationResult(true, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConfigurationValidationResult(false, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidPeriod(FileReRoute reRoute)
|
||||||
|
{
|
||||||
|
string period = reRoute.RateLimitOptions.Period;
|
||||||
|
|
||||||
|
return period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
using Ocelot.Errors;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ocelot.Configuration.Validator
|
||||||
|
{
|
||||||
|
public class RateLimitOptionsValidationError : Error
|
||||||
|
{
|
||||||
|
public RateLimitOptionsValidationError(string message)
|
||||||
|
: base(message, OcelotErrorCode.RateLimitOptionsError)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@
|
|||||||
RequestTimedOutError,
|
RequestTimedOutError,
|
||||||
UnableToFindQoSProviderError,
|
UnableToFindQoSProviderError,
|
||||||
UnableToSetConfigInConsulError,
|
UnableToSetConfigInConsulError,
|
||||||
UnmappableRequestError
|
UnmappableRequestError,
|
||||||
|
RateLimitOptionsError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Ocelot.RateLimit
|
namespace Ocelot.RateLimit
|
||||||
{
|
{
|
||||||
|
|
||||||
public class ClientRateLimitProcessor
|
public class ClientRateLimitProcessor
|
||||||
{
|
{
|
||||||
private readonly IRateLimitCounterHandler _counterHandler;
|
private readonly IRateLimitCounterHandler _counterHandler;
|
||||||
@ -23,7 +24,8 @@ namespace Ocelot.RateLimit
|
|||||||
return _core.ProcessRequest(requestIdentity, option);
|
return _core.ProcessRequest(requestIdentity, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
|
|
||||||
|
public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
|
||||||
{
|
{
|
||||||
return _core.RetryAfterFrom(timestamp, rule);
|
return _core.RetryAfterFrom(timestamp, rule);
|
||||||
}
|
}
|
||||||
@ -33,5 +35,11 @@ namespace Ocelot.RateLimit
|
|||||||
return _core.GetRateLimitHeaders(context, requestIdentity, option);
|
return _core.GetRateLimitHeaders(context, requestIdentity, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeSpan ConvertToTimeSpan(string timeSpan)
|
||||||
|
{
|
||||||
|
return _core.ConvertToTimeSpan(timeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ namespace Ocelot.RateLimit.Middleware
|
|||||||
_logger.LogDebug($"EndpointRateLimiting is not enabled for {DownstreamRoute.ReRoute.DownstreamPathTemplate}");
|
_logger.LogDebug($"EndpointRateLimiting is not enabled for {DownstreamRoute.ReRoute.DownstreamPathTemplate}");
|
||||||
|
|
||||||
_logger.TraceInvokeNext();
|
_logger.TraceInvokeNext();
|
||||||
await _next.Invoke(context);
|
await _next.Invoke(context);
|
||||||
_logger.TraceInvokeNextCompleted();
|
_logger.TraceInvokeNextCompleted();
|
||||||
_logger.TraceMiddlewareCompleted();
|
_logger.TraceMiddlewareCompleted();
|
||||||
return;
|
return;
|
||||||
@ -54,7 +54,7 @@ namespace Ocelot.RateLimit.Middleware
|
|||||||
_logger.LogDebug($"{DownstreamRoute.ReRoute.DownstreamPathTemplate} is white listed from rate limiting");
|
_logger.LogDebug($"{DownstreamRoute.ReRoute.DownstreamPathTemplate} is white listed from rate limiting");
|
||||||
|
|
||||||
_logger.TraceInvokeNext();
|
_logger.TraceInvokeNext();
|
||||||
await _next.Invoke(context);
|
await _next.Invoke(context);
|
||||||
_logger.TraceInvokeNextCompleted();
|
_logger.TraceInvokeNextCompleted();
|
||||||
_logger.TraceMiddlewareCompleted();
|
_logger.TraceMiddlewareCompleted();
|
||||||
return;
|
return;
|
||||||
@ -75,21 +75,24 @@ namespace Ocelot.RateLimit.Middleware
|
|||||||
// log blocked request
|
// log blocked request
|
||||||
LogBlockedRequest(context, identity, counter, rule);
|
LogBlockedRequest(context, identity, counter, rule);
|
||||||
|
|
||||||
|
var retrystring = retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
// break execution
|
// break execution
|
||||||
await ReturnQuotaExceededResponse(context, options, retryAfter);
|
await ReturnQuotaExceededResponse(context, options, retrystring);
|
||||||
_logger.TraceMiddlewareCompleted();
|
_logger.TraceMiddlewareCompleted();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//set X-Rate-Limit headers for the longest period
|
//set X-Rate-Limit headers for the longest period
|
||||||
if (!options.DisableRateLimitHeaders)
|
if (!options.DisableRateLimitHeaders)
|
||||||
{
|
{
|
||||||
var headers = _processor.GetRateLimitHeaders( context,identity, options);
|
var headers = _processor.GetRateLimitHeaders(context, identity, options);
|
||||||
context.Response.OnStarting(SetRateLimitHeaders, state: headers);
|
context.Response.OnStarting(SetRateLimitHeaders, state: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.TraceInvokeNext();
|
_logger.TraceInvokeNext();
|
||||||
await _next.Invoke(context);
|
await _next.Invoke(context);
|
||||||
_logger.TraceInvokeNextCompleted();
|
_logger.TraceInvokeNextCompleted();
|
||||||
_logger.TraceMiddlewareCompleted();
|
_logger.TraceMiddlewareCompleted();
|
||||||
}
|
}
|
||||||
@ -107,11 +110,15 @@ namespace Ocelot.RateLimit.Middleware
|
|||||||
httpContext.Request.Path.ToString().ToLowerInvariant(),
|
httpContext.Request.Path.ToString().ToLowerInvariant(),
|
||||||
httpContext.Request.Method.ToLowerInvariant()
|
httpContext.Request.Method.ToLowerInvariant()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
{
|
{
|
||||||
return option.ClientWhitelist.Contains(requestIdentity.ClientId);
|
if (option.ClientWhitelist.Contains(requestIdentity.ClientId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
|
public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
|
||||||
@ -144,6 +151,6 @@ namespace Ocelot.RateLimit.Middleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,23 +34,45 @@ namespace Ocelot.RateLimit
|
|||||||
if (entry.HasValue)
|
if (entry.HasValue)
|
||||||
{
|
{
|
||||||
// entry has not expired
|
// entry has not expired
|
||||||
if (entry.Value.Timestamp + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow)
|
if (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period) >= DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
// increment request count
|
// increment request count
|
||||||
var totalRequests = entry.Value.TotalRequests + 1;
|
var totalRequests = entry.Value.TotalRequests + 1;
|
||||||
|
|
||||||
// deep copy
|
// deep copy
|
||||||
counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
|
counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// stores: id (string) - timestamp (datetime) - total_requests (long)
|
|
||||||
_counterHandler.Set(counterId, counter, TimeSpan.FromSeconds(rule.PeriodTimespan));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (counter.TotalRequests > rule.Limit)
|
||||||
|
{
|
||||||
|
var retryAfter = RetryAfterFrom(counter.Timestamp, rule);
|
||||||
|
if (retryAfter > 0)
|
||||||
|
{
|
||||||
|
var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan);
|
||||||
|
_counterHandler.Set(counterId, counter, expirationTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_counterHandler.Remove(counterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var expirationTime = ConvertToTimeSpan(rule.Period);
|
||||||
|
_counterHandler.Set(counterId, counter, expirationTime);
|
||||||
|
}
|
||||||
return counter;
|
return counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime)
|
||||||
|
{
|
||||||
|
var counterId = ComputeCounterKey(requestIdentity, option);
|
||||||
|
var rule = option.RateLimitRule;
|
||||||
|
// stores: id (string) - timestamp (datetime) - total_requests (long)
|
||||||
|
_counterHandler.Set(counterId, counter, expirationTime);
|
||||||
|
}
|
||||||
|
|
||||||
public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
{
|
{
|
||||||
var rule = option.RateLimitRule;
|
var rule = option.RateLimitRule;
|
||||||
@ -63,7 +85,7 @@ namespace Ocelot.RateLimit
|
|||||||
(rule.Limit - entry.Value.TotalRequests).ToString(),
|
(rule.Limit - entry.Value.TotalRequests).ToString(),
|
||||||
(entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
|
(entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
headers = new RateLimitHeaders(context,
|
headers = new RateLimitHeaders(context,
|
||||||
@ -74,17 +96,17 @@ namespace Ocelot.RateLimit
|
|||||||
}
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
{
|
{
|
||||||
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
|
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
|
||||||
|
|
||||||
var idBytes = Encoding.UTF8.GetBytes(key);
|
var idBytes = Encoding.UTF8.GetBytes(key);
|
||||||
|
|
||||||
byte[] hashBytes;
|
byte[] hashBytes;
|
||||||
|
|
||||||
using (var algorithm = SHA1.Create())
|
using (var algorithm = SHA1.Create())
|
||||||
{
|
{
|
||||||
hashBytes = algorithm.ComputeHash(idBytes);
|
hashBytes = algorithm.ComputeHash(idBytes);
|
||||||
}
|
}
|
||||||
@ -92,12 +114,12 @@ namespace Ocelot.RateLimit
|
|||||||
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
|
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
|
public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
|
||||||
{
|
{
|
||||||
var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
|
var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
|
||||||
var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds);
|
var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds);
|
||||||
retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
|
retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
|
||||||
return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
return retryAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimeSpan ConvertToTimeSpan(string timeSpan)
|
public TimeSpan ConvertToTimeSpan(string timeSpan)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user