< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Security.TrustedProxyOptions
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Security/TrustedProxyOptions.cs
Line coverage
100%
Covered lines: 25
Uncovered lines: 0
Coverable lines: 25
Total lines: 115
Line coverage: 100%
Branch coverage
100%
Covered branches: 18
Total branches: 18
Branch coverage: 100%
Method coverage
100%
Covered methods: 7
Fully covered methods: 7
Total methods: 7
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Proxies()100%11100%
get_Networks()100%11100%
get_InvalidEntries()100%11100%
get_Configured()100%22100%
FromEnvironment()100%11100%
Parse(...)100%1212100%
Apply(...)100%44100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Security/TrustedProxyOptions.cs

#LineLine coverage
 1using System.Net;
 2using Microsoft.AspNetCore.HttpOverrides;
 3
 4namespace ArturRios.Heimdall.WebApi.Security;
 5
 6/// <summary>
 7///     The reverse proxies allowed to tell the API who its caller is, read once at start-up from
 8///     <c>HEIMDALL_TRUSTED_PROXIES</c>: a comma-separated list of addresses (<c>172.18.0.2</c>) and
 9///     CIDR networks (<c>172.18.0.0/16</c>).
 10/// </summary>
 11/// <remarks>
 12///     <para>
 13///         In production the API runs behind Traefik, so every connection's remote address is
 14///         Traefik's. The caller's own address arrives in <c>X-Forwarded-For</c> and the original
 15///         scheme in <c>X-Forwarded-Proto</c>, and ASP.NET Core's forwarded-headers middleware puts
 16///         them back on the connection. Both the rate limiter's partition key and the client address
 17///         <c>TraceActivityMiddleware</c> logs read the connection, so without this every caller
 18///         shares one rate-limit partition and every log line names the proxy.
 19///     </para>
 20///     <para>
 21///         The headers are only believed when the connection comes from a proxy listed here. Any
 22///         client can send <c>X-Forwarded-For</c>; trusting it from everyone would let a caller pick
 23///         the address it is rate limited and logged under. For the same reason only one hop is
 24///         read (the middleware's default <c>ForwardLimit</c> of 1): Traefik appends the address it
 25///         saw to whatever the client sent, so the rightmost entry is the only one Traefik vouches
 26///         for. Put a second proxy in front of Traefik and the limit has to rise with it.
 27///     </para>
 28///     <para>
 29///         Unset, nothing is trusted beyond loopback (ASP.NET Core's default), and the headers are
 30///         not processed at all — the safe reading of a deployment nobody has described.
 31///     </para>
 32/// </remarks>
 33public sealed class TrustedProxyOptions
 34{
 35    /// <summary>Comma-separated addresses and CIDR networks of the trusted reverse proxies.</summary>
 36    public const string ProxiesVariable = "HEIMDALL_TRUSTED_PROXIES";
 37
 38    /// <summary>Trusted proxies given as single addresses.</summary>
 273639    public IReadOnlyList<IPAddress> Proxies { get; private init; } = [];
 40
 41    /// <summary>Trusted proxies given as CIDR networks.</summary>
 273042    public IReadOnlyList<System.Net.IPNetwork> Networks { get; private init; } = [];
 43
 44    /// <summary>
 45    ///     Entries that were neither an address nor a network, and were ignored. Kept so start-up can
 46    ///     say so, instead of silently trusting less than the operator asked for.
 47    /// </summary>
 272448    public IReadOnlyList<string> InvalidEntries { get; private init; } = [];
 49
 50    /// <summary>Whether any proxy is trusted, and so whether forwarded headers are processed.</summary>
 90951    public bool Configured => Proxies.Count > 0 || Networks.Count > 0;
 52
 89953    public static TrustedProxyOptions FromEnvironment() => Parse(Environment.GetEnvironmentVariable(ProxiesVariable));
 54
 55    /// <summary>
 56    ///     Interprets a <see cref="ProxiesVariable" /> value. Split from <see cref="FromEnvironment" />
 57    ///     so it can be tested without mutating the process environment the functional suite's hosts
 58    ///     read.
 59    /// </summary>
 60    public static TrustedProxyOptions Parse(string? value)
 61    {
 91062        List<IPAddress> proxies = [];
 91063        List<System.Net.IPNetwork> networks = [];
 91064        List<string> invalid = [];
 65
 91066        var entries = (value ?? string.Empty)
 91067            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
 68
 184469        foreach (var entry in entries)
 70        {
 1271            if (entry.Contains('/') && System.Net.IPNetwork.TryParse(entry, out var network))
 72            {
 373                networks.Add(network);
 74            }
 975            else if (!entry.Contains('/') && IPAddress.TryParse(entry, out var address))
 76            {
 777                proxies.Add(address);
 78            }
 79            else
 80            {
 281                invalid.Add(entry);
 82            }
 83        }
 84
 91085        return new TrustedProxyOptions { Proxies = proxies, Networks = networks, InvalidEntries = invalid };
 86    }
 87
 88    /// <summary>
 89    ///     Configures the forwarded-headers middleware to read the client address and scheme from the
 90    ///     trusted proxies, and from them only. ASP.NET Core's loopback defaults are replaced rather
 91    ///     than added to: the list says exactly who is trusted.
 92    /// </summary>
 93    /// <remarks>
 94    ///     <c>X-Forwarded-Host</c> is deliberately not read. Traefik forwards the client's own
 95    ///     <c>Host</c> already, and nothing in the API decides anything on the host name — the
 96    ///     metrics endpoint is told apart by port precisely because the host is the caller's choice.
 97    /// </remarks>
 98    public void Apply(ForwardedHeadersOptions options)
 99    {
 5100        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
 101
 5102        options.KnownProxies.Clear();
 5103        options.KnownIPNetworks.Clear();
 104
 18105        foreach (var proxy in Proxies)
 106        {
 4107            options.KnownProxies.Add(proxy);
 108        }
 109
 12110        foreach (var network in Networks)
 111        {
 1112            options.KnownIPNetworks.Add(network);
 113        }
 5114    }
 115}