Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 37 additions & 122 deletions src/libraries/System.Private.Uri/src/System/IPv6AddressHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,61 +111,38 @@ private static bool IsLoopback(ReadOnlySpan<ushort> 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:
// <argument> 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 <name> 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<char> name, out int end, bool validateStrictAddress)
/// <summary>
/// Determine whether a name is a valid IPv6 address. Rules are:
/// <para>* 8 groups of 16-bit hex numbers, separated by ':'</para>
/// <para>* a *single* run of zeros can be compressed using the symbol '::'</para>
/// <para>* an optional string of a ScopeID delimited by '%'</para>
/// <para>* the last 32 bits in an address can be represented as an IPv4 address</para>
/// </summary>
/// <param name="name">The host to validate.</param>
/// <param name="length">The length of the IPv6 address (index of ']' + 1).</param>
/// <remarks>Assumes that the caller already checked that the first character is '['.</remarks>
public static bool IsValid(ReadOnlySpan<char> 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;
Expand All @@ -176,36 +153,49 @@ private static bool InternalIsValid(ReadOnlySpan<char> 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] == ':'))
{
Expand All @@ -226,19 +216,6 @@ private static bool InternalIsValid(ReadOnlySpan<char> 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)
{
Expand All @@ -264,69 +241,7 @@ private static bool InternalIsValid(ReadOnlySpan<char> 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:
// <argument> 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 <name> 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<char> name, out int end)
{
return InternalIsValid(name, out end, false);
}
}
}
9 changes: 5 additions & 4 deletions src/libraries/System.Private.Uri/src/System/Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -3847,9 +3848,9 @@ private unsafe int CheckAuthorityHelper(char* pString, int idx, int length,
}

if (ch == '[' && syntax.InFact(UriSyntaxFlags.AllowIPv6Host) &&
IPv6AddressHelper.IsValid(new ReadOnlySpan<char>(pString + (start + 1), end - (start + 1)), out int seqEnd))
IPv6AddressHelper.IsValid(new ReadOnlySpan<char>(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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public static IEnumerable<object[]> 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
Expand Down Expand Up @@ -449,8 +449,8 @@ public static IEnumerable<object[]> 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 };
}
Expand Down Expand Up @@ -1247,9 +1247,11 @@ public static IEnumerable<object[]> 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 };
Expand Down
Loading