mirror of
				https://github.com/nsnail/Ocelot.git
				synced 2025-11-04 19:50:49 +08:00 
			
		
		
		
	refactor code
This commit is contained in:
		@@ -117,12 +117,8 @@ namespace Ocelot.Configuration.Creator
 | 
				
			|||||||
                rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader, 
 | 
					                rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader, 
 | 
				
			||||||
                   fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders,
 | 
					                   fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders,
 | 
				
			||||||
                   globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix,
 | 
					                   globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix,
 | 
				
			||||||
                   new RateLimitRule()
 | 
					                   new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit)
 | 
				
			||||||
                   {
 | 
					                   , globalConfiguration.RateLimitOptions.HttpStatusCode);                
 | 
				
			||||||
                       Limit = fileReRoute.RateLimitOptions.Limit,
 | 
					 | 
				
			||||||
                       Period = fileReRoute.RateLimitOptions.Period,
 | 
					 | 
				
			||||||
                       PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan)
 | 
					 | 
				
			||||||
                   }, globalConfiguration.RateLimitOptions.HttpStatusCode);                
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
 | 
					            var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,7 @@ namespace Ocelot.Configuration.File
 | 
				
			|||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
 | 
					        /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public int HttpStatusCode { get; private set; } = 429;
 | 
					        public int HttpStatusCode { get;  set; } = 429;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ namespace Ocelot.Configuration.File
 | 
				
			|||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public string Period { get; set; }
 | 
					        public string Period { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public int PeriodTimespan { get; set; }
 | 
					        public double PeriodTimespan { get; set; }
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Maximum number of requests that a client can make in a defined period
 | 
					        /// Maximum number of requests that a client can make in a defined period
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,7 @@ namespace Ocelot.Configuration
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            EnableRateLimiting = enbleRateLimiting;
 | 
					            EnableRateLimiting = enbleRateLimiting;
 | 
				
			||||||
            ClientIdHeader = clientIdHeader;
 | 
					            ClientIdHeader = clientIdHeader;
 | 
				
			||||||
            ClientWhitelist = clientWhitelist;
 | 
					            ClientWhitelist = clientWhitelist?? new List<string>();
 | 
				
			||||||
            DisableRateLimitHeaders = disableRateLimitHeaders;
 | 
					            DisableRateLimitHeaders = disableRateLimitHeaders;
 | 
				
			||||||
            QuotaExceededMessage = quotaExceededMessage;
 | 
					            QuotaExceededMessage = quotaExceededMessage;
 | 
				
			||||||
            RateLimitCounterPrefix = rateLimitCounterPrefix;
 | 
					            RateLimitCounterPrefix = rateLimitCounterPrefix;
 | 
				
			||||||
@@ -62,15 +62,22 @@ namespace Ocelot.Configuration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public class RateLimitRule
 | 
					    public class RateLimitRule
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        /// <summary>
 | 
					        public RateLimitRule(string period, TimeSpan periodTimespan, long limit)
 | 
				
			||||||
        /// Rate limit period as in 1s, 1m, 1h
 | 
					        {
 | 
				
			||||||
        /// </summary>
 | 
					            Period = period;
 | 
				
			||||||
        public string Period { get; set; }
 | 
					            PeriodTimespan = periodTimespan;
 | 
				
			||||||
 | 
					            Limit = limit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public TimeSpan? PeriodTimespan { get; set; }
 | 
					        /// <summary>
 | 
				
			||||||
 | 
					        /// Rate limit period as in 1s, 1m, 1h,1d
 | 
				
			||||||
 | 
					        /// </summary>
 | 
				
			||||||
 | 
					        public string Period { get; private set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public TimeSpan PeriodTimespan { get; private set; }
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// Maximum number of requests that a client can make in a defined period
 | 
					        /// Maximum number of requests that a client can make in a defined period
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
        public long Limit { get; set; }
 | 
					        public long Limit { get; private set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -91,7 +91,7 @@ namespace Ocelot.DependencyInjection
 | 
				
			|||||||
            // could maybe use a scoped data repository
 | 
					            // could maybe use a scoped data repository
 | 
				
			||||||
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
 | 
					            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
 | 
				
			||||||
            services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
 | 
					            services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
 | 
				
			||||||
 | 
					            services.AddMemoryCache();
 | 
				
			||||||
            return services;
 | 
					            return services;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
using Ocelot.Configuration;
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
 | 
					using Ocelot.Configuration;
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
@@ -27,9 +28,9 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
            return _core.RetryAfterFrom(timestamp, rule);
 | 
					            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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,17 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public class ClientRequestIdentity
 | 
					    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; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -72,9 +72,7 @@ namespace Ocelot.RateLimit.Middleware
 | 
				
			|||||||
            //set X-Rate-Limit headers for the longest period
 | 
					            //set X-Rate-Limit headers for the longest period
 | 
				
			||||||
            if (!options.DisableRateLimitHeaders)
 | 
					            if (!options.DisableRateLimitHeaders)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var headers = _processor.GetRateLimitHeaders(identity, options);
 | 
					                var headers = _processor.GetRateLimitHeaders( context,identity, options);
 | 
				
			||||||
                headers.Context = context;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                context.Response.OnStarting(SetRateLimitHeaders, state: headers);
 | 
					                context.Response.OnStarting(SetRateLimitHeaders, state: headers);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,21 +87,19 @@ namespace Ocelot.RateLimit.Middleware
 | 
				
			|||||||
                clientId = httpContext.Request.Headers[option.ClientIdHeader].First();
 | 
					                clientId = httpContext.Request.Headers[option.ClientIdHeader].First();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return new ClientRequestIdentity
 | 
					            return new ClientRequestIdentity(
 | 
				
			||||||
            {
 | 
					                clientId,
 | 
				
			||||||
                Path = httpContext.Request.Path.ToString().ToLowerInvariant(),
 | 
					                httpContext.Request.Path.ToString().ToLowerInvariant(), 
 | 
				
			||||||
                HttpVerb = httpContext.Request.Method.ToLowerInvariant(),
 | 
					                httpContext.Request.Method.ToLowerInvariant()
 | 
				
			||||||
                ClientId = clientId,
 | 
					                );
 | 
				
			||||||
            };
 | 
					         }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
					        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 true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,11 @@
 | 
				
			|||||||
using Ocelot.Configuration;
 | 
					using Microsoft.AspNetCore.Http;
 | 
				
			||||||
 | 
					using Ocelot.Configuration;
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Globalization;
 | 
					using System.Globalization;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
 | 
					using System.Security.Cryptography;
 | 
				
			||||||
 | 
					using System.Text;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.RateLimit
 | 
					namespace Ocelot.RateLimit
 | 
				
			||||||
@@ -19,11 +22,7 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
					        public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var counter = new RateLimitCounter
 | 
					            RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1);
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Timestamp = DateTime.UtcNow,
 | 
					 | 
				
			||||||
                TotalRequests = 1
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
            var rule = option.RateLimitRule;
 | 
					            var rule = option.RateLimitRule;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var counterId = ComputeCounterKey(requestIdentity, option);
 | 
					            var counterId = ComputeCounterKey(requestIdentity, option);
 | 
				
			||||||
@@ -35,59 +34,57 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
                if (entry.HasValue)
 | 
					                if (entry.HasValue)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    // entry has not expired
 | 
					                    // entry has not expired
 | 
				
			||||||
                    if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
 | 
					                    if (entry.Value.Timestamp + rule.PeriodTimespan >= DateTime.UtcNow)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        // increment request count
 | 
					                        // increment request count
 | 
				
			||||||
                        var totalRequests = entry.Value.TotalRequests + 1;
 | 
					                        var totalRequests = entry.Value.TotalRequests + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        // deep copy
 | 
					                        // deep copy
 | 
				
			||||||
                        counter = new RateLimitCounter
 | 
					                        counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
 | 
				
			||||||
                        {
 | 
					                        
 | 
				
			||||||
                            Timestamp = entry.Value.Timestamp,
 | 
					 | 
				
			||||||
                            TotalRequests = totalRequests
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                // stores: id (string) - timestamp (datetime) - total_requests (long)
 | 
					                // stores: id (string) - timestamp (datetime) - total_requests (long)
 | 
				
			||||||
                _counterHandler.Set(counterId, counter, rule.PeriodTimespan.Value);
 | 
					                _counterHandler.Set(counterId, counter, rule.PeriodTimespan);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return counter;
 | 
					            return counter;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public RateLimitHeaders GetRateLimitHeaders(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
					        public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var rule = option.RateLimitRule;
 | 
					            var rule = option.RateLimitRule;
 | 
				
			||||||
            var headers = new RateLimitHeaders();
 | 
					            RateLimitHeaders headers = null;
 | 
				
			||||||
            var counterId = ComputeCounterKey(requestIdentity, option);
 | 
					            var counterId = ComputeCounterKey(requestIdentity, option);
 | 
				
			||||||
            var entry = _counterHandler.Get(counterId);
 | 
					            var entry = _counterHandler.Get(counterId);
 | 
				
			||||||
            if (entry.HasValue)
 | 
					            if (entry.HasValue)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                headers.Reset = (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
 | 
					                headers = new RateLimitHeaders(context, rule.Period,
 | 
				
			||||||
                headers.Limit = rule.Period;
 | 
					                    (rule.Limit - entry.Value.TotalRequests).ToString(),
 | 
				
			||||||
                headers.Remaining = (rule.Limit - entry.Value.TotalRequests).ToString();
 | 
					                    (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
 | 
				
			||||||
            }
 | 
					                    );
 | 
				
			||||||
 | 
					             }
 | 
				
			||||||
            else
 | 
					            else
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                headers.Reset = (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo);
 | 
					                headers = new RateLimitHeaders(context, 
 | 
				
			||||||
                headers.Limit = rule.Period;
 | 
					                    rule.Period,
 | 
				
			||||||
                headers.Remaining = rule.Limit.ToString();
 | 
					                    rule.Limit.ToString(), 
 | 
				
			||||||
 | 
					                    (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo));
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return headers;
 | 
					            return headers;
 | 
				
			||||||
            throw new NotImplementedException();
 | 
					         }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
					        public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var key =  $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
 | 
					            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;
 | 
					            byte[] hashBytes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            using (var algorithm = System.Security.Cryptography.SHA1.Create())
 | 
					            using (var algorithm =  SHA1.Create())
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                hashBytes = algorithm.ComputeHash(idBytes);
 | 
					                hashBytes = algorithm.ComputeHash(idBytes);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -98,7 +95,7 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
        public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
 | 
					        public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
 | 
					            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;
 | 
					            retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
 | 
				
			||||||
            return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
 | 
					            return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -111,11 +108,16 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            switch (type)
 | 
					            switch (type)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                case "d": return TimeSpan.FromDays(double.Parse(value));
 | 
					                case "d":
 | 
				
			||||||
                case "h": return TimeSpan.FromHours(double.Parse(value));
 | 
					                    return TimeSpan.FromDays(double.Parse(value));
 | 
				
			||||||
                case "m": return TimeSpan.FromMinutes(double.Parse(value));
 | 
					                case "h":
 | 
				
			||||||
                case "s": return TimeSpan.FromSeconds(double.Parse(value));
 | 
					                    return TimeSpan.FromHours(double.Parse(value));
 | 
				
			||||||
                default: throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
 | 
					                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}");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,8 +10,14 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    public struct RateLimitCounter
 | 
					    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; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,12 +8,20 @@ namespace Ocelot.RateLimit
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public class RateLimitHeaders
 | 
					    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; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,7 +67,9 @@ namespace Ocelot.AcceptanceTests
 | 
				
			|||||||
                        ClientIdHeader = "ClientId",
 | 
					                        ClientIdHeader = "ClientId",
 | 
				
			||||||
                        DisableRateLimitHeaders = false,
 | 
					                        DisableRateLimitHeaders = false,
 | 
				
			||||||
                        QuotaExceededMessage = "",
 | 
					                        QuotaExceededMessage = "",
 | 
				
			||||||
                        RateLimitCounterPrefix = ""                         
 | 
					                        RateLimitCounterPrefix = "",
 | 
				
			||||||
 | 
					                         HttpStatusCode = 428
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                     RequestIdKey ="oceclientrequest"
 | 
					                     RequestIdKey ="oceclientrequest"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -76,8 +78,12 @@ namespace Ocelot.AcceptanceTests
 | 
				
			|||||||
            this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit"))
 | 
					            this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit"))
 | 
				
			||||||
                .And(x => _steps.GivenThereIsAConfiguration(configuration))
 | 
					                .And(x => _steps.GivenThereIsAConfiguration(configuration))
 | 
				
			||||||
                .And(x => _steps.GivenOcelotIsRunning())
 | 
					                .And(x => _steps.GivenOcelotIsRunning())
 | 
				
			||||||
                .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 5))
 | 
					                .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1))
 | 
				
			||||||
                .Then(x => _steps.ThenTheStatusCodeShouldBe(429))
 | 
					                .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();
 | 
					                .BDDfy();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,55 +160,6 @@ namespace Ocelot.AcceptanceTests
 | 
				
			|||||||
            _builder.Start();
 | 
					            _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);
 | 
					 | 
				
			||||||
        //}
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -101,7 +101,6 @@ namespace Ocelot.AcceptanceTests
 | 
				
			|||||||
                        })
 | 
					                        })
 | 
				
			||||||
                        .WithDictionaryHandle();
 | 
					                        .WithDictionaryHandle();
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                    s.AddMemoryCache();
 | 
					 | 
				
			||||||
                    s.AddOcelotOutputCaching(settings);
 | 
					                    s.AddOcelotOutputCaching(settings);
 | 
				
			||||||
                    s.AddOcelotFileConfiguration(configuration);
 | 
					                    s.AddOcelotFileConfiguration(configuration);
 | 
				
			||||||
                    s.AddOcelot();
 | 
					                    s.AddOcelot();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}}}
 | 
					{"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}}}
 | 
				
			||||||
@@ -23,7 +23,6 @@
 | 
				
			|||||||
    "Microsoft.AspNetCore.Http": "1.1.0",
 | 
					    "Microsoft.AspNetCore.Http": "1.1.0",
 | 
				
			||||||
    "Microsoft.DotNet.InternalAbstractions": "1.0.0",
 | 
					    "Microsoft.DotNet.InternalAbstractions": "1.0.0",
 | 
				
			||||||
    "Ocelot": "0.0.0-dev",
 | 
					    "Ocelot": "0.0.0-dev",
 | 
				
			||||||
    "xunit": "2.2.0-beta2-build3300",
 | 
					 | 
				
			||||||
    "dotnet-test-xunit": "2.2.0-preview2-build1029",
 | 
					    "dotnet-test-xunit": "2.2.0-preview2-build1029",
 | 
				
			||||||
    "Ocelot.ManualTest": "0.0.0-dev",
 | 
					    "Ocelot.ManualTest": "0.0.0-dev",
 | 
				
			||||||
    "Microsoft.AspNetCore.TestHost": "1.1.0",
 | 
					    "Microsoft.AspNetCore.TestHost": "1.1.0",
 | 
				
			||||||
@@ -34,7 +33,8 @@
 | 
				
			|||||||
    "Shouldly": "2.8.2",
 | 
					    "Shouldly": "2.8.2",
 | 
				
			||||||
    "TestStack.BDDfy": "4.3.2",
 | 
					    "TestStack.BDDfy": "4.3.2",
 | 
				
			||||||
    "Consul": "0.7.2.1",
 | 
					    "Consul": "0.7.2.1",
 | 
				
			||||||
    "Microsoft.Extensions.Caching.Memory": "1.1.0"
 | 
					    "Microsoft.Extensions.Caching.Memory": "1.1.0",
 | 
				
			||||||
 | 
					    "xunit": "2.2.0-rc1-build3507"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "runtimes": {
 | 
					  "runtimes": {
 | 
				
			||||||
    "win10-x64": {},
 | 
					    "win10-x64": {},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,6 @@ namespace Ocelot.ManualTest
 | 
				
			|||||||
                })
 | 
					                })
 | 
				
			||||||
                .WithDictionaryHandle();
 | 
					                .WithDictionaryHandle();
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            services.AddMemoryCache();
 | 
					 | 
				
			||||||
            services.AddOcelotOutputCaching(settings);
 | 
					            services.AddOcelotOutputCaching(settings);
 | 
				
			||||||
            services.AddOcelotFileConfiguration(Configuration);
 | 
					            services.AddOcelotFileConfiguration(Configuration);
 | 
				
			||||||
            services.AddOcelot();
 | 
					            services.AddOcelot();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ using Xunit;
 | 
				
			|||||||
