merged develop and stolen binarymash dont publish unstable build script code

This commit is contained in:
TomPallister
2017-02-25 18:27:20 +00:00
46 changed files with 2985 additions and 212 deletions

View File

@ -28,10 +28,12 @@ namespace Ocelot.Configuration.Builder
private ServiceProviderConfiguraion _serviceProviderConfiguraion;
private bool _useQos;
private QoSOptions _qosOptions;
public bool _enableRateLimiting;
public RateLimitOptions _rateLimitOptions;
public ReRouteBuilder WithLoadBalancer(string loadBalancer)
{
_loadBalancer = loadBalancer;
_loadBalancer = loadBalancer;
return this;
}
@ -160,6 +162,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(
@ -184,7 +199,9 @@ namespace Ocelot.Configuration.Builder
_loadBalancerKey,
_serviceProviderConfiguraion,
_useQos,
_qosOptions);
_qosOptions,
_enableRateLimiting,
_rateLimitOptions);
}
}
}

View File

@ -116,13 +116,17 @@ namespace Ocelot.Configuration.Creator
var authOptionsForRoute = BuildAuthenticationOptions(fileReRoute);
var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest);
var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest);
var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest);
var qosOptions = BuildQoSOptions(fileReRoute);
var enableRateLimiting = IsEnableRateLimiting(fileReRoute);
var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting);
var reRoute = new ReRouteBuilder()
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
@ -146,13 +150,34 @@ namespace Ocelot.Configuration.Creator
.WithServiceProviderConfiguraion(serviceProviderConfiguration)
.WithIsQos(isQos)
.WithQosOptions(qosOptions)
.Build();
.WithEnableRateLimiting(enableRateLimiting)
.WithRateLimitOptions(rateLimitOption)
.Build();
await SetupLoadBalancer(reRoute);
SetupQosProvider(reRoute);
return reRoute;
}
private static RateLimitOptions BuildRateLimitOptions(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting)
{
RateLimitOptions rateLimitOption = null;
if (enableRateLimiting)
{
rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader,
fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders,
globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix,
new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit)
, globalConfiguration.RateLimitOptions.HttpStatusCode);
}
return rateLimitOption;
}
private static bool IsEnableRateLimiting(FileReRoute fileReRoute)
{
return (fileReRoute.RateLimitOptions != null && fileReRoute.RateLimitOptions.EnableRateLimiting) ? true : false;
}
private QoSOptions BuildQoSOptions(FileReRoute fileReRoute)
{
return new QoSOptionsBuilder()

View File

@ -6,10 +6,14 @@ namespace Ocelot.Configuration.File
public FileGlobalConfiguration()
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider();
RateLimitOptions = new FileRateLimitOptions();
}
public string RequestIdKey { get; set; }
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
public string AdministrationPath {get;set;}
public FileRateLimitOptions RateLimitOptions { get; set; }
}
}

View File

@ -0,0 +1,22 @@

