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" ],
"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 bool _useQos;
private QoSOptions _qosOptions;
public bool _enableRateLimiting;
public RateLimitOptions _rateLimitOptions;
public ReRouteBuilder()
{
@ -236,6 +237,19 @@ namespace Ocelot.Configuration.Builder
return this;
}
public ReRouteBuilder WithEnableRateLimiting(bool input)
{
_enableRateLimiting = input;
return this;
}
public ReRouteBuilder WithRateLimitOptions(RateLimitOptions input)
{
_rateLimitOptions = input;
return this;
}
public ReRoute Build()
{
return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
@ -244,7 +258,7 @@ namespace Ocelot.Configuration.Builder
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer,
_downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery,
_serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort),
_useQos,_qosOptions);
_useQos,_qosOptions,_enableRateLimiting,_rateLimitOptions);
}
}

View File

@ -110,7 +110,20 @@ namespace Ocelot.Configuration.Creator
ReRoute reRoute;
var enableRateLimiting = (fileReRoute.RateLimitOptions!= null && fileReRoute.RateLimitOptions.EnableRateLimiting)? true: false;
RateLimitOptions rateLimitOption = null;
if (enableRateLimiting)
{
rateLimitOption = new RateLimitOptions(enableRateLimiting, fileReRoute.RateLimitOptions.ClientIdHeader,
fileReRoute.RateLimitOptions.ClientWhitelist,fileReRoute.RateLimitOptions.DisableRateLimitHeaders,
fileReRoute.RateLimitOptions.QuotaExceededMessage,fileReRoute.RateLimitOptions.RateLimitCounterPrefix,
new RateLimitRule()
{
Limit = fileReRoute.RateLimitOptions.RateLimitRule.Limit,
Period = fileReRoute.RateLimitOptions.RateLimitRule.Period,
PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.RateLimitRule.PeriodTimespan)
});
}
var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName,
@ -138,7 +151,8 @@ namespace Ocelot.Configuration.Creator
, fileReRoute.DownstreamScheme,
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
serviceProviderConfiguration, isQos,
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue));
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue),
enableRateLimiting, rateLimitOption);
}
else
{
@ -151,7 +165,8 @@ namespace Ocelot.Configuration.Creator
fileReRoute.DownstreamScheme,
fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey,
serviceProviderConfiguration, isQos,
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue));
new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue),
enableRateLimiting, rateLimitOption);
}
var loadBalancer = await _loadBalanceFactory.Get(reRoute);

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();
FileCacheOptions = new FileCacheOptions();
QoSOptions = new FileQoSOptions();
RateLimitOptions = new FileRateLimitOptions();
}
public string DownstreamPathTemplate { get; set; }
@ -32,5 +33,6 @@ namespace Ocelot.Configuration.File
public int DownstreamPort { get; set; }
public FileQoSOptions QoSOptions { get; set; }
public string LoadBalancer {get;set;}
public FileRateLimitOptions RateLimitOptions { get; set; }
}
}

View File

@ -1,8 +1,5 @@
using Polly.Timeout;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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 downstreamScheme, string loadBalancer, string downstreamHost,
int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion,
bool isQos,QoSOptions qos)
bool isQos,QoSOptions qos, bool enableRateLimit, RateLimitOptions ratelimitOptions)
{
LoadBalancerKey = loadBalancerKey;
ServiceProviderConfiguraion = serviceProviderConfiguraion;
@ -44,6 +44,8 @@ namespace Ocelot.Configuration
DownstreamScheme = downstreamScheme;
IsQos = isQos;
QosOptions = qos;
EnableEndpointRateLimiting = enableRateLimit;
RateLimitOptions = ratelimitOptions;
}
public string LoadBalancerKey {get;private set;}
@ -68,5 +70,7 @@ namespace Ocelot.Configuration
public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; }
public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; }
public bool EnableEndpointRateLimiting { get; private set; }
public RateLimitOptions RateLimitOptions { get; private set; }
}
}

View File

@ -30,6 +30,7 @@ using Ocelot.Request.Builder;
using Ocelot.Requester;
using Ocelot.Responder;
using Ocelot.ServiceDiscovery;
using Ocelot.RateLimit;
namespace Ocelot.DependencyInjection
{
@ -84,6 +85,7 @@ namespace Ocelot.DependencyInjection
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();
services.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
// could maybe use a scoped data repository

View File

@ -11,6 +11,7 @@ using Ocelot.Request.Middleware;
using Ocelot.Requester.Middleware;
using Ocelot.RequestId.Middleware;
using Ocelot.Responder.Middleware;
using Ocelot.RateLimit.Middleware;
namespace Ocelot.Middleware
{
@ -57,6 +58,9 @@ namespace Ocelot.Middleware
// Then we get the downstream route information
builder.UseDownstreamRouteFinderMiddleware();
// We check whether the request is ratelimit, and if there is no continue processing
builder.UseRateLimiting();
// Now we can look for the requestId
builder.UseRequestIdMiddleware();

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.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
@ -100,7 +101,7 @@ namespace Ocelot.AcceptanceTests
})
.WithDictionaryHandle();
};
s.AddMemoryCache();
s.AddOcelotOutputCaching(settings);
s.AddOcelotFileConfiguration(configuration);
s.AddOcelot();

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",
"Shouldly": "2.8.2",
"TestStack.BDDfy": "4.3.2",
"Consul": "0.7.2.1"
"Consul": "0.7.2.1",
"Microsoft.Extensions.Caching.Memory": "1.1.0"
},
"runtimes": {
"win10-x64": {},

View File

@ -37,7 +37,7 @@ namespace Ocelot.ManualTest
})
.WithDictionaryHandle();
};
services.AddMemoryCache();
services.AddOcelotOutputCaching(settings);
services.AddOcelotFileConfiguration(Configuration);
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);
}
}
}