< Summary

Information
Class: ProjectTemplate.Web.Extensions.ForwardedHeadersExtensions
Assembly: ProjectTemplate.Web
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/ForwardedHeadersExtensions.cs
Line coverage
98%
Covered lines: 95
Uncovered lines: 1
Coverable lines: 96
Total lines: 180
Line coverage: 98.9%
Branch coverage
91%
Covered branches: 11
Total branches: 12
Branch coverage: 91.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddApplicationForwardedHeaders(...)100%11100%
UseApplicationForwardedHeaders(...)100%22100%
BuildForwardedHeaders(...)100%44100%
IsValidForwardedHeader(...)100%11100%
TryParseForwardedHeader(...)50%2287.5%
IsValidKnownNetwork(...)100%11100%
ParseIPNetwork(...)100%22100%
IsForwardedHost(...)100%22100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Extensions/ForwardedHeadersExtensions.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Net;
 3using Microsoft.AspNetCore.HttpOverrides;
 4using Microsoft.Extensions.Options;
 5using ProjectTemplate.Web.Diagnostics;
 6using ProjectTemplate.Web.Options;
 7using NetIPNetwork = System.Net.IPNetwork;
 8
 9namespace ProjectTemplate.Web.Extensions;
 10
 11/// <summary>
 12/// Provides service and middleware registration for configurable forwarded headers support.
 13/// </summary>
 14public static class ForwardedHeadersExtensions
 15{
 16    /// <summary>
 17    /// Registers forwarded headers configuration from appsettings.json.
 18    /// </summary>
 19    /// <param name="services">The service collection.</param>
 20    /// <param name="configuration">Application configuration.</param>
 21    /// <returns>The same service collection for chaining.</returns>
 22    public static IServiceCollection AddApplicationForwardedHeaders(
 23        this IServiceCollection services,
 24        IConfiguration configuration)
 25    {
 11826        services
 11827            .AddOptions<ApplicationForwardedHeadersOptions>()
 11828            .Bind(configuration.GetSection(ApplicationForwardedHeadersOptions.SectionName))
 11829            .Validate(
 11830                options => options.ForwardLimit is null or > 0,
 11831                "ProjectTemplate:ForwardedHeaders:ForwardLimit must be null or greater than zero.")
 11832            .Validate(
 11833                options => options.Headers.All(IsValidForwardedHeader),
 11834                "ProjectTemplate:ForwardedHeaders:Headers contains an invalid forwarded header value.")
 11835            .Validate(
 11836                options => options.KnownProxies.All(proxy => IPAddress.TryParse(proxy, out _)),
 11837                "ProjectTemplate:ForwardedHeaders:KnownProxies must contain valid IP addresses.")
 11838            .Validate(
 11839                options => options.KnownNetworks.All(IsValidKnownNetwork),
 11840                "ProjectTemplate:ForwardedHeaders:KnownNetworks must contain valid CIDR ranges such as 10.0.0.0/24.")
 11841            .Validate(
 11842                options =>
 11843                    !options.Headers.Any(IsForwardedHost) ||
 11844                    options.AllowedHosts.Any(host => !string.IsNullOrWhiteSpace(host)),
 11845                "ProjectTemplate:ForwardedHeaders:AllowedHosts must contain at least one host when XForwardedHost is ena
 11846            .ValidateOnStart();
 47
 11848        services.Configure<ForwardedHeadersOptions>(options =>
 11849        {
 11850            ApplicationForwardedHeadersOptions settings =
 11851                configuration
 11852                    .GetSection(ApplicationForwardedHeadersOptions.SectionName)
 11853                    .Get<ApplicationForwardedHeadersOptions>()
 11854                ?? new ApplicationForwardedHeadersOptions();
 11855
 11856            options.ForwardedHeaders = settings.Enabled
 11857                ? BuildForwardedHeaders(settings.Headers)
 11858                : ForwardedHeaders.None;
 11859
 11860            options.ForwardLimit = settings.ForwardLimit;
 11861            options.RequireHeaderSymmetry = settings.RequireHeaderSymmetry;
 11862
 11863            if (settings.ClearKnownNetworksAndProxies)
 11864            {
 11865                options.KnownProxies.Clear();
 11866                options.KnownIPNetworks.Clear();
 11867            }
 11868
 11869            foreach (string proxy in settings.KnownProxies.Where(value => !string.IsNullOrWhiteSpace(value)))
 11870            {
 11871                options.KnownProxies.Add(IPAddress.Parse(proxy));
 11872            }
 11873
 11874            foreach (string network in settings.KnownNetworks.Where(value => !string.IsNullOrWhiteSpace(value)))
 11875            {
 11876                options.KnownIPNetworks.Add(ParseIPNetwork(network));
 11877            }
 11878
 11879            if (settings.AllowedHosts.Length > 0)
 11880            {
 11881                options.AllowedHosts.Clear();
 11882
 11883                foreach (string host in settings.AllowedHosts.Where(value => !string.IsNullOrWhiteSpace(value)))
 11884                {
 11885                    options.AllowedHosts.Add(host);
 11886                }
 11887            }
 11888        });
 89
 11890        services.AddHostedService<ForwardedHeadersTrustDiagnosticsHostedService>();
 91
 11892        return services;
 93    }
 94
 95    /// <summary>
 96    /// Adds forwarded headers middleware only when enabled by configuration.
 97    /// </summary>
 98    /// <param name="app">The application builder.</param>
 99    /// <returns>The same application builder for chaining.</returns>
 100    public static IApplicationBuilder UseApplicationForwardedHeaders(this IApplicationBuilder app)
 101    {
 110102        ApplicationForwardedHeadersOptions settings =
 110103            app.ApplicationServices
 110104                .GetRequiredService<IOptions<ApplicationForwardedHeadersOptions>>()
 110105                .Value;
 106
 107107        return settings.Enabled ? app.UseForwardedHeaders() : app;
 108    }
 109
 110    private static ForwardedHeaders BuildForwardedHeaders(IEnumerable<string> headers)
 111    {
 99112        ForwardedHeaders forwardedHeaders = ForwardedHeaders.None;
 113
 994114        foreach (string header in headers)
 115        {
 398116            if (TryParseForwardedHeader(header, out ForwardedHeaders parsedHeader))
 117            {
 398118                forwardedHeaders |= parsedHeader;
 119            }
 120        }
 121
 99122        return forwardedHeaders;
 123    }
 124
 125    private static bool IsValidForwardedHeader(string value)
 126    {
 850127        return TryParseForwardedHeader(value, out _);
 128    }
 129
 130    private static bool TryParseForwardedHeader(string? value, out ForwardedHeaders forwardedHeader)
 131    {
 2098132        forwardedHeader = ForwardedHeaders.None;
 133
 2098134        if (string.IsNullOrWhiteSpace(value))
 135        {
 0136            return false;
 137        }
 138
 2098139        string normalizedValue = value
 2098140            .Trim()
 2098141            .Replace("-", string.Empty, StringComparison.Ordinal)
 2098142            .Replace("_", string.Empty, StringComparison.Ordinal);
 143
 2098144        return Enum.TryParse(normalizedValue, ignoreCase: true, out forwardedHeader);
 145    }
 146
 147    private static bool IsValidKnownNetwork(string value)
 148    {
 149        try
 150        {
 12151            _ = ParseIPNetwork(value);
 10152            return true;
 153        }
 2154        catch
 155        {
 2156            return false;
 157        }
 12158    }
 159
 160    private static NetIPNetwork ParseIPNetwork(string value)
 161    {
 16162        string[] parts = value.Split('/', StringSplitOptions.TrimEntries);
 163
 16164        if (parts.Length != 2)
 165        {
 2166            throw new FormatException($"Invalid CIDR notation: {value}");
 167        }
 168
 14169        var prefix = IPAddress.Parse(parts[0]);
 14170        int prefixLength = int.Parse(parts[1], CultureInfo.InvariantCulture);
 171
 14172        return new NetIPNetwork(prefix, prefixLength);
 173    }
 174
 175    private static bool IsForwardedHost(string value)
 176    {
 850177        return TryParseForwardedHeader(value, out ForwardedHeaders forwardedHeader) &&
 850178               forwardedHeader.HasFlag(ForwardedHeaders.XForwardedHost);
 179    }
 180}