diff --git a/README.md b/README.md index 101456b..b09a00c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ using PANiXiDA.Core.Presentation.Http.DependencyInjection; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttp(builder.Configuration.GetSection("ForwardedHeaders")); +builder.Services.AddHttp(builder.Configuration); var app = builder.Build(); @@ -68,7 +68,9 @@ ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto ``` -Additional values can be bound from the standard ASP.NET Core `ForwardedHeadersOptions` model by passing a configuration section to `AddHttp`. +The package also clears the default loopback-only `KnownIPNetworks` and `KnownProxies` restrictions so applications behind Kubernetes ingress or Gateway API proxies can process forwarded headers without per-service proxy registration. + +Additional values can be bound from the standard ASP.NET Core `ForwardedHeadersOptions` model by adding a `ForwardedHeaders` section to the application configuration. ```json { @@ -83,17 +85,17 @@ Additional values can be bound from the standard ASP.NET Core `ForwardedHeadersO } ``` -For advanced scenarios, configure `ForwardedHeadersOptions` directly after `AddHttp`. +For stricter trust boundaries, configure `ForwardedHeadersOptions` directly after `AddHttp`. ```csharp using Microsoft.AspNetCore.HttpOverrides; +using System.Net; -builder.Services.AddHttp(builder.Configuration.GetSection("ForwardedHeaders")); +builder.Services.AddHttp(builder.Configuration); builder.Services.Configure(options => { - options.KnownProxies.Clear(); - options.KnownIPNetworks.Clear(); + options.KnownProxies.Add(IPAddress.Parse("10.0.0.10")); }); ``` diff --git a/src/PANiXiDA.Core.Presentation.Http/Configurations/ForwardedHeadersConfiguration.cs b/src/PANiXiDA.Core.Presentation.Http/Configurations/ForwardedHeadersConfiguration.cs index 987110f..c9d0490 100644 --- a/src/PANiXiDA.Core.Presentation.Http/Configurations/ForwardedHeadersConfiguration.cs +++ b/src/PANiXiDA.Core.Presentation.Http/Configurations/ForwardedHeadersConfiguration.cs @@ -7,6 +7,8 @@ namespace PANiXiDA.Core.Presentation.Http.Configurations; internal static class ForwardedHeadersConfiguration { + private const string SectionName = "ForwardedHeaders"; + internal static IServiceCollection AddForwardedHeadersConfiguration( this IServiceCollection services, IConfiguration? configuration) @@ -17,11 +19,13 @@ internal static IServiceCollection AddForwardedHeadersConfiguration( ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); }); if (configuration is not null) { - services.Configure(configuration); + services.Configure(ResolveConfiguration(configuration)); } return services; @@ -33,4 +37,16 @@ internal static WebApplication UseForwardedHeadersConfiguration(this WebApplicat return app; } + + private static IConfiguration ResolveConfiguration(IConfiguration configuration) + { + IConfigurationSection forwardedHeadersSection = configuration.GetSection(SectionName); + + if (forwardedHeadersSection.GetChildren().Any()) + { + return forwardedHeadersSection; + } + + return configuration; + } } diff --git a/src/PANiXiDA.Core.Presentation.Http/DependencyInjection/ServiceCollectionExtensions.cs b/src/PANiXiDA.Core.Presentation.Http/DependencyInjection/ServiceCollectionExtensions.cs index ee37c40..bb8c0df 100644 --- a/src/PANiXiDA.Core.Presentation.Http/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/PANiXiDA.Core.Presentation.Http/DependencyInjection/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public static class ServiceCollectionExtensions /// Registers the default HTTP presentation services, including API versioning, OpenAPI, Problem Details, exception handling, validation, health checks, and forwarded headers. /// /// The application service collection. - /// The forwarded headers configuration section, or to use defaults. + /// The application configuration. The standard ForwardedHeaders section is used when present. Pass a custom forwarded headers section or to use defaults. /// The original service collection for further configuration. public static IServiceCollection AddHttp( this IServiceCollection services, diff --git a/tests/PANiXiDA.Core.Presentation.Http.UnitTests/Configurations/ForwardedHeadersConfigurationTests.cs b/tests/PANiXiDA.Core.Presentation.Http.UnitTests/Configurations/ForwardedHeadersConfigurationTests.cs index 65ebdaf..77a259d 100644 --- a/tests/PANiXiDA.Core.Presentation.Http.UnitTests/Configurations/ForwardedHeadersConfigurationTests.cs +++ b/tests/PANiXiDA.Core.Presentation.Http.UnitTests/Configurations/ForwardedHeadersConfigurationTests.cs @@ -14,7 +14,6 @@ public sealed class ForwardedHeadersConfigurationTests public void AddForwardedHeadersConfiguration_ShouldUseDefaultHeaders() { var services = new ServiceCollection(); - var defaultOptions = new ForwardedHeadersOptions(); var result = services.AddForwardedHeadersConfiguration(configuration: null); @@ -25,8 +24,8 @@ public void AddForwardedHeadersConfiguration_ShouldUseDefaultHeaders() ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto); - options.KnownIPNetworks.Count.ShouldBe(defaultOptions.KnownIPNetworks.Count); - options.KnownProxies.Count.ShouldBe(defaultOptions.KnownProxies.Count); + options.KnownIPNetworks.ShouldBeEmpty(); + options.KnownProxies.ShouldBeEmpty(); } [Fact(DisplayName = "ForwardedHeaders configuration binds standard options from configuration")] @@ -51,6 +50,26 @@ public void AddForwardedHeadersConfiguration_ShouldBindStandardOptionsFromConfig options.AllowedHosts.ShouldBe(["api.example.test"]); } + [Fact(DisplayName = "ForwardedHeaders configuration binds standard section from root configuration")] + public void AddForwardedHeadersConfiguration_ShouldBindStandardSectionFromRootConfiguration() + { + var services = new ServiceCollection(); + var configuration = CreateConfiguration(new Dictionary + { + ["ForwardedHeaders:ForwardedHeaders"] = nameof(ForwardedHeaders.XForwardedProto), + ["ForwardedHeaders:ForwardLimit"] = "3", + ["ForwardedHeaders:AllowedHosts:0"] = "api.example.test" + }); + + services.AddForwardedHeadersConfiguration(configuration); + + var options = CreateOptions(services); + + options.ForwardedHeaders.ShouldBe(ForwardedHeaders.XForwardedProto); + options.ForwardLimit.ShouldBe(3); + options.AllowedHosts.ShouldBe(["api.example.test"]); + } + [Fact(DisplayName = "ForwardedHeaders configuration accepts a custom section")] public void AddForwardedHeadersConfiguration_ShouldBindStandardOptionsFromCustomSection() { diff --git a/tests/PANiXiDA.Core.Presentation.Http.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/tests/PANiXiDA.Core.Presentation.Http.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs index dc7452b..2fb5d8c 100644 --- a/tests/PANiXiDA.Core.Presentation.Http.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/tests/PANiXiDA.Core.Presentation.Http.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -48,6 +48,25 @@ public void AddHttp_ShouldApplyForwardedHeadersConfiguration() options.ForwardLimit.ShouldBe(4); } + [Fact(DisplayName = "AddHttp applies ForwardedHeaders section from root configuration")] + public void AddHttp_ShouldApplyForwardedHeadersSectionFromRootConfiguration() + { + var services = new ServiceCollection(); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["ForwardedHeaders:" + nameof(ForwardedHeadersOptions.ForwardLimit)] = "6" + }) + .Build(); + + services.AddHttp(configuration); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>().Value; + + options.ForwardLimit.ShouldBe(6); + } + [Fact(DisplayName = "AddHttp registers health check services")] public void AddHttp_ShouldRegisterHealthCheckServices() {