From e1d5ef3aaefe567af4100de35afacbdde32936bc Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 11 Feb 2017 16:32:30 +0800 Subject: [PATCH 1/7] implement Request Rate limit, this feature is options --- global.json | 2 +- .../Configuration/Builder/ReRouteBuilder.cs | 18 ++- .../Creator/FileOcelotConfigurationCreator.cs | 21 ++- .../File/FileRateLimitOptions.cs | 66 ++++++++ src/Ocelot/Configuration/File/FileReRoute.cs | 2 + src/Ocelot/Configuration/QoSOptions.cs | 3 - src/Ocelot/Configuration/RateLimitOptions.cs | 76 +++++++++ src/Ocelot/Configuration/ReRoute.cs | 6 +- .../ServiceCollectionExtensions.cs | 2 + .../Middleware/OcelotMiddlewareExtensions.cs | 4 + .../RateLimit/ClientRateLimitProcessor.cs | 36 +++++ src/Ocelot/RateLimit/ClientRequestIdentity.cs | 11 ++ .../RateLimit/IRateLimitCounterHandler.cs | 15 ++ .../MemoryCacheRateLimitCounterHandler.cs | 45 ++++++ .../Middleware/ClientRateLimitMiddleware.cs | 142 +++++++++++++++++ .../RateLimitMiddlewareExtensions.cs | 16 ++ src/Ocelot/RateLimit/RateLimitCore.cs | 123 +++++++++++++++ src/Ocelot/RateLimit/RateLimitCounter.cs | 17 ++ src/Ocelot/RateLimit/RateLimitHeaders.cs | 19 +++ test/Ocelot.AcceptanceTests/Steps.cs | 3 +- .../Ocelot.AcceptanceTests/configuration.json | 2 +- test/Ocelot.AcceptanceTests/project.json | 3 +- test/Ocelot.ManualTest/Startup.cs | 2 +- .../ClientRateLimitMiddlewareTests.cs | 146 ++++++++++++++++++ 24 files changed, 766 insertions(+), 14 deletions(-) create mode 100644 src/Ocelot/Configuration/File/FileRateLimitOptions.cs create mode 100644 src/Ocelot/Configuration/RateLimitOptions.cs create mode 100644 src/Ocelot/RateLimit/ClientRateLimitProcessor.cs create mode 100644 src/Ocelot/RateLimit/ClientRequestIdentity.cs create mode 100644 src/Ocelot/RateLimit/IRateLimitCounterHandler.cs create mode 100644 src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs create mode 100644 src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs create mode 100644 src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs create mode 100644 src/Ocelot/RateLimit/RateLimitCore.cs create mode 100644 src/Ocelot/RateLimit/RateLimitCounter.cs create mode 100644 src/Ocelot/RateLimit/RateLimitHeaders.cs create mode 100644 test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs diff --git a/global.json b/global.json index ff8d898e..616b2c4e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@  { "projects": [ "src", "test" ], "sdk": { - "version": "1.0.0-preview2-003133" + "version": "1.0.0-preview2-003131" } } diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 568e7c68..028b89e3 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -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); } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index f550edac..a7244bb1 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -109,8 +109,21 @@ namespace Ocelot.Configuration.Creator var loadBalancerKey = $"{fileReRoute.UpstreamTemplate}{fileReRoute.UpstreamHttpMethod}"; 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); diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs new file mode 100644 index 00000000..bc555f33 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs @@ -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(); + } + + public FileRateLimitRule RateLimitRule { get; set; } + + public List ClientWhitelist { get; set; } + + /// + /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId + /// + public string ClientIdHeader { get; set; } = "ClientId"; + + /// + /// Gets or sets the policy prefix, used to compose the client policy cache key + /// + public string ClientPolicyPrefix { get; set; } = "crlp"; + + /// + /// 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} + /// + public string QuotaExceededMessage { get; set; } + + /// + /// Gets or sets the counter prefix, used to compose the rate limit counter cache key + /// + public string RateLimitCounterPrefix { get; set; } = "ocelot"; + + /// + /// Enables endpoint rate limiting based URL path and HTTP verb + /// + public bool EnableRateLimiting { get; set; } + + /// + /// Disables X-Rate-Limit and Rety-After headers + /// + public bool DisableRateLimitHeaders { get; set; } + } + + public class FileRateLimitRule + { + /// + /// Rate limit period as in 1s, 1m, 1h + /// + public string Period { get; set; } + + public int PeriodTimespan { get; set; } + /// + /// Maximum number of requests that a client can make in a defined period + /// + public long Limit { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index 9ab898ea..69b13e6a 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -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; } } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 8584c57e..27b862af 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -1,8 +1,5 @@ using Polly.Timeout; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Ocelot.Configuration { diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs new file mode 100644 index 00000000..51765874 --- /dev/null +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Configuration +{ + /// + /// RateLimit Options + /// + public class RateLimitOptions + { + public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List 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 ClientWhitelist { get; private set; } + + /// + /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId + /// + public string ClientIdHeader { get; private set; } = "ClientId"; + + /// + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) + /// + public int HttpStatusCode { get; private set; } = 429; + + /// + /// 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} + /// + public string QuotaExceededMessage { get; private set; } + + /// + /// Gets or sets the counter prefix, used to compose the rate limit counter cache key + /// + public string RateLimitCounterPrefix { get; private set; } = "ocelot"; + + /// + /// Enables endpoint rate limiting based URL path and HTTP verb + /// + public bool EnableRateLimiting { get; private set; } + + /// + /// Disables X-Rate-Limit and Rety-After headers + /// + public bool DisableRateLimitHeaders { get; private set; } + } + + public class RateLimitRule + { + /// + /// Rate limit period as in 1s, 1m, 1h + /// + public string Period { get; set; } + + public TimeSpan? PeriodTimespan { get; set; } + /// + /// Maximum number of requests that a client can make in a defined period + /// + public long Limit { get; set; } + } +} diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index c38d55d5..38e28af9 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -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; } } } \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 0a6bd42c..e83ad30c 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -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(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // 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 diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 352aa501..4d9f643c 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -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(); diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs new file mode 100644 index 00000000..38141b49 --- /dev/null +++ b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs @@ -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); + } + + } +} diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs new file mode 100644 index 00000000..9cceeb9e --- /dev/null +++ b/src/Ocelot/RateLimit/ClientRequestIdentity.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs new file mode 100644 index 00000000..cb745a44 --- /dev/null +++ b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs @@ -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); + } +} diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs new file mode 100644 index 00000000..9756f2ae --- /dev/null +++ b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs @@ -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); + } + } +} diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs new file mode 100644 index 00000000..64ac40c0 --- /dev/null +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -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(); + _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; + } + + } +} + + diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs new file mode 100644 index 00000000..b27d6d9d --- /dev/null +++ b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs @@ -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(); + } + } +} diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs new file mode 100644 index 00000000..39904b06 --- /dev/null +++ b/src/Ocelot/RateLimit/RateLimitCore.cs @@ -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}"); + } + } + + } +} diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs new file mode 100644 index 00000000..f0133b25 --- /dev/null +++ b/src/Ocelot/RateLimit/RateLimitCounter.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + /// + /// Stores the initial access time and the numbers of calls made from that point + /// + public struct RateLimitCounter + { + public DateTime Timestamp { get; set; } + + public long TotalRequests { get; set; } + } +} diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs new file mode 100644 index 00000000..a7bd4ae6 --- /dev/null +++ b/src/Ocelot/RateLimit/RateLimitHeaders.cs @@ -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; } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 9b5faa04..92ab6daf 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -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(); diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index b155bee6..a7a48f2a 100755 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -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}}} \ No newline at end of file +{"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}}} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json index f1aa378b..5457280f 100644 --- a/test/Ocelot.AcceptanceTests/project.json +++ b/test/Ocelot.AcceptanceTests/project.json @@ -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": {}, diff --git a/test/Ocelot.ManualTest/Startup.cs b/test/Ocelot.ManualTest/Startup.cs index 70448fcf..2d8b7653 100644 --- a/test/Ocelot.ManualTest/Startup.cs +++ b/test/Ocelot.ManualTest/Startup.cs @@ -37,7 +37,7 @@ namespace Ocelot.ManualTest }) .WithDictionaryHandle(); }; - + services.AddMemoryCache(); services.AddOcelotOutputCaching(settings); services.AddOcelotFileConfiguration(Configuration); services.AddOcelot(); diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs new file mode 100644 index 00000000..9d826305 --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -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 _scopedRepository; + private readonly Mock _counterHanlder; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _downstreamRoute; + private int responseStatusCode; + + public ClientRateLimitMiddlewareTests() + { + _url = "http://localhost:51879/api/ClientRateLimit"; + _scopedRepository = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddMemoryCache(); + x.AddSingleton(); + 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(), + new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), 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(), + new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "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); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .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); + } + } +} From 9b06afc781bc023ea0eda3ee124255f9b0b78cbd Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sat, 11 Feb 2017 20:52:20 +0800 Subject: [PATCH 2/7] refactor ratelimit config --- global.json | 2 +- .../Creator/FileOcelotConfigurationCreator.cs | 12 +++---- .../File/FileGlobalConfiguration.cs | 4 +++ .../File/FileRateLimitOptions.cs | 33 +----------------- .../Configuration/File/FileRateLimitRule.cs | 34 +++++++++++++++++++ src/Ocelot/Configuration/File/FileReRoute.cs | 4 +-- src/Ocelot/Configuration/RateLimitOptions.cs | 1 - .../Ocelot.AcceptanceTests/configuration.json | 2 +- .../ClientRateLimitMiddlewareTests.cs | 1 - 9 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 src/Ocelot/Configuration/File/FileRateLimitRule.cs diff --git a/global.json b/global.json index 616b2c4e..ff8d898e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@  { "projects": [ "src", "test" ], "sdk": { - "version": "1.0.0-preview2-003131" + "version": "1.0.0-preview2-003133" } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index a7244bb1..ae8cc047 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -114,14 +114,14 @@ namespace Ocelot.Configuration.Creator RateLimitOptions rateLimitOption = null; if (enableRateLimiting) { - rateLimitOption = new RateLimitOptions(enableRateLimiting, fileReRoute.RateLimitOptions.ClientIdHeader, - fileReRoute.RateLimitOptions.ClientWhitelist,fileReRoute.RateLimitOptions.DisableRateLimitHeaders, - fileReRoute.RateLimitOptions.QuotaExceededMessage,fileReRoute.RateLimitOptions.RateLimitCounterPrefix, + rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader, + fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders, + globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix, new RateLimitRule() { - Limit = fileReRoute.RateLimitOptions.RateLimitRule.Limit, - Period = fileReRoute.RateLimitOptions.RateLimitRule.Period, - PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.RateLimitRule.PeriodTimespan) + Limit = fileReRoute.RateLimitOptions.Limit, + Period = fileReRoute.RateLimitOptions.Period, + PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan) }); } var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index f414bc83..2264bee9 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -5,8 +5,12 @@ public FileGlobalConfiguration() { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider(); + RateLimitOptions = new FileRateLimitOptions(); } public string RequestIdKey { get; set; } + public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;} + + public FileRateLimitOptions RateLimitOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs index bc555f33..e685ee56 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs @@ -7,26 +7,12 @@ namespace Ocelot.Configuration.File { public class FileRateLimitOptions { - public FileRateLimitOptions() - { - RateLimitRule = new FileRateLimitRule(); - ClientWhitelist = new List(); - } - - public FileRateLimitRule RateLimitRule { get; set; } - - public List ClientWhitelist { get; set; } /// /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId /// public string ClientIdHeader { get; set; } = "ClientId"; - /// - /// Gets or sets the policy prefix, used to compose the client policy cache key - /// - public string ClientPolicyPrefix { get; set; } = "crlp"; - /// /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. /// If none specified the default will be: @@ -39,28 +25,11 @@ namespace Ocelot.Configuration.File /// public string RateLimitCounterPrefix { get; set; } = "ocelot"; - /// - /// Enables endpoint rate limiting based URL path and HTTP verb - /// - public bool EnableRateLimiting { get; set; } - /// /// Disables X-Rate-Limit and Rety-After headers /// public bool DisableRateLimitHeaders { get; set; } } - public class FileRateLimitRule - { - /// - /// Rate limit period as in 1s, 1m, 1h - /// - public string Period { get; set; } - - public int PeriodTimespan { get; set; } - /// - /// Maximum number of requests that a client can make in a defined period - /// - public long Limit { get; set; } - } + } diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs new file mode 100644 index 00000000..58d171de --- /dev/null +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -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(); + } + + public List ClientWhitelist { get; set; } + + /// + /// Enables endpoint rate limiting based URL path and HTTP verb + /// + public bool EnableRateLimiting { get; set; } + + /// + /// Rate limit period as in 1s, 1m, 1h + /// + public string Period { get; set; } + + public int PeriodTimespan { get; set; } + /// + /// Maximum number of requests that a client can make in a defined period + /// + public long Limit { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index 69b13e6a..f3658cbb 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -13,7 +13,7 @@ namespace Ocelot.Configuration.File AuthenticationOptions = new FileAuthenticationOptions(); FileCacheOptions = new FileCacheOptions(); QoSOptions = new FileQoSOptions(); - RateLimitOptions = new FileRateLimitOptions(); + RateLimitOptions = new FileRateLimitRule(); } public string DownstreamPathTemplate { get; set; } @@ -33,6 +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; } + public FileRateLimitRule RateLimitOptions { get; set; } } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs index 51765874..7d4d0f4a 100644 --- a/src/Ocelot/Configuration/RateLimitOptions.cs +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -22,7 +22,6 @@ namespace Ocelot.Configuration RateLimitRule = rateLimitRule; } - public RateLimitRule RateLimitRule { get; private set; } public List ClientWhitelist { get; private set; } diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index a7a48f2a..8626f7c1 100755 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -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,"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}}} \ No newline at end of file +{"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":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false}}} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index 9d826305..dd1dc0e0 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -26,7 +26,6 @@ namespace Ocelot.UnitTests.RateLimit public class ClientRateLimitMiddlewareTests { private readonly Mock _scopedRepository; - private readonly Mock _counterHanlder; private readonly string _url; private readonly TestServer _server; private readonly HttpClient _client; From e1f16c2be10550adf63c9047fd94fb5e368d6361 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sun, 12 Feb 2017 15:49:21 +0800 Subject: [PATCH 3/7] add ratelimit acceptance test --- global.json | 2 +- .../Creator/FileOcelotConfigurationCreator.cs | 2 +- .../File/FileRateLimitOptions.cs | 6 +- src/Ocelot/Configuration/RateLimitOptions.cs | 9 +- ...DistributedCacheRateLimitCounterHanlder.cs | 45 ++++ .../ClientRateLimitTests.cs | 208 ++++++++++++++++++ test/Ocelot.AcceptanceTests/Steps.cs | 18 ++ .../Ocelot.AcceptanceTests/configuration.json | 2 +- .../ClientRateLimitMiddlewareTests.cs | 5 +- 9 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs create mode 100644 test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs diff --git a/global.json b/global.json index ff8d898e..616b2c4e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@  { "projects": [ "src", "test" ], "sdk": { - "version": "1.0.0-preview2-003133" + "version": "1.0.0-preview2-003131" } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index ae8cc047..c5eababe 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -122,7 +122,7 @@ namespace Ocelot.Configuration.Creator Limit = fileReRoute.RateLimitOptions.Limit, Period = fileReRoute.RateLimitOptions.Period, PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan) - }); + }, globalConfiguration.RateLimitOptions.HttpStatusCode); } var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs index e685ee56..655b2442 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs @@ -7,7 +7,6 @@ namespace Ocelot.Configuration.File { public class FileRateLimitOptions { - /// /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId /// @@ -29,6 +28,11 @@ namespace Ocelot.Configuration.File /// Disables X-Rate-Limit and Rety-After headers /// public bool DisableRateLimitHeaders { get; set; } + + /// + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) + /// + public int HttpStatusCode { get; private set; } = 429; } diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs index 7d4d0f4a..d5c68d23 100644 --- a/src/Ocelot/Configuration/RateLimitOptions.cs +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -11,7 +11,7 @@ namespace Ocelot.Configuration public class RateLimitOptions { public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List clientWhitelist,bool disableRateLimitHeaders, - string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule) + string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode) { EnableRateLimiting = enbleRateLimiting; ClientIdHeader = clientIdHeader; @@ -20,6 +20,7 @@ namespace Ocelot.Configuration QuotaExceededMessage = quotaExceededMessage; RateLimitCounterPrefix = rateLimitCounterPrefix; RateLimitRule = rateLimitRule; + HttpStatusCode = httpStatusCode; } public RateLimitRule RateLimitRule { get; private set; } @@ -29,12 +30,12 @@ namespace Ocelot.Configuration /// /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId /// - public string ClientIdHeader { get; private set; } = "ClientId"; + public string ClientIdHeader { get; private set; } /// /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) /// - public int HttpStatusCode { get; private set; } = 429; + public int HttpStatusCode { get; private set; } /// /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. @@ -46,7 +47,7 @@ namespace Ocelot.Configuration /// /// Gets or sets the counter prefix, used to compose the rate limit counter cache key /// - public string RateLimitCounterPrefix { get; private set; } = "ocelot"; + public string RateLimitCounterPrefix { get; private set; } /// /// Enables endpoint rate limiting based URL path and HTTP verb diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs new file mode 100644 index 00000000..1db8f334 --- /dev/null +++ b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs @@ -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(stored); + } + return null; + } + + public void Remove(string id) + { + _memoryCache.Remove(id); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs new file mode 100644 index 00000000..2e8c3c90 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -0,0 +1,208 @@ +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 + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = "Get", + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + 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", 5)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(429)) + .BDDfy(); + } + + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = "Get", + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List() { "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(); + } + + //private void GetApiRateLimait(string url) + //{ + // var clientId = "ocelotclient1"; + // var request = new HttpRequestMessage(new HttpMethod("GET"), url); + // request.Headers.Add("ClientId", clientId); + + // var response = _client.SendAsync(request); + // responseStatusCode = (int)response.Result.StatusCode; + // } + + //} + + //public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + //{ + // var clientId = "ocelotclient1"; + // var tasks = new Task[times]; + + // for (int i = 0; i < times; i++) + // { + // var urlCopy = url; + // tasks[i] = GetForServiceDiscoveryTest(urlCopy); + // Thread.Sleep(_random.Next(40, 60)); + // } + + // Task.WaitAll(tasks); + //} + + //private void WhenICallTheMiddlewareWithWhiteClient() + //{ + // var clientId = "ocelotclient2"; + // // Act + // for (int i = 0; i < 2; i++) + // { + // var request = new HttpRequestMessage(new HttpMethod("GET"), apiRateLimitPath); + // 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); + //} + } +} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 92ab6daf..5981c343 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -184,6 +184,17 @@ namespace Ocelot.AcceptanceTests 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) { _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); @@ -211,6 +222,13 @@ namespace Ocelot.AcceptanceTests _response.StatusCode.ShouldBe(expectedHttpStatusCode); } + + public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) + { + var responseStatusCode = (int)_response.StatusCode; + responseStatusCode.ShouldBe(expectedHttpStatusCode); + } + public void Dispose() { _ocelotClient?.Dispose(); diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index 8626f7c1..af289aab 100755 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -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,"RateLimitOptions":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false}}} \ No newline at end of file +{"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":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index dd1dc0e0..11bca113 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -29,7 +29,6 @@ namespace Ocelot.UnitTests.RateLimit private readonly string _url; private readonly TestServer _server; private readonly HttpClient _client; - private HttpResponseMessage _result; private OkResponse _downstreamRoute; private int responseStatusCode; @@ -71,7 +70,7 @@ namespace Ocelot.UnitTests.RateLimit { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( - new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) })) + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429)) .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) @@ -85,7 +84,7 @@ namespace Ocelot.UnitTests.RateLimit { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( - new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) })) + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429)) .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) From 659d12478031b8469c5342c1a580621555aba547 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Sun, 12 Feb 2017 15:50:21 +0800 Subject: [PATCH 4/7] reset sdk version --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 616b2c4e..ff8d898e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@  { "projects": [ "src", "test" ], "sdk": { - "version": "1.0.0-preview2-003131" + "version": "1.0.0-preview2-003133" } } From 2fa6e66dd46422232323944df27d5227ff535044 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Mon, 13 Feb 2017 08:29:29 +0800 Subject: [PATCH 5/7] refactor code --- .../Creator/FileOcelotConfigurationCreator.cs | 8 +-- .../File/FileRateLimitOptions.cs | 2 +- .../Configuration/File/FileRateLimitRule.cs | 2 +- src/Ocelot/Configuration/RateLimitOptions.cs | 21 ++++-- .../ServiceCollectionExtensions.cs | 2 +- .../RateLimit/ClientRateLimitProcessor.cs | 7 +- src/Ocelot/RateLimit/ClientRequestIdentity.cs | 13 +++- .../Middleware/ClientRateLimitMiddleware.cs | 20 +++--- src/Ocelot/RateLimit/RateLimitCore.cs | 68 ++++++++++--------- src/Ocelot/RateLimit/RateLimitCounter.cs | 10 ++- src/Ocelot/RateLimit/RateLimitHeaders.cs | 16 +++-- .../ClientRateLimitTests.cs | 63 +++-------------- test/Ocelot.AcceptanceTests/Steps.cs | 1 - .../Ocelot.AcceptanceTests/configuration.json | 2 +- test/Ocelot.AcceptanceTests/project.json | 4 +- test/Ocelot.ManualTest/Startup.cs | 1 - .../ClientRateLimitMiddlewareTests.cs | 13 ++-- test/Ocelot.UnitTests/project.json | 4 +- 18 files changed, 119 insertions(+), 138 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index c5eababe..646af96c 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -117,12 +117,8 @@ namespace Ocelot.Configuration.Creator rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader, fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders, globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix, - new RateLimitRule() - { - Limit = fileReRoute.RateLimitOptions.Limit, - Period = fileReRoute.RateLimitOptions.Period, - PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan) - }, globalConfiguration.RateLimitOptions.HttpStatusCode); + new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit) + , globalConfiguration.RateLimitOptions.HttpStatusCode); } var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs index 655b2442..afb3fab5 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs @@ -32,7 +32,7 @@ namespace Ocelot.Configuration.File /// /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) /// - public int HttpStatusCode { get; private set; } = 429; + public int HttpStatusCode { get; set; } = 429; } diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs index 58d171de..727a9e82 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -25,7 +25,7 @@ namespace Ocelot.Configuration.File /// public string Period { get; set; } - public int PeriodTimespan { get; set; } + public double PeriodTimespan { get; set; } /// /// Maximum number of requests that a client can make in a defined period /// diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs index d5c68d23..85a3bf78 100644 --- a/src/Ocelot/Configuration/RateLimitOptions.cs +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -15,7 +15,7 @@ namespace Ocelot.Configuration { EnableRateLimiting = enbleRateLimiting; ClientIdHeader = clientIdHeader; - ClientWhitelist = clientWhitelist; + ClientWhitelist = clientWhitelist?? new List(); DisableRateLimitHeaders = disableRateLimitHeaders; QuotaExceededMessage = quotaExceededMessage; RateLimitCounterPrefix = rateLimitCounterPrefix; @@ -62,15 +62,22 @@ namespace Ocelot.Configuration public class RateLimitRule { - /// - /// Rate limit period as in 1s, 1m, 1h - /// - public string Period { get; set; } + public RateLimitRule(string period, TimeSpan periodTimespan, long limit) + { + Period = period; + PeriodTimespan = periodTimespan; + Limit = limit; + } - public TimeSpan? PeriodTimespan { get; set; } + /// + /// Rate limit period as in 1s, 1m, 1h,1d + /// + public string Period { get; private set; } + + public TimeSpan PeriodTimespan { get; private set; } /// /// Maximum number of requests that a client can make in a defined period /// - public long Limit { get; set; } + public long Limit { get; private set; } } } diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index e83ad30c..446720ca 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -91,7 +91,7 @@ namespace Ocelot.DependencyInjection // could maybe use a scoped data repository services.AddSingleton(); services.AddScoped(); - + services.AddMemoryCache(); return services; } } diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs index 38141b49..a2ee1202 100644 --- a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs +++ b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs @@ -1,4 +1,5 @@ -using Ocelot.Configuration; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using System; using System.Collections.Generic; using System.Linq; @@ -27,9 +28,9 @@ namespace Ocelot.RateLimit return _core.RetryAfterFrom(timestamp, rule); } - public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option) + public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) { - return _core.GetRateLimitHeaders(requestIdentity, option); + return _core.GetRateLimitHeaders(context, requestIdentity, option); } } diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs index 9cceeb9e..a27bc994 100644 --- a/src/Ocelot/RateLimit/ClientRequestIdentity.cs +++ b/src/Ocelot/RateLimit/ClientRequestIdentity.cs @@ -2,10 +2,17 @@ { public class ClientRequestIdentity { - public string ClientId { get; set; } + public ClientRequestIdentity(string clientId, string path, string httpverb) + { + ClientId = clientId; + Path = path; + HttpVerb = httpverb; + } - public string Path { get; set; } + public string ClientId { get; private set; } - public string HttpVerb { get; set; } + public string Path { get; private set; } + + public string HttpVerb { get; private set; } } } \ No newline at end of file diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs index 64ac40c0..4dbaeed1 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -72,9 +72,7 @@ namespace Ocelot.RateLimit.Middleware //set X-Rate-Limit headers for the longest period if (!options.DisableRateLimitHeaders) { - var headers = _processor.GetRateLimitHeaders(identity, options); - headers.Context = context; - + var headers = _processor.GetRateLimitHeaders( context,identity, options); context.Response.OnStarting(SetRateLimitHeaders, state: headers); } @@ -89,21 +87,19 @@ namespace Ocelot.RateLimit.Middleware clientId = httpContext.Request.Headers[option.ClientIdHeader].First(); } - return new ClientRequestIdentity - { - Path = httpContext.Request.Path.ToString().ToLowerInvariant(), - HttpVerb = httpContext.Request.Method.ToLowerInvariant(), - ClientId = clientId, - }; - } + return new ClientRequestIdentity( + clientId, + httpContext.Request.Path.ToString().ToLowerInvariant(), + httpContext.Request.Method.ToLowerInvariant() + ); + } public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option) { - if (option.ClientWhitelist != null && option.ClientWhitelist.Contains(requestIdentity.ClientId)) + if (option.ClientWhitelist.Contains(requestIdentity.ClientId)) { return true; } - return false; } diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs index 39904b06..07627f8d 100644 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ b/src/Ocelot/RateLimit/RateLimitCore.cs @@ -1,8 +1,11 @@ -using Ocelot.Configuration; +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 @@ -19,11 +22,7 @@ namespace Ocelot.RateLimit public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) { - var counter = new RateLimitCounter - { - Timestamp = DateTime.UtcNow, - TotalRequests = 1 - }; + RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1); var rule = option.RateLimitRule; var counterId = ComputeCounterKey(requestIdentity, option); @@ -35,59 +34,57 @@ namespace Ocelot.RateLimit if (entry.HasValue) { // entry has not expired - if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow) + if (entry.Value.Timestamp + rule.PeriodTimespan >= DateTime.UtcNow) { // increment request count var totalRequests = entry.Value.TotalRequests + 1; // deep copy - counter = new RateLimitCounter - { - Timestamp = entry.Value.Timestamp, - TotalRequests = totalRequests - }; + counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); + } } - // stores: id (string) - timestamp (datetime) - total_requests (long) - _counterHandler.Set(counterId, counter, rule.PeriodTimespan.Value); + _counterHandler.Set(counterId, counter, rule.PeriodTimespan); } return counter; } - public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option) + public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) { var rule = option.RateLimitRule; - var headers = new RateLimitHeaders(); + RateLimitHeaders headers = null; 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(); - } + 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.Reset = (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo); - headers.Limit = rule.Period; - headers.Remaining = rule.Limit.ToString(); + headers = new RateLimitHeaders(context, + rule.Period, + rule.Limit.ToString(), + (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } 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); + var idBytes = Encoding.UTF8.GetBytes(key); byte[] hashBytes; - using (var algorithm = System.Security.Cryptography.SHA1.Create()) + using (var algorithm = SHA1.Create()) { hashBytes = algorithm.ComputeHash(idBytes); } @@ -98,7 +95,7 @@ namespace Ocelot.RateLimit public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule) { var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); - var retryAfter = Convert.ToInt32(rule.PeriodTimespan.Value.TotalSeconds); + var retryAfter = Convert.ToInt32(rule.PeriodTimespan.TotalSeconds); retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); } @@ -111,11 +108,16 @@ namespace Ocelot.RateLimit 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}"); + 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}"); } } diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs index f0133b25..42dd03b7 100644 --- a/src/Ocelot/RateLimit/RateLimitCounter.cs +++ b/src/Ocelot/RateLimit/RateLimitCounter.cs @@ -10,8 +10,14 @@ namespace Ocelot.RateLimit /// public struct RateLimitCounter { - public DateTime Timestamp { get; set; } + public RateLimitCounter(DateTime timestamp, long totalRequest) + { + Timestamp = timestamp; + TotalRequests = totalRequest; + } - public long TotalRequests { get; set; } + public DateTime Timestamp { get; private set; } + + public long TotalRequests { get; private set; } } } diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs index a7bd4ae6..909f656d 100644 --- a/src/Ocelot/RateLimit/RateLimitHeaders.cs +++ b/src/Ocelot/RateLimit/RateLimitHeaders.cs @@ -8,12 +8,20 @@ namespace Ocelot.RateLimit { public class RateLimitHeaders { - public HttpContext Context { get; set; } + public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) + { + Context = context; + Limit = limit; + Remaining = remaining; + Reset = reset; + } - public string Limit { get; set; } + public HttpContext Context { get; private set; } - public string Remaining { get; set; } + public string Limit { get; private set; } - public string Reset { get; set; } + public string Remaining { get; private set; } + + public string Reset { get; private set; } } } diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs index 2e8c3c90..dc2b2b31 100644 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -67,7 +67,9 @@ namespace Ocelot.AcceptanceTests ClientIdHeader = "ClientId", DisableRateLimitHeaders = false, QuotaExceededMessage = "", - RateLimitCounterPrefix = "" + RateLimitCounterPrefix = "", + HttpStatusCode = 428 + }, RequestIdKey ="oceclientrequest" } @@ -76,8 +78,12 @@ namespace Ocelot.AcceptanceTests 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", 5)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(429)) + .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(); } @@ -154,55 +160,6 @@ namespace Ocelot.AcceptanceTests _builder.Start(); } - //private void GetApiRateLimait(string url) - //{ - // var clientId = "ocelotclient1"; - // var request = new HttpRequestMessage(new HttpMethod("GET"), url); - // request.Headers.Add("ClientId", clientId); - - // var response = _client.SendAsync(request); - // responseStatusCode = (int)response.Result.StatusCode; - // } - - //} - - //public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) - //{ - // var clientId = "ocelotclient1"; - // var tasks = new Task[times]; - - // for (int i = 0; i < times; i++) - // { - // var urlCopy = url; - // tasks[i] = GetForServiceDiscoveryTest(urlCopy); - // Thread.Sleep(_random.Next(40, 60)); - // } - - // Task.WaitAll(tasks); - //} - - //private void WhenICallTheMiddlewareWithWhiteClient() - //{ - // var clientId = "ocelotclient2"; - // // Act - // for (int i = 0; i < 2; i++) - // { - // var request = new HttpRequestMessage(new HttpMethod("GET"), apiRateLimitPath); - // 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); - //} + } } \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 5981c343..ebced901 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -101,7 +101,6 @@ namespace Ocelot.AcceptanceTests }) .WithDictionaryHandle(); }; - s.AddMemoryCache(); s.AddOcelotOutputCaching(settings); s.AddOcelotFileConfiguration(configuration); s.AddOcelot(); diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index af289aab..0a65abc2 100755 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -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,"RateLimitOptions":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}} \ No newline at end of file +{"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":{"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}}} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json index 5457280f..2dd3094e 100644 --- a/test/Ocelot.AcceptanceTests/project.json +++ b/test/Ocelot.AcceptanceTests/project.json @@ -23,7 +23,6 @@ "Microsoft.AspNetCore.Http": "1.1.0", "Microsoft.DotNet.InternalAbstractions": "1.0.0", "Ocelot": "0.0.0-dev", - "xunit": "2.2.0-beta2-build3300", "dotnet-test-xunit": "2.2.0-preview2-build1029", "Ocelot.ManualTest": "0.0.0-dev", "Microsoft.AspNetCore.TestHost": "1.1.0", @@ -34,7 +33,8 @@ "Shouldly": "2.8.2", "TestStack.BDDfy": "4.3.2", "Consul": "0.7.2.1", - "Microsoft.Extensions.Caching.Memory": "1.1.0" + "Microsoft.Extensions.Caching.Memory": "1.1.0", + "xunit": "2.2.0-rc1-build3507" }, "runtimes": { "win10-x64": {}, diff --git a/test/Ocelot.ManualTest/Startup.cs b/test/Ocelot.ManualTest/Startup.cs index 2d8b7653..767aed43 100644 --- a/test/Ocelot.ManualTest/Startup.cs +++ b/test/Ocelot.ManualTest/Startup.cs @@ -37,7 +37,6 @@ namespace Ocelot.ManualTest }) .WithDictionaryHandle(); }; - services.AddMemoryCache(); services.AddOcelotOutputCaching(settings); services.AddOcelotFileConfiguration(Configuration); services.AddOcelot(); diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index 11bca113..72f66faa 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -20,6 +20,7 @@ using Xunit; using TestStack.BDDfy; using Ocelot.Configuration.Builder; using Shouldly; +using Ocelot.Configuration; namespace Ocelot.UnitTests.RateLimit { @@ -70,11 +71,13 @@ namespace Ocelot.UnitTests.RateLimit { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( - new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429)) + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule("1s", TimeSpan.FromSeconds(100), 3), 429)) .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .When(x => x.WhenICallTheMiddleware()) + .When(x => x.WhenICallTheMiddlewareMultipleTime(2)) + .Then(x => x.ThenresponseStatusCodeIs200()) + .When(x => x.WhenICallTheMiddlewareMultipleTime(2)) .Then(x => x.ThenresponseStatusCodeIs429()) .BDDfy(); } @@ -84,7 +87,7 @@ namespace Ocelot.UnitTests.RateLimit { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( - new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429)) + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new RateLimitRule( "1s", TimeSpan.FromSeconds(100),3),429)) .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) @@ -102,11 +105,11 @@ namespace Ocelot.UnitTests.RateLimit .Returns(_downstreamRoute); } - private void WhenICallTheMiddleware() + private void WhenICallTheMiddlewareMultipleTime(int times) { var clientId = "ocelotclient1"; // Act - for (int i = 0; i <10; i++) + for (int i = 0; i < times; i++) { var request = new HttpRequestMessage(new HttpMethod("GET"), _url); request.Headers.Add("ClientId", clientId); diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index 3151ac57..2f42c283 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -14,7 +14,6 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.AspNetCore.Http": "1.1.0", "Ocelot": "0.0.0-dev", - "xunit": "2.2.0-beta2-build3300", "dotnet-test-xunit": "2.2.0-preview2-build1029", "Moq": "4.6.38-alpha", "Microsoft.AspNetCore.TestHost": "1.1.0", @@ -24,7 +23,8 @@ "Shouldly": "2.8.2", "TestStack.BDDfy": "4.3.2", "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": { "win10-x64": {}, From ab6ae8a062022f0a94b1d1a34e8797a2d4250e9b Mon Sep 17 00:00:00 2001 From: geffzhang Date: Wed, 15 Feb 2017 08:50:22 +0800 Subject: [PATCH 6/7] merge newest code for develop --- .../Configuration/Builder/ReRouteBuilder.cs | 129 +++------ .../Creator/FileOcelotConfigurationCreator.cs | 247 ++++++++++++------ src/Ocelot/Configuration/QoSOptions.cs | 10 +- src/Ocelot/Configuration/ReRoute.cs | 41 ++- 4 files changed, 241 insertions(+), 186 deletions(-) diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 028b89e3..2e442701 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -1,23 +1,19 @@ using System; using System.Collections.Generic; +using System.Net.Http; using Ocelot.Values; namespace Ocelot.Configuration.Builder { public class ReRouteBuilder { + private AuthenticationOptions _authenticationOptions; private string _loadBalancerKey; private string _downstreamPathTemplate; private string _upstreamTemplate; private string _upstreamTemplatePattern; private string _upstreamHttpMethod; private bool _isAuthenticated; - private string _authenticationProvider; - private string _authenticationProviderUrl; - private string _scopeName; - private List _additionalScopes; - private bool _requireHttps; - private string _scopeSecret; private List _configHeaderExtractorProperties; private List _claimToClaims; private Dictionary _routeClaimRequirement; @@ -26,29 +22,19 @@ namespace Ocelot.Configuration.Builder private string _requestIdHeaderKey; private bool _isCached; private CacheOptions _fileCacheOptions; - private bool _useServiceDiscovery; - private string _serviceName; - private string _serviceDiscoveryProvider; - private string _serviceDiscoveryAddress; private string _downstreamScheme; private string _downstreamHost; - private int _dsPort; + private int _downstreamPort; private string _loadBalancer; - private string _serviceProviderHost; - private int _serviceProviderPort; + private ServiceProviderConfiguraion _serviceProviderConfiguraion; private bool _useQos; private QoSOptions _qosOptions; public bool _enableRateLimiting; public RateLimitOptions _rateLimitOptions; - public ReRouteBuilder() - { - _additionalScopes = new List(); - } - public ReRouteBuilder WithLoadBalancer(string loadBalancer) { - _loadBalancer = loadBalancer; + _loadBalancer = loadBalancer; return this; } @@ -64,37 +50,13 @@ namespace Ocelot.Configuration.Builder return this; } - public ReRouteBuilder WithServiceDiscoveryAddress(string serviceDiscoveryAddress) - { - _serviceDiscoveryAddress = serviceDiscoveryAddress; - return this; - } - - public ReRouteBuilder WithServiceDiscoveryProvider(string serviceDiscoveryProvider) - { - _serviceDiscoveryProvider = serviceDiscoveryProvider; - return this; - } - - public ReRouteBuilder WithServiceName(string serviceName) - { - _serviceName = serviceName; - return this; - } - - public ReRouteBuilder WithUseServiceDiscovery(bool useServiceDiscovery) - { - _useServiceDiscovery = useServiceDiscovery; - return this; - } - public ReRouteBuilder WithDownstreamPathTemplate(string input) { _downstreamPathTemplate = input; return this; } - public ReRouteBuilder WithUpstreamTemplate(string input) + public ReRouteBuilder WithUpstreamPathTemplate(string input) { _upstreamTemplate = input; return this; @@ -122,42 +84,6 @@ namespace Ocelot.Configuration.Builder return this; } - public ReRouteBuilder WithAuthenticationProvider(string input) - { - _authenticationProvider = input; - return this; - } - - public ReRouteBuilder WithAuthenticationProviderUrl(string input) - { - _authenticationProviderUrl = input; - return this; - } - - public ReRouteBuilder WithAuthenticationProviderScopeName(string input) - { - _scopeName = input; - return this; - } - - public ReRouteBuilder WithAuthenticationProviderAdditionalScopes(List input) - { - _additionalScopes = input; - return this; - } - - public ReRouteBuilder WithRequireHttps(bool input) - { - _requireHttps = input; - return this; - } - - public ReRouteBuilder WithScopeSecret(string input) - { - _scopeSecret = input; - return this; - } - public ReRouteBuilder WithRequestIdKey(string input) { _requestIdHeaderKey = input; @@ -202,7 +128,7 @@ namespace Ocelot.Configuration.Builder public ReRouteBuilder WithDownstreamPort(int port) { - _dsPort = port; + _downstreamPort = port; return this; } @@ -225,15 +151,15 @@ namespace Ocelot.Configuration.Builder return this; } - public ReRouteBuilder WithServiceProviderHost(string serviceProviderHost) + public ReRouteBuilder WithServiceProviderConfiguraion(ServiceProviderConfiguraion serviceProviderConfiguraion) { - _serviceProviderHost = serviceProviderHost; + _serviceProviderConfiguraion = serviceProviderConfiguraion; return this; } - public ReRouteBuilder WithServiceProviderPort(int serviceProviderPort) + public ReRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions) { - _serviceProviderPort = serviceProviderPort; + _authenticationOptions = authenticationOptions; return this; } @@ -252,14 +178,31 @@ namespace Ocelot.Configuration.Builder public ReRoute Build() { - return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, - _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, - _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, - _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _downstreamScheme, _loadBalancer, - _downstreamHost, _dsPort, _loadBalancerKey, new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _dsPort, _useServiceDiscovery, - _serviceDiscoveryProvider, _serviceProviderHost, _serviceProviderPort), - _useQos,_qosOptions,_enableRateLimiting,_rateLimitOptions); - + return new ReRoute( + new PathTemplate(_downstreamPathTemplate), + new PathTemplate(_upstreamTemplate), + new HttpMethod(_upstreamHttpMethod), + _upstreamTemplatePattern, + _isAuthenticated, + _authenticationOptions, + _configHeaderExtractorProperties, + _claimToClaims, + _routeClaimRequirement, + _isAuthorised, + _claimToQueries, + _requestIdHeaderKey, + _isCached, + _fileCacheOptions, + _downstreamScheme, + _loadBalancer, + _downstreamHost, + _downstreamPort, + _loadBalancerKey, + _serviceProviderConfiguraion, + _useQos, + _qosOptions, + _enableRateLimiting, + _rateLimitOptions); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 646af96c..55f31bfa 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -4,13 +4,14 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Requester.QoS; using Ocelot.Responses; using Ocelot.Utilities; -using Ocelot.Values; namespace Ocelot.Configuration.Creator { @@ -24,11 +25,14 @@ namespace Ocelot.Configuration.Creator private const string RegExMatchEverything = ".*"; private const string RegExMatchEndString = "$"; private const string RegExIgnoreCase = "(?i)"; + private const string RegExForwardSlashOnly = "^/$"; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly ILogger _logger; private readonly ILoadBalancerFactory _loadBalanceFactory; private readonly ILoadBalancerHouse _loadBalancerHouse; + private readonly IQoSProviderFactory _qoSProviderFactory; + private readonly IQosProviderHouse _qosProviderHouse; public FileOcelotConfigurationCreator( IOptions options, @@ -36,10 +40,14 @@ namespace Ocelot.Configuration.Creator IClaimToThingConfigurationParser claimToThingConfigurationParser, ILogger logger, ILoadBalancerFactory loadBalancerFactory, - ILoadBalancerHouse loadBalancerHouse) + ILoadBalancerHouse loadBalancerHouse, + IQoSProviderFactory qoSProviderFactory, + IQosProviderHouse qosProviderHouse) { _loadBalanceFactory = loadBalancerFactory; _loadBalancerHouse = loadBalancerHouse; + _qoSProviderFactory = qoSProviderFactory; + _qosProviderHouse = qosProviderHouse; _options = options; _configurationValidator = configurationValidator; _claimToThingConfigurationParser = claimToThingConfigurationParser; @@ -53,10 +61,6 @@ namespace Ocelot.Configuration.Creator return new OkResponse(config); } - /// - /// This method is meant to be tempoary to convert a config to an ocelot config...probably wont keep this but we will see - /// will need a refactor at some point as its crap - /// private async Task SetUpConfiguration() { var response = _configurationValidator.IsValid(_options.Value); @@ -86,93 +90,179 @@ namespace Ocelot.Configuration.Creator private async Task SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) { - var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); + var isAuthenticated = IsAuthenticated(fileReRoute); - var upstreamTemplate = BuildUpstreamTemplate(fileReRoute); + var isAuthorised = IsAuthorised(fileReRoute); - var isAuthenticated = !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider); + var isCached = IsCached(fileReRoute); - var isAuthorised = fileReRoute.RouteClaimsRequirement?.Count > 0; + var requestIdKey = BuildRequestId(fileReRoute, globalConfiguration); - var isCached = fileReRoute.FileCacheOptions.TtlSeconds > 0; + var reRouteKey = BuildReRouteKey(fileReRoute); - var isQos = fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions.TimeoutValue >0; + var upstreamTemplatePattern = BuildUpstreamTemplatePattern(fileReRoute); - var requestIdKey = globalRequestIdConfiguration - ? globalConfiguration.RequestIdKey - : fileReRoute.RequestIdKey; + var isQos = IsQoS(fileReRoute); - var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName) - && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); + var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration); - //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain - var loadBalancerKey = $"{fileReRoute.UpstreamTemplate}{fileReRoute.UpstreamHttpMethod}"; + var authOptionsForRoute = BuildAuthenticationOptions(fileReRoute); - ReRoute reRoute; + var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest); - var enableRateLimiting = (fileReRoute.RateLimitOptions!= null && fileReRoute.RateLimitOptions.EnableRateLimiting)? true: false; - 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); - } - var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; + var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest); - var serviceProviderConfiguration = new ServiceProviderConfiguraion(fileReRoute.ServiceName, - fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, useServiceDiscovery, - globalConfiguration?.ServiceDiscoveryProvider?.Provider, globalConfiguration?.ServiceDiscoveryProvider?.Host, - serviceProviderPort); + var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest); - if (isAuthenticated) - { - var authOptionsForRoute = new AuthenticationOptions(fileReRoute.AuthenticationOptions.Provider, - fileReRoute.AuthenticationOptions.ProviderRootUrl, fileReRoute.AuthenticationOptions.ScopeName, - fileReRoute.AuthenticationOptions.RequireHttps, fileReRoute.AuthenticationOptions.AdditionalScopes, - fileReRoute.AuthenticationOptions.ScopeSecret); + var qosOptions = BuildQoSOptions(fileReRoute); - var claimsToHeaders = GetAddThingsToRequest(fileReRoute.AddHeadersToRequest); - var claimsToClaims = GetAddThingsToRequest(fileReRoute.AddClaimsToRequest); - var claimsToQueries = GetAddThingsToRequest(fileReRoute.AddQueriesToRequest); + var enableRateLimiting = IsEnableRateLimiting(fileReRoute); - reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate), - fileReRoute.UpstreamTemplate, - fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - authOptionsForRoute, claimsToHeaders, claimsToClaims, - fileReRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, - requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds) - , fileReRoute.DownstreamScheme, - fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey, - serviceProviderConfiguration, isQos, - new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue), - enableRateLimiting, rateLimitOption); - } - else - { - reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate), - fileReRoute.UpstreamTemplate, - fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - null, new List(), new List(), - fileReRoute.RouteClaimsRequirement, isAuthorised, new List(), - requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds), - fileReRoute.DownstreamScheme, - fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey, - serviceProviderConfiguration, isQos, - new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue), - enableRateLimiting, rateLimitOption); - } - - var loadBalancer = await _loadBalanceFactory.Get(reRoute); - _loadBalancerHouse.Add(reRoute.LoadBalancerKey, loadBalancer); + var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting); + + var reRoute = new ReRouteBuilder() + .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) + .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) + .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) + .WithUpstreamTemplatePattern(upstreamTemplatePattern) + .WithIsAuthenticated(isAuthenticated) + .WithAuthenticationOptions(authOptionsForRoute) + .WithClaimsToHeaders(claimsToHeaders) + .WithClaimsToClaims(claimsToClaims) + .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) + .WithIsAuthorised(isAuthorised) + .WithClaimsToQueries(claimsToQueries) + .WithRequestIdKey(requestIdKey) + .WithIsCached(isCached) + .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds)) + .WithDownstreamScheme(fileReRoute.DownstreamScheme) + .WithLoadBalancer(fileReRoute.LoadBalancer) + .WithDownstreamHost(fileReRoute.DownstreamHost) + .WithDownstreamPort(fileReRoute.DownstreamPort) + .WithLoadBalancerKey(reRouteKey) + .WithServiceProviderConfiguraion(serviceProviderConfiguration) + .WithIsQos(isQos) + .WithQosOptions(qosOptions) + .WithEnableRateLimiting(enableRateLimiting) + .WithRateLimitOptions(rateLimitOption) + .Build(); + await SetupLoadBalancer(reRoute); + SetupQosProvider(reRoute); return reRoute; } - private string BuildUpstreamTemplate(FileReRoute reRoute) + private static RateLimitOptions BuildRateLimitOptions(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting) { - var upstreamTemplate = reRoute.UpstreamTemplate; + 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) + { + return new QoSOptionsBuilder() + .WithExceptionsAllowedBeforeBreaking(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking) + .WithDurationOfBreak(fileReRoute.QoSOptions.DurationOfBreak) + .WithTimeoutValue(fileReRoute.QoSOptions.TimeoutValue) + .Build(); + } + + private bool IsQoS(FileReRoute fileReRoute) + { + return fileReRoute.QoSOptions?.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions?.TimeoutValue > 0; + } + + private bool IsAuthenticated(FileReRoute fileReRoute) + { + return !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider); + } + + private bool IsAuthorised(FileReRoute fileReRoute) + { + return fileReRoute.RouteClaimsRequirement?.Count > 0; + } + + private bool IsCached(FileReRoute fileReRoute) + { + return fileReRoute.FileCacheOptions.TtlSeconds > 0; + } + + private string BuildRequestId(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) + { + var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); + + var requestIdKey = globalRequestIdConfiguration + ? globalConfiguration.RequestIdKey + : fileReRoute.RequestIdKey; + + return requestIdKey; + } + + private string BuildReRouteKey(FileReRoute fileReRoute) + { + //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain + var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}{fileReRoute.UpstreamHttpMethod}"; + return loadBalancerKey; + } + + private AuthenticationOptions BuildAuthenticationOptions(FileReRoute fileReRoute) + { + return new AuthenticationOptionsBuilder() + .WithProvider(fileReRoute.AuthenticationOptions?.Provider) + .WithProviderRootUrl(fileReRoute.AuthenticationOptions?.ProviderRootUrl) + .WithScopeName(fileReRoute.AuthenticationOptions?.ScopeName) + .WithRequireHttps(fileReRoute.AuthenticationOptions.RequireHttps) + .WithAdditionalScopes(fileReRoute.AuthenticationOptions?.AdditionalScopes) + .WithScopeSecret(fileReRoute.AuthenticationOptions?.ScopeSecret) + .Build(); + } + + private async Task SetupLoadBalancer(ReRoute reRoute) + { + var loadBalancer = await _loadBalanceFactory.Get(reRoute); + _loadBalancerHouse.Add(reRoute.ReRouteKey, loadBalancer); + } + + private void SetupQosProvider(ReRoute reRoute) + { + var loadBalancer = _qoSProviderFactory.Get(reRoute); + _qosProviderHouse.Add(reRoute.ReRouteKey, loadBalancer); + } + + private ServiceProviderConfiguraion BuildServiceProviderConfiguration(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) + { + var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName) + && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); + + var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; + + return new ServiceProviderConfiguraionBuilder() + .WithServiceName(fileReRoute.ServiceName) + .WithDownstreamHost(fileReRoute.DownstreamHost) + .WithDownstreamPort(fileReRoute.DownstreamPort) + .WithUseServiceDiscovery(useServiceDiscovery) + .WithServiceDiscoveryProvider(globalConfiguration?.ServiceDiscoveryProvider?.Provider) + .WithServiceDiscoveryProviderHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) + .WithServiceDiscoveryProviderPort(serviceProviderPort) + .Build(); + } + + private string BuildUpstreamTemplatePattern(FileReRoute reRoute) + { + var upstreamTemplate = reRoute.UpstreamPathTemplate; upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/'); @@ -194,6 +284,11 @@ namespace Ocelot.Configuration.Creator upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); } + if (upstreamTemplate == "/") + { + return RegExForwardSlashOnly; + } + var route = reRoute.ReRouteIsCaseSensitive ? $"{upstreamTemplate}{RegExMatchEndString}" : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; @@ -201,7 +296,7 @@ namespace Ocelot.Configuration.Creator return route; } - private List GetAddThingsToRequest(Dictionary thingBeingAdded) + private List BuildAddThingsToRequest(Dictionary thingBeingAdded) { var claimsToTHings = new List(); diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 27b862af..29145888 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -1,11 +1,15 @@ -using Polly.Timeout; -using System; +using System; +using Polly.Timeout; namespace Ocelot.Configuration { public class QoSOptions { - public QoSOptions(int exceptionsAllowedBeforeBreaking, int durationofBreak, int timeoutValue, TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic) + public QoSOptions( + int exceptionsAllowedBeforeBreaking, + int durationofBreak, + int timeoutValue, + TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic) { ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; DurationOfBreak = TimeSpan.FromMilliseconds(durationofBreak); diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 38e28af9..bfce7c76 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -1,31 +1,44 @@ using System; using System.Collections.Generic; +using System.Net.Http; using Ocelot.Values; namespace Ocelot.Configuration { public class ReRoute { - public ReRoute(DownstreamPathTemplate downstreamPathTemplate, - string upstreamTemplate, string upstreamHttpMethod, + public ReRoute(PathTemplate downstreamPathTemplate, + PathTemplate upstreamTemplate, + HttpMethod upstreamHttpMethod, string upstreamTemplatePattern, - bool isAuthenticated, AuthenticationOptions authenticationOptions, + bool isAuthenticated, + AuthenticationOptions authenticationOptions, List configurationHeaderExtractorProperties, List claimsToClaims, - Dictionary routeClaimsRequirement, bool isAuthorised, + Dictionary routeClaimsRequirement, + bool isAuthorised, List claimsToQueries, - string requestIdKey, bool isCached, CacheOptions fileCacheOptions, - string downstreamScheme, string loadBalancer, string downstreamHost, - int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion, - bool isQos,QoSOptions qos, bool enableRateLimit, RateLimitOptions ratelimitOptions) + string requestIdKey, + bool isCached, + CacheOptions fileCacheOptions, + string downstreamScheme, + string loadBalancer, + string downstreamHost, + int downstreamPort, + string reRouteKey, + ServiceProviderConfiguraion serviceProviderConfiguraion, + bool isQos, + QoSOptions qos, + bool enableRateLimit, + RateLimitOptions ratelimitOptions) { - LoadBalancerKey = loadBalancerKey; + ReRouteKey = reRouteKey; ServiceProviderConfiguraion = serviceProviderConfiguraion; LoadBalancer = loadBalancer; DownstreamHost = downstreamHost; DownstreamPort = downstreamPort; DownstreamPathTemplate = downstreamPathTemplate; - UpstreamTemplate = upstreamTemplate; + UpstreamPathTemplate = upstreamTemplate; UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; IsAuthenticated = isAuthenticated; @@ -48,11 +61,11 @@ namespace Ocelot.Configuration RateLimitOptions = ratelimitOptions; } - public string LoadBalancerKey {get;private set;} - public DownstreamPathTemplate DownstreamPathTemplate { get; private set; } - public string UpstreamTemplate { get; private set; } + public string ReRouteKey {get;private set;} + public PathTemplate DownstreamPathTemplate { get; private set; } + public PathTemplate UpstreamPathTemplate { get; private set; } public string UpstreamTemplatePattern { get; private set; } - public string UpstreamHttpMethod { get; private set; } + public HttpMethod UpstreamHttpMethod { get; private set; } public bool IsAuthenticated { get; private set; } public bool IsAuthorised { get; private set; } public AuthenticationOptions AuthenticationOptions { get; private set; } From f302ee77bfd2ce6d162d017ff69f1493835cc662 Mon Sep 17 00:00:00 2001 From: geffzhang Date: Wed, 15 Feb 2017 08:54:20 +0800 Subject: [PATCH 7/7] remove bak file --- global.json | 2 +- .../FileOcelotConfigurationCreator.cs.bak | 356 ------------------ src/Ocelot/Configuration/QoSOptions.cs.bak | 35 -- src/Ocelot/Configuration/ReRoute.cs.bak | 94 ----- .../Ocelot.AcceptanceTests/configuration.json | 2 +- 5 files changed, 2 insertions(+), 487 deletions(-) delete mode 100644 src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs.bak delete mode 100644 src/Ocelot/Configuration/QoSOptions.cs.bak delete mode 100644 src/Ocelot/Configuration/ReRoute.cs.bak diff --git a/global.json b/global.json index 616b2c4e..ff8d898e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@  { "projects": [ "src", "test" ], "sdk": { - "version": "1.0.0-preview2-003131" + "version": "1.0.0-preview2-003133" } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs.bak b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs.bak deleted file mode 100644 index 86cd0ff2..00000000 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs.bak +++ /dev/null @@ -1,356 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Parser; -using Ocelot.Configuration.Validator; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Requester.QoS; -using Ocelot.Responses; -using Ocelot.Utilities; - -namespace Ocelot.Configuration.Creator -{ - /// - /// Register as singleton - /// - public class FileOcelotConfigurationCreator : IOcelotConfigurationCreator - { - private readonly IOptions _options; - private readonly IConfigurationValidator _configurationValidator; - private const string RegExMatchEverything = ".*"; - private const string RegExMatchEndString = "$"; - private const string RegExIgnoreCase = "(?i)"; - private const string RegExForwardSlashOnly = "^/$"; - - private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; - private readonly ILogger _logger; - private readonly ILoadBalancerFactory _loadBalanceFactory; - private readonly ILoadBalancerHouse _loadBalancerHouse; - private readonly IQoSProviderFactory _qoSProviderFactory; - private readonly IQosProviderHouse _qosProviderHouse; - - public FileOcelotConfigurationCreator( - IOptions options, - IConfigurationValidator configurationValidator, - IClaimToThingConfigurationParser claimToThingConfigurationParser, - ILogger logger, - ILoadBalancerFactory loadBalancerFactory, - ILoadBalancerHouse loadBalancerHouse, - IQoSProviderFactory qoSProviderFactory, - IQosProviderHouse qosProviderHouse) - { - _loadBalanceFactory = loadBalancerFactory; - _loadBalancerHouse = loadBalancerHouse; - _qoSProviderFactory = qoSProviderFactory; - _qosProviderHouse = qosProviderHouse; - _options = options; - _configurationValidator = configurationValidator; - _claimToThingConfigurationParser = claimToThingConfigurationParser; - _logger = logger; - } - - public async Task> Create() - { - var config = await SetUpConfiguration(); - - return new OkResponse(config); - } - - private async Task SetUpConfiguration() - { - var response = _configurationValidator.IsValid(_options.Value); - - if (response.Data.IsError) - { - var errorBuilder = new StringBuilder(); - - foreach (var error in response.Errors) - { - errorBuilder.AppendLine(error.Message); - } - - throw new Exception($"Unable to start Ocelot..configuration, errors were {errorBuilder}"); - } - - var reRoutes = new List(); - - foreach (var reRoute in _options.Value.ReRoutes) - { - var ocelotReRoute = await SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); - reRoutes.Add(ocelotReRoute); - } - - return new OcelotConfiguration(reRoutes); - } - - private async Task SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) - { - var isAuthenticated = IsAuthenticated(fileReRoute); - - var isAuthorised = IsAuthorised(fileReRoute); - - var isCached = IsCached(fileReRoute); - - var requestIdKey = BuildRequestId(fileReRoute, globalConfiguration); - - var reRouteKey = BuildReRouteKey(fileReRoute); - - var upstreamTemplatePattern = BuildUpstreamTemplatePattern(fileReRoute); - - var isQos = IsQoS(fileReRoute); - - var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration); - - var authOptionsForRoute = BuildAuthenticationOptions(fileReRoute); - -<<<<<<< HEAD - ReRoute reRoute; - - var enableRateLimiting = (fileReRoute.RateLimitOptions!= null && fileReRoute.RateLimitOptions.EnableRateLimiting)? true: false; - 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); - } - var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; -======= - var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest); - - var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest); - - var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest); - - var qosOptions = BuildQoSOptions(fileReRoute); - - var reRoute = new ReRouteBuilder() - .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) - .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) - .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) - .WithUpstreamTemplatePattern(upstreamTemplatePattern) - .WithIsAuthenticated(isAuthenticated) - .WithAuthenticationOptions(authOptionsForRoute) - .WithClaimsToHeaders(claimsToHeaders) - .WithClaimsToClaims(claimsToClaims) - .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) - .WithIsAuthorised(isAuthorised) - .WithClaimsToQueries(claimsToQueries) - .WithRequestIdKey(requestIdKey) - .WithIsCached(isCached) - .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds)) - .WithDownstreamScheme(fileReRoute.DownstreamScheme) - .WithLoadBalancer(fileReRoute.LoadBalancer) - .WithDownstreamHost(fileReRoute.DownstreamHost) - .WithDownstreamPort(fileReRoute.DownstreamPort) - .WithLoadBalancerKey(reRouteKey) - .WithServiceProviderConfiguraion(serviceProviderConfiguration) - .WithIsQos(isQos) - .WithQosOptions(qosOptions) - .Build(); - - await SetupLoadBalancer(reRoute); - SetupQosProvider(reRoute); - return reRoute; - } - - private QoSOptions BuildQoSOptions(FileReRoute fileReRoute) - { - return new QoSOptionsBuilder() - .WithExceptionsAllowedBeforeBreaking(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking) - .WithDurationOfBreak(fileReRoute.QoSOptions.DurationOfBreak) - .WithTimeoutValue(fileReRoute.QoSOptions.TimeoutValue) - .Build(); - } ->>>>>>> refs/remotes/origin/develop - - private bool IsQoS(FileReRoute fileReRoute) - { - return fileReRoute.QoSOptions?.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions?.TimeoutValue > 0; - } - -<<<<<<< HEAD - if (isAuthenticated) - { - var authOptionsForRoute = new AuthenticationOptions(fileReRoute.AuthenticationOptions.Provider, - fileReRoute.AuthenticationOptions.ProviderRootUrl, fileReRoute.AuthenticationOptions.ScopeName, - fileReRoute.AuthenticationOptions.RequireHttps, fileReRoute.AuthenticationOptions.AdditionalScopes, - fileReRoute.AuthenticationOptions.ScopeSecret); - - var claimsToHeaders = GetAddThingsToRequest(fileReRoute.AddHeadersToRequest); - var claimsToClaims = GetAddThingsToRequest(fileReRoute.AddClaimsToRequest); - var claimsToQueries = GetAddThingsToRequest(fileReRoute.AddQueriesToRequest); - - reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate), - fileReRoute.UpstreamTemplate, - fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - authOptionsForRoute, claimsToHeaders, claimsToClaims, - fileReRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, - requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds) - , fileReRoute.DownstreamScheme, - fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey, - serviceProviderConfiguration, isQos, - new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue), - enableRateLimiting, rateLimitOption); - } - else - { - reRoute = new ReRoute(new DownstreamPathTemplate(fileReRoute.DownstreamPathTemplate), - fileReRoute.UpstreamTemplate, - fileReRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - null, new List(), new List(), - fileReRoute.RouteClaimsRequirement, isAuthorised, new List(), - requestIdKey, isCached, new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds), - fileReRoute.DownstreamScheme, - fileReRoute.LoadBalancer, fileReRoute.DownstreamHost, fileReRoute.DownstreamPort, loadBalancerKey, - serviceProviderConfiguration, isQos, - new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue), - enableRateLimiting, rateLimitOption); - } -======= - private bool IsAuthenticated(FileReRoute fileReRoute) - { - return !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider); - } ->>>>>>> refs/remotes/origin/develop - - private bool IsAuthorised(FileReRoute fileReRoute) - { - return fileReRoute.RouteClaimsRequirement?.Count > 0; - } - - private bool IsCached(FileReRoute fileReRoute) - { - return fileReRoute.FileCacheOptions.TtlSeconds > 0; - } - - private string BuildRequestId(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) - { - var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); - - var requestIdKey = globalRequestIdConfiguration - ? globalConfiguration.RequestIdKey - : fileReRoute.RequestIdKey; - - return requestIdKey; - } - - private string BuildReRouteKey(FileReRoute fileReRoute) - { - //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain - var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}{fileReRoute.UpstreamHttpMethod}"; - return loadBalancerKey; - } - - private AuthenticationOptions BuildAuthenticationOptions(FileReRoute fileReRoute) - { - return new AuthenticationOptionsBuilder() - .WithProvider(fileReRoute.AuthenticationOptions?.Provider) - .WithProviderRootUrl(fileReRoute.AuthenticationOptions?.ProviderRootUrl) - .WithScopeName(fileReRoute.AuthenticationOptions?.ScopeName) - .WithRequireHttps(fileReRoute.AuthenticationOptions.RequireHttps) - .WithAdditionalScopes(fileReRoute.AuthenticationOptions?.AdditionalScopes) - .WithScopeSecret(fileReRoute.AuthenticationOptions?.ScopeSecret) - .Build(); - } - - private async Task SetupLoadBalancer(ReRoute reRoute) - { - var loadBalancer = await _loadBalanceFactory.Get(reRoute); - _loadBalancerHouse.Add(reRoute.ReRouteKey, loadBalancer); - } - - private void SetupQosProvider(ReRoute reRoute) - { - var loadBalancer = _qoSProviderFactory.Get(reRoute); - _qosProviderHouse.Add(reRoute.ReRouteKey, loadBalancer); - } - - private ServiceProviderConfiguraion BuildServiceProviderConfiguration(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) - { - var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName) - && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); - - var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; - - return new ServiceProviderConfiguraionBuilder() - .WithServiceName(fileReRoute.ServiceName) - .WithDownstreamHost(fileReRoute.DownstreamHost) - .WithDownstreamPort(fileReRoute.DownstreamPort) - .WithUseServiceDiscovery(useServiceDiscovery) - .WithServiceDiscoveryProvider(globalConfiguration?.ServiceDiscoveryProvider?.Provider) - .WithServiceDiscoveryProviderHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) - .WithServiceDiscoveryProviderPort(serviceProviderPort) - .Build(); - } - - private string BuildUpstreamTemplatePattern(FileReRoute reRoute) - { - var upstreamTemplate = reRoute.UpstreamPathTemplate; - - upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/'); - - var placeholders = new List(); - - for (var i = 0; i < upstreamTemplate.Length; i++) - { - if (IsPlaceHolder(upstreamTemplate, i)) - { - var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); - var difference = postitionOfPlaceHolderClosingBracket - i + 1; - var variableName = upstreamTemplate.Substring(i, difference); - placeholders.Add(variableName); - } - } - - foreach (var placeholder in placeholders) - { - upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); - } - - if (upstreamTemplate == "/") - { - return RegExForwardSlashOnly; - } - - var route = reRoute.ReRouteIsCaseSensitive - ? $"{upstreamTemplate}{RegExMatchEndString}" - : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; - - return route; - } - - private List BuildAddThingsToRequest(Dictionary thingBeingAdded) - { - var claimsToTHings = new List(); - - foreach (var add in thingBeingAdded) - { - var claimToHeader = _claimToThingConfigurationParser.Extract(add.Key, add.Value); - - if (claimToHeader.IsError) - { - _logger.LogCritical(new EventId(1, "Application Failed to start"), - $"Unable to extract configuration for key: {add.Key} and value: {add.Value} your configuration file is incorrect"); - - throw new Exception(claimToHeader.Errors[0].Message); - } - claimsToTHings.Add(claimToHeader.Data); - } - - return claimsToTHings; - } - - private bool IsPlaceHolder(string upstreamTemplate, int i) - { - return upstreamTemplate[i] == '{'; - } - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/QoSOptions.cs.bak b/src/Ocelot/Configuration/QoSOptions.cs.bak deleted file mode 100644 index 8e41e7d7..00000000 --- a/src/Ocelot/Configuration/QoSOptions.cs.bak +++ /dev/null @@ -1,35 +0,0 @@ -<<<<<<< HEAD -using Polly.Timeout; -using System; -======= -using System; -using Polly.Timeout; ->>>>>>> refs/remotes/origin/develop - -namespace Ocelot.Configuration -{ - public class QoSOptions - { - public QoSOptions( - int exceptionsAllowedBeforeBreaking, - int durationofBreak, - int timeoutValue, - TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic) - { - ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; - DurationOfBreak = TimeSpan.FromMilliseconds(durationofBreak); - TimeoutValue = TimeSpan.FromMilliseconds(timeoutValue); - TimeoutStrategy = timeoutStrategy; - } - - - public int ExceptionsAllowedBeforeBreaking { get; private set; } - - public TimeSpan DurationOfBreak { get; private set; } - - public TimeSpan TimeoutValue { get; private set; } - - public TimeoutStrategy TimeoutStrategy { get; private set; } - - } -} diff --git a/src/Ocelot/Configuration/ReRoute.cs.bak b/src/Ocelot/Configuration/ReRoute.cs.bak deleted file mode 100644 index 4ef5b08a..00000000 --- a/src/Ocelot/Configuration/ReRoute.cs.bak +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using Ocelot.Values; - -namespace Ocelot.Configuration -{ - public class ReRoute - { - public ReRoute(PathTemplate downstreamPathTemplate, - PathTemplate upstreamTemplate, - HttpMethod upstreamHttpMethod, - string upstreamTemplatePattern, - bool isAuthenticated, - AuthenticationOptions authenticationOptions, - List configurationHeaderExtractorProperties, - List claimsToClaims, - Dictionary routeClaimsRequirement, - bool isAuthorised, - List claimsToQueries, -<<<<<<< HEAD - string requestIdKey, bool isCached, CacheOptions fileCacheOptions, - string downstreamScheme, string loadBalancer, string downstreamHost, - int downstreamPort, string loadBalancerKey, ServiceProviderConfiguraion serviceProviderConfiguraion, - bool isQos,QoSOptions qos, bool enableRateLimit, RateLimitOptions ratelimitOptions) -======= - string requestIdKey, - bool isCached, - CacheOptions fileCacheOptions, - string downstreamScheme, - string loadBalancer, - string downstreamHost, - int downstreamPort, - string reRouteKey, - ServiceProviderConfiguraion serviceProviderConfiguraion, - bool isQos, - QoSOptions qos) ->>>>>>> refs/remotes/origin/develop - { - ReRouteKey = reRouteKey; - ServiceProviderConfiguraion = serviceProviderConfiguraion; - LoadBalancer = loadBalancer; - DownstreamHost = downstreamHost; - DownstreamPort = downstreamPort; - DownstreamPathTemplate = downstreamPathTemplate; - UpstreamPathTemplate = upstreamTemplate; - UpstreamHttpMethod = upstreamHttpMethod; - UpstreamTemplatePattern = upstreamTemplatePattern; - IsAuthenticated = isAuthenticated; - AuthenticationOptions = authenticationOptions; - RouteClaimsRequirement = routeClaimsRequirement; - IsAuthorised = isAuthorised; - RequestIdKey = requestIdKey; - IsCached = isCached; - FileCacheOptions = fileCacheOptions; - ClaimsToQueries = claimsToQueries - ?? new List(); - ClaimsToClaims = claimsToClaims - ?? new List(); - ClaimsToHeaders = configurationHeaderExtractorProperties - ?? new List(); - DownstreamScheme = downstreamScheme; - IsQos = isQos; - QosOptions = qos; - EnableEndpointRateLimiting = enableRateLimit; - RateLimitOptions = ratelimitOptions; - } - - public string ReRouteKey {get;private set;} - public PathTemplate DownstreamPathTemplate { get; private set; } - public PathTemplate UpstreamPathTemplate { get; private set; } - public string UpstreamTemplatePattern { get; private set; } - public HttpMethod UpstreamHttpMethod { get; private set; } - public bool IsAuthenticated { get; private set; } - public bool IsAuthorised { get; private set; } - public AuthenticationOptions AuthenticationOptions { get; private set; } - public List ClaimsToQueries { get; private set; } - public List ClaimsToHeaders { get; private set; } - public List ClaimsToClaims { get; private set; } - public Dictionary RouteClaimsRequirement { get; private set; } - public string RequestIdKey { get; private set; } - public bool IsCached { get; private set; } - public CacheOptions FileCacheOptions { get; private set; } - public string DownstreamScheme {get;private set;} - public bool IsQos { get; private set; } - public QoSOptions QosOptions { get; private set; } - public string LoadBalancer {get;private set;} - 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; } - } -} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index 0a65abc2..aba4cdf9 100755 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -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,"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}}} \ No newline at end of file +{"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}}} \ No newline at end of file