mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 06:22: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" ],
|
"projects": [ "src", "test" ],
|
||||||
"sdk": {
|
"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 int _serviceProviderPort;
|
||||||
private bool _useQos;
|
private bool _useQos;
|
||||||
private QoSOptions _qosOptions;
|
private QoSOptions _qosOptions;
|
||||||
|
public bool _enableRateLimiting;
|
||||||
|
public RateLimitOptions _rateLimitOptions;
|
||||||
|
|
||||||
public ReRouteBuilder()
|
public ReRouteBuilder()
|
||||||
{
|
{
|
||||||
@ -236,6 +237,19 @@ namespace Ocelot.Configuration.Builder
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReRouteBuilder WithEnableRateLimiting(bool input)
|
||||||
|
{
|
||||||
|
_enableRateLimiting = input;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReRouteBuilder WithRateLimitOptions(RateLimitOptions input)
|
||||||
|
{
|
||||||
|
_rateLimitOptions = input;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public ReRoute Build()
|
public ReRoute Build()
|
||||||
{
|
{
|
||||||
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
|
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
|
||||||
@ -244,7 +258,7 @@ namespace Ocelot.Configuration.Builder
|
|||||||
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer,
|
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer,
|
||||||
_downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery,
|
_downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery,
|
||||||
_serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort),
|
_serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort),
|
||||||
_useQos,_qosOptions);
|
_useQos,_qosOptions,_enableRateLimiting,_rateLimitOptions);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,20 @@ namespace Ocelot.Configuration.Creator
|
|||||||
|
|
||||||
ReRoute reRoute;
|
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 serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
|
||||||
|
|
||||||
var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName,
|
var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName,
|
||||||
@ -138,7 +151,8 @@ namespace Ocelot.Configuration.Creator
|
|||||||
, fileReRoute.DownstreamScheme,
|
, fileReRoute.DownstreamScheme,
|
||||||
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
|
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
|
||||||
serviceProviderConfiguration, isQos,
|
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
|
else
|
||||||
{
|
{
|
||||||
@ -151,7 +165,8 @@ namespace Ocelot.Configuration.Creator
|
|||||||
fileReRoute.DownstreamScheme,
|
fileReRoute.DownstreamScheme,
|
||||||
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
|
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
|
||||||
serviceProviderConfiguration, isQos,
|
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);
|
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();
|
AuthenticationOptions = new FileAuthenticationOptions();
|
||||||
FileCacheOptions = new FileCacheOptions();
|
FileCacheOptions = new FileCacheOptions();
|
||||||
QoSOptions = new FileQoSOptions();
|
QoSOptions = new FileQoSOptions();
|
||||||
|
RateLimitOptions = new FileRateLimitOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DownstreamPathTemplate { get; set; }
|
public string DownstreamPathTemplate { get; set; }
|
||||||
@ -32,5 +33,6 @@ namespace Ocelot.Configuration.File
|
|||||||
public int DownstreamPort { get; set; }
|
public int DownstreamPort { get; set; }
|
||||||
public FileQoSOptions QoSOptions { get; set; }
|
public FileQoSOptions QoSOptions { get; set; }
|
||||||
public string LoadBalancer {get;set;}
|
public string LoadBalancer {get;set;}
|
||||||
|
public FileRateLimitOptions RateLimitOptions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,5 @@
|
|||||||
using Polly.Timeout;
|
using Polly.Timeout;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Ocelot.Configuration
|
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 requestIdKey, bool isCached, CacheOptions fileCacheOptions,
|
||||||
string downstreamScheme, string loadBalancer, string downstreamHost,
|
string downstreamScheme, string loadBalancer, string downstreamHost,
|
||||||
int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion,
|
int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion,
|
||||||
bool isQos,QoSOptions qos)
|
bool isQos,QoSOptions qos, bool enableRateLimit, RateLimitOptions ratelimitOptions)
|
||||||
{
|
{
|
||||||
LoadBalancerKey = loadBalancerKey;
|
LoadBalancerKey = loadBalancerKey;
|
||||||
ServiceProviderConfiguraion = serviceProviderConfiguraion;
|
ServiceProviderConfiguraion = serviceProviderConfiguraion;
|
||||||
@ -44,6 +44,8 @@ namespace Ocelot.Configuration
|
|||||||
DownstreamScheme = downstreamScheme;
|
DownstreamScheme = downstreamScheme;
|
||||||
IsQos = isQos;
|
IsQos = isQos;
|
||||||
QosOptions = qos;
|
QosOptions = qos;
|
||||||
|
EnableEndpointRateLimiting = enableRateLimit;
|
||||||
|
RateLimitOptions = ratelimitOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string LoadBalancerKey {get;private set;}
|
public string LoadBalancerKey {get;private set;}
|
||||||
@ -68,5 +70,7 @@ namespace Ocelot.Configuration
|
|||||||
public string DownstreamHost { get; private set; }
|
public string DownstreamHost { get; private set; }
|
||||||
public int DownstreamPort { get; private set; }
|
public int DownstreamPort { get; private set; }
|
||||||
public ServiceProviderConfiguraion ServiceProviderConfiguraion { 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.Requester;
|
||||||
using Ocelot.Responder;
|
using Ocelot.Responder;
|
||||||
using Ocelot.ServiceDiscovery;
|
using Ocelot.ServiceDiscovery;
|
||||||
|
using Ocelot.RateLimit;
|
||||||
|
|
||||||
namespace Ocelot.DependencyInjection
|
namespace Ocelot.DependencyInjection
|
||||||
{
|
{
|
||||||
@ -84,6 +85,7 @@ namespace Ocelot.DependencyInjection
|
|||||||
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
|
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
|
||||||
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
|
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
|
||||||
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();
|
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
|
// 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
|
// could maybe use a scoped data repository
|
||||||
|
@ -11,6 +11,7 @@ using Ocelot.Request.Middleware;
|
|||||||
using Ocelot.Requester.Middleware;
|
using Ocelot.Requester.Middleware;
|
||||||
using Ocelot.RequestId.Middleware;
|
using Ocelot.RequestId.Middleware;
|
||||||
using Ocelot.Responder.Middleware;
|
using Ocelot.Responder.Middleware;
|
||||||
|
using Ocelot.RateLimit.Middleware;
|
||||||
|
|
||||||
namespace Ocelot.Middleware
|
namespace Ocelot.Middleware
|
||||||
{
|
{
|
||||||
@ -57,6 +58,9 @@ namespace Ocelot.Middleware
|
|||||||
// Then we get the downstream route information
|
// Then we get the downstream route information
|
||||||
builder.UseDownstreamRouteFinderMiddleware();
|
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
|
// Now we can look for the requestId
|
||||||
builder.UseRequestIdMiddleware();
|
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.Hosting;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Ocelot.Configuration.File;
|
using Ocelot.Configuration.File;
|
||||||
@ -100,7 +101,7 @@ namespace Ocelot.AcceptanceTests
|
|||||||
})
|
})
|
||||||
.WithDictionaryHandle();
|
.WithDictionaryHandle();
|
||||||
};
|
};
|
||||||
|
s.AddMemoryCache();
|
||||||
s.AddOcelotOutputCaching(settings);
|
s.AddOcelotOutputCaching(settings);
|
||||||
s.AddOcelotFileConfiguration(configuration);
|
s.AddOcelotFileConfiguration(configuration);
|
||||||
s.AddOcelot();
|
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",
|
"Microsoft.NETCore.App": "1.1.0",
|
||||||
"Shouldly": "2.8.2",
|
"Shouldly": "2.8.2",
|
||||||
"TestStack.BDDfy": "4.3.2",
|
"TestStack.BDDfy": "4.3.2",
|
||||||
"Consul": "0.7.2.1"
|
"Consul": "0.7.2.1",
|
||||||
|
"Microsoft.Extensions.Caching.Memory": "1.1.0"
|
||||||
},
|
},
|
||||||
"runtimes": {
|
"runtimes": {
|
||||||
"win10-x64": {},
|
"win10-x64": {},
|
||||||
|
@ -37,7 +37,7 @@ namespace Ocelot.ManualTest
|
|||||||
})
|
})
|
||||||
.WithDictionaryHandle();
|
.WithDictionaryHandle();
|
||||||
};
|
};
|
||||||
|
services.AddMemoryCache();
|
||||||
services.AddOcelotOutputCaching(settings);
|
services.AddOcelotOutputCaching(settings);
|
||||||
services.AddOcelotFileConfiguration(Configuration);
|
services.AddOcelotFileConfiguration(Configuration);
|
||||||
services.AddOcelot();
|
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