< 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: 94
Uncovered lines: 1
Coverable lines: 95
Total lines: 177
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.Options;
 6using NetIPNetwork = System.Net.IPNetwork;
 7
 8namespace ProjectTemplate.Web.Extensions;
 9
 10/// <summary>
 11/// Provides service and middleware registration for configurable forwarded headers support.
 12/// </summary>
 13public static class ForwardedHeadersExtensions
 14{
 15    /// <summary>
 16    /// Registers forwarded headers configuration from appsettings.json.
 17    /// </summary>
 18    /// <param name="services">The service collection.</param>
 19    /// <param name="configuration">Application configuration.</param>
 20    /// <returns>The same service collection for chaining.</returns>
 21    public static IServiceCollection AddApplicationForwardedHeaders(
 22        this IServiceCollection services,
 23        IConfiguration configuration)
 24    {
 16425        services
 16426            .AddOptions<ApplicationForwardedHeadersOptions>()
 16427            .Bind(configuration.GetSection(ApplicationForwardedHeadersOptions.SectionName))
 16428            .Validate(
 28629                options => options.ForwardLimit is null or > 0,
 16430                "ProjectTemplate:ForwardedHeaders:ForwardLimit must be null or greater than zero.")
 16431            .Validate(
 28632                options => options.Headers.All(IsValidForwardedHeader),
 16433                "ProjectTemplate:ForwardedHeaders:Headers contains an invalid forwarded header value.")
 16434            .Validate(
 31435                options => options.KnownProxies.All(proxy => IPAddress.TryParse(proxy, out _)),
 16436                "ProjectTemplate:ForwardedHeaders:KnownProxies must contain valid IP addresses.")
 16437            .Validate(
 28638                options => options.KnownNetworks.All(IsValidKnownNetwork),
 16439                "ProjectTemplate:ForwardedHeaders:KnownNetworks must contain valid CIDR ranges such as 10.0.0.0/24.")
 16440            .Validate(
 16441                options =>
 28642                    !options.Headers.Any(IsForwardedHost) ||
 29043                    options.AllowedHosts.Any(host => !string.IsNullOrWhiteSpace(host)),
 16444                "ProjectTemplate:ForwardedHeaders:AllowedHosts must contain at least one host when XForwardedHost is ena
 16445            .ValidateOnStart();
 46
 16447        services.Configure<ForwardedHeadersOptions>(options =>
 16448        {
 13249            ApplicationForwardedHeadersOptions settings =
 13250                configuration
 13251                    .GetSection(ApplicationForwardedHeadersOptions.SectionName)
 13252                    .Get<ApplicationForwardedHeadersOptions>()
 13253                ?? new ApplicationForwardedHeadersOptions();
 16454
 13255            options.ForwardedHeaders = settings.Enabled
 13256                ? BuildForwardedHeaders(settings.Headers)
 13257                : ForwardedHeaders.None;
 16458
 13259            options.ForwardLimit = settings.ForwardLimit;
 13260            options.RequireHeaderSymmetry = settings.RequireHeaderSymmetry;
 16461
 13262            if (settings.ClearKnownNetworksAndProxies)
 16463            {
 1064                options.KnownProxies.Clear();
 1065                options.KnownIPNetworks.Clear();
 16466            }
 16467
 29468            foreach (string proxy in settings.KnownProxies.Where(value => !string.IsNullOrWhiteSpace(value)))
 16469            {
 1070                options.KnownProxies.Add(IPAddress.Parse(proxy));
 16471            }
 16472
 28873            foreach (string network in settings.KnownNetworks.Where(value => !string.IsNullOrWhiteSpace(value)))
 16474            {
 875                options.KnownIPNetworks.Add(ParseIPNetwork(network));
 16476            }
 16477
 13278            if (settings.AllowedHosts.Length > 0)
 16479            {
 480                options.AllowedHosts.Clear();
 16481
 2682                foreach (string host in settings.AllowedHosts.Where(value => !string.IsNullOrWhiteSpace(value)))
 16483                {
 684                    options.AllowedHosts.Add(host);
 16485                }
 16486            }
 29687        });
 88
 16489        return services;
 90    }
 91
 92    /// <summary>
 93    /// Adds forwarded headers middleware only when enabled by configuration.
 94    /// </summary>
 95    /// <param name="app">The application builder.</param>
 96    /// <returns>The same application builder for chaining.</returns>
 97    public static IApplicationBuilder UseApplicationForwardedHeaders(this IApplicationBuilder app)
 98    {
 14899        ApplicationForwardedHeadersOptions settings =
 148100            app.ApplicationServices
 148101                .GetRequiredService<IOptions<ApplicationForwardedHeadersOptions>>()
 148102                .Value;
 103
 142104        return settings.Enabled ? app.UseForwardedHeaders() : app;
 105    }
 106
 107    private static ForwardedHeaders BuildForwardedHeaders(IEnumerable<string> headers)
 108    {
 128109        ForwardedHeaders forwardedHeaders = ForwardedHeaders.None;
 110
 1288111        foreach (string header in headers)
 112        {
 516113            if (TryParseForwardedHeader(header, out ForwardedHeaders parsedHeader))
 114            {
 516115                forwardedHeaders |= parsedHeader;
 116            }
 117        }
 118
 128119        return forwardedHeaders;
 120    }
 121
 122    private static bool IsValidForwardedHeader(string value)
 123    {
 1140124        return TryParseForwardedHeader(value, out _);
 125    }
 126
 127    private static bool TryParseForwardedHeader(string? value, out ForwardedHeaders forwardedHeader)
 128    {
 2796129        forwardedHeader = ForwardedHeaders.None;
 130
 2796131        if (string.IsNullOrWhiteSpace(value))
 132        {
 0133            return false;
 134        }
 135
 2796136        string normalizedValue = value
 2796137            .Trim()
 2796138            .Replace("-", string.Empty, StringComparison.Ordinal)
 2796139            .Replace("_", string.Empty, StringComparison.Ordinal);
 140
 2796141        return Enum.TryParse(normalizedValue, ignoreCase: true, out forwardedHeader);
 142    }
 143
 144    private static bool IsValidKnownNetwork(string value)
 145    {
 146        try
 147        {
 24148            _ = ParseIPNetwork(value);
 20149            return true;
 150        }
 4151        catch
 152        {
 4153            return false;
 154        }
 24155    }
 156
 157    private static NetIPNetwork ParseIPNetwork(string value)
 158    {
 32159        string[] parts = value.Split('/', StringSplitOptions.TrimEntries);
 160
 32161        if (parts.Length != 2)
 162        {
 4163            throw new FormatException($"Invalid CIDR notation: {value}");
 164        }
 165
 28166        var prefix = IPAddress.Parse(parts[0]);
 28167        int prefixLength = int.Parse(parts[1], CultureInfo.InvariantCulture);
 168
 28169        return new NetIPNetwork(prefix, prefixLength);
 170    }
 171
 172    private static bool IsForwardedHost(string value)
 173    {
 1140174        return TryParseForwardedHeader(value, out ForwardedHeaders forwardedHeader) &&
 1140175               forwardedHeader.HasFlag(ForwardedHeaders.XForwardedHost);
 176    }
 177}