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"
+ };
}