using TestStack.BDDfy;
 | 
					using TestStack.BDDfy;
 | 
				
			||||||
using Ocelot.Configuration.Builder;
 | 
					using Ocelot.Configuration.Builder;
 | 
				
			||||||
using Shouldly;
 | 
					using Shouldly;
 | 
				
			||||||
 | 
					using Ocelot.Configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Ocelot.UnitTests.RateLimit
 | 
					namespace Ocelot.UnitTests.RateLimit
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -70,11 +71,13 @@ namespace Ocelot.UnitTests.RateLimit
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            var downstreamRoute = new DownstreamRoute(new List<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
 | 
					            var downstreamRoute = new DownstreamRoute(new List<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
 | 
				
			||||||
                 new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
 | 
					                 new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
 | 
				
			||||||
                     new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429))
 | 
					                     new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>(), false, "", "", new Ocelot.Configuration.RateLimitRule("1s", TimeSpan.FromSeconds(100), 3), 429))
 | 
				
			||||||
                     .Build());
 | 
					                     .Build());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
 | 
					            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())
 | 
					                .Then(x => x.ThenresponseStatusCodeIs429())
 | 
				
			||||||
                .BDDfy();
 | 
					                .BDDfy();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -84,7 +87,7 @@ namespace Ocelot.UnitTests.RateLimit
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            var downstreamRoute = new DownstreamRoute(new List<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
 | 
					            var downstreamRoute = new DownstreamRoute(new List<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
 | 
				
			||||||
                 new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
 | 
					                 new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
 | 
				
			||||||
                     new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429))
 | 
					                     new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>() { "ocelotclient2" }, false, "", "", new  RateLimitRule( "1s", TimeSpan.FromSeconds(100),3),429))
 | 
				
			||||||
                     .Build());
 | 
					                     .Build());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
 | 
					            this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
 | 
				
			||||||
