diff --git a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs index a93638779cbd6b..1d5d94e6b32bf5 100644 --- a/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs +++ b/src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs @@ -111,61 +111,38 @@ private static bool IsLoopback(ReadOnlySpan numbers) || (numbers[5] == 0xFFFF)))); } - // - // InternalIsValid - // - // Determine whether a name is a valid IPv6 address. Rules are: - // - // * 8 groups of 16-bit hex numbers, separated by ':' - // * a *single* run of zeros can be compressed using the symbol '::' - // * an optional string of a ScopeID delimited by '%' - // * an optional (last) 1 or 2 character prefix length field delimited by '/' - // * the last 32 bits in an address can be represented as an IPv4 address - // - // Inputs: - // name - // Domain name field of a URI to check for pattern match with - // IPv6 address - // validateStrictAddress: if set to true, it expects strict ipv6 address. Otherwise it expects - // part of the string in ipv6 format. - // - // Outputs: - // Nothing - // - // Assumes: - // the correct name is terminated by ']' character - // - // Returns: - // true if has IPv6 format/ipv6 address based on validateStrictAddress, else false - // - // Throws: - // Nothing - // - - // Remarks: MUST NOT be used unless all input indexes are verified and trusted. - // start must be next to '[' position, or error is reported - private static bool InternalIsValid(ReadOnlySpan name, out int end, bool validateStrictAddress) + /// + /// Determine whether a name is a valid IPv6 address. Rules are: + /// * 8 groups of 16-bit hex numbers, separated by ':' + /// * a *single* run of zeros can be compressed using the symbol '::' + /// * an optional string of a ScopeID delimited by '%' + /// * the last 32 bits in an address can be represented as an IPv4 address + /// + /// The host to validate. + /// The length of the IPv6 address (index of ']' + 1). + /// Assumes that the caller already checked that the first character is '['. + public static bool IsValid(ReadOnlySpan name, out int length) { - end = 0; // Default value in case of failure + Debug.Assert(name.StartsWith('[')); + + length = 0; // Default value in case of failure int sequenceCount = 0; int sequenceLength = 0; bool haveCompressor = false; bool haveIPv4Address = false; - bool havePrefix = false; bool expectingNumber = true; int lastSequence = 1; // Starting with a colon character is only valid if another colon follows. - if (name.Length < 2 || (name[0] == ':' && name[1] != ':')) + if (name.Length < 3 || (name[1] == ':' && name[2] != ':')) { return false; } - int start = 0; int i; - for (i = 0; i < name.Length; ++i) + for (i = 1; i < name.Length; ++i) { - if (havePrefix ? char.IsAsciiDigit(name[i]) : char.IsAsciiHexDigit(name[i])) + if (char.IsAsciiHexDigit(name[i])) { ++sequenceLength; expectingNumber = false; @@ -176,36 +153,49 @@ private static bool InternalIsValid(ReadOnlySpan name, out int end, bool v { return false; } + if (sequenceLength != 0) { ++sequenceCount; lastSequence = i - sequenceLength; } + switch (name[i]) { case '%': while (true) { - //accept anything in scopeID if (++i == name.Length) { // no closing ']', fail return false; } + if (name[i] == ']') { goto case ']'; } - else if (name[i] == '/') + + // Our general IPv6 parsing rules are very lenient on the ZoneID. + // Since this is the logic specific to Uri, we restrict the set of allowed characters (mainly to exclude delimiters). + if (name[i] != '%' && !UriHelper.Unreserved.Contains(name[i])) { - goto case '/'; + return false; } } + case ']': - start = i; - i = name.Length; - //this will make i = end+1 - continue; + const int ExpectedSequenceCount = 8; + + if (!expectingNumber && + (haveCompressor ? (sequenceCount < ExpectedSequenceCount) : (sequenceCount == ExpectedSequenceCount))) + { + length = i + 1; + return true; + } + + return false; + case ':': if ((i > 0) && (name[i - 1] == ':')) { @@ -226,19 +216,6 @@ private static bool InternalIsValid(ReadOnlySpan name, out int end, bool v } break; - case '/': - if (validateStrictAddress) - { - return false; - } - if ((sequenceCount == 0) || havePrefix) - { - return false; - } - havePrefix = true; - expectingNumber = true; - break; - case '.': if (haveIPv4Address) { @@ -264,69 +241,7 @@ private static bool InternalIsValid(ReadOnlySpan name, out int end, bool v } } - // - // if the last token was a prefix, check number of digits - // - - if (havePrefix && ((sequenceLength < 1) || (sequenceLength > 2))) - { - return false; - } - - // - // these sequence counts are -1 because it is implied in end-of-sequence - // - - int expectedSequenceCount = 8 + (havePrefix ? 1 : 0); - - if (!expectingNumber && (sequenceLength <= 4) && (haveCompressor ? (sequenceCount < expectedSequenceCount) : (sequenceCount == expectedSequenceCount))) - { - if (i == name.Length + 1) - { - // ']' was found - end = start + 1; - return true; - } - return false; - } return false; } - - // - // IsValid - // - // Determine whether a name is a valid IPv6 address. Rules are: - // - // * 8 groups of 16-bit hex numbers, separated by ':' - // * a *single* run of zeros can be compressed using the symbol '::' - // * an optional string of a ScopeID delimited by '%' - // * an optional (last) 1 or 2 character prefix length field delimited by '/' - // * the last 32 bits in an address can be represented as an IPv4 address - // - // Inputs: - // name - // Domain name field of a URI to check for pattern match with - // IPv6 address - // - // Outputs: - // Nothing - // - // Assumes: - // the correct name is terminated by ']' character - // - // Returns: - // true if has IPv6 format, else false - // - // Throws: - // Nothing - // - - // Remarks: MUST NOT be used unless all input indexes are verified and trusted. - // start must be next to '[' position, or error is reported - - internal static bool IsValid(ReadOnlySpan name, out int end) - { - return InternalIsValid(name, out end, false); - } } } diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 753acae3221f6c..aa84ae41e2148b 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -1314,7 +1314,7 @@ public static UriHostNameType CheckHostName(string? name) if (name.StartsWith('[') && name.EndsWith(']')) { // we require that _entire_ name is recognized as ipv6 address - if (IPv6AddressHelper.IsValid(name.AsSpan(1), out end) && end == name.Length - 1) + if (IPv6AddressHelper.IsValid(name, out end) && end == name.Length) { return UriHostNameType.IPv6; } @@ -1337,10 +1337,11 @@ public static UriHostNameType CheckHostName(string? name) // This checks the form without [] // we require that _entire_ name is recognized as ipv6 address - if (IPv6AddressHelper.IsValid(name + "]", out end) && end - 1 == name.Length) + if (IPv6AddressHelper.IsValid($"[{name}]", out end) && end - 2 == name.Length) { return UriHostNameType.IPv6; } + return UriHostNameType.Unknown; } @@ -3847,9 +3848,9 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length, } if (ch == '[' && syntax.InFact(UriSyntaxFlags.AllowIPv6Host) && - IPv6AddressHelper.IsValid(new ReadOnlySpan(pString + (start + 1), end - (start + 1)), out int seqEnd)) + IPv6AddressHelper.IsValid(new ReadOnlySpan(pString + start, end - start), out int seqEnd)) { - end = start + 1 + seqEnd; + end = start + seqEnd; if (end < length && pString[end] is not ('/' or '\\') && (IsImplicitFile || pString[end] is not (':' or '?' or '#'))) { // A valid IPv6 address wasn't followed by a valid delimiter (e.g. http://[::]extra). diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriEscapingTest.cs b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriEscapingTest.cs index be427489dd9d63..ea4be66631b546 100644 --- a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriEscapingTest.cs +++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriEscapingTest.cs @@ -18,7 +18,7 @@ public class UriEscapingTest private const string AlphaNumeric = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private const string RFC2396Unreserved = AlphaNumeric + "-_.!~*'()"; private const string RFC2396Reserved = @";/:@&=+$,?"; - private const string RFC3986Unreserved = AlphaNumeric + "-._~"; + public const string RFC3986Unreserved = AlphaNumeric + "-._~"; private const string RFC3986Reserved = @":/[]@!$&'()*+,;=?#"; private const string GB18030CertificationString1 = "\u6570\u636E eq '\uD840\uDC00\uD840\uDC01\uD840\uDC02\uD840\uDC03\uD869\uDED1\uD869\uDED2\uD869\uDED3" diff --git a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriIpHostTest.cs b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriIpHostTest.cs index 5247888065b677..481b711f4ec93a 100644 --- a/src/libraries/System.Private.Uri/tests/FunctionalTests/UriIpHostTest.cs +++ b/src/libraries/System.Private.Uri/tests/FunctionalTests/UriIpHostTest.cs @@ -289,6 +289,22 @@ public void UriIPv6Host_CompressionRangeSelection_Success(string address, string public void UriIPv6Host_ScopeId_Success(string address) { ParseIPv6Address(address); + + // Test various suffixes + for (int i = 0; i < 65536; i++) + { + char c = (char)i; + string testAddress = address + c; + + if (c == '%' || UriEscapingTest.RFC3986Unreserved.Contains(c)) + { + ParseIPv6Address(testAddress); + } + else + { + ParseBadIPv6Address(testAddress); + } + } } [Theory] @@ -316,6 +332,7 @@ public void UriIPv6Host_EmbeddedIPv4_Success(string address, string expected) [InlineData(":1:2:3:4:5")] // leading single colon [InlineData(":1:2:3:4:5:6")] // leading single colon [InlineData(":1:2:3:4:5:6:7")] // leading single colon + [InlineData(":1:2:3:4:5:6:7:8")] // leading single colon [InlineData(":1:2:3:4:5:6:7:8:9")] // leading single colon [InlineData("::1:2:3:4:5:6:7:8")] // compressor with too many number groups [InlineData("1::2:3:4:5:6:7:8")] // compressor with too many number groups @@ -346,14 +363,6 @@ public void UriIPv6Host_BadAddress(string address) ParseBadIPv6Address(address); } - [Theory] - [InlineData(":1:2:3:4:5:6:7:8")] // leading single colon - public void UriIPv6Host_BadAddress_SkipOnFramework(string address) - { - ParseBadIPv6Address(address); - } - - #region Helpers private void ParseIPv6Address(string ipv6String) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Uri.CreateStringTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Uri.CreateStringTests.cs index 2665f554b77fc3..918df38dad2b5b 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Uri.CreateStringTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Uri.CreateStringTests.cs @@ -199,7 +199,7 @@ public static IEnumerable Scheme_Authority_TestData() yield return new object[] { "http://[::ffff:0:192.168.0.1]/", "http", "", "[::ffff:0:192.168.0.1]", UriHostNameType.IPv6, 80, true, false }; // SIIT yield return new object[] { "http://[::ffff:1:192.168.0.1]/", "http", "", "[::ffff:1:c0a8:1]", UriHostNameType.IPv6, 80, true, false }; // SIIT (invalid) yield return new object[] { "http://[fe80::0000:5efe:192.168.0.1]/", "http", "", "[fe80::5efe:192.168.0.1]", UriHostNameType.IPv6, 80, true, false }; // ISATAP - yield return new object[] { "http://[1111:2222:3333::431/20]", "http", "", "[1111:2222:3333::431]", UriHostNameType.IPv6, 80, true, false }; // Prefix + yield return new object[] { "http://[1111:2222:3333::431]", "http", "", "[1111:2222:3333::431]", UriHostNameType.IPv6, 80, true, false }; // IPv6 Host - implicit UNC if (s_isWindowsSystem) // Unc can only start with '/' on Windows @@ -449,8 +449,8 @@ public static IEnumerable Scheme_Authority_IdnHost_TestData() yield return new object[] { "http://ascii.\u043F\u0440\u0438\u0432\u0435\u0442/", "http", "", "ascii.\u043F\u0440\u0438\u0432\u0435\u0442", "ascii.xn--b1agh1afp", "ascii.\u043F\u0440\u0438\u0432\u0435\u0442", UriHostNameType.Dns, 80, true, false }; yield return new object[] { "http://\u043F\u0440\u0438\u0432\u0435\u0442.\u03B2\u03AD\u03BB\u03B1\u03C3\u03BC\u03B1/", "http", "", "\u043F\u0440\u0438\u0432\u0435\u0442.\u03B2\u03AD\u03BB\u03B1\u03C3\u03BC\u03B1", "xn--b1agh1afp.xn--ixaiab0ch2c", "\u043F\u0440\u0438\u0432\u0435\u0442.\u03B2\u03AD\u03BB\u03B1\u03C3\u03BC\u03B1", UriHostNameType.Dns, 80, true, false }; + yield return new object[] { "http://[1111:2222:3333::431%16]", "http", "", "[1111:2222:3333::431]", "1111:2222:3333::431%16", "1111:2222:3333::431%16", UriHostNameType.IPv6, 80, true, false }; // Scope ID yield return new object[] { "http://[1111:2222:3333::431%16]:50/", "http", "", "[1111:2222:3333::431]", "1111:2222:3333::431%16", "1111:2222:3333::431%16", UriHostNameType.IPv6, 50, false, false }; // Scope ID - yield return new object[] { "http://[1111:2222:3333::431%16/20]", "http", "", "[1111:2222:3333::431]", "1111:2222:3333::431%16", "1111:2222:3333::431%16", UriHostNameType.IPv6, 80, true, false }; // Scope ID and prefix yield return new object[] { "http://\u1234\u2345\u3456/", "http", "", "\u1234\u2345\u3456", "xn--ryd258fr0m", "\u1234\u2345\u3456", UriHostNameType.Dns, 80, true, false }; } @@ -1247,9 +1247,11 @@ public static IEnumerable Create_String_Invalid_TestData() yield return new object[] { "http://[::1::1]", UriKind.Absolute }; yield return new object[] { "http://[11111:2222:3333::431]", UriKind.Absolute }; yield return new object[] { "http://[/12]", UriKind.Absolute }; + yield return new object[] { "http://[1111:2222:3333::431%16/12]", UriKind.Absolute }; yield return new object[] { "http://[1111:2222:3333::431/12/12]", UriKind.Absolute }; yield return new object[] { "http://[1111:2222:3333::431%16/]", UriKind.Absolute }; yield return new object[] { "http://[1111:2222:3333::431/123]", UriKind.Absolute }; + yield return new object[] { "http://[1111:2222:3333::431/20]", UriKind.Absolute }; yield return new object[] { "http://[192.168.0.9/192.168.0.9]", UriKind.Absolute }; yield return new object[] { "http://[192.168.0.9%192.168.0.9]", UriKind.Absolute };