namespace Ocelot.Configuration.File
{
public class FileGlobalConfiguration
{
public FileGlobalConfiguration()
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider();
RateLimitOptions = new FileRateLimitOptions();
}
public string RequestIdKey { get; set; }
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
<<<<<<< HEAD
public string AdministrationPath {get;set;}
=======
public FileRateLimitOptions RateLimitOptions { get; set; }
>>>>>>> develop
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Configuration.File
{
public class FileRateLimitOptions
{
/// <summary>
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
/// </summary>
public string ClientIdHeader { get; set; } = "ClientId";
/// <summary>
/// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
/// If none specified the default will be:
/// API calls quota exceeded! maximum admitted {0} per {1}
/// </summary>
public string QuotaExceededMessage { get; set; }
/// <summary>
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
/// </summary>
public string RateLimitCounterPrefix { get; set; } = "ocelot";
/// <summary>
/// Disables X-Rate-Limit and Rety-After headers
/// </summary>
public bool DisableRateLimitHeaders { get; set; }
/// <summary>
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
/// </summary>
public int HttpStatusCode { get; set; } = 429;
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Configuration.File
{
public class FileRateLimitRule
{
public FileRateLimitRule()
{
ClientWhitelist = new List<string>();
}
public List<string> ClientWhitelist { get; set; }
/// <summary>
/// Enables endpoint rate limiting based URL path and HTTP verb
/// </summary>
public bool EnableRateLimiting { get; set; }
/// <summary>
/// Rate limit period as in 1s, 1m, 1h
/// </summary>
public string Period { get; set; }
public double PeriodTimespan { get; set; }
/// <summary>
/// Maximum number of requests that a client can make in a defined period
/// </summary>
public long Limit { get; set; }
}
}

View File

@ -13,6 +13,7 @@ namespace Ocelot.Configuration.File
AuthenticationOptions = new FileAuthenticationOptions();
FileCacheOptions = new FileCacheOptions();
QoSOptions = new FileQoSOptions();
RateLimitOptions = new FileRateLimitRule();
}
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 FileRateLimitRule RateLimitOptions { get; set; }
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Configuration
{
/// <summary>
/// RateLimit Options
/// </summary>
public class RateLimitOptions
{
public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List<string> clientWhitelist,bool disableRateLimitHeaders,
string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
{
EnableRateLimiting = enbleRateLimiting;
ClientIdHeader = clientIdHeader;
ClientWhitelist = clientWhitelist?? new List<string>();
DisableRateLimitHeaders = disableRateLimitHeaders;
QuotaExceededMessage = quotaExceededMessage;
RateLimitCounterPrefix = rateLimitCounterPrefix;
RateLimitRule = rateLimitRule;
HttpStatusCode = httpStatusCode;
}
public RateLimitRule RateLimitRule { get; private set; }
public List<string> ClientWhitelist { get; private set; }
/// <summary>
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
/// </summary>
public string ClientIdHeader { get; private set; }
/// <summary>
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
/// </summary>
public int HttpStatusCode { get; private set; }
/// <summary>
/// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
/// If none specified the default will be:
/// API calls quota exceeded! maximum admitted {0} per {1}
/// </summary>
public string QuotaExceededMessage { get; private set; }
/// <summary>
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
/// </summary>
public string RateLimitCounterPrefix { get; private set; }
/// <summary>
/// Enables endpoint rate limiting based URL path and HTTP verb
/// </summary>
public bool EnableRateLimiting { get; private set; }
/// <summary>
/// Disables X-Rate-Limit and Rety-After headers
/// </summary>
public bool DisableRateLimitHeaders { get; private set; }
}
public class RateLimitRule
{
public RateLimitRule(string period, TimeSpan periodTimespan, long limit)
{
Period = period;
PeriodTimespan = periodTimespan;
Limit = limit;
}
/// <summary>
/// Rate limit period as in 1s, 1m, 1h,1d
/// </summary>
public string Period { get; private set; }
public TimeSpan PeriodTimespan { get; private set; }
/// <summary>
/// Maximum number of requests that a client can make in a defined period
/// </summary>
public long Limit { get; private set; }
}
}

View File

@ -27,7 +27,9 @@ namespace Ocelot.Configuration
string reRouteKey,
ServiceProviderConfiguraion serviceProviderConfiguraion,
bool isQos,
QoSOptions qos)
QoSOptions qos,
bool enableRateLimit,
RateLimitOptions ratelimitOptions)
{
ReRouteKey = reRouteKey;
ServiceProviderConfiguraion = serviceProviderConfiguraion;
@ -54,6 +56,8 @@ namespace Ocelot.Configuration
DownstreamScheme = downstreamScheme;
IsQos = isQos;
QosOptions = qos;
EnableEndpointRateLimiting = enableRateLimit;
RateLimitOptions = ratelimitOptions;
}
public string ReRouteKey {get;private set;}
@ -78,5 +82,7 @@ namespace Ocelot.Configuration
public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; }
public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; }
public bool EnableEndpointRateLimiting { get; private set; }
public RateLimitOptions RateLimitOptions { get; private set; }
}
}

View File

@ -37,6 +37,7 @@ using Ocelot.Requester.QoS;
using Ocelot.Responder;
using Ocelot.ServiceDiscovery;
using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider;
using Ocelot.RateLimit;
namespace Ocelot.DependencyInjection
{
@ -133,12 +134,13 @@ 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
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
services.AddMemoryCache();
return services;
}
}

