diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index 8c2345c45c7164..f88067dc3ad04f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -584,10 +584,17 @@ public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string } // In the common case, SslHostName (when present) is equal to Host. If so, don't include in hash. - public override int GetHashCode() => - (SslHostName == Host ? + public override int GetHashCode() + { + int hashCode = SslHostName == Host ? HashCode.Combine(Kind, Host, Port, ProxyUri, Identity) : - HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity)); + HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity); + + // Uri equality ignores IPv6 scope IDs, but they identify distinct proxy endpoints. + return ProxyUri is { HostNameType: UriHostNameType.IPv6 } proxyUri ? + HashCode.Combine(hashCode, proxyUri.IdnHost) : + hashCode; + } public override bool Equals([NotNullWhen(true)] object? obj) => obj is HttpConnectionKey hck && @@ -598,6 +605,7 @@ public bool Equals(HttpConnectionKey other) => Host == other.Host && Port == other.Port && ProxyUri == other.ProxyUri && + (ProxyUri is not { HostNameType: UriHostNameType.IPv6 } || ProxyUri.IdnHost == other.ProxyUri?.IdnHost) && SslHostName == other.SslHostName && Identity == other.Identity; } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpConnectionKeyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpConnectionKeyTest.cs index 3f6a332d754149..cceeac4be4dcac 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpConnectionKeyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpConnectionKeyTest.cs @@ -33,5 +33,40 @@ public void Equals_DifferentParameters_ReturnsTrueIfAllEqual(string kindString, object actualKey = Activator.CreateInstance(keyType, Enum.Parse(connectionKindType, kindString), host, port, sslHostName, proxyUri, identity); Assert.Equal(expected, referenceKey.Equals(actualKey)); } + + [ConditionalTheory(typeof(SocketsHttpHandler), nameof(SocketsHttpHandler.IsSupported))] + [InlineData("http://[fe80::1%1]/", "http://[fe80::1%2]/", false)] + [InlineData("http://[fe80::1]/", "http://[fe80::1%1]/", false)] + [InlineData("http://[fe80::1%eth0]/", "http://[fe80::1%eth1]/", false)] + [InlineData("http://[fe80::1%eth0]/", "http://[fe80::1%ETH0]/", false)] + [InlineData("http://[fe80::1%1]/", "http://[fe80::1%1]/", true)] + [InlineData("http://[fe80::1%1]/", "HTTP://[fe80:0:0:0:0:0:0:1%1]:80/", true)] + [InlineData("http://[fe80::1]/", "http://[fe80:0:0:0:0:0:0:1]:80/", true)] + [InlineData("http://user@[fe80::1%1]/#first", "http://other@[fe80::1%1]/#second", true)] + [InlineData("http://[fe80::1%1]:80/", "http://[fe80::1%1]:81/", false)] + [InlineData("http://[fe80::1%1]/", "https://[fe80::1%1]/", false)] + [InlineData("http://[fe80::1%1]/first", "http://[fe80::1%1]/second", false)] + [InlineData("http://PROXY.TEST/", "http://proxy.test:80/", true)] + [InlineData("http://127.0.0.1/", "http://127.1:80/", true)] + public void Equals_ProxyUris_DistinguishesIPv6Scope(string firstProxyUri, string secondProxyUri, bool expected) + { + Type connectionKindType = Type.GetType("System.Net.Http.HttpConnectionKind, System.Net.Http"); + Type keyType = Type.GetType("System.Net.Http.HttpConnectionPoolManager+HttpConnectionKey, System.Net.Http"); + object kind = Enum.Parse(connectionKindType, "Http"); + + foreach (string sslHostName in new[] { "localhost", "localhost-ssl" }) + { + object firstKey = Activator.CreateInstance(keyType, kind, "localhost", 80, sslHostName, new Uri(firstProxyUri), string.Empty); + object secondKey = Activator.CreateInstance(keyType, kind, "localhost", 80, sslHostName, new Uri(secondProxyUri), string.Empty); + + Assert.Equal(expected, firstKey.Equals(secondKey)); + Assert.Equal(expected, secondKey.Equals(firstKey)); + Assert.Equal(expected ? 1 : 2, new HashSet { firstKey, secondKey }.Count); + if (expected) + { + Assert.Equal(firstKey.GetHashCode(), secondKey.GetHashCode()); + } + } + } } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index fb66bb6c6f6955..838bbeb8ff7250 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -817,6 +817,49 @@ public sealed class SocketsHttpHandler_HttpClientHandler_Proxy_Test : HttpClient { public SocketsHttpHandler_HttpClientHandler_Proxy_Test(ITestOutputHelper output) : base(output) { } + [Theory] + [InlineData("http", false, false)] + [InlineData("http", true, false)] + [InlineData("https", false, false)] + [InlineData("https", true, false)] + [InlineData("socks5", false, false)] + [InlineData("socks5", true, false)] + [InlineData("http", false, true)] + [InlineData("http", true, true)] + [InlineData("https", false, true)] + [InlineData("https", true, true)] + [InlineData("socks5", false, true)] + [InlineData("socks5", true, true)] + public async Task Proxy_IPv6ScopeChange_ConnectsToSelectedProxy(string proxyScheme, bool useSsl, bool useHttp2) + { + 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) + { + DefaultRequestVersion = useHttp2 ? HttpVersion.Version20 : HttpVersion.Version11, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + 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(() => client.GetAsync(useSsl ? "https://example.test/" : "http://example.test/")); + Assert.Equal(nameof(Proxy_IPv6ScopeChange_ConnectsToSelectedProxy), Assert.IsType(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)]