From 1cc144758b12cb018d6ae1a5b0930b89dc3c43ee Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 12 Sep 2025 08:26:18 -0700 Subject: [PATCH 1/2] Refactor unit tests to handle the obsolete property --- .../test/ForwardedHeadersMiddlewareTest.cs | 188 +++++++++++++++++- 1 file changed, 184 insertions(+), 4 deletions(-) diff --git a/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs b/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs index 317d2853d023..606fe2ebc91c 100644 --- a/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs +++ b/src/Middleware/HttpOverrides/test/ForwardedHeadersMiddlewareTest.cs @@ -104,7 +104,7 @@ public async Task XForwardedForFirstValueIsInvalid(int limit, string header, str [InlineData(2, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "13.113.113.13:34567", true)] [InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "13.113.113.13", 34567, "", false)] [InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "13.113.113.13", 34567, "", true)] - public async Task XForwardedForForwardLimit(int limit, string header, string expectedIp, int expectedPort, string remainingHeader, bool requireSymmetry) + public async Task XForwardedForForwardLimit_Obsolete(int limit, string header, string expectedIp, int expectedPort, string remainingHeader, bool requireSymmetry) { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => @@ -123,6 +123,61 @@ public async Task XForwardedForForwardLimit(int limit, string header, string exp #pragma warning disable ASPDEPR005 // KnownNetworks is obsolete options.KnownNetworks.Clear(); #pragma warning restore ASPDEPR005 // KnownNetworks is obsolete + app.UseForwardedHeaders(options); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-For"] = header; + c.Connection.RemoteIpAddress = IPAddress.Parse("10.0.0.1"); + c.Connection.RemotePort = 99; + }); + + Assert.Equal(expectedIp, context.Connection.RemoteIpAddress.ToString()); + Assert.Equal(expectedPort, context.Connection.RemotePort); + Assert.Equal(remainingHeader, context.Request.Headers["X-Forwarded-For"].ToString()); + } + + [Theory] + [InlineData(1, "11.111.111.11:12345", "11.111.111.11", 12345, "", false)] + [InlineData(1, "11.111.111.11:12345", "11.111.111.11", 12345, "", true)] + [InlineData(10, "11.111.111.11:12345", "11.111.111.11", 12345, "", false)] + [InlineData(10, "11.111.111.11:12345", "11.111.111.11", 12345, "", true)] + [InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12:23456", false)] + [InlineData(1, "12.112.112.12:23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12:23456", true)] + [InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", false)] + [InlineData(2, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", true)] + [InlineData(10, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", false)] + [InlineData(10, "12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "", true)] + [InlineData(10, "12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12.23456", false)] // Invalid 2nd value + [InlineData(10, "12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "12.112.112.12.23456", true)] // Invalid 2nd value + [InlineData(10, "13.113.113.13:34567, 12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "13.113.113.13:34567,12.112.112.12.23456", false)] // Invalid 2nd value + [InlineData(10, "13.113.113.13:34567, 12.112.112.12.23456, 11.111.111.11:12345", "11.111.111.11", 12345, "13.113.113.13:34567,12.112.112.12.23456", true)] // Invalid 2nd value + [InlineData(2, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "13.113.113.13:34567", false)] + [InlineData(2, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "12.112.112.12", 23456, "13.113.113.13:34567", true)] + [InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "13.113.113.13", 34567, "", false)] + [InlineData(3, "13.113.113.13:34567, 12.112.112.12:23456, 11.111.111.11:12345", "13.113.113.13", 34567, "", true)] + public async Task XForwardedForForwardLimit(int limit, string header, string expectedIp, int expectedPort, string remainingHeader, bool requireSymmetry) + { + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + var options = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor, + RequireHeaderSymmetry = requireSymmetry, + ForwardLimit = limit, + }; + options.KnownProxies.Clear(); options.KnownIPNetworks.Clear(); app.UseForwardedHeaders(options); }); @@ -847,7 +902,7 @@ public async Task XForwardedProtoOverrideCanBeIndependentOfXForwardedForCount(in [InlineData("h2, h1", "", "::1", true, "http")] [InlineData("h2, h1", "F::, D::", "::1", true, "h1")] [InlineData("h2, h1", "E::, D::", "F::", true, "http")] - public async Task XForwardedProtoOverrideLimitedByLoopback(string protoHeader, string forHeader, string remoteIp, bool loopback, string expected) + public async Task XForwardedProtoOverrideLimitedByLoopback_Obsolete(string protoHeader, string forHeader, string remoteIp, bool loopback, string expected) { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => @@ -867,6 +922,56 @@ public async Task XForwardedProtoOverrideLimitedByLoopback(string protoHeader, s #pragma warning disable ASPDEPR005 // KnownNetworks is obsolete options.KnownNetworks.Clear(); #pragma warning restore ASPDEPR005 // KnownNetworks is obsolete + options.KnownProxies.Clear(); + } + app.UseForwardedHeaders(options); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-Proto"] = protoHeader; + c.Request.Headers["X-Forwarded-For"] = forHeader; + c.Connection.RemoteIpAddress = IPAddress.Parse(remoteIp); + }); + + Assert.Equal(expected, context.Request.Scheme); + } + + [Theory] + [InlineData("", "", "::1", false, "http")] + [InlineData("h1", "", "::1", false, "http")] + [InlineData("h1", "F::", "::1", false, "h1")] + [InlineData("h1", "F::", "E::", false, "h1")] + [InlineData("", "", "::1", true, "http")] + [InlineData("h1", "", "::1", true, "http")] + [InlineData("h1", "F::", "::1", true, "h1")] + [InlineData("h1", "", "F::", true, "http")] + [InlineData("h1", "E::", "F::", true, "http")] + [InlineData("h2, h1", "", "::1", true, "http")] + [InlineData("h2, h1", "F::, D::", "::1", true, "h1")] + [InlineData("h2, h1", "E::, D::", "F::", true, "http")] + public async Task XForwardedProtoOverrideLimitedByLoopback(string protoHeader, string forHeader, string remoteIp, bool loopback, string expected) + { + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + var options = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor, + RequireHeaderSymmetry = true, + ForwardLimit = 5, + }; + if (!loopback) + { options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); } @@ -1127,7 +1232,7 @@ public async Task XForwardForIPv4ToIPv6Mapping(string forHeader, string knownPro [Theory] [InlineData(1, "httpa, httpb, httpc", "httpc", "httpa,httpb")] [InlineData(2, "httpa, httpb, httpc", "httpb", "httpa")] - public async Task ForwardersWithDIOptionsRunsOnce(int limit, string header, string expectedScheme, string remainingHeader) + public async Task ForwardersWithDIOptionsRunsOnce_Obsolete(int limit, string header, string expectedScheme, string remainingHeader) { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => @@ -1143,6 +1248,45 @@ public async Task ForwardersWithDIOptionsRunsOnce(int limit, string header, stri #pragma warning disable ASPDEPR005 // KnownNetworks is obsolete options.KnownNetworks.Clear(); #pragma warning restore ASPDEPR005 // KnownNetworks is obsolete + options.ForwardLimit = limit; + }); + }) + .Configure(app => + { + app.UseForwardedHeaders(); + app.UseForwardedHeaders(); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-Proto"] = header; + }); + + Assert.Equal(expectedScheme, context.Request.Scheme); + Assert.Equal(remainingHeader, context.Request.Headers["X-Forwarded-Proto"].ToString()); + } + + [Theory] + [InlineData(1, "httpa, httpb, httpc", "httpc", "httpa,httpb")] + [InlineData(2, "httpa, httpb, httpc", "httpb", "httpa")] + public async Task ForwardersWithDIOptionsRunsOnce(int limit, string header, string expectedScheme, string remainingHeader) + { + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; + options.KnownProxies.Clear(); options.KnownIPNetworks.Clear(); options.ForwardLimit = limit; }); @@ -1170,7 +1314,7 @@ public async Task ForwardersWithDIOptionsRunsOnce(int limit, string header, stri [Theory] [InlineData(1, "httpa, httpb, httpc", "httpb", "httpa")] [InlineData(2, "httpa, httpb, httpc", "httpa", "")] - public async Task ForwardersWithDirectOptionsRunsTwice(int limit, string header, string expectedScheme, string remainingHeader) + public async Task ForwardersWithDirectOptionsRunsTwice_Obsolete(int limit, string header, string expectedScheme, string remainingHeader) { using var host = new HostBuilder() .ConfigureWebHost(webHostBuilder => @@ -1188,6 +1332,42 @@ public async Task ForwardersWithDirectOptionsRunsTwice(int limit, string header, #pragma warning disable ASPDEPR005 // KnownNetworks is obsolete options.KnownNetworks.Clear(); #pragma warning restore ASPDEPR005 // KnownNetworks is obsolete + app.UseForwardedHeaders(options); + app.UseForwardedHeaders(options); + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Forwarded-Proto"] = header; + }); + + Assert.Equal(expectedScheme, context.Request.Scheme); + Assert.Equal(remainingHeader, context.Request.Headers["X-Forwarded-Proto"].ToString()); + } + + [Theory] + [InlineData(1, "httpa, httpb, httpc", "httpb", "httpa")] + [InlineData(2, "httpa, httpb, httpc", "httpa", "")] + public async Task ForwardersWithDirectOptionsRunsTwice(int limit, string header, string expectedScheme, string remainingHeader) + { + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + var options = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedProto, + ForwardLimit = limit, + }; + options.KnownProxies.Clear(); options.KnownIPNetworks.Clear(); app.UseForwardedHeaders(options); app.UseForwardedHeaders(options); From f5900999f9fdd66a224ae2aaab9461f11cfe1c1e Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 12 Sep 2025 08:57:53 -0700 Subject: [PATCH 2/2] Implement KnownNetworks dual list Fixes #63627 --- .../src/ForwardedHeadersOptionsSetup.cs | 3 - .../HttpOverrides/src/DualIPNetworkList.cs | 140 ++++++++++ .../src/ForwardedHeadersMiddleware.cs | 15 +- .../src/ForwardedHeadersOptions.cs | 8 +- .../test/DualIPNetworkListTests.cs | 248 ++++++++++++++++++ 5 files changed, 395 insertions(+), 19 deletions(-) create mode 100644 src/Middleware/HttpOverrides/src/DualIPNetworkList.cs create mode 100644 src/Middleware/HttpOverrides/test/DualIPNetworkListTests.cs diff --git a/src/DefaultBuilder/src/ForwardedHeadersOptionsSetup.cs b/src/DefaultBuilder/src/ForwardedHeadersOptionsSetup.cs index 8109ca39b323..9d2fe4d790cb 100644 --- a/src/DefaultBuilder/src/ForwardedHeadersOptionsSetup.cs +++ b/src/DefaultBuilder/src/ForwardedHeadersOptionsSetup.cs @@ -27,9 +27,6 @@ public void Configure(ForwardedHeadersOptions options) options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; // Only loopback proxies are allowed by default. Clear that restriction because forwarders are // being enabled by explicit configuration. -#pragma warning disable ASPDEPR005 // KnownNetworks is obsolete - options.KnownNetworks.Clear(); -#pragma warning restore ASPDEPR005 // KnownNetworks is obsolete options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); } diff --git a/src/Middleware/HttpOverrides/src/DualIPNetworkList.cs b/src/Middleware/HttpOverrides/src/DualIPNetworkList.cs new file mode 100644 index 000000000000..7a3b92b1038f --- /dev/null +++ b/src/Middleware/HttpOverrides/src/DualIPNetworkList.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable ASPDEPR005 // Type or member is obsolete + +using AspNetIPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +using IPAddress = System.Net.IPAddress; +using IPNetwork = System.Net.IPNetwork; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Internal list implementation that keeps and the obsolete +/// collections in sync. Modifications +/// through either interface are reflected in the other. +/// +internal sealed class DualIPNetworkList : IList, IList +{ + // Two independent underlying lists so each side behaves exactly like a List with respect to + // enumeration versioning, capacity growth, etc. They are kept strictly in sync by all mutating operations. + private readonly List _system = new(); + private readonly List _aspnet = new(); + + public DualIPNetworkList() + { + // Default entry (loopback) added to both representations. + var loopback = new IPNetwork(IPAddress.Loopback, 8); + _system.Add(loopback); + _aspnet.Add(new AspNetIPNetwork(loopback.BaseAddress, loopback.PrefixLength)); + } + + int ICollection.Count => _system.Count; + int ICollection.Count => _aspnet.Count; + + bool ICollection.IsReadOnly => false; + bool ICollection.IsReadOnly => false; + + IPNetwork IList.this[int index] + { + get => _system[index]; + set + { + _system[index] = value; + _aspnet[index] = new AspNetIPNetwork(value.BaseAddress, value.PrefixLength); + } + } + + AspNetIPNetwork IList.this[int index] + { + get => _aspnet[index]; + set + { + _aspnet[index] = value; + _system[index] = new IPNetwork(value.Prefix, value.PrefixLength); + } + } + + void ICollection.Add(IPNetwork item) + { + _system.Add(item); + _aspnet.Add(new AspNetIPNetwork(item.BaseAddress, item.PrefixLength)); + } + + void ICollection.Add(AspNetIPNetwork item) + { + _aspnet.Add(item); + _system.Add(new IPNetwork(item.Prefix, item.PrefixLength)); + } + + public void Clear() + { + _system.Clear(); + _aspnet.Clear(); + } + + void ICollection.Clear() => Clear(); + void ICollection.Clear() => Clear(); + + bool ICollection.Contains(IPNetwork item) => _system.Contains(item); + bool ICollection.Contains(AspNetIPNetwork item) => _aspnet.Contains(item); + + public void CopyTo(IPNetwork[] array, int arrayIndex) => _system.CopyTo(array, arrayIndex); + public void CopyTo(AspNetIPNetwork[] array, int arrayIndex) => _aspnet.CopyTo(array, arrayIndex); + + void ICollection.CopyTo(IPNetwork[] array, int arrayIndex) => CopyTo(array, arrayIndex); + void ICollection.CopyTo(AspNetIPNetwork[] array, int arrayIndex) => CopyTo(array, arrayIndex); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => _system.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _system.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _aspnet.GetEnumerator(); + + int IList.IndexOf(IPNetwork item) => _system.IndexOf(item); + int IList.IndexOf(AspNetIPNetwork item) => _aspnet.IndexOf(item); + + void IList.Insert(int index, IPNetwork item) + { + _system.Insert(index, item); + _aspnet.Insert(index, new AspNetIPNetwork(item.BaseAddress, item.PrefixLength)); + } + + void IList.Insert(int index, AspNetIPNetwork item) + { + _aspnet.Insert(index, item); + _system.Insert(index, new IPNetwork(item.Prefix, item.PrefixLength)); + } + + bool ICollection.Remove(IPNetwork item) + { + var idx = _system.IndexOf(item); + if (idx >= 0) + { + RemoveAt(idx); + return true; + } + return false; + } + + bool ICollection.Remove(AspNetIPNetwork item) + { + var idx = _aspnet.IndexOf(item); + if (idx >= 0) + { + RemoveAt(idx); + return true; + } + return false; + } + + public void RemoveAt(int index) + { + _system.RemoveAt(index); + _aspnet.RemoveAt(index); + } + + void IList.RemoveAt(int index) => RemoveAt(index); + void IList.RemoveAt(int index) => RemoveAt(index); +} + +#pragma warning restore ASPDEPR005 // Type or member is obsolete diff --git a/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs b/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs index fb1e757ff2e2..d60191021f08 100644 --- a/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs +++ b/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs @@ -213,11 +213,7 @@ public void ApplyForwarders(HttpContext context) // Host and Scheme initial values are never inspected, no need to set them here. }; - var checkKnownIps = _options.KnownIPNetworks.Count > 0 -#pragma warning disable ASPDEPR005 // KnownNetworks is obsolete - || _options.KnownNetworks.Count > 0 -#pragma warning restore ASPDEPR005 // KnownNetworks is obsolete - || _options.KnownProxies.Count > 0; + var checkKnownIps = _options.KnownIPNetworks.Count > 0 || _options.KnownProxies.Count > 0; bool applyChanges = false; int entriesConsumed = 0; @@ -410,15 +406,6 @@ private bool CheckKnownAddress(IPAddress address) return true; } } -#pragma warning disable ASPDEPR005 // KnownNetworks is obsolete - foreach (var network in _options.KnownNetworks) - { - if (network.Contains(address)) - { - return true; - } - } -#pragma warning restore ASPDEPR005 // KnownNetworks is obsolete return false; } diff --git a/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs b/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs index e0ed1820c001..bd835a21a673 100644 --- a/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs +++ b/src/Middleware/HttpOverrides/src/ForwardedHeadersOptions.cs @@ -13,6 +13,10 @@ namespace Microsoft.AspNetCore.Builder; /// public class ForwardedHeadersOptions { + // Backing dual list that keeps the obsolete and new types in sync. + // Once the obsolete IList property is removed this can be changed to a simple List. + private readonly DualIPNetworkList _knownNetworks = new(); + /// /// Gets or sets the header used to retrieve the originating client IP. Defaults to the value specified by /// . @@ -87,12 +91,12 @@ public class ForwardedHeadersOptions /// Obsolete, please use instead /// [Obsolete("Please use KnownIPNetworks instead. For more information, visit https://aka.ms/aspnet/deprecate/005.", DiagnosticId = "ASPDEPR005")] - public IList KnownNetworks { get; } = new List() { new(IPAddress.Loopback, 8) }; + public IList KnownNetworks => _knownNetworks; /// /// Address ranges of known proxies to accept forwarded headers from. /// - public IList KnownIPNetworks { get; } = new List() { new(IPAddress.Loopback, 8) }; + public IList KnownIPNetworks => _knownNetworks; /// /// The allowed values from x-forwarded-host. If the list is empty then all hosts are allowed. diff --git a/src/Middleware/HttpOverrides/test/DualIPNetworkListTests.cs b/src/Middleware/HttpOverrides/test/DualIPNetworkListTests.cs new file mode 100644 index 000000000000..581d197acb85 --- /dev/null +++ b/src/Middleware/HttpOverrides/test/DualIPNetworkListTests.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using Microsoft.AspNetCore.Builder; +using Xunit; +using AspNetIPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; + +namespace Microsoft.AspNetCore.HttpOverrides; + +public class DualIPNetworkListTests +{ + [Fact] + public void DefaultContainsLoopback() + { + var options = new ForwardedHeadersOptions(); + Assert.Single(options.KnownIPNetworks); + Assert.Equal("127.0.0.0", options.KnownIPNetworks[0].BaseAddress.ToString()); + Assert.Equal(8, options.KnownIPNetworks[0].PrefixLength); +#pragma warning disable ASPDEPR005 + Assert.Single(options.KnownNetworks); + Assert.Equal("127.0.0.0", options.KnownNetworks[0].Prefix.ToString()); + Assert.Equal(8, options.KnownNetworks[0].PrefixLength); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void AddThroughSystemCollectionVisibleViaObsolete() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); +#pragma warning disable ASPDEPR005 + var obsoleteList = options.KnownNetworks; + Assert.Equal(2, obsoleteList.Count); + Assert.Equal(IPAddress.Parse("10.0.0.0"), obsoleteList[1].Prefix); + Assert.Equal(8, obsoleteList[1].PrefixLength); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void AddThroughObsoleteCollectionVisibleViaSystem() + { +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + options.KnownNetworks.Add(new AspNetIPNetwork(IPAddress.Parse("192.168.0.0"), 16)); + Assert.Equal(2, options.KnownIPNetworks.Count); + Assert.Equal("192.168.0.0/16", options.KnownIPNetworks[1].ToString()); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void ReplaceViaSystemIndexerUpdatesObsolete() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks[0] = System.Net.IPNetwork.Parse("172.16.0.0/12"); +#pragma warning disable ASPDEPR005 + Assert.Equal(IPAddress.Parse("172.16.0.0"), options.KnownNetworks[0].Prefix); + Assert.Equal(12, options.KnownNetworks[0].PrefixLength); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void ReplaceViaObsoleteIndexerUpdatesSystem() + { +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + options.KnownNetworks[0] = new AspNetIPNetwork(IPAddress.Parse("172.16.0.0"), 12); + Assert.Equal("172.16.0.0/12", options.KnownIPNetworks[0].ToString()); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void ClearClearsBoth() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Clear(); +#pragma warning disable ASPDEPR005 + Assert.Empty(options.KnownNetworks); +#pragma warning restore ASPDEPR005 + Assert.Empty(options.KnownIPNetworks); + } + + [Fact] + public void RemoveThroughEitherCollectionRemovesFromBoth() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); + var first = options.KnownIPNetworks[0]; + var removed = options.KnownIPNetworks.Remove(first); + Assert.True(removed); +#pragma warning disable ASPDEPR005 + var obsoleteList = options.KnownNetworks; + Assert.DoesNotContain(obsoleteList, n => n.Prefix.Equals(IPAddress.Loopback)); +#pragma warning restore ASPDEPR005 + Assert.Single(options.KnownIPNetworks); // only the 10.0.0.0/8 entry should remain + } + + // New tests to cover each IList member for both interfaces + + [Fact] + public void ContainsWorksForBothLists() + { + var options = new ForwardedHeadersOptions(); + var loopback = options.KnownIPNetworks[0]; + Assert.Contains(loopback, options.KnownIPNetworks); +#pragma warning disable ASPDEPR005 + Assert.Contains(options.KnownNetworks, n => n.Prefix.Equals(loopback.BaseAddress) && n.PrefixLength == loopback.PrefixLength); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void CopyToSystem() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); + var arr = new System.Net.IPNetwork[5]; + options.KnownIPNetworks.CopyTo(arr, 1); + Assert.Equal("127.0.0.0/8", arr[1].ToString()); + Assert.Equal("10.0.0.0/8", arr[2].ToString()); + } + + [Fact] + public void CopyToObsolete() + { +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + options.KnownNetworks.Add(new AspNetIPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + var arr = new AspNetIPNetwork[5]; + options.KnownNetworks.CopyTo(arr, 2); + Assert.Null(arr[0]); + Assert.Equal(IPAddress.Parse("127.0.0.0"), arr[2].Prefix); + Assert.Equal(8, arr[2].PrefixLength); + Assert.Equal(IPAddress.Parse("10.0.0.0"), arr[3].Prefix); + Assert.Equal(8, arr[3].PrefixLength); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void IndexOfSystem() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); + Assert.Equal(1, options.KnownIPNetworks.IndexOf(System.Net.IPNetwork.Parse("10.0.0.0/8"))); + } + + [Fact] + public void IndexOfObsolete() + { + // AspNetIPNetwork doesn't implement Equals, so IndexOf uses reference equality. + // This keeps the obsolete behavior intact. + +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + var item = new AspNetIPNetwork(IPAddress.Parse("10.0.0.0"), 8); + options.KnownNetworks.Add(item); + Assert.Equal(-1, options.KnownNetworks.IndexOf(new AspNetIPNetwork(IPAddress.Parse("10.0.0.0"), 8))); + Assert.Equal(1, options.KnownNetworks.IndexOf(item)); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void InsertSystem() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Insert(0, System.Net.IPNetwork.Parse("10.0.0.0/8")); + Assert.Equal("10.0.0.0/8", options.KnownIPNetworks[0].ToString()); +#pragma warning disable ASPDEPR005 + Assert.Equal(IPAddress.Parse("10.0.0.0"), options.KnownNetworks[0].Prefix); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void InsertObsolete() + { +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + options.KnownNetworks.Insert(0, new AspNetIPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + Assert.Equal("10.0.0.0/8", options.KnownIPNetworks[0].ToString()); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void RemoveAtSystem() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); + options.KnownIPNetworks.RemoveAt(0); // remove loopback +#pragma warning disable ASPDEPR005 + Assert.DoesNotContain(options.KnownNetworks, n => n.Prefix.Equals(IPAddress.Loopback)); +#pragma warning restore ASPDEPR005 + Assert.Single(options.KnownIPNetworks); // only 10.0.0.0/8 + } + + [Fact] + public void RemoveAtObsolete() + { +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + options.KnownNetworks.Add(new AspNetIPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + options.KnownNetworks.RemoveAt(0); // remove loopback + Assert.DoesNotContain(options.KnownIPNetworks, n => n.BaseAddress.Equals(IPAddress.Loopback)); + Assert.Single(options.KnownIPNetworks); // only 10.0.0.0/8 +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void EnumerateSystem() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); + var list = options.KnownIPNetworks.ToList(); + Assert.Equal(2, list.Count); + Assert.Contains(list, n => n.BaseAddress.Equals(IPAddress.Parse("10.0.0.0"))); + } + + [Fact] + public void EnumerateObsolete() + { +#pragma warning disable ASPDEPR005 + var options = new ForwardedHeadersOptions(); + options.KnownNetworks.Add(new AspNetIPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + var list = options.KnownNetworks.ToList(); + Assert.Equal(2, list.Count); + Assert.Contains(list, n => n.Prefix.Equals(IPAddress.Parse("10.0.0.0"))); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void IsReadOnlyFalse() + { + var options = new ForwardedHeadersOptions(); + Assert.False(options.KnownIPNetworks.IsReadOnly); +#pragma warning disable ASPDEPR005 + Assert.False(options.KnownNetworks.IsReadOnly); +#pragma warning restore ASPDEPR005 + } + + [Fact] + public void CountSyncAfterMixedOperations() + { + var options = new ForwardedHeadersOptions(); + options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.0.0.0/8")); +#pragma warning disable ASPDEPR005 + options.KnownNetworks.Add(new AspNetIPNetwork(IPAddress.Parse("192.168.0.0"), 16)); + Assert.Equal(options.KnownIPNetworks.Count, options.KnownNetworks.Count); +#pragma warning restore ASPDEPR005 + } +}