View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using CacheManager.Core;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Authentication.Handler.Creator;
using Ocelot.Authentication.Handler.Factory;
using Ocelot.Authorisation;
using Ocelot.Cache;
using Ocelot.Claims;
using Ocelot.Configuration.Authentication;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Provider;
using Ocelot.Configuration.Repository;
using Ocelot.Configuration.Setter;
using Ocelot.Configuration.Validator;
using Ocelot.DownstreamRouteFinder.Finder;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.DownstreamUrlCreator;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Headers;
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Infrastructure.RequestData;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Logging;
using Ocelot.Middleware;
using Ocelot.QueryStrings;
using Ocelot.Request.Builder;
using Ocelot.Requester;
using Ocelot.Requester.QoS;
using Ocelot.Responder;
using Ocelot.ServiceDiscovery;
<<<<<<< HEAD
using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider;
=======
using Ocelot.RateLimit;
>>>>>>> develop
namespace Ocelot.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action<ConfigurationBuilderCachePart> settings)
{
var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings);
var ocelotCacheManager = new OcelotCacheManagerCache<HttpResponseMessage>(cacheManagerOutputCache);
services.AddSingleton<ICacheManager<HttpResponseMessage>>(cacheManagerOutputCache);
services.AddSingleton<IOcelotCache<HttpResponseMessage>>(ocelotCacheManager);
return services;
}
public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot)
{
services.Configure<FileConfiguration>(configurationRoot);
services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>();
services.AddSingleton<IBaseUrlFinder, BaseUrlFinder>();
var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration();
if(identityServerConfiguration != null)
{
services.AddSingleton<IIdentityServerConfiguration>(identityServerConfiguration);
services.AddSingleton<IHashMatcher, HashMatcher>();
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(new List<ApiResource>
{
new ApiResource
{
Name = identityServerConfiguration.ApiName,
Description = identityServerConfiguration.Description,
Enabled = identityServerConfiguration.Enabled,
DisplayName = identityServerConfiguration.ApiName,
Scopes = identityServerConfiguration.AllowedScopes.Select(x => new Scope(x)).ToList(),
ApiSecrets = new List<Secret>
{
new Secret
{
Value = identityServerConfiguration.ApiSecret.Sha256()
}
}
}
})
.AddInMemoryClients(new List<Client>
{
new Client
{
ClientId = identityServerConfiguration.ApiName,
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = new List<Secret> {new Secret(identityServerConfiguration.ApiSecret.Sha256())},
AllowedScopes = identityServerConfiguration.AllowedScopes,
AccessTokenType = identityServerConfiguration.AccessTokenType,
Enabled = identityServerConfiguration.Enabled,
RequireClientSecret = identityServerConfiguration.RequireClientSecret
}
}).AddResourceOwnerValidator<OcelotResourceOwnerPasswordValidator>();
}
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddLogging();
services.AddSingleton<IFileConfigurationRepository, FileConfigurationRepository>();
services.AddSingleton<IFileConfigurationSetter, FileConfigurationSetter>();
services.AddSingleton<IFileConfigurationProvider, FileConfigurationProvider>();
services.AddSingleton<IQosProviderHouse, QosProviderHouse>();
services.AddSingleton<IQoSProviderFactory, QoSProviderFactory>();
services.AddSingleton<IServiceDiscoveryProviderFactory, ServiceDiscoveryProviderFactory>();
services.AddSingleton<ILoadBalancerFactory, LoadBalancerFactory>();
services.AddSingleton<ILoadBalancerHouse, LoadBalancerHouse>();
services.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
services.AddSingleton<IUrlBuilder, UrlBuilder>();
services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
services.AddSingleton<IAuthoriser, ClaimsAuthoriser>();
services.AddSingleton<IAddClaimsToRequest, AddClaimsToRequest>();
services.AddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
services.AddSingleton<IAddQueriesToRequest, AddQueriesToRequest>();
services.AddSingleton<IClaimsParser, ClaimsParser>();
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();
services.AddSingleton<IUrlPathPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>();
services.AddSingleton<IDownstreamPathPlaceholderReplacer, DownstreamTemplatePathPlaceholderReplacer>();
services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>();
services.AddSingleton<IHttpResponder, HttpContextResponder>();
services.AddSingleton<IRequestCreator, HttpRequestCreator>();
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
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
services.AddMemoryCache();
return services;
}
}
}

View File

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

View File

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public class ClientRateLimitProcessor
{
private readonly IRateLimitCounterHandler _counterHandler;
private readonly RateLimitCore _core;
public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler)
{
_counterHandler = counterHandler;
_core = new RateLimitCore(_counterHandler);
}
public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
return _core.ProcessRequest(requestIdentity, option);
}
public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
{
return _core.RetryAfterFrom(timestamp, rule);
}
public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
return _core.GetRateLimitHeaders(context, requestIdentity, option);
}
}
}

