implement Request Rate limit, this feature is options

This commit is contained in:
geffzhang 2017-02-11 16:32:30 +08:00
parent 08c9700a4a
commit e1d5ef3aae
24 changed files with 766 additions and 14 deletions

View File

@ -1,6 +1,6 @@
 {  {
"projects": [ "src", "test" ], "projects": [ "src", "test" ],
"sdk": { "sdk": {
"version": "1.0.0-preview2-003133" "version": "1.0.0-preview2-003131"
} }
} }

View File

@ -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);
} }
} }

View File

@ -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);

View 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; }
}
}

View File

@ -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; }
} }
} }

View File

@ -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
{ {

View 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; }
}
}

View File

@ -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; }
} }
} }

View File

@ -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

View File

@ -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();

View 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);
}
}
}

View 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; }
}
}

View 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);
}
}

View 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);
}
}
}

View 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;
}
}
}

View File

@ -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>();
}
}
}

View 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}");
}
}
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -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();

View File

@ -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}}}

View File

@ -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": {},

View File

@ -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();

View File

@ -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);
}
}
}