Merge pull request #10 from TomPallister/develop

merge rate limit
This commit is contained in:
geffzhang 2017-02-15 18:28:04 +08:00 committed by GitHub
commit 2fcf944d55
27 changed files with 1051 additions and 13 deletions

View File

@ -29,6 +29,8 @@ 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)
{ {
@ -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);
} }
} }
} }

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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