View File

@ -0,0 +1,18 @@
namespace Ocelot.RateLimit
{
public class ClientRequestIdentity
{
public ClientRequestIdentity(string clientId, string path, string httpverb)
{
ClientId = clientId;
Path = path;
HttpVerb = httpverb;
}
public string ClientId { get; private set; }
public string Path { get; private set; }
public string HttpVerb { get; private set; }
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public class DistributedCacheRateLimitCounterHanlder : IRateLimitCounterHandler
{
private readonly IDistributedCache _memoryCache;
public DistributedCacheRateLimitCounterHanlder(IDistributedCache memoryCache)
{
_memoryCache = memoryCache;
}
public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
{
_memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
}
public bool Exists(string id)
{
var stored = _memoryCache.GetString(id);
return !string.IsNullOrEmpty(stored);
}
public RateLimitCounter? Get(string id)
{
var stored = _memoryCache.GetString(id);
if (!string.IsNullOrEmpty(stored))
{
return JsonConvert.DeserializeObject<RateLimitCounter>(stored);
}
return null;
}
public void Remove(string id)
{
_memoryCache.Remove(id);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public interface IRateLimitCounterHandler
{
bool Exists(string id);
RateLimitCounter? Get(string id);
void Remove(string id);
void Set(string id, RateLimitCounter counter, TimeSpan expirationTime);
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler
{
private readonly IMemoryCache _memoryCache;
public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
{
_memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
}
public bool Exists(string id)
{
RateLimitCounter counter;
return _memoryCache.TryGetValue(id, out counter);
}
public RateLimitCounter? Get(string id)
{
RateLimitCounter counter;
if (_memoryCache.TryGetValue(id, out counter))
{
return counter;
}
return null;
}
public void Remove(string id)
{
_memoryCache.Remove(id);
}
}
}

View File

@ -0,0 +1,138 @@
using Ocelot.Middleware;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ocelot.Infrastructure.RequestData;
using Microsoft.AspNetCore.Http;
using Ocelot.Logging;
using Ocelot.Configuration;
namespace Ocelot.RateLimit.Middleware
{
public class ClientRateLimitMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly IOcelotLogger _logger;
private readonly IRateLimitCounterHandler _counterHandler;
private readonly ClientRateLimitProcessor _processor;
public ClientRateLimitMiddleware(RequestDelegate next,
IOcelotLoggerFactory loggerFactory,
IRequestScopedDataRepository requestScopedDataRepository,
IRateLimitCounterHandler counterHandler)
: base(requestScopedDataRepository)
{
_next = next;
_logger = loggerFactory.CreateLogger<ClientRateLimitMiddleware>();
_counterHandler = counterHandler;
_processor = new ClientRateLimitProcessor(counterHandler);
}
public async Task Invoke(HttpContext context)
{
_logger.LogDebug("started calling RateLimit middleware");
var options = DownstreamRoute.ReRoute.RateLimitOptions;
// check if rate limiting is enabled
if (!DownstreamRoute.ReRoute.EnableEndpointRateLimiting)
{
await _next.Invoke(context);
return;
}
// compute identity from request
var identity = SetIdentity(context, options);
// check white list
if (IsWhitelisted(identity, options))
{
await _next.Invoke(context);
return;
}
var rule = options.RateLimitRule;
if (rule.Limit > 0)
{
// increment counter
var counter = _processor.ProcessRequest(identity, options);
// check if limit is reached
if (counter.TotalRequests > rule.Limit)
{
//compute retry after value
var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule);
// log blocked request
LogBlockedRequest(context, identity, counter, rule);
// break execution
await ReturnQuotaExceededResponse(context, options, retryAfter);
return;
}
}
//set X-Rate-Limit headers for the longest period
if (!options.DisableRateLimitHeaders)
{
var headers = _processor.GetRateLimitHeaders( context,identity, options);
context.Response.OnStarting(SetRateLimitHeaders, state: headers);
}
await _next.Invoke(context);
}
public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option)
{
var clientId = "client";
if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader))
{
clientId = httpContext.Request.Headers[option.ClientIdHeader].First();
}
return new ClientRequestIdentity(
clientId,
httpContext.Request.Path.ToString().ToLowerInvariant(),
httpContext.Request.Method.ToLowerInvariant()
);
}
public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
if (option.ClientWhitelist.Contains(requestIdentity.ClientId))
{
return true;
}
return false;
}
public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
{
_logger.LogDebug($"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { DownstreamRoute.ReRoute.UpstreamPathTemplate }, TraceIdentifier {httpContext.TraceIdentifier}.");
}
public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter)
{
var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage;
if (!option.DisableRateLimitHeaders)
{
httpContext.Response.Headers["Retry-After"] = retryAfter;
}
httpContext.Response.StatusCode = option.HttpStatusCode;
return httpContext.Response.WriteAsync(message);
}
private Task SetRateLimitHeaders(object rateLimitHeaders)
{
var headers = (RateLimitHeaders)rateLimitHeaders;
headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit;
headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining;
headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset;
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit.Middleware
{
public static class RateLimitMiddlewareExtensions
{
public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ClientRateLimitMiddleware>();
}
}
}

