From 3fd4e9ced05c59cafbe5cbd41d8412c161564037 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Sun, 18 Dec 2022 09:07:49 +0300 Subject: [PATCH 01/10] Added support for AspNetCore 7 rate limiting --- docs/docfx/articles/rate-limiting.md | 78 +++++++++++++++++++ samples/KubernetesIngress.Sample/README.md | 8 ++ .../Converters/YarpIngressOptions.cs | 1 + .../Converters/YarpParser.cs | 9 ++- .../ConfigurationConfigProvider.cs | 1 + .../Configuration/ConfigValidator.cs | 42 ++++++++++ .../Configuration/RateLimitingConstants.cs | 9 +++ src/ReverseProxy/Configuration/RouteConfig.cs | 28 +++++-- .../Routing/ProxyEndpointFactory.cs | 18 +++++ .../testassets/annotations/ingress.yaml | 1 + .../testassets/annotations/routes.json | 1 + .../testassets/basic-ingress/routes.json | 1 + .../testassets/exact-match/routes.json | 1 + .../testassets/hostname-routing/routes.json | 1 + .../testassets/https/routes.json | 1 + .../testassets/mapped-port/routes.json | 1 + .../multiple-endpoints-ports/routes.json | 1 + .../testassets/multiple-hosts/routes.json | 3 + .../multiple-ingresses-one-svc/routes.json | 2 + .../testassets/multiple-ingresses/routes.json | 2 + .../multiple-namespaces/routes.json | 2 + .../testassets/route-headers/routes.json | 1 + .../testassets/route-metadata/routes.json | 1 + .../testassets/route-order/routes.json | 1 + .../ConfigurationConfigProviderTests.cs | 3 + .../Configuration/RouteConfigTests.cs | 6 ++ .../Routing/ProxyEndpointFactoryTests.cs | 75 ++++++++++++++++++ 27 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 docs/docfx/articles/rate-limiting.md create mode 100644 src/ReverseProxy/Configuration/RateLimitingConstants.cs diff --git a/docs/docfx/articles/rate-limiting.md b/docs/docfx/articles/rate-limiting.md new file mode 100644 index 000000000..7e29d7c92 --- /dev/null +++ b/docs/docfx/articles/rate-limiting.md @@ -0,0 +1,78 @@ +# Rate Limiting + +## Introduction +The reverse proxy can be used to rate-limit requests before they are proxied to the destination servers. This can reduce load on the destination servers, add a layer of protection, and ensure consistent policies are implemented across your applications. + +## Defaults + +No rate limiting is performed on requests unless enabled in the route or application configuration. + +## Configuration +Rate Limiter policies can be specified per route via [RouteConfig.RateLimiterPolicy](xref:Yarp.ReverseProxy.Configuration.RouteConfig) and can be bound from the `Routes` sections of the config file. As with other route properties, this can be modified and reloaded without restarting the proxy. Policy names are case insensitive. + +Example: +```JSON +{ + "ReverseProxy": { + "Routes": { + "route1" : { + "ClusterId": "cluster1", + "RateLimiterPolicy": "customPolicy", + "Match": { + "Hosts": [ "localhost" ] + }, + } + }, + "Clusters": { + "cluster1": { + "Destinations": { + "cluster1/destination1": { + "Address": "https://localhost:10001/" + } + } + } + } + } +} +``` + +[RateLimiter policies](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware. + +RateLimiter policies can be configured in Startup.ConfigureServices as follows: +``` +public void ConfigureServices(IServiceCollection services) +{ + services.AddRateLimiter(options => + { + options.AddFixedWindowLimiter("customPolicy", opt => + { + opt.PermitLimit = 4; + opt.Window = TimeSpan.FromSeconds(12); + opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + opt.QueueLimit = 2; + }); + }); +} +``` + +In Startup.Configure add the RateLimiter middleware between Routing and Endpoints. + +``` +public void Configure(IApplicationBuilder app) +{ + app.UseRouting(); + + app.UseRateLimiter(); + + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxy(); + }); +} +``` + +See the [Rate Limiting](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) docs for setting up your preferred kind of rate limiting. + +### Disable CORS + +Specifying the value `disable` in a route's `RateLimiterPolicy` parameter means the rate limiter middleware will not rate limit requests. diff --git a/samples/KubernetesIngress.Sample/README.md b/samples/KubernetesIngress.Sample/README.md index 43b317136..dce8af4b1 100644 --- a/samples/KubernetesIngress.Sample/README.md +++ b/samples/KubernetesIngress.Sample/README.md @@ -42,6 +42,7 @@ metadata: namespace: default annotations: yarp.ingress.kubernetes.io/authorization-policy: authzpolicy + yarp.ingress.kubernetes.io/rate-limiter-policy: ratelimiterpolicy yarp.ingress.kubernetes.io/transforms: | - PathRemovePrefix: "/apis" yarp.ingress.kubernetes.io/route-headers: | @@ -73,6 +74,7 @@ The table below lists the available annotations. |Annotation|Data Type| |---|---| |yarp.ingress.kubernetes.io/authorization-policy|string| +|yarp.ingress.kubernetes.io/rate-limiter-policy|string| |yarp.ingress.kubernetes.io/backend-protocol|string| |yarp.ingress.kubernetes.io/cors-policy|string| |yarp.ingress.kubernetes.io/health-check|[ActivateHealthCheckConfig](https://microsoft.github.io/reverse-proxy/api/Yarp.ReverseProxy.Configuration.ActiveHealthCheckConfig.html)| @@ -90,6 +92,12 @@ See https://microsoft.github.io/reverse-proxy/articles/authn-authz.html for a li `yarp.ingress.kubernetes.io/authorization-policy: anonymous` +#### RateLimiter Policy + +See https://microsoft.github.io/reverse-proxy/articles/rate-limiting.html for a list of available policies, or how to add your own custom policies. + +`yarp.ingress.kubernetes.io/rate-limiter-policy: mypolicy` + #### Backend Protocol Specifies the protocol of the backend service. Defaults to http. diff --git a/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs b/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs index 164ecfbd4..25f30a95f 100644 --- a/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs +++ b/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs @@ -11,6 +11,7 @@ internal sealed class YarpIngressOptions public bool Https { get; set; } public List> Transforms { get; set; } public string AuthorizationPolicy { get; set; } + public string RateLimiterPolicy { get; set; } public SessionAffinityConfig SessionAffinity { get; set; } public HttpClientConfig HttpClientConfig { get; set; } public string LoadBalancingPolicy { get; set; } diff --git a/src/Kubernetes.Controller/Converters/YarpParser.cs b/src/Kubernetes.Controller/Converters/YarpParser.cs index 4e3092c1e..48e555fd1 100644 --- a/src/Kubernetes.Controller/Converters/YarpParser.cs +++ b/src/Kubernetes.Controller/Converters/YarpParser.cs @@ -104,6 +104,7 @@ private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1S RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}", Transforms = ingressContext.Options.Transforms, AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy, + RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy, CorsPolicy = ingressContext.Options.CorsPolicy, Metadata = ingressContext.Options.RouteMetadata, Order = ingressContext.Options.RouteOrder, @@ -171,16 +172,20 @@ private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, if (annotations.TryGetValue("yarp.ingress.kubernetes.io/backend-protocol", out var http)) { - options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase); + options.Https = http.Equals("https", StringComparison.OrdinalIgnoreCase); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/transforms", out var transforms)) { - options.Transforms = YamlDeserializer.Deserialize>>(transforms); + options.Transforms = YamlDeserializer.Deserialize>>(transforms); } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/authorization-policy", out var authorizationPolicy)) { options.AuthorizationPolicy = authorizationPolicy; } + if (annotations.TryGetValue("yarp.ingress.kubernetes.io/rate-limiter-policy", out var rateLimiterPolicy)) + { + options.RateLimiterPolicy = rateLimiterPolicy; + } if (annotations.TryGetValue("yarp.ingress.kubernetes.io/cors-policy", out var corsPolicy)) { options.CorsPolicy = corsPolicy; diff --git a/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs b/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs index 5198143c0..e8ebf8d92 100644 --- a/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs +++ b/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs @@ -147,6 +147,7 @@ private static RouteConfig CreateRoute(IConfigurationSection section) MaxRequestBodySize = section.ReadInt64(nameof(RouteConfig.MaxRequestBodySize)), ClusterId = section[nameof(RouteConfig.ClusterId)], AuthorizationPolicy = section[nameof(RouteConfig.AuthorizationPolicy)], + RateLimiterPolicy = section[nameof(RouteConfig.RateLimiterPolicy)], CorsPolicy = section[nameof(RouteConfig.CorsPolicy)], Metadata = section.GetSection(nameof(RouteConfig.Metadata)).ReadStringDictionary(), Transforms = CreateTransforms(section.GetSection(nameof(RouteConfig.Transforms))), diff --git a/src/ReverseProxy/Configuration/ConfigValidator.cs b/src/ReverseProxy/Configuration/ConfigValidator.cs index 72a24cecd..3db872f4f 100644 --- a/src/ReverseProxy/Configuration/ConfigValidator.cs +++ b/src/ReverseProxy/Configuration/ConfigValidator.cs @@ -72,6 +72,7 @@ public async ValueTask> ValidateRouteAsync(RouteConfig route) errors.AddRange(_transformBuilder.ValidateRoute(route)); await ValidateAuthorizationPolicyAsync(errors, route.AuthorizationPolicy, route.RouteId); + await ValidateRateLimiterPolicyAsync(errors, route.RateLimiterPolicy, route.RouteId); await ValidateCorsPolicyAsync(errors, route.CorsPolicy, route.RouteId); if (route.Match is null) @@ -287,6 +288,47 @@ private async ValueTask ValidateAuthorizationPolicyAsync(IList errors } } + private ValueTask ValidateRateLimiterPolicyAsync(IList errors, string? rateLimiterPolicyName, string routeId) + { + if (string.IsNullOrEmpty(rateLimiterPolicyName)) + { + //return; + return ValueTask.CompletedTask; + } + + // TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies (maybe .NET8?) + + if (string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) + { +#if NET7_0_OR_GREATER + //var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); + //if (policy is not null) + //{ + // errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); + //} +#endif + //return; + return ValueTask.CompletedTask; + } + + try + { +#if NET7_0_OR_GREATER + //var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); + //if (policy is null) + //{ + // errors.Add(new ArgumentException($"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeId}'.")); + //} +#endif + } + catch (Exception ex) + { + errors.Add(new ArgumentException($"Unable to retrieve the RateLimiter policy '{rateLimiterPolicyName}' for route '{routeId}'.", ex)); + } + + return ValueTask.CompletedTask; + } + private async ValueTask ValidateCorsPolicyAsync(IList errors, string? corsPolicyName, string routeId) { if (string.IsNullOrEmpty(corsPolicyName)) diff --git a/src/ReverseProxy/Configuration/RateLimitingConstants.cs b/src/ReverseProxy/Configuration/RateLimitingConstants.cs new file mode 100644 index 000000000..146a07b68 --- /dev/null +++ b/src/ReverseProxy/Configuration/RateLimitingConstants.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Yarp.ReverseProxy.Configuration; + +internal static class RateLimitingConstants +{ + internal const string Disable = "Disable"; +} diff --git a/src/ReverseProxy/Configuration/RouteConfig.cs b/src/ReverseProxy/Configuration/RouteConfig.cs index c773b95d3..21068cea3 100644 --- a/src/ReverseProxy/Configuration/RouteConfig.cs +++ b/src/ReverseProxy/Configuration/RouteConfig.cs @@ -44,6 +44,13 @@ public sealed record RouteConfig /// public string? AuthorizationPolicy { get; init; } + /// + /// The name of the RateLimiterPolicy to apply to this route. + /// If not set then only the GlobalLimiter will apply. + /// Set to "Disable" to disable rate limiting for this route. + /// + public string? RateLimiterPolicy { get; init; } + /// /// The name of the CorsPolicy to apply to this route. /// If not set then the route won't be automatically matched for cors preflight requests. @@ -79,6 +86,7 @@ public bool Equals(RouteConfig? other) && string.Equals(RouteId, other.RouteId, StringComparison.OrdinalIgnoreCase) && string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase) && string.Equals(AuthorizationPolicy, other.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase) + && string.Equals(RateLimiterPolicy, other.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase) && string.Equals(CorsPolicy, other.CorsPolicy, StringComparison.OrdinalIgnoreCase) && Match == other.Match && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata) @@ -87,13 +95,17 @@ public bool Equals(RouteConfig? other) public override int GetHashCode() { - return HashCode.Combine(Order, - RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase), - ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase), - AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase), - CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase), - Match, - CaseSensitiveEqualHelper.GetHashCode(Metadata), - CaseSensitiveEqualHelper.GetHashCode(Transforms)); + // HashCode.Combine(...) takes only 8 arguments + var hash = new HashCode(); + hash.Add(Order); + hash.Add(RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase)); + hash.Add(ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase)); + hash.Add(AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); + hash.Add(RateLimiterPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); + hash.Add(CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); + hash.Add(Match); + hash.Add(CaseSensitiveEqualHelper.GetHashCode(Metadata)); + hash.Add(CaseSensitiveEqualHelper.GetHashCode(Transforms)); + return hash.ToHashCode(); } } diff --git a/src/ReverseProxy/Routing/ProxyEndpointFactory.cs b/src/ReverseProxy/Routing/ProxyEndpointFactory.cs index 34da22d89..523c182b1 100644 --- a/src/ReverseProxy/Routing/ProxyEndpointFactory.cs +++ b/src/ReverseProxy/Routing/ProxyEndpointFactory.cs @@ -9,17 +9,24 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http; +#if NET7_0_OR_GREATER +using Microsoft.AspNetCore.RateLimiting; +#endif using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Yarp.ReverseProxy.Model; using CorsConstants = Yarp.ReverseProxy.Configuration.CorsConstants; using AuthorizationConstants = Yarp.ReverseProxy.Configuration.AuthorizationConstants; +using RateLimitingConstants = Yarp.ReverseProxy.Configuration.RateLimitingConstants; namespace Yarp.ReverseProxy.Routing; internal sealed class ProxyEndpointFactory { private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute(); +#if NET7_0_OR_GREATER + private static readonly DisableRateLimitingAttribute _disableRateLimit = new DisableRateLimitingAttribute(); +#endif private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute(); private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute(); private static readonly IAllowAnonymous _allowAnonymous = new AllowAnonymousAttribute(); @@ -110,6 +117,17 @@ public Endpoint CreateEndpoint(RouteModel route, IReadOnlyList() { { "f", "f1" } } }; var g = a with { Order = null }; var h = a with { RouteId = "h" }; + var i = a with { RateLimiterPolicy = "i" }; Assert.False(a.Equals(b)); Assert.False(a.Equals(c)); @@ -122,6 +126,7 @@ public void Equals_Negative() Assert.False(a.Equals(f)); Assert.False(a.Equals(g)); Assert.False(a.Equals(h)); + Assert.False(a.Equals(i)); } [Fact] @@ -136,6 +141,7 @@ public void RouteConfig_CanBeJsonSerialized() var route1 = new RouteConfig() { AuthorizationPolicy = "a", + RateLimiterPolicy = "rl", ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() diff --git a/test/ReverseProxy.Tests/Routing/ProxyEndpointFactoryTests.cs b/test/ReverseProxy.Tests/Routing/ProxyEndpointFactoryTests.cs index ddfaee8fc..4e93c4111 100644 --- a/test/ReverseProxy.Tests/Routing/ProxyEndpointFactoryTests.cs +++ b/test/ReverseProxy.Tests/Routing/ProxyEndpointFactoryTests.cs @@ -5,6 +5,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors.Infrastructure; +#if NET7_0_OR_GREATER +using Microsoft.AspNetCore.RateLimiting; +#endif using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; @@ -321,6 +324,78 @@ public void AddEndpoint_NoAuth_Works() Assert.Null(routeEndpoint.Metadata.GetMetadata()); } +#if NET7_0_OR_GREATER + [Fact] + public void AddEndpoint_CustomRateLimiter_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new RouteConfig + { + RouteId = "route1", + RateLimiterPolicy = "custom", + Order = 12, + Match = new RouteMatch(), + }; + var cluster = new ClusterState("cluster1"); + var routeState = new RouteState("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); + + var attribute = routeEndpoint.Metadata.GetMetadata(); + Assert.NotNull(attribute); + Assert.Equal("custom", attribute.PolicyName); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + + [Fact] + public void AddEndpoint_DisableRateLimiter_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new RouteConfig + { + RouteId = "route1", + RateLimiterPolicy = "disAble", + Order = 12, + Match = new RouteMatch(), + }; + var cluster = new ClusterState("cluster1"); + var routeState = new RouteState("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); + + Assert.NotNull(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + + [Fact] + public void AddEndpoint_NoRateLimiter_Works() + { + var services = CreateServices(); + var factory = services.GetRequiredService(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new RouteConfig + { + RouteId = "route1", + Order = 12, + Match = new RouteMatch(), + }; + var cluster = new ClusterState("cluster1"); + var routeState = new RouteState("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); + + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } +#endif + [Fact] public void AddEndpoint_DefaultCors_Works() { From bb68b3fb0615dd6774cd485cd96c871d20f3df8f Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 20 Dec 2022 08:25:07 +0300 Subject: [PATCH 02/10] Documentation updates --- docs/docfx/articles/rate-limiting.md | 10 ++++++---- docs/docfx/articles/toc.yml | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/docfx/articles/rate-limiting.md b/docs/docfx/articles/rate-limiting.md index 7e29d7c92..1f7621faa 100644 --- a/docs/docfx/articles/rate-limiting.md +++ b/docs/docfx/articles/rate-limiting.md @@ -3,6 +3,8 @@ ## Introduction The reverse proxy can be used to rate-limit requests before they are proxied to the destination servers. This can reduce load on the destination servers, add a layer of protection, and ensure consistent policies are implemented across your applications. +> This feature is only available when using .NET 7.0 or later + ## Defaults No rate limiting is performed on requests unless enabled in the route or application configuration. @@ -36,7 +38,7 @@ Example: } ``` -[RateLimiter policies](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware. +[RateLimiter policies](https://learn.microsoft.com//aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware. RateLimiter policies can be configured in Startup.ConfigureServices as follows: ``` @@ -71,8 +73,8 @@ public void Configure(IApplicationBuilder app) } ``` -See the [Rate Limiting](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) docs for setting up your preferred kind of rate limiting. +See the [Rate Limiting](https://learn.microsoft.com//aspnet/core/performance/rate-limit) docs for setting up your preferred kind of rate limiting. -### Disable CORS +### Disable Rate Limiting -Specifying the value `disable` in a route's `RateLimiterPolicy` parameter means the rate limiter middleware will not rate limit requests. +Specifying the value `disable` in a route's `RateLimiterPolicy` parameter means the rate limiter middleware will not apply any policies to this route, even the default policy. diff --git a/docs/docfx/articles/toc.yml b/docs/docfx/articles/toc.yml index da16a848a..38a1eb346 100644 --- a/docs/docfx/articles/toc.yml +++ b/docs/docfx/articles/toc.yml @@ -20,6 +20,8 @@ href: header-routing.md - name: Authentication and Authorization href: authn-authz.md +- name: Rate Limiting + href: rate-limiting.md - name: Cross-Origin Requests (CORS) href: cors.md - name: Session Affinity From 6bac908dde5ea991707878dae02fb138bb609781 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 20 Dec 2022 08:49:42 +0300 Subject: [PATCH 03/10] Support default case --- .../Configuration/ConfigValidator.cs | 13 +++++++++++ .../Configuration/RateLimitingConstants.cs | 1 + .../Routing/ProxyEndpointFactory.cs | 8 +++++-- .../ConfigurationConfigProviderTests.cs | 4 ++-- .../Routing/ProxyEndpointFactoryTests.cs | 23 +++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/ReverseProxy/Configuration/ConfigValidator.cs b/src/ReverseProxy/Configuration/ConfigValidator.cs index 3db872f4f..32db6ba45 100644 --- a/src/ReverseProxy/Configuration/ConfigValidator.cs +++ b/src/ReverseProxy/Configuration/ConfigValidator.cs @@ -298,6 +298,19 @@ private ValueTask ValidateRateLimiterPolicyAsync(IList errors, string // TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies (maybe .NET8?) + if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) + { +#if NET7_0_OR_GREATER + //var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); + //if (policy is not null) + //{ + // errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); + //} +#endif + //return; + return ValueTask.CompletedTask; + } + if (string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) { #if NET7_0_OR_GREATER diff --git a/src/ReverseProxy/Configuration/RateLimitingConstants.cs b/src/ReverseProxy/Configuration/RateLimitingConstants.cs index 146a07b68..49a5cd1d2 100644 --- a/src/ReverseProxy/Configuration/RateLimitingConstants.cs +++ b/src/ReverseProxy/Configuration/RateLimitingConstants.cs @@ -5,5 +5,6 @@ namespace Yarp.ReverseProxy.Configuration; internal static class RateLimitingConstants { + internal const string Default = "Default"; internal const string Disable = "Disable"; } diff --git a/src/ReverseProxy/Routing/ProxyEndpointFactory.cs b/src/ReverseProxy/Routing/ProxyEndpointFactory.cs index 523c182b1..2ab8aeae8 100644 --- a/src/ReverseProxy/Routing/ProxyEndpointFactory.cs +++ b/src/ReverseProxy/Routing/ProxyEndpointFactory.cs @@ -25,7 +25,7 @@ internal sealed class ProxyEndpointFactory { private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute(); #if NET7_0_OR_GREATER - private static readonly DisableRateLimitingAttribute _disableRateLimit = new DisableRateLimitingAttribute(); + private static readonly DisableRateLimitingAttribute _disableRateLimit = new(); #endif private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute(); private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute(); @@ -118,7 +118,11 @@ public Endpoint CreateEndpoint(RouteModel route, IReadOnlyList(); + factory.SetProxyPipeline(context => Task.CompletedTask); + + var route = new RouteConfig + { + RouteId = "route1", + RateLimiterPolicy = "defaulT", + Order = 12, + Match = new RouteMatch(), + }; + var cluster = new ClusterState("cluster1"); + var routeState = new RouteState("route1"); + + var (routeEndpoint, _) = CreateEndpoint(factory, routeState, route, cluster); + + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + Assert.Null(routeEndpoint.Metadata.GetMetadata()); + } + [Fact] public void AddEndpoint_CustomRateLimiter_Works() { From f364ee75e836e9ce82f51b534c6bee2ddca1b410 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 20 Dec 2022 09:15:28 +0300 Subject: [PATCH 04/10] Reference relevant issue --- docs/docfx/articles/rate-limiting.md | 7 ++++++- src/ReverseProxy/Configuration/ConfigValidator.cs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/docfx/articles/rate-limiting.md b/docs/docfx/articles/rate-limiting.md index 1f7621faa..ee4600594 100644 --- a/docs/docfx/articles/rate-limiting.md +++ b/docs/docfx/articles/rate-limiting.md @@ -7,7 +7,12 @@ The reverse proxy can be used to rate-limit requests before they are proxied to ## Defaults -No rate limiting is performed on requests unless enabled in the route or application configuration. +No rate limiting is performed on requests unless enabled in the route or application configuration. However, the Rate Limiting middleware (`app.UseRateLimiter()`) can apply a default limiter applied to all routes, and this doesn't require any opt-in from the config. + +Example: +``` +builder.Services.AddRateLimiter(options => options.GlobalLimiter = globalLimiter); +``` ## Configuration Rate Limiter policies can be specified per route via [RouteConfig.RateLimiterPolicy](xref:Yarp.ReverseProxy.Configuration.RouteConfig) and can be bound from the `Routes` sections of the config file. As with other route properties, this can be modified and reloaded without restarting the proxy. Policy names are case insensitive. diff --git a/src/ReverseProxy/Configuration/ConfigValidator.cs b/src/ReverseProxy/Configuration/ConfigValidator.cs index 32db6ba45..22d7d6f5f 100644 --- a/src/ReverseProxy/Configuration/ConfigValidator.cs +++ b/src/ReverseProxy/Configuration/ConfigValidator.cs @@ -296,7 +296,7 @@ private ValueTask ValidateRateLimiterPolicyAsync(IList errors, string return ValueTask.CompletedTask; } - // TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies (maybe .NET8?) + // TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684 if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) { From ff033883ba578143599892452513bd1dfc1a851b Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 20 Dec 2022 09:36:25 +0300 Subject: [PATCH 05/10] Fix typos --- docs/docfx/articles/rate-limiting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docfx/articles/rate-limiting.md b/docs/docfx/articles/rate-limiting.md index ee4600594..ee68cadb5 100644 --- a/docs/docfx/articles/rate-limiting.md +++ b/docs/docfx/articles/rate-limiting.md @@ -43,7 +43,7 @@ Example: } ``` -[RateLimiter policies](https://learn.microsoft.com//aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware. +[RateLimiter policies](https://learn.microsoft.com/aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware. RateLimiter policies can be configured in Startup.ConfigureServices as follows: ``` @@ -78,7 +78,7 @@ public void Configure(IApplicationBuilder app) } ``` -See the [Rate Limiting](https://learn.microsoft.com//aspnet/core/performance/rate-limit) docs for setting up your preferred kind of rate limiting. +See the [Rate Limiting](https://learn.microsoft.com/aspnet/core/performance/rate-limit) docs for setting up your preferred kind of rate limiting. ### Disable Rate Limiting From 9aa5ba62fd3bbbffd01582d1ba94d9da24dd5d1e Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Wed, 21 Dec 2022 05:40:54 +0300 Subject: [PATCH 06/10] Update src/ReverseProxy/Configuration/RouteConfig.cs Co-authored-by: Chris Ross --- src/ReverseProxy/Configuration/RouteConfig.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ReverseProxy/Configuration/RouteConfig.cs b/src/ReverseProxy/Configuration/RouteConfig.cs index 21068cea3..4e46a4a68 100644 --- a/src/ReverseProxy/Configuration/RouteConfig.cs +++ b/src/ReverseProxy/Configuration/RouteConfig.cs @@ -48,6 +48,7 @@ public sealed record RouteConfig /// The name of the RateLimiterPolicy to apply to this route. /// If not set then only the GlobalLimiter will apply. /// Set to "Disable" to disable rate limiting for this route. + /// Set to "Default" or leave empty use the global rate limits, if any. /// public string? RateLimiterPolicy { get; init; } From eea89bcf070a5580b08c62728b38a90e70de4afd Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Wed, 21 Dec 2022 05:41:30 +0300 Subject: [PATCH 07/10] Update docs/docfx/articles/rate-limiting.md Co-authored-by: Arvin Kahbazi --- docs/docfx/articles/rate-limiting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docfx/articles/rate-limiting.md b/docs/docfx/articles/rate-limiting.md index ee68cadb5..d2fdf79da 100644 --- a/docs/docfx/articles/rate-limiting.md +++ b/docs/docfx/articles/rate-limiting.md @@ -82,4 +82,4 @@ See the [Rate Limiting](https://learn.microsoft.com/aspnet/core/performance/rate ### Disable Rate Limiting -Specifying the value `disable` in a route's `RateLimiterPolicy` parameter means the rate limiter middleware will not apply any policies to this route, even the default policy. +Specifying the value `disable` in a route's `RateLimiterPolicy` parameter means the rate limiter middleware will not apply any policies to this route, even the default policy. From 26ce1d15f868cb8da1891d65db1e59a20fd6ecbf Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Tue, 10 Jan 2023 12:22:35 +0300 Subject: [PATCH 08/10] Validate reverse proxy policies using reflection --- .../Configuration/ConfigValidator.cs | 64 +++++++++---------- .../IYarpRateLimiterPolicyProvider.cs | 44 +++++++++++++ .../IReverseProxyBuilderExtensions.cs | 3 + .../YarpRateLimiterPolicyProviderTests.cs | 41 ++++++++++++ 4 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs create mode 100644 test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs diff --git a/src/ReverseProxy/Configuration/ConfigValidator.cs b/src/ReverseProxy/Configuration/ConfigValidator.cs index 22d7d6f5f..376ccb94c 100644 --- a/src/ReverseProxy/Configuration/ConfigValidator.cs +++ b/src/ReverseProxy/Configuration/ConfigValidator.cs @@ -29,6 +29,9 @@ internal sealed class ConfigValidator : IConfigValidator private readonly ITransformBuilder _transformBuilder; private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider; +#if NET7_0_OR_GREATER + private readonly IYarpRateLimiterPolicyProvider _rateLimiterPolicyProvider; +#endif private readonly ICorsPolicyProvider _corsPolicyProvider; private readonly IDictionary _loadBalancingPolicies; private readonly IDictionary _affinityFailurePolicies; @@ -40,6 +43,9 @@ internal sealed class ConfigValidator : IConfigValidator public ConfigValidator(ITransformBuilder transformBuilder, IAuthorizationPolicyProvider authorizationPolicyProvider, +#if NET7_0_OR_GREATER + IYarpRateLimiterPolicyProvider rateLimiterPolicyProvider, +#endif ICorsPolicyProvider corsPolicyProvider, IEnumerable loadBalancingPolicies, IEnumerable affinityFailurePolicies, @@ -50,6 +56,9 @@ public ConfigValidator(ITransformBuilder transformBuilder, { _transformBuilder = transformBuilder ?? throw new ArgumentNullException(nameof(transformBuilder)); _authorizationPolicyProvider = authorizationPolicyProvider ?? throw new ArgumentNullException(nameof(authorizationPolicyProvider)); +#if NET7_0_OR_GREATER + _rateLimiterPolicyProvider = rateLimiterPolicyProvider ?? throw new ArgumentNullException(nameof(rateLimiterPolicyProvider)); +#endif _corsPolicyProvider = corsPolicyProvider ?? throw new ArgumentNullException(nameof(corsPolicyProvider)); _loadBalancingPolicies = loadBalancingPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(loadBalancingPolicies)); _affinityFailurePolicies = affinityFailurePolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(affinityFailurePolicies)); @@ -72,7 +81,9 @@ public async ValueTask> ValidateRouteAsync(RouteConfig route) errors.AddRange(_transformBuilder.ValidateRoute(route)); await ValidateAuthorizationPolicyAsync(errors, route.AuthorizationPolicy, route.RouteId); +#if NET7_0_OR_GREATER await ValidateRateLimiterPolicyAsync(errors, route.RateLimiterPolicy, route.RouteId); +#endif await ValidateCorsPolicyAsync(errors, route.CorsPolicy, route.RouteId); if (route.Match is null) @@ -288,59 +299,48 @@ private async ValueTask ValidateAuthorizationPolicyAsync(IList errors } } - private ValueTask ValidateRateLimiterPolicyAsync(IList errors, string? rateLimiterPolicyName, string routeId) +#if NET7_0_OR_GREATER + private async ValueTask ValidateRateLimiterPolicyAsync(IList errors, string? rateLimiterPolicyName, string routeId) { if (string.IsNullOrEmpty(rateLimiterPolicyName)) { - //return; - return ValueTask.CompletedTask; + return; } - // TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684 - if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) { -#if NET7_0_OR_GREATER - //var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); - //if (policy is not null) - //{ - // errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); - //} -#endif - //return; - return ValueTask.CompletedTask; + var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); + if (policy is not null) + { + errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); + } + return; } if (string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) { -#if NET7_0_OR_GREATER - //var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); - //if (policy is not null) - //{ - // errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); - //} -#endif - //return; - return ValueTask.CompletedTask; + var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); + if (policy is not null) + { + errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); + } + return; } try { -#if NET7_0_OR_GREATER - //var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); - //if (policy is null) - //{ - // errors.Add(new ArgumentException($"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeId}'.")); - //} -#endif + var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); + if (policy is null) + { + errors.Add(new ArgumentException($"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeId}'.")); + } } catch (Exception ex) { errors.Add(new ArgumentException($"Unable to retrieve the RateLimiter policy '{rateLimiterPolicyName}' for route '{routeId}'.", ex)); } - - return ValueTask.CompletedTask; } +#endif private async ValueTask ValidateCorsPolicyAsync(IList errors, string? corsPolicyName, string routeId) { diff --git a/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs b/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs new file mode 100644 index 000000000..ac9a04446 --- /dev/null +++ b/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Reflection; +using System.Threading.Tasks; +#if NET7_0_OR_GREATER +using Microsoft.AspNetCore.RateLimiting; +#endif +using Microsoft.Extensions.Options; + +namespace Yarp.ReverseProxy.Configuration; + +// TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684 + +#if NET7_0_OR_GREATER + +internal interface IYarpRateLimiterPolicyProvider +{ + ValueTask GetPolicyAsync(string policyName); +} + +internal class YarpRateLimiterPolicyProvider : IYarpRateLimiterPolicyProvider +{ + private readonly RateLimiterOptions _rateLimiterOptions; + + private readonly System.Collections.IDictionary _policyMap, _unactivatedPolicyMap; + + public YarpRateLimiterPolicyProvider(IOptions rateLimiterOptions) + { + _rateLimiterOptions = rateLimiterOptions?.Value ?? throw new ArgumentNullException(nameof(rateLimiterOptions)); + + var type = typeof(RateLimiterOptions); + var flags = BindingFlags.Instance | BindingFlags.NonPublic; + _policyMap = (System.Collections.IDictionary)type.GetProperty("PolicyMap", flags)!.GetValue(_rateLimiterOptions, null)!; + _unactivatedPolicyMap = (System.Collections.IDictionary)type.GetProperty("UnactivatedPolicyMap", flags)!.GetValue(_rateLimiterOptions, null)!; + } + + public ValueTask GetPolicyAsync(string policyName) + { + return ValueTask.FromResult(_policyMap[policyName] ?? _unactivatedPolicyMap[policyName]); + } +} +#endif diff --git a/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs b/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs index 158831d1d..2f22fd17c 100644 --- a/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs +++ b/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs @@ -22,6 +22,9 @@ internal static class IReverseProxyBuilderExtensions { public static IReverseProxyBuilder AddConfigBuilder(this IReverseProxyBuilder builder) { +#if NET7_0_OR_GREATER + builder.Services.TryAddSingleton(); +#endif builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.AddTransformFactory(); diff --git a/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs b/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs new file mode 100644 index 000000000..8074b08b8 --- /dev/null +++ b/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +#if NET7_0_OR_GREATER +using System.Threading.RateLimiting; +#endif +using Microsoft.AspNetCore.Builder; +#if NET7_0_OR_GREATER +using Microsoft.AspNetCore.RateLimiting; +#endif +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Yarp.ReverseProxy.Configuration; + +public class YarpRateLimiterPolicyProviderTests +{ +#if NET7_0_OR_GREATER + [Fact] + public async Task GetPolicyAsync_Works() + { + var services = new ServiceCollection(); + + services.AddRateLimiter(options => + { + options.AddFixedWindowLimiter("customPolicy", opt => + { + opt.PermitLimit = 4; + opt.Window = TimeSpan.FromSeconds(12); + opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + opt.QueueLimit = 2; + }); + }); + + services.AddReverseProxy(); + var provider = services.BuildServiceProvider(); + var rateLimiterPolicyProvider = provider.GetRequiredService(); + Assert.Null(await rateLimiterPolicyProvider.GetPolicyAsync("anotherPolicy")); + Assert.NotNull(await rateLimiterPolicyProvider.GetPolicyAsync("customPolicy")); + } +#endif +} From 970b50500bac23d065659dfbb98e3f70dde8ab2c Mon Sep 17 00:00:00 2001 From: Chris R Date: Thu, 12 Jan 2023 13:23:45 -0800 Subject: [PATCH 09/10] Cleanup ifdefs --- .../Converters/YarpIngressOptions.cs | 4 ++- .../Converters/YarpParser.cs | 4 +++ .../ConfigurationConfigProvider.cs | 2 ++ .../Configuration/ConfigValidator.cs | 34 ++++--------------- .../IYarpRateLimiterPolicyProvider.cs | 27 ++++++++++----- src/ReverseProxy/Configuration/RouteConfig.cs | 10 ++++-- .../IReverseProxyBuilderExtensions.cs | 2 -- .../IngressConversionTests.cs | 5 +++ .../testassets/annotations/routes.net6.json | 23 +++++++++++++ .../ConfigurationConfigProviderTests.cs | 2 ++ .../Configuration/RouteConfigTests.cs | 12 +++++++ .../YarpRateLimiterPolicyProviderTests.cs | 11 +++--- 12 files changed, 88 insertions(+), 48 deletions(-) create mode 100644 test/Kubernetes.Tests/testassets/annotations/routes.net6.json diff --git a/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs b/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs index 25f30a95f..6d2957698 100644 --- a/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs +++ b/src/Kubernetes.Controller/Converters/YarpIngressOptions.cs @@ -11,7 +11,9 @@ internal sealed class YarpIngressOptions public bool Https { get; set; } public List> Transforms { get; set; } public string AuthorizationPolicy { get; set; } +#if NET7_0_OR_GREATER public string RateLimiterPolicy { get; set; } +#endif public SessionAffinityConfig SessionAffinity { get; set; } public HttpClientConfig HttpClientConfig { get; set; } public string LoadBalancingPolicy { get; set; } @@ -39,4 +41,4 @@ public RouteHeader ToRouteHeader() IsCaseSensitive = IsCaseSensitive }; } -} \ No newline at end of file +} diff --git a/src/Kubernetes.Controller/Converters/YarpParser.cs b/src/Kubernetes.Controller/Converters/YarpParser.cs index 48e555fd1..013373abb 100644 --- a/src/Kubernetes.Controller/Converters/YarpParser.cs +++ b/src/Kubernetes.Controller/Converters/YarpParser.cs @@ -104,7 +104,9 @@ private static void HandleIngressRulePath(YarpIngressContext ingressContext, V1S RouteId = $"{ingressContext.Ingress.Metadata.Name}.{ingressContext.Ingress.Metadata.NamespaceProperty}:{host}{path.Path}", Transforms = ingressContext.Options.Transforms, AuthorizationPolicy = ingressContext.Options.AuthorizationPolicy, +#if NET7_0_OR_GREATER RateLimiterPolicy = ingressContext.Options.RateLimiterPolicy, +#endif CorsPolicy = ingressContext.Options.CorsPolicy, Metadata = ingressContext.Options.RouteMetadata, Order = ingressContext.Options.RouteOrder, @@ -182,10 +184,12 @@ private static YarpIngressOptions HandleAnnotations(YarpIngressContext context, { options.AuthorizationPolicy = authorizationPolicy; } +#if NET7_0_OR_GREATER if (annotations.TryGetValue("yarp.ingress.kubernetes.io/rate-limiter-policy", out var rateLimiterPolicy)) { options.RateLimiterPolicy = rateLimiterPolicy; } +#endif if (annotations.TryGetValue("yarp.ingress.kubernetes.io/cors-policy", out var corsPolicy)) { options.CorsPolicy = corsPolicy; diff --git a/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs b/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs index e8ebf8d92..def838ded 100644 --- a/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs +++ b/src/ReverseProxy/Configuration/ConfigProvider/ConfigurationConfigProvider.cs @@ -147,7 +147,9 @@ private static RouteConfig CreateRoute(IConfigurationSection section) MaxRequestBodySize = section.ReadInt64(nameof(RouteConfig.MaxRequestBodySize)), ClusterId = section[nameof(RouteConfig.ClusterId)], AuthorizationPolicy = section[nameof(RouteConfig.AuthorizationPolicy)], +#if NET7_0_OR_GREATER RateLimiterPolicy = section[nameof(RouteConfig.RateLimiterPolicy)], +#endif CorsPolicy = section[nameof(RouteConfig.CorsPolicy)], Metadata = section.GetSection(nameof(RouteConfig.Metadata)).ReadStringDictionary(), Transforms = CreateTransforms(section.GetSection(nameof(RouteConfig.Transforms))), diff --git a/src/ReverseProxy/Configuration/ConfigValidator.cs b/src/ReverseProxy/Configuration/ConfigValidator.cs index 376ccb94c..b06cc277f 100644 --- a/src/ReverseProxy/Configuration/ConfigValidator.cs +++ b/src/ReverseProxy/Configuration/ConfigValidator.cs @@ -29,9 +29,7 @@ internal sealed class ConfigValidator : IConfigValidator private readonly ITransformBuilder _transformBuilder; private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider; -#if NET7_0_OR_GREATER private readonly IYarpRateLimiterPolicyProvider _rateLimiterPolicyProvider; -#endif private readonly ICorsPolicyProvider _corsPolicyProvider; private readonly IDictionary _loadBalancingPolicies; private readonly IDictionary _affinityFailurePolicies; @@ -43,9 +41,7 @@ internal sealed class ConfigValidator : IConfigValidator public ConfigValidator(ITransformBuilder transformBuilder, IAuthorizationPolicyProvider authorizationPolicyProvider, -#if NET7_0_OR_GREATER IYarpRateLimiterPolicyProvider rateLimiterPolicyProvider, -#endif ICorsPolicyProvider corsPolicyProvider, IEnumerable loadBalancingPolicies, IEnumerable affinityFailurePolicies, @@ -56,9 +52,7 @@ public ConfigValidator(ITransformBuilder transformBuilder, { _transformBuilder = transformBuilder ?? throw new ArgumentNullException(nameof(transformBuilder)); _authorizationPolicyProvider = authorizationPolicyProvider ?? throw new ArgumentNullException(nameof(authorizationPolicyProvider)); -#if NET7_0_OR_GREATER _rateLimiterPolicyProvider = rateLimiterPolicyProvider ?? throw new ArgumentNullException(nameof(rateLimiterPolicyProvider)); -#endif _corsPolicyProvider = corsPolicyProvider ?? throw new ArgumentNullException(nameof(corsPolicyProvider)); _loadBalancingPolicies = loadBalancingPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(loadBalancingPolicies)); _affinityFailurePolicies = affinityFailurePolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(affinityFailurePolicies)); @@ -299,7 +293,6 @@ private async ValueTask ValidateAuthorizationPolicyAsync(IList errors } } -#if NET7_0_OR_GREATER private async ValueTask ValidateRateLimiterPolicyAsync(IList errors, string? rateLimiterPolicyName, string routeId) { if (string.IsNullOrEmpty(rateLimiterPolicyName)) @@ -307,32 +300,20 @@ private async ValueTask ValidateRateLimiterPolicyAsync(IList errors, return; } - if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) + try { var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); - if (policy is not null) - { - errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); - } - return; - } - if (string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) - { - var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); - if (policy is not null) + if (policy is null) { - errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); + errors.Add(new ArgumentException($"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeId}'.")); + return; } - return; - } - try - { - var policy = await _rateLimiterPolicyProvider.GetPolicyAsync(rateLimiterPolicyName); - if (policy is null) + if (string.Equals(RateLimitingConstants.Default, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase) + || string.Equals(RateLimitingConstants.Disable, rateLimiterPolicyName, StringComparison.OrdinalIgnoreCase)) { - errors.Add(new ArgumentException($"RateLimiter policy '{rateLimiterPolicyName}' not found for route '{routeId}'.")); + errors.Add(new ArgumentException($"The application has registered a RateLimiter policy named '{rateLimiterPolicyName}' that conflicts with the reserved RateLimiter policy name used on this route. The registered policy name needs to be changed for this route to function.")); } } catch (Exception ex) @@ -340,7 +321,6 @@ private async ValueTask ValidateRateLimiterPolicyAsync(IList errors, errors.Add(new ArgumentException($"Unable to retrieve the RateLimiter policy '{rateLimiterPolicyName}' for route '{routeId}'.", ex)); } } -#endif private async ValueTask ValidateCorsPolicyAsync(IList errors, string? corsPolicyName, string routeId) { diff --git a/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs b/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs index ac9a04446..7a1315af1 100644 --- a/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs +++ b/src/ReverseProxy/Configuration/IYarpRateLimiterPolicyProvider.cs @@ -1,19 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#if NET7_0_OR_GREATER using System; +using System.Collections; using System.Reflection; -using System.Threading.Tasks; -#if NET7_0_OR_GREATER using Microsoft.AspNetCore.RateLimiting; -#endif using Microsoft.Extensions.Options; +#endif + +using System.Threading.Tasks; namespace Yarp.ReverseProxy.Configuration; -// TODO: update this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684 +// TODO: update or remove this once AspNetCore provides a mechanism to validate the RateLimiter policies https://github.com/dotnet/aspnetcore/issues/45684 -#if NET7_0_OR_GREATER internal interface IYarpRateLimiterPolicyProvider { @@ -22,9 +23,10 @@ internal interface IYarpRateLimiterPolicyProvider internal class YarpRateLimiterPolicyProvider : IYarpRateLimiterPolicyProvider { +#if NET7_0_OR_GREATER private readonly RateLimiterOptions _rateLimiterOptions; - private readonly System.Collections.IDictionary _policyMap, _unactivatedPolicyMap; + private readonly IDictionary _policyMap, _unactivatedPolicyMap; public YarpRateLimiterPolicyProvider(IOptions rateLimiterOptions) { @@ -32,13 +34,20 @@ public YarpRateLimiterPolicyProvider(IOptions rateLimiterOpt var type = typeof(RateLimiterOptions); var flags = BindingFlags.Instance | BindingFlags.NonPublic; - _policyMap = (System.Collections.IDictionary)type.GetProperty("PolicyMap", flags)!.GetValue(_rateLimiterOptions, null)!; - _unactivatedPolicyMap = (System.Collections.IDictionary)type.GetProperty("UnactivatedPolicyMap", flags)!.GetValue(_rateLimiterOptions, null)!; + _policyMap = type.GetProperty("PolicyMap", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary + ?? throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core."); + _unactivatedPolicyMap = type.GetProperty("UnactivatedPolicyMap", flags)?.GetValue(_rateLimiterOptions, null) as IDictionary + ?? throw new NotSupportedException("This version of YARP is incompatible with the current version of ASP.NET Core."); } public ValueTask GetPolicyAsync(string policyName) { return ValueTask.FromResult(_policyMap[policyName] ?? _unactivatedPolicyMap[policyName]); } -} +#else + public ValueTask GetPolicyAsync(string policyName) + { + return default; + } #endif +} diff --git a/src/ReverseProxy/Configuration/RouteConfig.cs b/src/ReverseProxy/Configuration/RouteConfig.cs index 4e46a4a68..0ca059266 100644 --- a/src/ReverseProxy/Configuration/RouteConfig.cs +++ b/src/ReverseProxy/Configuration/RouteConfig.cs @@ -43,15 +43,15 @@ public sealed record RouteConfig /// Set to "Anonymous" to disable all authorization checks for this route. /// public string? AuthorizationPolicy { get; init; } - +#if NET7_0_OR_GREATER /// /// The name of the RateLimiterPolicy to apply to this route. /// If not set then only the GlobalLimiter will apply. /// Set to "Disable" to disable rate limiting for this route. - /// Set to "Default" or leave empty use the global rate limits, if any. + /// Set to "Default" or leave empty to use the global rate limits, if any. /// public string? RateLimiterPolicy { get; init; } - +#endif /// /// The name of the CorsPolicy to apply to this route. /// If not set then the route won't be automatically matched for cors preflight requests. @@ -87,7 +87,9 @@ public bool Equals(RouteConfig? other) && string.Equals(RouteId, other.RouteId, StringComparison.OrdinalIgnoreCase) && string.Equals(ClusterId, other.ClusterId, StringComparison.OrdinalIgnoreCase) && string.Equals(AuthorizationPolicy, other.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase) +#if NET7_0_OR_GREATER && string.Equals(RateLimiterPolicy, other.RateLimiterPolicy, StringComparison.OrdinalIgnoreCase) +#endif && string.Equals(CorsPolicy, other.CorsPolicy, StringComparison.OrdinalIgnoreCase) && Match == other.Match && CaseSensitiveEqualHelper.Equals(Metadata, other.Metadata) @@ -102,7 +104,9 @@ public override int GetHashCode() hash.Add(RouteId?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(ClusterId?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(AuthorizationPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); +#if NET7_0_OR_GREATER hash.Add(RateLimiterPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); +#endif hash.Add(CorsPolicy?.GetHashCode(StringComparison.OrdinalIgnoreCase)); hash.Add(Match); hash.Add(CaseSensitiveEqualHelper.GetHashCode(Metadata)); diff --git a/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs b/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs index 2f22fd17c..6fb05e439 100644 --- a/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs +++ b/src/ReverseProxy/Management/IReverseProxyBuilderExtensions.cs @@ -22,9 +22,7 @@ internal static class IReverseProxyBuilderExtensions { public static IReverseProxyBuilder AddConfigBuilder(this IReverseProxyBuilder builder) { -#if NET7_0_OR_GREATER builder.Services.TryAddSingleton(); -#endif builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.AddTransformFactory(); diff --git a/test/Kubernetes.Tests/IngressConversionTests.cs b/test/Kubernetes.Tests/IngressConversionTests.cs index b37421a80..5422b7727 100644 --- a/test/Kubernetes.Tests/IngressConversionTests.cs +++ b/test/Kubernetes.Tests/IngressConversionTests.cs @@ -81,7 +81,12 @@ private static void VerifyClusters(string clusterJson, string name) private static void VerifyRoutes(string routesJson, string name) { +#if NET7_0_OR_GREATER VerifyJson(routesJson, name, "routes.json"); +#else + VerifyJson(routesJson, name, + string.Equals("annotations", name, StringComparison.OrdinalIgnoreCase) ? "routes.net6.json" : "routes.json"); +#endif } private static string StripNullProperties(string json) diff --git a/test/Kubernetes.Tests/testassets/annotations/routes.net6.json b/test/Kubernetes.Tests/testassets/annotations/routes.net6.json new file mode 100644 index 000000000..dcb0e7e27 --- /dev/null +++ b/test/Kubernetes.Tests/testassets/annotations/routes.net6.json @@ -0,0 +1,23 @@ +[ + { + "RouteId": "minimal-ingress.default:/foo", + "Match": { + "Methods": null, + "Hosts": [], + "Path": "/foo/{**catch-all}", + "Headers": null + }, + "Order": null, + "ClusterId": "frontend.default:80", + "AuthorizationPolicy": "authzpolicy", + "CorsPolicy": "corspolicy", + "Metadata": null, + "Transforms": [ + { "PathPrefix": "/apis" }, + { + "RequestHeader": "header1", + "Append": "bar" + } + ] + } +] diff --git a/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs b/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs index 1cac2f42b..fbf345dfb 100644 --- a/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs +++ b/test/ReverseProxy.Tests/Configuration/ConfigProvider/ConfigurationConfigProviderTests.cs @@ -127,7 +127,9 @@ public class ConfigurationConfigProviderTests RouteId = "routeA", ClusterId = "cluster1", AuthorizationPolicy = "Default", +#if NET7_0_OR_GREATER RateLimiterPolicy = "Default", +#endif CorsPolicy = "Default", Order = -1, MaxRequestBodySize = -1, diff --git a/test/ReverseProxy.Tests/Configuration/RouteConfigTests.cs b/test/ReverseProxy.Tests/Configuration/RouteConfigTests.cs index 36e983917..073e2b843 100644 --- a/test/ReverseProxy.Tests/Configuration/RouteConfigTests.cs +++ b/test/ReverseProxy.Tests/Configuration/RouteConfigTests.cs @@ -15,7 +15,9 @@ public void Equals_Positive() var a = new RouteConfig() { AuthorizationPolicy = "a", +#if NET7_0_OR_GREATER RateLimiterPolicy = "rl", +#endif ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() @@ -44,7 +46,9 @@ public void Equals_Positive() var b = new RouteConfig() { AuthorizationPolicy = "A", +#if NET7_0_OR_GREATER RateLimiterPolicy = "RL", +#endif ClusterId = "C", CorsPolicy = "Co", Match = new RouteMatch() @@ -84,7 +88,9 @@ public void Equals_Negative() var a = new RouteConfig() { AuthorizationPolicy = "a", +#if NET7_0_OR_GREATER RateLimiterPolicy = "rl", +#endif ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() @@ -117,7 +123,9 @@ public void Equals_Negative() var f = a with { Metadata = new Dictionary() { { "f", "f1" } } }; var g = a with { Order = null }; var h = a with { RouteId = "h" }; +#if NET7_0_OR_GREATER var i = a with { RateLimiterPolicy = "i" }; +#endif Assert.False(a.Equals(b)); Assert.False(a.Equals(c)); @@ -126,7 +134,9 @@ public void Equals_Negative() Assert.False(a.Equals(f)); Assert.False(a.Equals(g)); Assert.False(a.Equals(h)); +#if NET7_0_OR_GREATER Assert.False(a.Equals(i)); +#endif } [Fact] @@ -141,7 +151,9 @@ public void RouteConfig_CanBeJsonSerialized() var route1 = new RouteConfig() { AuthorizationPolicy = "a", +#if NET7_0_OR_GREATER RateLimiterPolicy = "rl", +#endif ClusterId = "c", CorsPolicy = "co", Match = new RouteMatch() diff --git a/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs b/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs index 8074b08b8..a00aea558 100644 --- a/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs +++ b/test/ReverseProxy.Tests/Configuration/YarpRateLimiterPolicyProviderTests.cs @@ -1,12 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#if NET7_0_OR_GREATER using System; using System.Threading.Tasks; -#if NET7_0_OR_GREATER using System.Threading.RateLimiting; -#endif using Microsoft.AspNetCore.Builder; -#if NET7_0_OR_GREATER using Microsoft.AspNetCore.RateLimiting; -#endif using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -14,7 +14,6 @@ namespace Yarp.ReverseProxy.Configuration; public class YarpRateLimiterPolicyProviderTests { -#if NET7_0_OR_GREATER [Fact] public async Task GetPolicyAsync_Works() { @@ -37,5 +36,5 @@ public async Task GetPolicyAsync_Works() Assert.Null(await rateLimiterPolicyProvider.GetPolicyAsync("anotherPolicy")); Assert.NotNull(await rateLimiterPolicyProvider.GetPolicyAsync("customPolicy")); } -#endif } +#endif From 152fafc3099e86544ddfbad3c8c93c53a89ee370 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 12 Jan 2023 13:30:35 -0800 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: Miha Zupan --- docs/docfx/articles/rate-limiting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docfx/articles/rate-limiting.md b/docs/docfx/articles/rate-limiting.md index d2fdf79da..39a488c48 100644 --- a/docs/docfx/articles/rate-limiting.md +++ b/docs/docfx/articles/rate-limiting.md @@ -10,7 +10,7 @@ The reverse proxy can be used to rate-limit requests before they are proxied to No rate limiting is performed on requests unless enabled in the route or application configuration. However, the Rate Limiting middleware (`app.UseRateLimiter()`) can apply a default limiter applied to all routes, and this doesn't require any opt-in from the config. Example: -``` +```c# builder.Services.AddRateLimiter(options => options.GlobalLimiter = globalLimiter); ``` @@ -46,7 +46,7 @@ Example: [RateLimiter policies](https://learn.microsoft.com/aspnet/core/performance/rate-limit) are an ASP.NET Core concept that the proxy utilizes. The proxy provides the above configuration to specify a policy per route and the rest is handled by existing ASP.NET Core rate limiting middleware. RateLimiter policies can be configured in Startup.ConfigureServices as follows: -``` +```c# public void ConfigureServices(IServiceCollection services) { services.AddRateLimiter(options => @@ -64,7 +64,7 @@ public void ConfigureServices(IServiceCollection services) In Startup.Configure add the RateLimiter middleware between Routing and Endpoints. -``` +```c# public void Configure(IApplicationBuilder app) { app.UseRouting();