mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-23 00:32:50 +08:00
implement Request Rate limit, this feature is options
This commit is contained in:
parent
08c9700a4a
commit
e1d5ef3aae
@ -1,6 +1,6 @@
|
||||
{
|
||||
"projects": [ "src", "test" ],
|
||||
"sdk": {
|
||||
"version": "1.0.0-preview2-003133"
|
||||
"version": "1.0.0-preview2-003131"
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,8 @@ namespace Ocelot.Configuration.Builder
|
||||
private int _serviceProviderPort;
|
||||
private bool _useQos;
|
||||
private QoSOptions _qosOptions;
|
||||
|
||||
public bool _enableRateLimiting;
|
||||
public RateLimitOptions _rateLimitOptions;
|
||||
|
||||
public ReRouteBuilder()
|
||||
{
|
||||
@ -236,6 +237,19 @@ namespace Ocelot.Configuration.Builder
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReRouteBuilder WithEnableRateLimiting(bool input)
|
||||
{
|
||||
_enableRateLimiting = input;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReRouteBuilder WithRateLimitOptions(RateLimitOptions input)
|
||||
{
|
||||
_rateLimitOptions = input;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public ReRoute Build()
|
||||
{
|
||||
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
|
||||
@ -244,7 +258,7 @@ namespace Ocelot.Configuration.Builder
|
||||
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer,
|
||||
_downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery,
|
||||
_serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort),
|
||||
_useQos,_qosOptions);
|
||||
_useQos,_qosOptions,_enableRateLimiting,_rateLimitOptions);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,20 @@ namespace Ocelot.Configuration.Creator
|
||||
|
||||
ReRoute reRoute;
|
||||
|
||||
|
||||
var enableRateLimiting = (fileReRoute.RateLimitOptions!= null && fileReRoute.RateLimitOptions.EnableRateLimiting)? true: false;
|
||||
RateLimitOptions rateLimitOption = null;
|
||||
if (enableRateLimiting)
|
||||
{
|
||||
rateLimitOption = new RateLimitOptions(enableRateLimiting, fileReRoute.RateLimitOptions.ClientIdHeader,
|
||||
fileReRoute.RateLimitOptions.ClientWhitelist,fileReRoute.RateLimitOptions.DisableRateLimitHeaders,
|
||||
fileReRoute.RateLimitOptions.QuotaExceededMessage,fileReRoute.RateLimitOptions.RateLimitCounterPrefix,
|
||||
new RateLimitRule()
|
||||
{
|
||||
Limit = fileReRoute.RateLimitOptions.RateLimitRule.Limit,
|
||||
Period = fileReRoute.RateLimitOptions.RateLimitRule.Period,
|
||||
PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.RateLimitRule.PeriodTimespan)
|
||||
});
|
||||
}
|
||||
var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
|
||||
|
||||
var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName,
|
||||
@ -138,7 +151,8 @@ namespace Ocelot.Configuration.Creator
|
||||
, fileReRoute.DownstreamScheme,
|
||||
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
|
||||
serviceProviderConfiguration, isQos,
|
||||
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue));
|
||||
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue),
|
||||
enableRateLimiting, rateLimitOption);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -151,7 +165,8 @@ namespace Ocelot.Configuration.Creator
|
||||
fileReRoute.DownstreamScheme,
|
||||
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
|
||||
serviceProviderConfiguration, isQos,
|
||||
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue));
|
||||
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue),
|
||||
enableRateLimiting, rateLimitOption);
|
||||
}
|
||||
|
||||
var loadBalancer = await _loadBalanceFactory.Get(reRoute);
|
||||
|
66
src/Ocelot/Configuration/File/FileRateLimitOptions.cs
Normal file
66
src/Ocelot/Configuration/File/FileRateLimitOptions.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.Configuration.File
|
||||
{
|
||||
public class FileRateLimitOptions
|
||||
{
|
||||
public FileRateLimitOptions()
|
||||
{
|
||||
RateLimitRule = new FileRateLimitRule();
|
||||
ClientWhitelist = new List<string>();
|
||||
}
|
||||
|
||||
public FileRateLimitRule RateLimitRule { get; set; }
|
||||
|
||||
public List<string> ClientWhitelist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
|
||||
/// </summary>
|
||||
public string ClientIdHeader { get; set; } = "ClientId";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the policy prefix, used to compose the client policy cache key
|
||||
/// </summary>
|
||||
public string ClientPolicyPrefix { get; set; } = "crlp";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
|
||||
/// If none specified the default will be:
|
||||
/// API calls quota exceeded! maximum admitted {0} per {1}
|
||||
/// </summary>
|
||||
public string QuotaExceededMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
|
||||
/// </summary>
|
||||
public string RateLimitCounterPrefix { get; set; } = "ocelot";
|
||||
|
||||
/// <summary>
|
||||
/// Enables endpoint rate limiting based URL path and HTTP verb
|
||||
/// </summary>
|
||||
public bool EnableRateLimiting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disables X-Rate-Limit and Rety-After headers
|
||||
/// </summary>
|
||||
public bool DisableRateLimitHeaders { get; set; }
|
||||
}
|
||||
|
||||
public class FileRateLimitRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit period as in 1s, 1m, 1h
|
||||
/// </summary>
|
||||
public string Period { get; set; }
|
||||
|
||||
public int PeriodTimespan { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum number of requests that a client can make in a defined period
|
||||
/// </summary>
|
||||
public long Limit { get; set; }
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ namespace Ocelot.Configuration.File
|
||||
AuthenticationOptions = new FileAuthenticationOptions();
|
||||
FileCacheOptions = new FileCacheOptions();
|
||||
QoSOptions = new FileQoSOptions();
|
||||
RateLimitOptions = new FileRateLimitOptions();
|
||||
}
|
||||
|
||||
public string DownstreamPathTemplate { get; set; }
|
||||
@ -32,5 +33,6 @@ namespace Ocelot.Configuration.File
|
||||
public int DownstreamPort { get; set; }
|
||||
public FileQoSOptions QoSOptions { get; set; }
|
||||
public string LoadBalancer {get;set;}
|
||||
public FileRateLimitOptions RateLimitOptions { get; set; }
|
||||
}
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
using Polly.Timeout;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.Configuration
|
||||
{
|
||||
|
76
src/Ocelot/Configuration/RateLimitOptions.cs
Normal file
76
src/Ocelot/Configuration/RateLimitOptions.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// RateLimit Options
|
||||
/// </summary>
|
||||
public class RateLimitOptions
|
||||
{
|
||||
public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List<string> clientWhitelist,bool disableRateLimitHeaders,
|
||||
string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule)
|
||||
{
|
||||
EnableRateLimiting = enbleRateLimiting;
|
||||
ClientIdHeader = clientIdHeader;
|
||||
ClientWhitelist = clientWhitelist;
|
||||
DisableRateLimitHeaders = disableRateLimitHeaders;
|
||||
QuotaExceededMessage = quotaExceededMessage;
|
||||
RateLimitCounterPrefix = rateLimitCounterPrefix;
|
||||
RateLimitRule = rateLimitRule;
|
||||
}
|
||||
|
||||
|
||||
public RateLimitRule RateLimitRule { get; private set; }
|
||||
|
||||
public List<string> ClientWhitelist { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
|
||||
/// </summary>
|
||||
public string ClientIdHeader { get; private set; } = "ClientId";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
|
||||
/// </summary>
|
||||
public int HttpStatusCode { get; private set; } = 429;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
|
||||
/// If none specified the default will be:
|
||||
/// API calls quota exceeded! maximum admitted {0} per {1}
|
||||
/// </summary>
|
||||
public string QuotaExceededMessage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
|
||||
/// </summary>
|
||||
public string RateLimitCounterPrefix { get; private set; } = "ocelot";
|
||||
|
||||
/// <summary>
|
||||
/// Enables endpoint rate limiting based URL path and HTTP verb
|
||||
/// </summary>
|
||||
public bool EnableRateLimiting { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disables X-Rate-Limit and Rety-After headers
|
||||
/// </summary>
|
||||
public bool DisableRateLimitHeaders { get; private set; }
|
||||
}
|
||||
|
||||
public class RateLimitRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit period as in 1s, 1m, 1h
|
||||
/// </summary>
|
||||
public string Period { get; set; }
|
||||
|
||||
public TimeSpan? PeriodTimespan { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum number of requests that a client can make in a defined period
|
||||
/// </summary>
|
||||
public long Limit { get; set; }
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ namespace Ocelot.Configuration
|
||||
string requestIdKey, bool isCached, CacheOptions fileCacheOptions,
|
||||
string downstreamScheme, string loadBalancer, string downstreamHost,
|
||||
int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion,
|
||||
bool isQos,QoSOptions qos)
|
||||
bool isQos,QoSOptions qos, bool enableRateLimit, RateLimitOptions ratelimitOptions)
|
||||
{
|
||||
LoadBalancerKey = loadBalancerKey;
|
||||
ServiceProviderConfiguraion = serviceProviderConfiguraion;
|
||||
@ -44,6 +44,8 @@ namespace Ocelot.Configuration
|
||||
DownstreamScheme = downstreamScheme;
|
||||
IsQos = isQos;
|
||||
QosOptions = qos;
|
||||
EnableEndpointRateLimiting = enableRateLimit;
|
||||
RateLimitOptions = ratelimitOptions;
|
||||
}
|
||||
|
||||
public string LoadBalancerKey {get;private set;}
|
||||
@ -68,5 +70,7 @@ namespace Ocelot.Configuration
|
||||
public string DownstreamHost { get; private set; }
|
||||
public int DownstreamPort { get; private set; }
|
||||
public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; }
|
||||
public bool EnableEndpointRateLimiting { get; private set; }
|
||||
public RateLimitOptions RateLimitOptions { get; private set; }
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ using Ocelot.Request.Builder;
|
||||
using Ocelot.Requester;
|
||||
using Ocelot.Responder;
|
||||
using Ocelot.ServiceDiscovery;
|
||||
using Ocelot.RateLimit;
|
||||
|
||||
namespace Ocelot.DependencyInjection
|
||||
{
|
||||
@ -84,6 +85,7 @@ namespace Ocelot.DependencyInjection
|
||||
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
|
||||
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
|
||||
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();
|
||||
services.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
|
||||
|
||||
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
|
||||
// could maybe use a scoped data repository
|
||||
|
@ -11,6 +11,7 @@ using Ocelot.Request.Middleware;
|
||||
using Ocelot.Requester.Middleware;
|
||||
using Ocelot.RequestId.Middleware;
|
||||
using Ocelot.Responder.Middleware;
|
||||
using Ocelot.RateLimit.Middleware;
|
||||
|
||||
namespace Ocelot.Middleware
|
||||
{
|
||||
@ -57,6 +58,9 @@ namespace Ocelot.Middleware
|
||||
// Then we get the downstream route information
|
||||
builder.UseDownstreamRouteFinderMiddleware();
|
||||
|
||||
// We check whether the request is ratelimit, and if there is no continue processing
|
||||
builder.UseRateLimiting();
|
||||
|
||||
// Now we can look for the requestId
|
||||
builder.UseRequestIdMiddleware();
|
||||
|
||||
|
36
src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
Normal file
36
src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Ocelot.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
public class ClientRateLimitProcessor
|
||||
{
|
||||
private readonly IRateLimitCounterHandler _counterHandler;
|
||||
private readonly RateLimitCore _core;
|
||||
|
||||
public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler)
|
||||
{
|
||||
_counterHandler = counterHandler;
|
||||
_core = new RateLimitCore(_counterHandler);
|
||||
}
|
||||
|
||||
public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||
{
|
||||
return _core.ProcessRequest(requestIdentity, option);
|
||||
}
|
||||
|
||||
public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
|
||||
{
|
||||
return _core.RetryAfterFrom(timestamp, rule);
|
||||
}
|
||||
|
||||
public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||
{
|
||||
return _core.GetRateLimitHeaders(requestIdentity, option);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
11
src/Ocelot/RateLimit/ClientRequestIdentity.cs
Normal file
11
src/Ocelot/RateLimit/ClientRequestIdentity.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
public class ClientRequestIdentity
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public string HttpVerb { get; set; }
|
||||
}
|
||||
}
|
15
src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
Normal file
15
src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
public interface IRateLimitCounterHandler
|
||||
{
|
||||
bool Exists(string id);
|
||||
RateLimitCounter? Get(string id);
|
||||
void Remove(string id);
|
||||
void Set(string id, RateLimitCounter counter, TimeSpan expirationTime);
|
||||
}
|
||||
}
|
45
src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
Normal file
45
src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
|
||||
{
|
||||
_memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
|
||||
}
|
||||
|
||||
public bool Exists(string id)
|
||||
{
|
||||
RateLimitCounter counter;
|
||||
return _memoryCache.TryGetValue(id, out counter);
|
||||
}
|
||||
|
||||
public RateLimitCounter? Get(string id)
|
||||
{
|
||||
RateLimitCounter counter;
|
||||
if (_memoryCache.TryGetValue(id, out counter))
|
||||
{
|
||||
return counter;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Remove(string id)
|
||||
{
|
||||
_memoryCache.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
142
src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
Normal file
142
src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
Normal file
@ -0,0 +1,142 @@
|
||||
using Ocelot.Middleware;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Ocelot.Infrastructure.RequestData;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Ocelot.Logging;
|
||||
using Ocelot.Configuration;
|
||||
|
||||
namespace Ocelot.RateLimit.Middleware
|
||||
{
|
||||
public class ClientRateLimitMiddleware : OcelotMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IOcelotLogger _logger;
|
||||
private readonly IRateLimitCounterHandler _counterHandler;
|
||||
private readonly ClientRateLimitProcessor _processor;
|
||||
|
||||
public ClientRateLimitMiddleware(RequestDelegate next,
|
||||
IOcelotLoggerFactory loggerFactory,
|
||||
IRequestScopedDataRepository requestScopedDataRepository,
|
||||
IRateLimitCounterHandler counterHandler)
|
||||
: base(requestScopedDataRepository)
|
||||
{
|
||||
_next = next;
|
||||
_logger = loggerFactory.CreateLogger<ClientRateLimitMiddleware>();
|
||||
_counterHandler = counterHandler;
|
||||
_processor = new ClientRateLimitProcessor(counterHandler);
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
_logger.LogDebug("started calling RateLimit middleware");
|
||||
var options = DownstreamRoute.ReRoute.RateLimitOptions;
|
||||
// check if rate limiting is enabled
|
||||
if (!DownstreamRoute.ReRoute.EnableEndpointRateLimiting)
|
||||
{
|
||||
await _next.Invoke(context);
|
||||
return;
|
||||
}
|
||||
// compute identity from request
|
||||
var identity = SetIdentity(context, options);
|
||||
|
||||
// check white list
|
||||
if (IsWhitelisted(identity, options))
|
||||
{
|
||||
await _next.Invoke(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var rule = options.RateLimitRule;
|
||||
if (rule.Limit > 0)
|
||||
{
|
||||
// increment counter
|
||||
var counter = _processor.ProcessRequest(identity, options);
|
||||
|
||||
// check if limit is reached
|
||||
if (counter.TotalRequests > rule.Limit)
|
||||
{
|
||||
//compute retry after value
|
||||
var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule);
|
||||
|
||||
// log blocked request
|
||||
LogBlockedRequest(context, identity, counter, rule);
|
||||
|
||||
// break execution
|
||||
await ReturnQuotaExceededResponse(context, options, retryAfter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//set X-Rate-Limit headers for the longest period
|
||||
if (!options.DisableRateLimitHeaders)
|
||||
{
|
||||
var headers = _processor.GetRateLimitHeaders(identity, options);
|
||||
headers.Context = context;
|
||||
|
||||
context.Response.OnStarting(SetRateLimitHeaders, state: headers);
|
||||
}
|
||||
|
||||
await _next.Invoke(context);
|
||||
}
|
||||
|
||||
public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option)
|
||||
{
|
||||
var clientId = "client";
|
||||
if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader))
|
||||
{
|
||||
clientId = httpContext.Request.Headers[option.ClientIdHeader].First();
|
||||
}
|
||||
|
||||
return new ClientRequestIdentity
|
||||
{
|
||||
Path = httpContext.Request.Path.ToString().ToLowerInvariant(),
|
||||
HttpVerb = httpContext.Request.Method.ToLowerInvariant(),
|
||||
ClientId = clientId,
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||
{
|
||||
if (option.ClientWhitelist != null && option.ClientWhitelist.Contains(requestIdentity.ClientId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
|
||||
{
|
||||
_logger.LogDebug($"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { DownstreamRoute.ReRoute.UpstreamTemplate }, TraceIdentifier {httpContext.TraceIdentifier}.");
|
||||
}
|
||||
|
||||
public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter)
|
||||
{
|
||||
var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage;
|
||||
|
||||
if (!option.DisableRateLimitHeaders)
|
||||
{
|
||||
httpContext.Response.Headers["Retry-After"] = retryAfter;
|
||||
}
|
||||
|
||||
httpContext.Response.StatusCode = option.HttpStatusCode;
|
||||
return httpContext.Response.WriteAsync(message);
|
||||
}
|
||||
|
||||
private Task SetRateLimitHeaders(object rateLimitHeaders)
|
||||
{
|
||||
var headers = (RateLimitHeaders)rateLimitHeaders;
|
||||
|
||||
headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit;
|
||||
headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining;
|
||||
headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit.Middleware
|
||||
{
|
||||
public static class RateLimitMiddlewareExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<ClientRateLimitMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
123
src/Ocelot/RateLimit/RateLimitCore.cs
Normal file
123
src/Ocelot/RateLimit/RateLimitCore.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using Ocelot.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
public class RateLimitCore
|
||||
{
|
||||
private readonly IRateLimitCounterHandler _counterHandler;
|
||||
private static readonly object _processLocker = new object();
|
||||
|
||||
public RateLimitCore(IRateLimitCounterHandler counterStore)
|
||||
{
|
||||
_counterHandler = counterStore;
|
||||
}
|
||||
|
||||
public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||
{
|
||||
var counter = new RateLimitCounter
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
TotalRequests = 1
|
||||
};
|
||||
var rule = option.RateLimitRule;
|
||||
|
||||
var counterId = ComputeCounterKey(requestIdentity, option);
|
||||
|
||||
// serial reads and writes
|
||||
lock (_processLocker)
|
||||
{
|
||||
var entry = _counterHandler.Get(counterId);
|
||||
if (entry.HasValue)
|
||||
{
|
||||
// entry has not expired
|
||||
if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
|
||||
{
|
||||
// increment request count
|
||||
var totalRequests = entry.Value.TotalRequests + 1;
|
||||
|
||||
// deep copy
|
||||
counter = new RateLimitCounter
|
||||
{
|
||||
Timestamp = entry.Value.Timestamp,
|
||||
TotalRequests = totalRequests
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// stores: id (string) - timestamp (datetime) - total_requests (long)
|
||||
_counterHandler.Set(counterId, counter, rule.PeriodTimespan.Value);
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||
{
|
||||
var rule = option.RateLimitRule;
|
||||
var headers = new RateLimitHeaders();
|
||||
var counterId = ComputeCounterKey(requestIdentity, option);
|
||||
var entry = _counterHandler.Get(counterId);
|
||||
if (entry.HasValue)
|
||||
{
|
||||
headers.Reset = (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
|
||||
headers.Limit = rule.Period;
|
||||
headers.Remaining = (rule.Limit - entry.Value.TotalRequests).ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.Reset = (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
|
||||
headers.Limit = rule.Period;
|
||||
headers.Remaining = rule.Limit.ToString();
|
||||
}
|
||||
|
||||
return headers;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||
{
|
||||
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
|
||||
|
||||
var idBytes = System.Text.Encoding.UTF8.GetBytes(key);
|
||||
|
||||
byte[] hashBytes;
|
||||
|
||||
using (var algorithm = System.Security.Cryptography.SHA1.Create())
|
||||
{
|
||||
hashBytes = algorithm.ComputeHash(idBytes);
|
||||
}
|
||||
|
||||
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
|
||||
}
|
||||
|
||||
public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
|
||||
{
|
||||
var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
|
||||
var retryAfter = Convert.ToInt32(rule.PeriodTimespan.Value.TotalSeconds);
|
||||
retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
|
||||
return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public TimeSpan ConvertToTimeSpan(string timeSpan)
|
||||
{
|
||||
var l = timeSpan.Length - 1;
|
||||
var value = timeSpan.Substring(0, l);
|
||||
var type = timeSpan.Substring(l, 1);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "d": return TimeSpan.FromDays(double.Parse(value));
|
||||
case "h": return TimeSpan.FromHours(double.Parse(value));
|
||||
case "m": return TimeSpan.FromMinutes(double.Parse(value));
|
||||
case "s": return TimeSpan.FromSeconds(double.Parse(value));
|
||||
default: throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
17
src/Ocelot/RateLimit/RateLimitCounter.cs
Normal file
17
src/Ocelot/RateLimit/RateLimitCounter.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the initial access time and the numbers of calls made from that point
|
||||
/// </summary>
|
||||
public struct RateLimitCounter
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
public long TotalRequests { get; set; }
|
||||
}
|
||||
}
|
19
src/Ocelot/RateLimit/RateLimitHeaders.cs
Normal file
19
src/Ocelot/RateLimit/RateLimitHeaders.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
public class RateLimitHeaders
|
||||
{
|
||||
public HttpContext Context { get; set; }
|
||||
|
||||
public string Limit { get; set; }
|
||||
|
||||
public string Remaining { get; set; }
|
||||
|
||||
public string Reset { get; set; }
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ using CacheManager.Core;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Ocelot.Configuration.File;
|
||||
@ -100,7 +101,7 @@ namespace Ocelot.AcceptanceTests
|
||||
})
|
||||
.WithDictionaryHandle();
|
||||
};
|
||||
|
||||
s.AddMemoryCache();
|
||||
s.AddOcelotOutputCaching(settings);
|
||||
s.AddOcelotFileConfiguration(configuration);
|
||||
s.AddOcelot();
|
||||
|
@ -1 +1 @@
|
||||
{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0}}}
|
||||
{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null,"RateLimitOptions":{"RateLimitRule":{"Period":null,"PeriodTimespan":0,"Limit":0},"ClientWhitelist":[],"ClientIdHeader":"ClientId","ClientPolicyPrefix":"crlp","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","EnableRateLimiting":false,"DisableRateLimitHeaders":false}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0}}}
|
@ -33,7 +33,8 @@
|
||||
"Microsoft.NETCore.App": "1.1.0",
|
||||
"Shouldly": "2.8.2",
|
||||
"TestStack.BDDfy": "4.3.2",
|
||||
"Consul": "0.7.2.1"
|
||||
"Consul": "0.7.2.1",
|
||||
"Microsoft.Extensions.Caching.Memory": "1.1.0"
|
||||
},
|
||||
"runtimes": {
|
||||
"win10-x64": {},
|
||||
|
@ -37,7 +37,7 @@ namespace Ocelot.ManualTest
|
||||
})
|
||||
.WithDictionaryHandle();
|
||||
};
|
||||
|
||||
services.AddMemoryCache();
|
||||
services.AddOcelotOutputCaching(settings);
|
||||
services.AddOcelotFileConfiguration(Configuration);
|
||||
services.AddOcelot();
|
||||
|
@ -0,0 +1,146 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
using Ocelot.Infrastructure.RequestData;
|
||||
using Ocelot.RateLimit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Ocelot.Logging;
|
||||
using System.IO;
|
||||
using Ocelot.RateLimit.Middleware;
|
||||
using Ocelot.DownstreamRouteFinder;
|
||||
using Ocelot.Responses;
|
||||
using Xunit;
|
||||
using TestStack.BDDfy;
|
||||
using Ocelot.Configuration.Builder;
|
||||
using Shouldly;
|
||||
|
||||
namespace Ocelot.UnitTests.RateLimit
|
||||
{
|
||||
public class ClientRateLimitMiddlewareTests
|
||||
{
|
||||
private readonly Mock<IRequestScopedDataRepository> _scopedRepository;
|
||||
private readonly Mock<IRateLimitCounterHandler> _counterHanlder;
|
||||
private readonly string _url;
|
||||
private readonly TestServer _server;
|
||||
private readonly HttpClient _client;
|
||||
private HttpResponseMessage _result;
|
||||
private OkResponse<DownstreamRoute> _downstreamRoute;
|
||||
private int responseStatusCode;
|
||||
|
||||
public ClientRateLimitMiddlewareTests()
|
||||
{
|
||||
_url = "http://localhost:51879/api/ClientRateLimit";
|
||||
_scopedRepository = new Mock<IRequestScopedDataRepository>();
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(x =>
|
||||
{
|
||||
x.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
|
||||
x.AddLogging();
|
||||
x.AddMemoryCache();
|
||||
x.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
|
||||
x.AddSingleton(_scopedRepository.Object);
|
||||
})
|
||||
.UseUrls(_url)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseUrls(_url)
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseRateLimiting();
|
||||
app.Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = 200;
|
||||
await context.Response.WriteAsync("This is ratelimit test");
|
||||
});
|
||||
});
|
||||
|
||||
_server = new TestServer(builder);
|
||||
_client = _server.CreateClient();
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void should_call_middleware_and_ratelimiting()
|
||||
{
|
||||
var downstreamRoute = new DownstreamRoute(new List<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
|
||||
new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
|
||||
new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) }))
|
||||
.Build());
|
||||
|
||||
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
|
||||
.When(x => x.WhenICallTheMiddleware())
|
||||
.Then(x => x.ThenresponseStatusCodeIs429())
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void should_call_middleware_withWhitelistClient()
|
||||
{
|
||||
var downstreamRoute = new DownstreamRoute(new List<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
|
||||
new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
|
||||
new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) }))
|
||||
.Build());
|
||||
|
||||
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
|
||||
.When(x => x.WhenICallTheMiddlewareWithWhiteClient())
|
||||
.Then(x => x.ThenresponseStatusCodeIs200())
|
||||
.BDDfy();
|
||||
}
|
||||
|
||||
|
||||
private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)
|
||||
{
|
||||
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
|
||||
_scopedRepository
|
||||
.Setup(x => x.Get<DownstreamRoute>(It.IsAny<string>()))
|
||||
.Returns(_downstreamRoute);
|
||||
}
|
||||
|
||||
private void WhenICallTheMiddleware()
|
||||
{
|
||||
var clientId = "ocelotclient1";
|
||||
// Act
|
||||
for (int i = 0; i <10; i++)
|
||||
{
|
||||
var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
|
||||
request.Headers.Add("ClientId", clientId);
|
||||
|
||||
var response = _client.SendAsync(request);
|
||||
responseStatusCode = (int)response.Result.StatusCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void WhenICallTheMiddlewareWithWhiteClient()
|
||||
{
|
||||
var clientId = "ocelotclient2";
|
||||
// Act
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
|
||||
request.Headers.Add("ClientId", clientId);
|
||||
|
||||
var response = _client.SendAsync(request);
|
||||
responseStatusCode = (int)response.Result.StatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThenresponseStatusCodeIs429()
|
||||
{
|
||||
responseStatusCode.ShouldBe(429);
|
||||
}
|
||||
|
||||
private void ThenresponseStatusCodeIs200()
|
||||
{
|
||||
responseStatusCode.ShouldBe(200);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user