< 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
90%
Covered branches: 29
Total branches: 32
Branch coverage: 90.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddApplicationForwardedHeaders(...)90%2020100%
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    {
 17626        services
 17627            .AddOptions<ApplicationForwardedHeadersOptions>()
 17628            .Bind(configuration.GetSection(ApplicationForwardedHeadersOptions.SectionName))
 17629            .Validate(
 30630                options => options.ForwardLimit is null or > 0,
 17631                "ProjectTemplate:ForwardedHeaders:ForwardLimit must be null or greater than zero.")
 17632            .Validate(
 30633                options => options.Headers.All(IsValidForwardedHeader),
 17634                "ProjectTemplate:ForwardedHeaders:Headers contains an invalid forwarded header value.")
 17635            .Validate(
 33436                options => options.KnownProxies.All(proxy => IPAddress.TryParse(proxy, out _)),
 17637                "ProjectTemplate:ForwardedHeaders:KnownProxies must contain valid IP addresses.")
 17638            .Validate(
 30639                options => options.KnownNetworks.All(IsValidKnownNetwork),
 17640                "ProjectTemplate:ForwardedHeaders:KnownNetworks must contain valid CIDR ranges such as 10.0.0.0/24.")
 17641            .Validate(
 17642                options =>
 30643                    !options.Headers.Any(IsForwardedHost) ||
 31044                    options.AllowedHosts.Any(host => !string.IsNullOrWhiteSpace(host)),
 17645                "ProjectTemplate:ForwardedHeaders:AllowedHosts must contain at least one host when XForwardedHost is ena
 17646            .ValidateOnStart();
 47
 17648        services.Configure<ForwardedHeadersOptions>(options =>
 17649        {
 14250            ApplicationForwardedHeadersOptions settings =
 14251                configuration
 14252                    .GetSection(ApplicationForwardedHeadersOptions.SectionName)
 14253                    .Get<ApplicationForwardedHeadersOptions>()
 14254                ?? new ApplicationForwardedHeadersOptions();
 17655
 14256            options.ForwardedHeaders = settings.Enabled
 14257                ? BuildForwardedHeaders(settings.Headers)
 14258                : ForwardedHeaders.None;
 17659
 14260            options.ForwardLimit = settings.ForwardLimit;
 14261            options.RequireHeaderSymmetry = settings.RequireHeaderSymmetry;
 17662
 14263            if (settings.ClearKnownNetworksAndProxies)
 17664            {
 1065                options.KnownProxies.Clear();
 1066                options.KnownIPNetworks.Clear();
 17667            }
 17668
 31469            foreach (string proxy in settings.KnownProxies.Where(value => !string.IsNullOrWhiteSpace(value)))
 17670            {
 1071                options.KnownProxies.Add(IPAddress.Parse(proxy));
 17672            }
 17673
 30874            foreach (string network in settings.KnownNetworks.Where(value => !string.IsNullOrWhiteSpace(value)))
 17675            {
 876                options.KnownIPNetworks.Add(ParseIPNetwork(network));
 17677            }
 17678
 14279            if (settings.AllowedHosts.Length > 0)
 17680            {
 481                options.AllowedHosts.Clear();
 17682
 2683                foreach (string host in settings.AllowedHosts.Where(value => !string.IsNullOrWhiteSpace(value)))
 17684                {
 685                    options.AllowedHosts.Add(host);
 17686                }
 17687            }
 31888        });
 89
 17690        services.AddHostedService<ForwardedHeadersTrustDiagnosticsHostedService>();
 91
 17692        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    {
 160102        ApplicationForwardedHeadersOptions settings =
 160103            app.ApplicationServices
 160104                .GetRequiredService<IOptions<ApplicationForwardedHeadersOptions>>()
 160105                .Value;
 106
 154107        return settings.Enabled ? app.UseForwardedHeaders() : app;
 108    }
 109
 110    private static ForwardedHeaders BuildForwardedHeaders(IEnumerable<string> headers)
 111    {
 138112        ForwardedHeaders forwardedHeaders = ForwardedHeaders.None;
 113
 1388114        foreach (string header in headers)
 115        {
 556116            if (TryParseForwardedHeader(header, out ForwardedHeaders parsedHeader))
 117            {
 556118                forwardedHeaders |= parsedHeader;
 119            }
 120        }
 121
 138122        return forwardedHeaders;
 123    }
 124
 125    private static bool IsValidForwardedHeader(string value)
 126    {
 1220127        return TryParseForwardedHeader(value, out _);
 128    }
 129
 130    private static bool TryParseForwardedHeader(string? value, out ForwardedHeaders forwardedHeader)
 131    {
 2996132        forwardedHeader = ForwardedHeaders.None;
 133
 2996134        if (string.IsNullOrWhiteSpace(value))
 135        {
 0136            return false;
 137        }
 138
 2996139        string normalizedValue = value
 2996140            .Trim()
 2996141            .Replace("-", string.Empty, StringComparison.Ordinal)
 2996142            .Replace("_", string.Empty, StringComparison.Ordinal);
 143
 2996144        return Enum.TryParse(normalizedValue, ignoreCase: true, out forwardedHeader);
 145    }
 146
 147    private static bool IsValidKnownNetwork(string value)
 148    {
 149        try
 150        {
 24151            _ = ParseIPNetwork(value);
 20152            return true;
 153        }
 4154        catch
 155        {
 4156            return false;
 157        }
 24158    }
 159
 160    private static NetIPNetwork ParseIPNetwork(string value)
 161    {
 32162        string[] parts = value.Split('/', StringSplitOptions.TrimEntries);
 163
 32164        if (parts.Length != 2)
 165        {
 4166            throw new FormatException($"Invalid CIDR notation: {value}");
 167        }
 168
 28169        var prefix = IPAddress.Parse(parts[0]);
 28170        int prefixLength = int.Parse(parts[1], CultureInfo.InvariantCulture);
 171
 28172        return new NetIPNetwork(prefix, prefixLength);
 173    }
 174
 175    private static bool IsForwardedHost(string value)
 176    {
 1220177        return TryParseForwardedHeader(value, out ForwardedHeaders forwardedHeader) &&
 1220178               forwardedHeader.HasFlag(ForwardedHeaders.XForwardedHost);
 179    }
 180}