@@ -102,11 +105,11 @@ namespace Ocelot.UnitTests.RateLimit
 | 
				
			|||||||
                .Returns(_downstreamRoute);
 | 
					                .Returns(_downstreamRoute);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void WhenICallTheMiddleware()
 | 
					        private void WhenICallTheMiddlewareMultipleTime(int times)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var clientId = "ocelotclient1";
 | 
					            var clientId = "ocelotclient1";
 | 
				
			||||||
            // Act    
 | 
					            // Act    
 | 
				
			||||||
            for (int i = 0; i <10; i++)
 | 
					            for (int i = 0; i < times; i++)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
 | 
					                var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
 | 
				
			||||||
                request.Headers.Add("ClientId", clientId);
 | 
					                request.Headers.Add("ClientId", clientId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,6 @@
 | 
				
			|||||||
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
 | 
					    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
 | 
				
			||||||
    "Microsoft.AspNetCore.Http": "1.1.0",
 | 
					    "Microsoft.AspNetCore.Http": "1.1.0",
 | 
				
			||||||
    "Ocelot": "0.0.0-dev",
 | 
					    "Ocelot": "0.0.0-dev",
 | 
				
			||||||
    "xunit": "2.2.0-beta2-build3300",
 | 
					 | 
				
			||||||
    "dotnet-test-xunit": "2.2.0-preview2-build1029",
 | 
					    "dotnet-test-xunit": "2.2.0-preview2-build1029",
 | 
				
			||||||
    "Moq": "4.6.38-alpha",
 | 
					    "Moq": "4.6.38-alpha",
 | 
				
			||||||
    "Microsoft.AspNetCore.TestHost": "1.1.0",
 | 
					    "Microsoft.AspNetCore.TestHost": "1.1.0",
 | 
				
			||||||
@@ -24,7 +23,8 @@
 | 
				
			|||||||
    "Shouldly": "2.8.2",
 | 
					    "Shouldly": "2.8.2",
 | 
				
			||||||
    "TestStack.BDDfy": "4.3.2",
 | 
					    "TestStack.BDDfy": "4.3.2",
 | 
				
			||||||
    "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
 | 
					    "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
 | 
				
			||||||
    "Microsoft.DotNet.InternalAbstractions": "1.0.0"
 | 
					    "Microsoft.DotNet.InternalAbstractions": "1.0.0",
 | 
				
			||||||
 | 
					    "xunit": "2.2.0-rc1-build3507"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "runtimes": {
 | 
					  "runtimes": {
 | 
				
			||||||
    "win10-x64": {},
 | 
					    "win10-x64": {},
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user