diff --git a/src/Middleware/HttpOverrides/src/IPNetwork.cs b/src/Middleware/HttpOverrides/src/IPNetwork.cs index b71d20df01aa..9888de2d1535 100644 --- a/src/Middleware/HttpOverrides/src/IPNetwork.cs +++ b/src/Middleware/HttpOverrides/src/IPNetwork.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -16,9 +17,18 @@ public class IPNetwork /// /// The . /// The prefix length. - public IPNetwork(IPAddress prefix, int prefixLength) + /// is out of range. + public IPNetwork(IPAddress prefix, int prefixLength) : this(prefix, prefixLength, true) { - CheckPrefixLengthRange(prefix, prefixLength); + } + + private IPNetwork(IPAddress prefix, int prefixLength, bool checkPrefixLengthRange) + { + if (checkPrefixLengthRange && + !IsValidPrefixLengthRange(prefix, prefixLength)) + { + throw new ArgumentOutOfRangeException(nameof(prefixLength), "The prefix length was out of range."); + } Prefix = prefix; PrefixLength = prefixLength; @@ -83,21 +93,114 @@ private byte[] CreateMask() return mask; } - private static void CheckPrefixLengthRange(IPAddress prefix, int prefixLength) + private static bool IsValidPrefixLengthRange(IPAddress prefix, int prefixLength) { if (prefixLength < 0) { - throw new ArgumentOutOfRangeException(nameof(prefixLength)); + return false; } - if (prefix.AddressFamily == AddressFamily.InterNetwork && prefixLength > 32) + return prefix.AddressFamily switch + { + AddressFamily.InterNetwork => prefixLength <= 32, + AddressFamily.InterNetworkV6 => prefixLength <= 128, + _ => true + }; + } + + /// + /// Converts the specified of representation of + /// an IP address and a prefix length to its equivalent. + /// + /// The of to convert, in CIDR notation. + /// + ///The equivalent to the IP address and prefix length contained in . + /// + /// is not in the correct format. + /// The prefix length contained in is out of range. + /// + public static IPNetwork Parse(ReadOnlySpan networkSpan) + { + if (!TryParseComponents(networkSpan, out var prefix, out var prefixLength)) { - throw new ArgumentOutOfRangeException(nameof(prefixLength)); + throw new FormatException("An invalid IP address or prefix length was specified."); } - if (prefix.AddressFamily == AddressFamily.InterNetworkV6 && prefixLength > 128) + if (!IsValidPrefixLengthRange(prefix, prefixLength)) { - throw new ArgumentOutOfRangeException(nameof(prefixLength)); + throw new ArgumentOutOfRangeException(nameof(networkSpan), "The prefix length was out of range."); } + + return new IPNetwork(prefix, prefixLength, false); + } + + /// + /// Converts the specified of representation of + /// an IP address and a prefix length to its equivalent, and returns a value + /// that indicates whether the conversion succeeded. + /// + /// The of to validate. + /// + /// When this method returns, contains the equivalent to the IP Address + /// and prefix length contained in , if the conversion succeeded, + /// or if the conversion failed. This parameter is passed uninitialized. + /// + /// + /// if the parameter was + /// converted successfully; otherwise . + /// + /// + public static bool TryParse(ReadOnlySpan networkSpan, [NotNullWhen(true)] out IPNetwork? network) + { + network = null; + + if (!TryParseComponents(networkSpan, out var prefix, out var prefixLength)) + { + return false; + } + + if (!IsValidPrefixLengthRange(prefix, prefixLength)) + { + return false; + } + + network = new IPNetwork(prefix, prefixLength, false); + return true; + } + + /// + /// + /// The specified representation must be expressed using CIDR (Classless Inter-Domain Routing) notation, or 'slash notation', + /// which contains an IPv4 or IPv6 address and the subnet mask prefix length, separated by a forward slash. + /// + /// + /// e.g. "192.168.0.1/31" for IPv4, "2001:db8:3c4d::1/127" for IPv6 + /// + /// + private static bool TryParseComponents( + ReadOnlySpan networkSpan, + [NotNullWhen(true)] out IPAddress? prefix, + out int prefixLength) + { + prefix = null; + prefixLength = default; + + var forwardSlashIndex = networkSpan.IndexOf('/'); + if (forwardSlashIndex < 0) + { + return false; + } + + if (!IPAddress.TryParse(networkSpan.Slice(0, forwardSlashIndex), out prefix)) + { + return false; + } + + if (!int.TryParse(networkSpan.Slice(forwardSlashIndex + 1), out prefixLength)) + { + return false; + } + + return true; } } diff --git a/src/Middleware/HttpOverrides/src/PublicAPI.Unshipped.txt b/src/Middleware/HttpOverrides/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..19721899c9fe 100644 --- a/src/Middleware/HttpOverrides/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/HttpOverrides/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +static Microsoft.AspNetCore.HttpOverrides.IPNetwork.Parse(System.ReadOnlySpan networkSpan) -> Microsoft.AspNetCore.HttpOverrides.IPNetwork! +static Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(System.ReadOnlySpan networkSpan, out Microsoft.AspNetCore.HttpOverrides.IPNetwork? network) -> bool diff --git a/src/Middleware/HttpOverrides/test/IPNetworkTest.cs b/src/Middleware/HttpOverrides/test/IPNetworkTest.cs index 4f86e8865fbd..c8f33f7a333b 100644 --- a/src/Middleware/HttpOverrides/test/IPNetworkTest.cs +++ b/src/Middleware/HttpOverrides/test/IPNetworkTest.cs @@ -39,4 +39,159 @@ public void Contains_Negative(string prefixText, int length, string addressText) var network = new IPNetwork(IPAddress.Parse(prefixText), length); Assert.False(network.Contains(IPAddress.Parse(addressText))); } + + [Theory] + [InlineData("192.168.1.1", 0)] + [InlineData("192.168.1.1", 32)] + [InlineData("2001:db8:3c4d::1", 0)] + [InlineData("2001:db8:3c4d::1", 128)] + public void Ctor_WithValidFormat_IsSuccessfullyCreated(string prefixText, int prefixLength) + { + // Arrange + var address = IPAddress.Parse(prefixText); + + // Act + var network = new IPNetwork(address, prefixLength); + + // Assert + Assert.Equal(prefixText, network.Prefix.ToString()); + Assert.Equal(prefixLength, network.PrefixLength); + } + + [Theory] + [InlineData("192.168.1.1", -1)] + [InlineData("192.168.1.1", 33)] + [InlineData("2001:db8:3c4d::1", -1)] + [InlineData("2001:db8:3c4d::1", 129)] + public void Ctor_WithPrefixLengthOutOfRange_ThrowsArgumentOutOfRangeException(string prefixText, int prefixLength) + { + // Arrange + var address = IPAddress.Parse(prefixText); + + // Act + var ex = Assert.Throws(() => new IPNetwork(address, prefixLength)); + + // Assert + Assert.StartsWith("The prefix length was out of range.", ex.Message); + } + + [Theory] + [MemberData(nameof(ValidPrefixWithPrefixLengthData))] + public void Parse_WithValidFormat_ParsedCorrectly(string input, string expectedPrefix, int expectedPrefixLength) + { + // Act + var network = IPNetwork.Parse(input); + + // Assert + Assert.Equal(expectedPrefix, network.Prefix.ToString()); + Assert.Equal(expectedPrefixLength, network.PrefixLength); + } + + [Theory] + [InlineData(null)] + [MemberData(nameof(InvalidPrefixOrPrefixLengthData))] + public void Parse_WithInvalidFormat_ThrowsFormatException(string input) + { + // Arrange & Act & Assert + var ex = Assert.Throws(() => IPNetwork.Parse(input)); + Assert.Equal("An invalid IP address or prefix length was specified.", ex.Message); + } + + [Theory] + [MemberData(nameof(PrefixLengthOutOfRangeData))] + public void Parse_WithOutOfRangePrefixLength_ThrowsArgumentOutOfRangeException(string input) + { + // Arrange & Act & Assert + var ex = Assert.Throws(() => IPNetwork.Parse(input)); + Assert.StartsWith("The prefix length was out of range.", ex.Message); + } + + [Theory] + [MemberData(nameof(ValidPrefixWithPrefixLengthData))] + public void TryParse_WithValidFormat_ParsedCorrectly(string input, string expectedPrefix, int expectedPrefixLength) + { + // Act + var result = IPNetwork.TryParse(input, out var network); + + // Assert + Assert.True(result); + Assert.NotNull(network); + Assert.Equal(expectedPrefix, network.Prefix.ToString()); + Assert.Equal(expectedPrefixLength, network.PrefixLength); + } + + [Theory] + [InlineData(null)] + [MemberData(nameof(InvalidPrefixOrPrefixLengthData))] + [MemberData(nameof(PrefixLengthOutOfRangeData))] + public void TryParse_WithInvalidFormat_ReturnsFalse(string input) + { + // Act + var result = IPNetwork.TryParse(input, out var network); + + // Assert + Assert.False(result); + Assert.Null(network); + } + + public static TheoryData ValidPrefixWithPrefixLengthData() => new() + { + // IPv4 + { "10.1.0.0/16", "10.1.0.0", 16 }, + { "10.1.1.0/8", "10.1.1.0", 8 }, + { "174.0.0.0/7", "174.0.0.0", 7 }, + { "10.174.0.0/15", "10.174.0.0", 15 }, + { "10.168.0.0/14", "10.168.0.0", 14 }, + { "192.168.0.1/31", "192.168.0.1", 31 }, + { "192.168.0.1/31", "192.168.0.1", 31 }, + { "192.168.0.1/32", "192.168.0.1", 32 }, + { "192.168.1.1/0", "192.168.1.1", 0 }, + { "192.168.1.1/0", "192.168.1.1", 0 }, + + // IPv6 + { "2001:db8:3c4d::/127", "2001:db8:3c4d::", 127 }, + { "2001:db8:3c4d::1/128", "2001:db8:3c4d::1", 128 }, + { "2001:db8:3c4d::1/0", "2001:db8:3c4d::1", 0 }, + { "2001:db8:3c4d::1/0", "2001:db8:3c4d::1", 0 } + }; + + public static TheoryData InvalidPrefixOrPrefixLengthData() => new() + { + string.Empty, + "abcdefg", + + // Missing forward slash + "10.1.0.016", + "2001:db8:3c4d::1127", + + // Invalid prefix + "/16", + "10.1./16", + "10.1.0./16", + "10.1.ABC.0/16", + "200123:db8:3c4d::/127", + ":db8:3c4d::/127", + "2001:?:3c4d::1/0", + + // Invalid prefix length + "10.1.0.0/", + "10.1.0.0/16-", + "10.1.0.0/ABC", + "2001:db8:3c4d::/", + "2001:db8:3c4d::1/128-", + "2001:db8:3c4d::1/ABC" + }; + + public static TheoryData PrefixLengthOutOfRangeData() => new() + { + // Negative prefix length + "10.1.0.0/-16", + "2001:db8:3c4d::/-127", + + // Prefix length out of range (IPv4) + "10.1.0.0/33", + + // Prefix length out of range (IPv6) + "2001:db8:3c4d::/129" + }; }