View File

@ -0,0 +1,125 @@
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public class RateLimitCore
{
private readonly IRateLimitCounterHandler _counterHandler;
private static readonly object _processLocker = new object();
public RateLimitCore(IRateLimitCounterHandler counterStore)
{
_counterHandler = counterStore;
}
public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1);
var rule = option.RateLimitRule;
var counterId = ComputeCounterKey(requestIdentity, option);
// serial reads and writes
lock (_processLocker)
{
var entry = _counterHandler.Get(counterId);
if (entry.HasValue)
{
// entry has not expired
if (entry.Value.Timestamp + rule.PeriodTimespan >= DateTime.UtcNow)
{
// increment request count
var totalRequests = entry.Value.TotalRequests + 1;
// deep copy
counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
}
}
// stores: id (string) - timestamp (datetime) - total_requests (long)
_counterHandler.Set(counterId, counter, rule.PeriodTimespan);
}
return counter;
}
public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
var rule = option.RateLimitRule;
RateLimitHeaders headers = null;
var counterId = ComputeCounterKey(requestIdentity, option);
var entry = _counterHandler.Get(counterId);
if (entry.HasValue)
{
headers = new RateLimitHeaders(context, rule.Period,
(rule.Limit - entry.Value.TotalRequests).ToString(),
(entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
);
}
else
{
headers = new RateLimitHeaders(context,
rule.Period,
rule.Limit.ToString(),
(DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo));
}
return headers;
}
public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
{
var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
var idBytes = Encoding.UTF8.GetBytes(key);
byte[] hashBytes;
using (var algorithm = SHA1.Create())
{
hashBytes = algorithm.ComputeHash(idBytes);
}
return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
}
public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
{
var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
var retryAfter = Convert.ToInt32(rule.PeriodTimespan.TotalSeconds);
retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
public TimeSpan ConvertToTimeSpan(string timeSpan)
{
var l = timeSpan.Length - 1;
var value = timeSpan.Substring(0, l);
var type = timeSpan.Substring(l, 1);
switch (type)
{
case "d":
return TimeSpan.FromDays(double.Parse(value));
case "h":
return TimeSpan.FromHours(double.Parse(value));
case "m":
return TimeSpan.FromMinutes(double.Parse(value));
case "s":
return TimeSpan.FromSeconds(double.Parse(value));
default:
throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
}
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
/// <summary>
/// Stores the initial access time and the numbers of calls made from that point
/// </summary>
public struct RateLimitCounter
{
public RateLimitCounter(DateTime timestamp, long totalRequest)
{
Timestamp = timestamp;
TotalRequests = totalRequest;
}
public DateTime Timestamp { get; private set; }
public long TotalRequests { get; private set; }
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.RateLimit
{
public class RateLimitHeaders
{
public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset)
{
Context = context;
Limit = limit;
Remaining = remaining;
Reset = reset;
}
public HttpContext Context { get; private set; }
public string Limit { get; private set; }
public string Remaining { get; private set; }
public string Reset { get; private set; }
}
}

View File

@ -1,37 +1,45 @@
{
"version": "0.0.0-dev",
"dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0",
"System.Text.RegularExpressions": "4.3.0",
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0",
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0",
"Microsoft.AspNetCore.Authentication.Cookies": "1.1.0",
"Microsoft.AspNetCore.Authentication.Google": "1.1.0",
"Microsoft.AspNetCore.Authentication.Facebook": "1.1.0",
"Microsoft.AspNetCore.Authentication.Twitter": "1.1.0",
"Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0",
"Microsoft.AspNetCore.Authentication": "1.1.0",
"IdentityServer4.AccessTokenValidation": "1.0.2",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0",
"CacheManager.Core": "0.9.2",
"CacheManager.Microsoft.Extensions.Configuration": "0.9.2",
"CacheManager.Microsoft.Extensions.Logging": "0.9.2",
"Consul": "0.7.2.1",
"Polly": "5.0.3",
"IdentityServer4": "1.0.1",
"Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0"
},
"title": "Ocelot",
"summary": "API Gateway created using .NET core.",
"projectUrl": "https://github.com/TomPallister/Ocelot",
"description": "This project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features.",
"tags": [
"API Gateway",
".NET core"
],
"dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0",
"System.Text.RegularExpressions": "4.3.0",
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0",
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0",
"Microsoft.AspNetCore.Authentication.Cookies": "1.1.0",
"Microsoft.AspNetCore.Authentication.Google": "1.1.0",
"Microsoft.AspNetCore.Authentication.Facebook": "1.1.0",
"Microsoft.AspNetCore.Authentication.Twitter": "1.1.0",
"Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0",
"Microsoft.AspNetCore.Authentication": "1.1.0",
"IdentityServer4.AccessTokenValidation": "1.0.2",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0",
"CacheManager.Core": "0.9.2",
"CacheManager.Microsoft.Extensions.Configuration": "0.9.2",
"CacheManager.Microsoft.Extensions.Logging": "0.9.2",
"Consul": "0.7.2.1",
"Polly": "5.0.3",
"IdentityServer4": "1.0.1",
"Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0"
},
"runtimes": {
"win10-x64": {},
"osx.10.11-x64": {},

View File

@ -0,0 +1,88 @@
{
"version": "0.0.0-dev",
<<<<<<< HEAD
"dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0",
"System.Text.RegularExpressions": "4.3.0",
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0",
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0",
"Microsoft.AspNetCore.Authentication.Cookies": "1.1.0",
"Microsoft.AspNetCore.Authentication.Google": "1.1.0",
"Microsoft.AspNetCore.Authentication.Facebook": "1.1.0",
"Microsoft.AspNetCore.Authentication.Twitter": "1.1.0",
"Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0",
"Microsoft.AspNetCore.Authentication": "1.1.0",
"IdentityServer4.AccessTokenValidation": "1.0.2",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0",
"CacheManager.Core": "0.9.2",
"CacheManager.Microsoft.Extensions.Configuration": "0.9.2",
"CacheManager.Microsoft.Extensions.Logging": "0.9.2",
"Consul": "0.7.2.1",
"Polly": "5.0.3",
"IdentityServer4": "1.0.1",
"Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0"
},
=======
"title": "Ocelot",
"summary": "API Gateway created using .NET core.",
"projectUrl": "https://github.com/TomPallister/Ocelot",
"description": "This project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features.",
"tags": [
"API Gateway",
".NET core"
],
"dependencies": {
"Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.1.0",
"Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.Extensions.Logging.Console": "1.1.0",
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0",
"System.Text.RegularExpressions": "4.3.0",
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
"Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0",
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0",
"Microsoft.AspNetCore.Authentication.Cookies": "1.1.0",
"Microsoft.AspNetCore.Authentication.Google": "1.1.0",
"Microsoft.AspNetCore.Authentication.Facebook": "1.1.0",
"Microsoft.AspNetCore.Authentication.Twitter": "1.1.0",
"Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0",
"Microsoft.AspNetCore.Authentication": "1.1.0",
"IdentityServer4.AccessTokenValidation": "1.0.2",
"Microsoft.AspNetCore.Mvc": "1.1.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0",
"CacheManager.Core": "0.9.2",
"CacheManager.Microsoft.Extensions.Configuration": "0.9.2",
"CacheManager.Microsoft.Extensions.Logging": "0.9.2",
"Consul": "0.7.2.1",
"Polly": "5.0.3"
},
>>>>>>> develop
"runtimes": {
"win10-x64": {},
"osx.10.11-x64": {},
"osx.10.12-x64": {},
"win7-x64": {}
},
"frameworks": {
"netcoreapp1.1": {
"imports": [
]
}
}
}