mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 06:42:50 +08:00
commit
9203e7f465
@ -29,10 +29,12 @@ namespace Ocelot.Configuration.Builder
|
|||||||
private ServiceProviderConfiguraion _serviceProviderConfiguraion;
|
private ServiceProviderConfiguraion _serviceProviderConfiguraion;
|
||||||
private bool _useQos;
|
private bool _useQos;
|
||||||
private QoSOptions _qosOptions;
|
private QoSOptions _qosOptions;
|
||||||
|
public bool _enableRateLimiting;
|
||||||
|
public RateLimitOptions _rateLimitOptions;
|
||||||
|
|
||||||
public ReRouteBuilder WithLoadBalancer(string loadBalancer)
|
public ReRouteBuilder WithLoadBalancer(string loadBalancer)
|
||||||
{
|
{
|
||||||
_loadBalancer = loadBalancer;
|
_loadBalancer = loadBalancer;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,6 +163,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(
|
return new ReRoute(
|
||||||
@ -185,7 +200,9 @@ namespace Ocelot.Configuration.Builder
|
|||||||
_loadBalancerKey,
|
_loadBalancerKey,
|
||||||
_serviceProviderConfiguraion,
|
_serviceProviderConfiguraion,
|
||||||
_useQos,
|
_useQos,
|
||||||
_qosOptions);
|
_qosOptions,
|
||||||
|
_enableRateLimiting,
|
||||||
|
_rateLimitOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,10 @@ namespace Ocelot.Configuration.Creator
|
|||||||
|
|
||||||
var qosOptions = BuildQoSOptions(fileReRoute);
|
var qosOptions = BuildQoSOptions(fileReRoute);
|
||||||
|
|
||||||
|
var enableRateLimiting = IsEnableRateLimiting(fileReRoute);
|
||||||
|
|
||||||
|
var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting);
|
||||||
|
|
||||||
var reRoute = new ReRouteBuilder()
|
var reRoute = new ReRouteBuilder()
|
||||||
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
|
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
|
||||||
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
|
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
|
||||||
@ -139,13 +143,34 @@ namespace Ocelot.Configuration.Creator
|
|||||||
.WithServiceProviderConfiguraion(serviceProviderConfiguration)
|
.WithServiceProviderConfiguraion(serviceProviderConfiguration)
|
||||||
.WithIsQos(isQos)
|
.WithIsQos(isQos)
|
||||||
.WithQosOptions(qosOptions)
|
.WithQosOptions(qosOptions)
|
||||||
|
.WithEnableRateLimiting(enableRateLimiting)
|
||||||
|
.WithRateLimitOptions(rateLimitOption)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await SetupLoadBalancer(reRoute);
|
await SetupLoadBalancer(reRoute);
|
||||||
SetupQosProvider(reRoute);
|
SetupQosProvider(reRoute);
|
||||||
return reRoute;
|
return reRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RateLimitOptions BuildRateLimitOptions(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting)
|
||||||
|
{
|
||||||
|
RateLimitOptions rateLimitOption = null;
|
||||||
|
if (enableRateLimiting)
|
||||||
|
{
|
||||||
|
rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader,
|
||||||
|
fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders,
|
||||||
|
globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix,
|
||||||
|
new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit)
|
||||||
|
, globalConfiguration.RateLimitOptions.HttpStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rateLimitOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEnableRateLimiting(FileReRoute fileReRoute)
|
||||||
|
{
|
||||||
|
return (fileReRoute.RateLimitOptions != null && fileReRoute.RateLimitOptions.EnableRateLimiting) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
private QoSOptions BuildQoSOptions(FileReRoute fileReRoute)
|
private QoSOptions BuildQoSOptions(FileReRoute fileReRoute)
|
||||||
{
|
{
|
||||||
return new QoSOptionsBuilder()
|
return new QoSOptionsBuilder()
|
||||||
|
@ -5,8 +5,12 @@
|
|||||||
public FileGlobalConfiguration()
|
public FileGlobalConfiguration()
|
||||||
{
|
{
|
||||||
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider();
|
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider();
|
||||||
|
RateLimitOptions = new FileRateLimitOptions();
|
||||||
}
|
}
|
||||||
public string RequestIdKey { get; set; }
|
public string RequestIdKey { get; set; }
|
||||||
|
|
||||||
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
|
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
|
||||||
|
|
||||||
|
public FileRateLimitOptions RateLimitOptions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
src/Ocelot/Configuration/File/FileRateLimitOptions.cs
Normal file
39
src/Ocelot/Configuration/File/FileRateLimitOptions.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ocelot.Configuration.File
|
||||||
|
{
|
||||||
|
public class FileRateLimitOptions
|
||||||
|
{
|
||||||
|
/// <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 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>
|
||||||
|
/// Disables X-Rate-Limit and Rety-After headers
|
||||||
|
/// </summary>
|
||||||
|
public bool DisableRateLimitHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <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; set; } = 429;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
34
src/Ocelot/Configuration/File/FileRateLimitRule.cs
Normal file
34
src/Ocelot/Configuration/File/FileRateLimitRule.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ocelot.Configuration.File
|
||||||
|
{
|
||||||
|
|
||||||
|
public class FileRateLimitRule
|
||||||
|
{
|
||||||
|
public FileRateLimitRule()
|
||||||
|
{
|
||||||
|
ClientWhitelist = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> ClientWhitelist { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables endpoint rate limiting based URL path and HTTP verb
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableRateLimiting { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit period as in 1s, 1m, 1h
|
||||||
|
/// </summary>
|
||||||
|
public string Period { get; set; }
|
||||||
|
|
||||||
|
public double 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 FileRateLimitRule();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 FileRateLimitRule RateLimitOptions { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
83
src/Ocelot/Configuration/RateLimitOptions.cs
Normal file
83
src/Ocelot/Configuration/RateLimitOptions.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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, int httpStatusCode)
|
||||||
|
{
|
||||||
|
EnableRateLimiting = enbleRateLimiting;
|
||||||
|
ClientIdHeader = clientIdHeader;
|
||||||
|
ClientWhitelist = clientWhitelist?? new List<string>();
|
||||||
|
DisableRateLimitHeaders = disableRateLimitHeaders;
|
||||||
|
QuotaExceededMessage = quotaExceededMessage;
|
||||||
|
RateLimitCounterPrefix = rateLimitCounterPrefix;
|
||||||
|
RateLimitRule = rateLimitRule;
|
||||||
|
HttpStatusCode = httpStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
public RateLimitRule(string period, TimeSpan periodTimespan, long limit)
|
||||||
|
{
|
||||||
|
Period = period;
|
||||||
|
PeriodTimespan = periodTimespan;
|
||||||
|
Limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit period as in 1s, 1m, 1h,1d
|
||||||
|
/// </summary>
|
||||||
|
public string Period { get; private set; }
|
||||||
|
|
||||||
|
public TimeSpan PeriodTimespan { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of requests that a client can make in a defined period
|
||||||
|
/// </summary>
|
||||||
|
public long Limit { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,9 @@ namespace Ocelot.Configuration
|
|||||||
string reRouteKey,
|
string reRouteKey,
|
||||||
ServiceProviderConfiguraion serviceProviderConfiguraion,
|
ServiceProviderConfiguraion serviceProviderConfiguraion,
|
||||||
bool isQos,
|
bool isQos,
|
||||||
QoSOptions qos)
|
QoSOptions qos,
|
||||||
|
bool enableRateLimit,
|
||||||
|
RateLimitOptions ratelimitOptions)
|
||||||
{
|
{
|
||||||
ReRouteKey = reRouteKey;
|
ReRouteKey = reRouteKey;
|
||||||
ServiceProviderConfiguraion = serviceProviderConfiguraion;
|
ServiceProviderConfiguraion = serviceProviderConfiguraion;
|
||||||
@ -55,6 +57,8 @@ namespace Ocelot.Configuration
|
|||||||
DownstreamScheme = downstreamScheme;
|
DownstreamScheme = downstreamScheme;
|
||||||
IsQos = isQos;
|
IsQos = isQos;
|
||||||
QosOptions = qos;
|
QosOptions = qos;
|
||||||
|
EnableEndpointRateLimiting = enableRateLimit;
|
||||||
|
RateLimitOptions = ratelimitOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReRouteKey {get;private set;}
|
public string ReRouteKey {get;private set;}
|
||||||
@ -79,5 +83,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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -31,6 +31,7 @@ using Ocelot.Requester;
|
|||||||
using Ocelot.Requester.QoS;
|
using Ocelot.Requester.QoS;
|
||||||
using Ocelot.Responder;
|
using Ocelot.Responder;
|
||||||
using Ocelot.ServiceDiscovery;
|
using Ocelot.ServiceDiscovery;
|
||||||
|
using Ocelot.RateLimit;
|
||||||
|
|
||||||
namespace Ocelot.DependencyInjection
|
namespace Ocelot.DependencyInjection
|
||||||
{
|
{
|
||||||
@ -87,12 +88,13 @@ 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
|
||||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
|
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
|
||||||
|
services.AddMemoryCache();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
37
src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
Normal file
37
src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
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(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
|
{
|
||||||
|
return _core.GetRateLimitHeaders(context, requestIdentity, option);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
18
src/Ocelot/RateLimit/ClientRequestIdentity.cs
Normal file
18
src/Ocelot/RateLimit/ClientRequestIdentity.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace Ocelot.RateLimit
|
||||||
|
{
|
||||||
|
public class ClientRequestIdentity
|
||||||
|
{
|
||||||
|
public ClientRequestIdentity(string clientId, string path, string httpverb)
|
||||||
|
{
|
||||||
|
ClientId = clientId;
|
||||||
|
Path = path;
|
||||||
|
HttpVerb = httpverb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ClientId { get; private set; }
|
||||||
|
|
||||||
|
public string Path { get; private set; }
|
||||||
|
|
||||||
|
public string HttpVerb { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ocelot.RateLimit
|
||||||
|
{
|
||||||
|
public class DistributedCacheRateLimitCounterHanlder : IRateLimitCounterHandler
|
||||||
|
{
|
||||||
|
private readonly IDistributedCache _memoryCache;
|
||||||
|
|
||||||
|
public DistributedCacheRateLimitCounterHanlder(IDistributedCache memoryCache)
|
||||||
|
{
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
|
||||||
|
{
|
||||||
|
_memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(string id)
|
||||||
|
{
|
||||||
|
var stored = _memoryCache.GetString(id);
|
||||||
|
return !string.IsNullOrEmpty(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimitCounter? Get(string id)
|
||||||
|
{
|
||||||
|
var stored = _memoryCache.GetString(id);
|
||||||
|
if (!string.IsNullOrEmpty(stored))
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<RateLimitCounter>(stored);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string id)
|
||||||
|
{
|
||||||
|
_memoryCache.Remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
138
src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
Normal file
138
src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
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( context,identity, options);
|
||||||
|
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(
|
||||||
|
clientId,
|
||||||
|
httpContext.Request.Path.ToString().ToLowerInvariant(),
|
||||||
|
httpContext.Request.Method.ToLowerInvariant()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
|
{
|
||||||
|
if (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.UpstreamPathTemplate }, 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
125
src/Ocelot/RateLimit/RateLimitCore.cs
Normal file
125
src/Ocelot/RateLimit/RateLimitCore.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 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 >= DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
// increment request count
|
||||||
|
var totalRequests = entry.Value.TotalRequests + 1;
|
||||||
|
|
||||||
|
// deep copy
|
||||||
|
counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stores: id (string) - timestamp (datetime) - total_requests (long)
|
||||||
|
_counterHandler.Set(counterId, counter, rule.PeriodTimespan);
|
||||||
|
}
|
||||||
|
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
|
{
|
||||||
|
var rule = option.RateLimitRule;
|
||||||
|
RateLimitHeaders headers = null;
|
||||||
|
var counterId = ComputeCounterKey(requestIdentity, option);
|
||||||
|
var entry = _counterHandler.Get(counterId);
|
||||||
|
if (entry.HasValue)
|
||||||
|
{
|
||||||
|
headers = new RateLimitHeaders(context, rule.Period,
|
||||||
|
(rule.Limit - entry.Value.TotalRequests).ToString(),
|
||||||
|
(entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headers = new RateLimitHeaders(context,
|
||||||
|
rule.Period,
|
||||||
|
rule.Limit.ToString(),
|
||||||
|
(DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
|
||||||
|
{
|
||||||
|
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
|
||||||
|
|
||||||
|
var idBytes = Encoding.UTF8.GetBytes(key);
|
||||||
|
|
||||||
|
byte[] hashBytes;
|
||||||
|
|
||||||
|
using (var algorithm = 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.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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
23
src/Ocelot/RateLimit/RateLimitCounter.cs
Normal file
23
src/Ocelot/RateLimit/RateLimitCounter.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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 RateLimitCounter(DateTime timestamp, long totalRequest)
|
||||||
|
{
|
||||||
|
Timestamp = timestamp;
|
||||||
|
TotalRequests = totalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; private set; }
|
||||||
|
|
||||||
|
public long TotalRequests { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
27
src/Ocelot/RateLimit/RateLimitHeaders.cs
Normal file
27
src/Ocelot/RateLimit/RateLimitHeaders.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ocelot.RateLimit
|
||||||
|
{
|
||||||
|
public class RateLimitHeaders
|
||||||
|
{
|
||||||
|
public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset)
|
||||||
|
{
|
||||||
|
Context = context;
|
||||||
|
Limit = limit;
|
||||||
|
Remaining = remaining;
|
||||||
|
Reset = reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpContext Context { get; private set; }
|
||||||
|
|
||||||
|
public string Limit { get; private set; }
|
||||||
|
|
||||||
|
public string Remaining { get; private set; }
|
||||||
|
|
||||||
|
public string Reset { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
165
test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
Normal file
165
test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Ocelot.Configuration.File;
|
||||||
|
using Shouldly;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TestStack.BDDfy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Ocelot.AcceptanceTests
|
||||||
|
{
|
||||||
|
public class ClientRateLimitTests : IDisposable
|
||||||
|
{
|
||||||
|
private IWebHost _builder;
|
||||||
|
private readonly Steps _steps;
|
||||||
|
private int _counterOne;
|
||||||
|
|
||||||
|
|
||||||
|
public ClientRateLimitTests()
|
||||||
|
{
|
||||||
|
_steps = new Steps();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_builder?.Dispose();
|
||||||
|
_steps.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_call_withratelimiting()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/ClientRateLimit",
|
||||||
|
DownstreamPort = 51879,
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHost = "localhost",
|
||||||
|
UpstreamPathTemplate = "/api/ClientRateLimit",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
RequestIdKey = _steps.RequestIdKey,
|
||||||
|
|
||||||
|
RateLimitOptions = new FileRateLimitRule()
|
||||||
|
{
|
||||||
|
EnableRateLimiting = true,
|
||||||
|
ClientWhitelist = new List<string>(),
|
||||||
|
Limit = 3,
|
||||||
|
Period = "1s",
|
||||||
|
PeriodTimespan = 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
RateLimitOptions = new FileRateLimitOptions()
|
||||||
|
{
|
||||||
|
ClientIdHeader = "ClientId",
|
||||||
|
DisableRateLimitHeaders = false,
|
||||||
|
QuotaExceededMessage = "",
|
||||||
|
RateLimitCounterPrefix = "",
|
||||||
|
HttpStatusCode = 428
|
||||||
|
|
||||||
|
},
|
||||||
|
RequestIdKey ="oceclientrequest"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(428))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_call_middleware_withWhitelistClient()
|
||||||
|
{
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/api/ClientRateLimit",
|
||||||
|
DownstreamPort = 51879,
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
DownstreamHost = "localhost",
|
||||||
|
UpstreamPathTemplate = "/api/ClientRateLimit",
|
||||||
|
UpstreamHttpMethod = "Get",
|
||||||
|
RequestIdKey = _steps.RequestIdKey,
|
||||||
|
|
||||||
|
RateLimitOptions = new FileRateLimitRule()
|
||||||
|
{
|
||||||
|
EnableRateLimiting = true,
|
||||||
|
ClientWhitelist = new List<string>() { "ocelotclient1"},
|
||||||
|
Limit = 3,
|
||||||
|
Period = "1s",
|
||||||
|
PeriodTimespan = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration()
|
||||||
|
{
|
||||||
|
RateLimitOptions = new FileRateLimitOptions()
|
||||||
|
{
|
||||||
|
ClientIdHeader = "ClientId",
|
||||||
|
DisableRateLimitHeaders = false,
|
||||||
|
QuotaExceededMessage = "",
|
||||||
|
RateLimitCounterPrefix = ""
|
||||||
|
},
|
||||||
|
RequestIdKey = "oceclientrequest"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit"))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunning())
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4))
|
||||||
|
.Then(x => _steps.ThenTheStatusCodeShouldBe(200))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void GivenThereIsAServiceRunningOn(string url)
|
||||||
|
{
|
||||||
|
_builder = new WebHostBuilder()
|
||||||
|
.UseUrls(url)
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseUrls(url)
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.Run(context =>
|
||||||
|
{
|
||||||
|
_counterOne++;
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
context.Response.WriteAsync(_counterOne.ToString());
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_builder.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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,6 @@ namespace Ocelot.AcceptanceTests
|
|||||||
})
|
})
|
||||||
.WithDictionaryHandle();
|
.WithDictionaryHandle();
|
||||||
};
|
};
|
||||||
|
|
||||||
s.AddOcelotOutputCaching(settings);
|
s.AddOcelotOutputCaching(settings);
|
||||||
s.AddOcelotFileConfiguration(configuration);
|
s.AddOcelotFileConfiguration(configuration);
|
||||||
s.AddOcelot();
|
s.AddOcelot();
|
||||||
@ -183,6 +183,17 @@ namespace Ocelot.AcceptanceTests
|
|||||||
count.ShouldBeGreaterThan(0);
|
count.ShouldBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < times; i++)
|
||||||
|
{
|
||||||
|
var clientId = "ocelotclient1";
|
||||||
|
var request = new HttpRequestMessage(new HttpMethod("GET"), url);
|
||||||
|
request.Headers.Add("ClientId", clientId);
|
||||||
|
_response = _ocelotClient.SendAsync(request).Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void WhenIGetUrlOnTheApiGateway(string url, string requestId)
|
public void WhenIGetUrlOnTheApiGateway(string url, string requestId)
|
||||||
{
|
{
|
||||||
_ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId);
|
_ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId);
|
||||||
@ -210,6 +221,13 @@ namespace Ocelot.AcceptanceTests
|
|||||||
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode)
|
||||||
|
{
|
||||||
|
var responseStatusCode = (int)_response.StatusCode;
|
||||||
|
responseStatusCode.ShouldBe(expectedHttpStatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_ocelotClient?.Dispose();
|
_ocelotClient?.Dispose();
|
||||||
|
1
test/Ocelot.AcceptanceTests/configuration.json
Executable file
1
test/Ocelot.AcceptanceTests/configuration.json
Executable file
@ -0,0 +1 @@
|
|||||||
|
{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamPathTemplate":"/","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":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0.0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}}
|
@ -23,7 +23,6 @@
|
|||||||
"Microsoft.AspNetCore.Http": "1.1.0",
|
"Microsoft.AspNetCore.Http": "1.1.0",
|
||||||
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
|
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
|
||||||
"Ocelot": "0.0.0-dev",
|
"Ocelot": "0.0.0-dev",
|
||||||
"xunit": "2.2.0-beta2-build3300",
|
|
||||||
"dotnet-test-xunit": "2.2.0-preview2-build1029",
|
"dotnet-test-xunit": "2.2.0-preview2-build1029",
|
||||||
"Ocelot.ManualTest": "0.0.0-dev",
|
"Ocelot.ManualTest": "0.0.0-dev",
|
||||||
"Microsoft.AspNetCore.TestHost": "1.1.0",
|
"Microsoft.AspNetCore.TestHost": "1.1.0",
|
||||||
@ -33,7 +32,9 @@
|
|||||||
"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",
|
||||||
|
"xunit": "2.2.0-rc1-build3507"
|
||||||
},
|
},
|
||||||
"runtimes": {
|
"runtimes": {
|
||||||
"win10-x64": {},
|
"win10-x64": {},
|
||||||
|
@ -37,7 +37,6 @@ namespace Ocelot.ManualTest
|
|||||||
})
|
})
|
||||||
.WithDictionaryHandle();
|
.WithDictionaryHandle();
|
||||||
};
|
};
|
||||||
|
|
||||||
services.AddOcelotOutputCaching(settings);
|
services.AddOcelotOutputCaching(settings);
|
||||||
services.AddOcelotFileConfiguration(Configuration);
|
services.AddOcelotFileConfiguration(Configuration);
|
||||||
services.AddOcelot();
|
services.AddOcelot();
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
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;
|
||||||
|
using Ocelot.Configuration;
|
||||||
|
|
||||||
|
namespace Ocelot.UnitTests.RateLimit
|
||||||
|
{
|
||||||
|
public class ClientRateLimitMiddlewareTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IRequestScopedDataRepository> _scopedRepository;
|
||||||
|
private readonly string _url;
|
||||||
|
private readonly TestServer _server;
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
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("1s", TimeSpan.FromSeconds(100), 3), 429))
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
|
||||||
|
.When(x => x.WhenICallTheMiddlewareMultipleTime(2))
|
||||||
|
.Then(x => x.ThenresponseStatusCodeIs200())
|
||||||
|
.When(x => x.WhenICallTheMiddlewareMultipleTime(2))
|
||||||
|
.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 RateLimitRule( "1s", TimeSpan.FromSeconds(100),3),429))
|
||||||
|
.WithUpstreamHttpMethod("Get")
|
||||||
|
.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 WhenICallTheMiddlewareMultipleTime(int times)
|
||||||
|
{
|
||||||
|
var clientId = "ocelotclient1";
|
||||||
|
// Act
|
||||||
|
for (int i = 0; i < times; 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,6 @@
|
|||||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
|
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
|
||||||
"Microsoft.AspNetCore.Http": "1.1.0",
|
"Microsoft.AspNetCore.Http": "1.1.0",
|
||||||
"Ocelot": "0.0.0-dev",
|
"Ocelot": "0.0.0-dev",
|
||||||
"xunit": "2.2.0-beta2-build3300",
|
|
||||||
"dotnet-test-xunit": "2.2.0-preview2-build1029",
|
"dotnet-test-xunit": "2.2.0-preview2-build1029",
|
||||||
"Moq": "4.6.38-alpha",
|
"Moq": "4.6.38-alpha",
|
||||||
"Microsoft.AspNetCore.TestHost": "1.1.0",
|
"Microsoft.AspNetCore.TestHost": "1.1.0",
|
||||||
@ -24,7 +23,8 @@
|
|||||||
"Shouldly": "2.8.2",
|
"Shouldly": "2.8.2",
|
||||||
"TestStack.BDDfy": "4.3.2",
|
"TestStack.BDDfy": "4.3.2",
|
||||||
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
|
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
|
||||||
"Microsoft.DotNet.InternalAbstractions": "1.0.0"
|
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
|
||||||
|
"xunit": "2.2.0-rc1-build3507"
|
||||||
},
|
},
|
||||||
"runtimes": {
|
"runtimes": {
|
||||||
"win10-x64": {},
|
"win10-x64": {},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user