mirror of
				https://github.com/nsnail/Ocelot.git
				synced 2025-11-04 09:15:27 +08:00 
			
		
		
		
	implement Request Rate limit, this feature is options
This commit is contained in:
		@@ -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);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								src/Ocelot/Configuration/File/FileRateLimitOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/Ocelot/Configuration/File/FileRateLimitOptions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.Configuration.File
 | 
			
		||||
{
 | 
			
		||||
    public class FileRateLimitOptions
 | 
			
		||||
    {
 | 
			
		||||
        public FileRateLimitOptions()
 | 
			
		||||
        {
 | 
			
		||||
            RateLimitRule = new FileRateLimitRule();
 | 
			
		||||
            ClientWhitelist = new List<string>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public FileRateLimitRule RateLimitRule { get; set; }
 | 
			
		||||
 | 
			
		||||
        public List<string> ClientWhitelist { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string ClientIdHeader { get; set; } = "ClientId";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the policy prefix, used to compose the client policy cache key
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string ClientPolicyPrefix { get; set; } = "crlp";
 | 
			
		||||
 
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
 | 
			
		||||
        /// If none specified the default will be: 
 | 
			
		||||
        /// API calls quota exceeded! maximum admitted {0} per {1}
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string QuotaExceededMessage { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the counter prefix, used to compose the rate limit counter cache key
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string RateLimitCounterPrefix { get; set; } = "ocelot";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Enables endpoint rate limiting based URL path and HTTP verb
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool EnableRateLimiting { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Disables X-Rate-Limit and Rety-After headers
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool DisableRateLimitHeaders { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class  FileRateLimitRule
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Rate limit period as in 1s, 1m, 1h
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Period { get; set; }
 | 
			
		||||
 | 
			
		||||
        public int PeriodTimespan { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Maximum number of requests that a client can make in a defined period
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public long Limit { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,5 @@
 | 
			
		||||
using Polly.Timeout;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.Configuration
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										76
									
								
								src/Ocelot/Configuration/RateLimitOptions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/Ocelot/Configuration/RateLimitOptions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.Configuration
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// RateLimit Options
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class RateLimitOptions
 | 
			
		||||
    {
 | 
			
		||||
        public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List<string> clientWhitelist,bool disableRateLimitHeaders,
 | 
			
		||||
            string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule)
 | 
			
		||||
        {
 | 
			
		||||
            EnableRateLimiting = enbleRateLimiting;
 | 
			
		||||
            ClientIdHeader = clientIdHeader;
 | 
			
		||||
            ClientWhitelist = clientWhitelist;
 | 
			
		||||
            DisableRateLimitHeaders = disableRateLimitHeaders;
 | 
			
		||||
            QuotaExceededMessage = quotaExceededMessage;
 | 
			
		||||
            RateLimitCounterPrefix = rateLimitCounterPrefix;
 | 
			
		||||
            RateLimitRule = rateLimitRule;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        public RateLimitRule RateLimitRule { get;  private set; }
 | 
			
		||||
 | 
			
		||||
        public List<string> ClientWhitelist { get; private set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string ClientIdHeader { get; private set; } = "ClientId";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public int HttpStatusCode { get; private set; } = 429;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
 | 
			
		||||
        /// If none specified the default will be: 
 | 
			
		||||
        /// API calls quota exceeded! maximum admitted {0} per {1}
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string QuotaExceededMessage { get; private set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the counter prefix, used to compose the rate limit counter cache key
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string RateLimitCounterPrefix { get; private set; } = "ocelot";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Enables endpoint rate limiting based URL path and HTTP verb
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool EnableRateLimiting { get; private set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Disables X-Rate-Limit and Rety-After headers
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public bool DisableRateLimitHeaders { get; private set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class RateLimitRule
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Rate limit period as in 1s, 1m, 1h
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public string Period { get; set; }
 | 
			
		||||
 | 
			
		||||
        public TimeSpan? PeriodTimespan { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Maximum number of requests that a client can make in a defined period
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public long Limit { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -30,6 +30,7 @@ using Ocelot.Request.Builder;
 | 
			
		||||
using Ocelot.Requester;
 | 
			
		||||
using Ocelot.Responder;
 | 
			
		||||
using Ocelot.ServiceDiscovery;
 | 
			
		||||
using Ocelot.RateLimit;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.DependencyInjection
 | 
			
		||||
{
 | 
			
		||||
@@ -84,6 +85,7 @@ namespace Ocelot.DependencyInjection
 | 
			
		||||
            services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
 | 
			
		||||
            services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
 | 
			
		||||
            services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();
 | 
			
		||||
            services.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
 | 
			
		||||
 | 
			
		||||
            // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
 | 
			
		||||
            // could maybe use a scoped data repository
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
using Ocelot.Configuration;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    public class ClientRateLimitProcessor
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IRateLimitCounterHandler _counterHandler;
 | 
			
		||||
        private readonly RateLimitCore _core;
 | 
			
		||||
 | 
			
		||||
        public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler)
 | 
			
		||||
        {
 | 
			
		||||
            _counterHandler = counterHandler;
 | 
			
		||||
            _core = new RateLimitCore(_counterHandler);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            return _core.ProcessRequest(requestIdentity, option);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
 | 
			
		||||
        {
 | 
			
		||||
            return _core.RetryAfterFrom(timestamp, rule);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            return _core.GetRateLimitHeaders(requestIdentity, option);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/Ocelot/RateLimit/ClientRequestIdentity.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/Ocelot/RateLimit/ClientRequestIdentity.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    public class ClientRequestIdentity
 | 
			
		||||
    {
 | 
			
		||||
        public string ClientId { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string Path { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string HttpVerb { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    public interface IRateLimitCounterHandler
 | 
			
		||||
    {
 | 
			
		||||
        bool Exists(string id);
 | 
			
		||||
        RateLimitCounter? Get(string id);
 | 
			
		||||
        void Remove(string id);
 | 
			
		||||
        void Set(string id, RateLimitCounter counter, TimeSpan expirationTime);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
using Microsoft.Extensions.Caching.Memory;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IMemoryCache _memoryCache;
 | 
			
		||||
 | 
			
		||||
        public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache)
 | 
			
		||||
        {
 | 
			
		||||
            _memoryCache = memoryCache;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
 | 
			
		||||
        {
 | 
			
		||||
            _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool Exists(string id)
 | 
			
		||||
        {
 | 
			
		||||
            RateLimitCounter counter;
 | 
			
		||||
            return _memoryCache.TryGetValue(id, out counter);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RateLimitCounter? Get(string id)
 | 
			
		||||
        {
 | 
			
		||||
            RateLimitCounter counter;
 | 
			
		||||
            if (_memoryCache.TryGetValue(id, out counter))
 | 
			
		||||
            {
 | 
			
		||||
                return counter;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Remove(string id)
 | 
			
		||||
        {
 | 
			
		||||
            _memoryCache.Remove(id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
using Ocelot.Middleware;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Ocelot.Infrastructure.RequestData;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Ocelot.Logging;
 | 
			
		||||
using Ocelot.Configuration;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit.Middleware
 | 
			
		||||
{
 | 
			
		||||
    public class ClientRateLimitMiddleware : OcelotMiddleware
 | 
			
		||||
    {
 | 
			
		||||
        private readonly RequestDelegate _next;
 | 
			
		||||
        private readonly IOcelotLogger _logger;
 | 
			
		||||
        private readonly IRateLimitCounterHandler _counterHandler;
 | 
			
		||||
        private readonly ClientRateLimitProcessor _processor;
 | 
			
		||||
 | 
			
		||||
        public ClientRateLimitMiddleware(RequestDelegate next,
 | 
			
		||||
            IOcelotLoggerFactory loggerFactory,
 | 
			
		||||
            IRequestScopedDataRepository requestScopedDataRepository,
 | 
			
		||||
            IRateLimitCounterHandler counterHandler)
 | 
			
		||||
            : base(requestScopedDataRepository)
 | 
			
		||||
        {
 | 
			
		||||
            _next = next;
 | 
			
		||||
            _logger = loggerFactory.CreateLogger<ClientRateLimitMiddleware>();
 | 
			
		||||
            _counterHandler = counterHandler;
 | 
			
		||||
            _processor = new ClientRateLimitProcessor(counterHandler);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task Invoke(HttpContext context)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug("started calling RateLimit middleware");
 | 
			
		||||
            var options = DownstreamRoute.ReRoute.RateLimitOptions;
 | 
			
		||||
            // check if rate limiting is enabled
 | 
			
		||||
            if (!DownstreamRoute.ReRoute.EnableEndpointRateLimiting)
 | 
			
		||||
            {
 | 
			
		||||
                await _next.Invoke(context);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            // compute identity from request
 | 
			
		||||
            var identity = SetIdentity(context, options);
 | 
			
		||||
 | 
			
		||||
            // check white list
 | 
			
		||||
            if (IsWhitelisted(identity, options))
 | 
			
		||||
            {
 | 
			
		||||
                await _next.Invoke(context);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var rule = options.RateLimitRule;
 | 
			
		||||
            if (rule.Limit > 0)
 | 
			
		||||
            {
 | 
			
		||||
                // increment counter
 | 
			
		||||
                var counter = _processor.ProcessRequest(identity, options);
 | 
			
		||||
 | 
			
		||||
                // check if limit is reached
 | 
			
		||||
                if (counter.TotalRequests > rule.Limit)
 | 
			
		||||
                {
 | 
			
		||||
                    //compute retry after value
 | 
			
		||||
                    var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule);
 | 
			
		||||
 | 
			
		||||
                    // log blocked request
 | 
			
		||||
                    LogBlockedRequest(context, identity, counter, rule);
 | 
			
		||||
 | 
			
		||||
                    // break execution
 | 
			
		||||
                    await ReturnQuotaExceededResponse(context, options, retryAfter);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            //set X-Rate-Limit headers for the longest period
 | 
			
		||||
            if (!options.DisableRateLimitHeaders)
 | 
			
		||||
            {
 | 
			
		||||
                var headers = _processor.GetRateLimitHeaders(identity, options);
 | 
			
		||||
                headers.Context = context;
 | 
			
		||||
 | 
			
		||||
                context.Response.OnStarting(SetRateLimitHeaders, state: headers);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await _next.Invoke(context);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            var clientId = "client";
 | 
			
		||||
            if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader))
 | 
			
		||||
            {
 | 
			
		||||
                clientId = httpContext.Request.Headers[option.ClientIdHeader].First();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new ClientRequestIdentity
 | 
			
		||||
            {
 | 
			
		||||
                Path = httpContext.Request.Path.ToString().ToLowerInvariant(),
 | 
			
		||||
                HttpVerb = httpContext.Request.Method.ToLowerInvariant(),
 | 
			
		||||
                ClientId = clientId,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            if (option.ClientWhitelist != null && option.ClientWhitelist.Contains(requestIdentity.ClientId))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
 | 
			
		||||
        {
 | 
			
		||||
            _logger.LogDebug($"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { DownstreamRoute.ReRoute.UpstreamTemplate }, TraceIdentifier {httpContext.TraceIdentifier}.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter)
 | 
			
		||||
        {
 | 
			
		||||
            var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage;
 | 
			
		||||
 | 
			
		||||
            if (!option.DisableRateLimitHeaders)
 | 
			
		||||
            {
 | 
			
		||||
                httpContext.Response.Headers["Retry-After"] = retryAfter;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            httpContext.Response.StatusCode = option.HttpStatusCode;
 | 
			
		||||
            return httpContext.Response.WriteAsync(message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Task SetRateLimitHeaders(object rateLimitHeaders)
 | 
			
		||||
        {
 | 
			
		||||
            var headers = (RateLimitHeaders)rateLimitHeaders;
 | 
			
		||||
 | 
			
		||||
            headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit;
 | 
			
		||||
            headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining;
 | 
			
		||||
            headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset;
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit.Middleware
 | 
			
		||||
{
 | 
			
		||||
    public static class RateLimitMiddlewareExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
 | 
			
		||||
        {
 | 
			
		||||
            return builder.UseMiddleware<ClientRateLimitMiddleware>();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										123
									
								
								src/Ocelot/RateLimit/RateLimitCore.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/Ocelot/RateLimit/RateLimitCore.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
using Ocelot.Configuration;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    public class RateLimitCore
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IRateLimitCounterHandler _counterHandler;
 | 
			
		||||
        private static readonly object _processLocker = new object();
 | 
			
		||||
 | 
			
		||||
        public RateLimitCore(IRateLimitCounterHandler counterStore)
 | 
			
		||||
        {
 | 
			
		||||
            _counterHandler = counterStore;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            var counter = new RateLimitCounter
 | 
			
		||||
            {
 | 
			
		||||
                Timestamp = DateTime.UtcNow,
 | 
			
		||||
                TotalRequests = 1
 | 
			
		||||
            };
 | 
			
		||||
            var rule = option.RateLimitRule;
 | 
			
		||||
 | 
			
		||||
            var counterId = ComputeCounterKey(requestIdentity, option);
 | 
			
		||||
 | 
			
		||||
            // serial reads and writes
 | 
			
		||||
            lock (_processLocker)
 | 
			
		||||
            {
 | 
			
		||||
                var entry = _counterHandler.Get(counterId);
 | 
			
		||||
                if (entry.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
                    // entry has not expired
 | 
			
		||||
                    if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
 | 
			
		||||
                    {
 | 
			
		||||
                        // increment request count
 | 
			
		||||
                        var totalRequests = entry.Value.TotalRequests + 1;
 | 
			
		||||
 | 
			
		||||
                        // deep copy
 | 
			
		||||
                        counter = new RateLimitCounter
 | 
			
		||||
                        {
 | 
			
		||||
                            Timestamp = entry.Value.Timestamp,
 | 
			
		||||
                            TotalRequests = totalRequests
 | 
			
		||||
                        };
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // stores: id (string) - timestamp (datetime) - total_requests (long)
 | 
			
		||||
                _counterHandler.Set(counterId, counter, rule.PeriodTimespan.Value);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return counter;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            var rule = option.RateLimitRule;
 | 
			
		||||
            var headers = new RateLimitHeaders();
 | 
			
		||||
            var counterId = ComputeCounterKey(requestIdentity, option);
 | 
			
		||||
            var entry = _counterHandler.Get(counterId);
 | 
			
		||||
            if (entry.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                headers.Reset = (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
 | 
			
		||||
                headers.Limit = rule.Period;
 | 
			
		||||
                headers.Remaining = (rule.Limit - entry.Value.TotalRequests).ToString();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                headers.Reset = (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
 | 
			
		||||
                headers.Limit = rule.Period;
 | 
			
		||||
                headers.Remaining = rule.Limit.ToString();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return headers;
 | 
			
		||||
            throw new NotImplementedException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
			
		||||
        {
 | 
			
		||||
            var key =  $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
 | 
			
		||||
 | 
			
		||||
            var idBytes = System.Text.Encoding.UTF8.GetBytes(key);
 | 
			
		||||
 | 
			
		||||
            byte[] hashBytes;
 | 
			
		||||
 | 
			
		||||
            using (var algorithm = System.Security.Cryptography.SHA1.Create())
 | 
			
		||||
            {
 | 
			
		||||
                hashBytes = algorithm.ComputeHash(idBytes);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
 | 
			
		||||
        {
 | 
			
		||||
            var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
 | 
			
		||||
            var retryAfter = Convert.ToInt32(rule.PeriodTimespan.Value.TotalSeconds);
 | 
			
		||||
            retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
 | 
			
		||||
            return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public TimeSpan ConvertToTimeSpan(string timeSpan)
 | 
			
		||||
        {
 | 
			
		||||
            var l = timeSpan.Length - 1;
 | 
			
		||||
            var value = timeSpan.Substring(0, l);
 | 
			
		||||
            var type = timeSpan.Substring(l, 1);
 | 
			
		||||
 | 
			
		||||
            switch (type)
 | 
			
		||||
            {
 | 
			
		||||
                case "d": return TimeSpan.FromDays(double.Parse(value));
 | 
			
		||||
                case "h": return TimeSpan.FromHours(double.Parse(value));
 | 
			
		||||
                case "m": return TimeSpan.FromMinutes(double.Parse(value));
 | 
			
		||||
                case "s": return TimeSpan.FromSeconds(double.Parse(value));
 | 
			
		||||
                default: throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/Ocelot/RateLimit/RateLimitCounter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/Ocelot/RateLimit/RateLimitCounter.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Stores the initial access time and the numbers of calls made from that point
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public struct RateLimitCounter
 | 
			
		||||
    {
 | 
			
		||||
        public DateTime Timestamp { get; set; }
 | 
			
		||||
 | 
			
		||||
        public long TotalRequests { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/Ocelot/RateLimit/RateLimitHeaders.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/Ocelot/RateLimit/RateLimitHeaders.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Ocelot.RateLimit
 | 
			
		||||
{
 | 
			
		||||
    public class RateLimitHeaders
 | 
			
		||||
    {
 | 
			
		||||
        public HttpContext Context { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string Limit { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string Remaining { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string Reset { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user