Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,36 @@ public sealed class SocketsHttpHandler_HttpClientHandler_Proxy_Test : HttpClient
{
public SocketsHttpHandler_HttpClientHandler_Proxy_Test(ITestOutputHelper output) : base(output) { }

[Theory]
[InlineData("http")]
[InlineData("https")]
[InlineData("socks5")]
public async Task Proxy_IPv6ScopeChange_ConnectsToSelectedProxy(string proxyScheme)
{
var proxy = new WebProxy();
using var handler = new SocketsHttpHandler { Proxy = proxy };
DnsEndPoint endPoint = null;
handler.ConnectCallback = (context, _) =>
{
endPoint = context.DnsEndPoint;
throw new InvalidOperationException(nameof(Proxy_IPv6ScopeChange_ConnectsToSelectedProxy));
};
using var client = new HttpClient(handler);

for (int scope = 1; scope <= 2; scope++)
{
var proxyUri = new Uri($"{proxyScheme}://[fe80::1%{scope}]:8080/");
proxy.Address = proxyUri;
endPoint = null;

HttpRequestException exception = await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync("http://example.test/"));
Assert.Equal(nameof(Proxy_IPv6ScopeChange_ConnectsToSelectedProxy), Assert.IsType<InvalidOperationException>(exception.InnerException).Message);
Assert.NotNull(endPoint);
Assert.Equal(IPAddress.Parse(proxyUri.IdnHost), IPAddress.Parse(endPoint.Host));
Assert.Equal(proxyUri.Port, endPoint.Port);
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
40 changes: 30 additions & 10 deletions src/libraries/System.Private.Uri/src/System/Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1550,14 +1550,16 @@ public override int GetHashCode()

string remoteUrl = info.RemoteUrl ??= GetParts(components, UriFormat.SafeUnescaped);

if (IsFile)
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(remoteUrl);
}
else
{
return remoteUrl.GetHashCode();
}
int hashCode = IsFile ?
StringComparer.OrdinalIgnoreCase.GetHashCode(remoteUrl) :
remoteUrl.GetHashCode();

// Custom parsers define identity through HttpRequestUrl and may return it without
// initializing the host. ScopeId can remain null until Host or IdnHost is accessed,
// so including it could change the hash after those properties populate the cache.
Comment thread
MihaZupan marked this conversation as resolved.
return HostType == Flags.IPv6HostType && _syntax.IsSimple && info.ScopeId is string scopeId ?
HashCode.Combine(hashCode, scopeId) :
hashCode;
}
}

Expand Down Expand Up @@ -1760,7 +1762,8 @@ public bool Equals([NotNullWhen(true)] Uri? other)
if (NotAny(Flags.AllUriInfoSet) || other.NotAny(Flags.AllUriInfoSet))
{
// Try raw compare for _strings as the last chance to keep the working set small
if (string.Equals(_string, other._string, IsUncOrDosPath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
// Keep IPv6 scope IDs case-sensitive even when UNC/DOS paths ignore case.
if (string.Equals(_string, other._string, IsUncOrDosPath && HostType != Flags.IPv6HostType ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
return true;
}
Expand Down Expand Up @@ -1851,7 +1854,24 @@ public bool Equals([NotNullWhen(true)] Uri? other)
string otherUrl = otherInfo.RemoteUrl ??= other.GetParts(components, UriFormat.SafeUnescaped);

// if IsUncOrDosPath is true then we ignore case in the path comparison
return string.Equals(selfUrl, otherUrl, IsUncOrDosPath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
if (!string.Equals(selfUrl, otherUrl, IsUncOrDosPath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
return false;
}

// Scope IDs are omitted from HttpRequestUrl and must be compared separately.
if (HostType == Flags.IPv6HostType || other.HostType == Flags.IPv6HostType)
Comment thread
makazeu marked this conversation as resolved.
{
// Custom parsers define equality through HttpRequestUrl.
string? selfScopeId = HostType == Flags.IPv6HostType && _syntax.IsSimple ? selfInfo.ScopeId : null;
string? otherScopeId = other.HostType == Flags.IPv6HostType && other._syntax.IsSimple ? otherInfo.ScopeId : null;
if (selfScopeId != otherScopeId)
{
return false;
}
}

return true;
}

public Uri MakeRelativeUri(Uri uri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ public void DisableCanonicalization_OnlyEqualToUrisWithMatchingFlag()
var absoluteRawSameAuthority = new Uri(AbsoluteUri + ":80" + Path, new UriCreationOptions { DangerousDisablePathAndQueryCanonicalization = true });
Equal(absoluteRaw, absoluteRawSameAuthority);

var rawOptions = new UriCreationOptions { DangerousDisablePathAndQueryCanonicalization = true };
var scoped = new Uri("http://[fe80::1%1]/foo", rawOptions);
NotEqual(scoped, new Uri("http://[fe80::1%2]/foo", rawOptions));
Equal(scoped, new Uri("http://[FE80:0:0:0:0:0:0:1%1]:80/foo", rawOptions));

static void Equal(Uri left, Uri right)
{
Assert.True(left.Equals(right));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,11 @@ public static IEnumerable<object[]> Equals_TestData()
yield return new object[] { new Uri("http://www.domain.com:100/path?name#fragment"), new Uri("http://www.domain.com:800/path?name#fragment"), false }; // Different port
yield return new object[] { new Uri("http://www.domain.com:100/path?name#fragment"), new Uri("http://www.domain.com:80/path?name#fragment"), false }; // Different port

yield return new object[] { new Uri("http://[fe80::1%1]/"), new Uri("http://[fe80::1%2]/"), false };
yield return new object[] { new Uri("http://[fe80::1]/"), new Uri("http://[fe80::1%0]/"), false };
yield return new object[] { new Uri("http://[fe80::1%1]/"), new Uri("HTTP://[FE80:0:0:0:0:0:0:1%1]:80/"), true };
yield return new object[] { new Uri("mailto:user@[fe80::1%1]"), new Uri("mailto:user@[fe80::1%2]"), false };

// File paths
yield return new object[] { new Uri("file://C:/path1/path2/file"), new Uri("file://C:/path1/path2/file"), true };
yield return new object[] { new Uri("file://C:/path1/path2/file"), new Uri("file://C:/path1/Path2/File"), true };
Expand All @@ -346,6 +351,8 @@ public static IEnumerable<object[]> Equals_TestData()
yield return new object[] { new Uri("file://C:/path1/path2/file"), new Uri("file://C:/path1/path2/file!"), false };
yield return new object[] { new Uri("file://C:/path1/path2/file"), new Uri("http://domain.com"), false };
yield return new object[] { new Uri("file://C:/path1/path2/file"), new Uri(@"\\server\path1\path2\file"), false };
yield return new object[] { new Uri("file://[fe80::1%eth0]/path"), new Uri("file://[fe80::1%ETH0]/path"), false };
yield return new object[] { new Uri("file://[fe80::1%eth0]/PATH"), new Uri("file://[FE80::1%eth0]/path"), true };

// UNC paths that normalize to the same value as a regular file: URI
yield return new object[] { new Uri("\\\\\u202a"), new Uri("file:///"), true };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System.Collections.Generic;
using Xunit;

namespace System.PrivateUri.Tests
Expand Down Expand Up @@ -606,6 +607,105 @@ public static void CustomParserCanRecoverOnInvalidUri(UriKind uriKind, bool reco
}
}

[Theory]
[InlineData("[fe80::1%1]", "[fe80::1%2]", false, true)]
[InlineData("[fe80::1%1]", "[fe80::1]", true, true)]
[InlineData("[fe80::1%1]", "[fe80::1%1]", true, false)]
[InlineData("first.test", "[fe80::1]", true, true)]
public static void CustomParser_EqualsAndGetHashCode_UseRequestUrl(string firstHost, string secondHost, bool useHttpScheme, bool expected)
{
bool rewriteHost = firstHost == "first.test";
string scheme = $"custom-ipv6-scope-{useHttpScheme}-{rewriteHost}-{expected}";
var parser = new CustomParser_FormatsRequestUrlWithoutParsingHost(rewriteHost ? "[fe80::1%1]" : null);
UriParser.Register(parser, scheme, 80);

var firstUri = new Uri($"{scheme}://{firstHost}/");
var secondUri = new Uri($"{(useHttpScheme ? Uri.UriSchemeHttp : scheme)}://{secondHost}/");
Assert.Equal(expected, firstUri.Equals(secondUri));
Assert.Equal(expected, secondUri.Equals(firstUri));

int firstHashCode = firstUri.GetHashCode();
var uris = new HashSet<Uri> { firstUri, secondUri };
Assert.Equal(firstUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.SafeUnescaped).GetHashCode(), firstHashCode);
Assert.Equal(0, parser.HostRequests);
Assert.Equal(0, parser.PortRequests);

Assert.Equal("fe80::1%1", firstUri.IdnHost);
Assert.Equal(firstHashCode, firstUri.GetHashCode());
Assert.Equal(expected, firstUri.Equals(secondUri));
Assert.Equal(expected, secondUri.Equals(firstUri));
Assert.Equal(expected ? 1 : 2, uris.Count);
Assert.Contains(firstUri, uris);
if (expected)
{
Assert.Equal(firstHashCode, secondUri.GetHashCode());
}
}

[Theory]
[InlineData("2", false)]
[InlineData("1", true)]
public static void RegisteredSimpleParser_EqualsAndGetHashCode_IncludeIPv6ScopeId(string secondScopeId, bool expected)
{
string scheme = $"simple-ipv6-scope-{secondScopeId}";
UriParser.Register(new GenericUriParser(GenericUriParserOptions.Default), scheme, 80);

var firstUri = new Uri($"{scheme}://[fe80::1%1]/");
var secondUri = new Uri($"{scheme}://[FE80:0:0:0:0:0:0:1%{secondScopeId}]/");
Assert.Equal(expected, firstUri.Equals(secondUri));
Assert.Equal(expected, secondUri.Equals(firstUri));
Assert.Equal(expected ? 1 : 2, new HashSet<Uri> { firstUri, secondUri }.Count);
if (expected)
{
Assert.Equal(firstUri.GetHashCode(), secondUri.GetHashCode());
}
}

private sealed class CustomParser_FormatsRequestUrlWithoutParsingHost(string? hostOverride) : GenericUriParser(GenericUriParserOptions.Default)
{
public int HostRequests;
public int PortRequests;

protected override void InitializeAndValidate(Uri uri, out UriFormatException? parsingError)
{
if (hostOverride is not null)
{
parsingError = null;
}
else
{
base.InitializeAndValidate(uri, out parsingError);
}
}

protected override string GetComponents(Uri uri, UriComponents components, UriFormat format)
{
if (components == UriComponents.HttpRequestUrl)
{
return "http://[fe80::1]/";
}

if (components == UriComponents.Host)
{
HostRequests++;
if (hostOverride is not null)
{
return hostOverride;
}
}
else if (components == UriComponents.StrongPort)
{
PortRequests++;
if (hostOverride is not null)
{
return "80";
}
}

return base.GetComponents(uri, components, format);
}
}

private sealed class CustomParser_RecoversOnInvalidUri(bool recover) : GenericUriParser(GenericUriParserOptions.Default)
{
protected override void InitializeAndValidate(Uri uri, out UriFormatException? parsingError)
Expand Down
Loading