From 1aa1906f6079fcff07f0698dc86d9d497d4f97e9 Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:14:05 +0200 Subject: [PATCH] Refresh cached credentials after PreAuthenticate fails (#101053) * Support refreshing credentials in pre-auth cache * Fix minor bug in CredentialCache * Add unit test * Fix tests * Fix tests attempt 2 * Merge two lock statements. * Fix build --- .../src/System/Net/CredentialCacheKey.cs | 133 ++++++++++++++++++ .../src/System.Net.Http.csproj | 3 + .../AuthenticationHelper.cs | 36 +++-- .../ConnectionPool/HttpConnectionPool.cs | 6 +- .../PreAuthCredentialCache.cs | 61 ++++++++ .../HttpClientHandlerTest.BasicAuth.cs | 114 +++++++++++++++ .../System.Net.Http.Functional.Tests.csproj | 1 + .../src/System.Net.Primitives.csproj | 2 + .../src/System/Net/CredentialCache.cs | 126 +---------------- .../FunctionalTests/CredentialCacheTest.cs | 11 ++ 10 files changed, 363 insertions(+), 130 deletions(-) create mode 100644 src/libraries/Common/src/System/Net/CredentialCacheKey.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/PreAuthCredentialCache.cs create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.BasicAuth.cs diff --git a/src/libraries/Common/src/System/Net/CredentialCacheKey.cs b/src/libraries/Common/src/System/Net/CredentialCacheKey.cs new file mode 100644 index 00000000000000..0d59c101493157 --- /dev/null +++ b/src/libraries/Common/src/System/Net/CredentialCacheKey.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace System.Net +{ + internal sealed class CredentialCacheKey : IEquatable + { + public readonly Uri UriPrefix; + public readonly int UriPrefixLength = -1; + public readonly string AuthenticationType; + + internal CredentialCacheKey(Uri uriPrefix, string authenticationType) + { + Debug.Assert(uriPrefix != null); + Debug.Assert(authenticationType != null); + + UriPrefix = uriPrefix; + UriPrefixLength = UriPrefix.AbsolutePath.LastIndexOf('/'); + AuthenticationType = authenticationType; + } + + internal bool Match(Uri uri, string authenticationType) + { + if (uri == null || authenticationType == null) + { + return false; + } + + // If the protocols don't match, this credential is not applicable for the given Uri. + if (!string.Equals(authenticationType, AuthenticationType, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Match({UriPrefix} & {uri})"); + + return IsPrefix(uri, UriPrefix); + } + + // IsPrefix (Uri) + // + // Determines whether is a prefix of this URI. A prefix + // match is defined as: + // + // scheme match + // + host match + // + port match, if any + // + path is a prefix of path, if any + // + // Returns: + // True if is a prefix of this URI + private static bool IsPrefix(Uri uri, Uri prefixUri) + { + Debug.Assert(uri != null); + Debug.Assert(prefixUri != null); + + if (prefixUri.Scheme != uri.Scheme || prefixUri.Host != uri.Host || prefixUri.Port != uri.Port) + { + return false; + } + + int prefixLen = prefixUri.AbsolutePath.LastIndexOf('/'); + if (prefixLen > uri.AbsolutePath.LastIndexOf('/')) + { + return false; + } + + return string.Compare(uri.AbsolutePath, 0, prefixUri.AbsolutePath, 0, prefixLen, StringComparison.OrdinalIgnoreCase) == 0; + } + + public override int GetHashCode() => + StringComparer.OrdinalIgnoreCase.GetHashCode(AuthenticationType) ^ + UriPrefix.GetHashCode(); + + public bool Equals([NotNullWhen(true)] CredentialCacheKey? other) + { + if (other == null) + { + return false; + } + + bool equals = + string.Equals(AuthenticationType, other.AuthenticationType, StringComparison.OrdinalIgnoreCase) && + UriPrefix.Equals(other.UriPrefix); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Equals({this},{other}) returns {equals}"); + + return equals; + } + + public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as CredentialCacheKey); + + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, $"[{UriPrefixLength}]:{UriPrefix}:{AuthenticationType}"); + } + + internal static class CredentialCacheHelper + { + public static bool TryGetCredential(Dictionary cache, Uri uriPrefix, string authType, [NotNullWhen(true)] out Uri? mostSpecificMatchUri, [NotNullWhen(true)] out NetworkCredential? mostSpecificMatch) + { + int longestMatchPrefix = -1; + mostSpecificMatch = null; + mostSpecificMatchUri = null; + + // Enumerate through every credential in the cache + foreach ((CredentialCacheKey key, NetworkCredential value) in cache) + { + // Determine if this credential is applicable to the current Uri/AuthType + if (key.Match(uriPrefix, authType)) + { + int prefixLen = key.UriPrefixLength; + + // Check if the match is better than the current-most-specific match + if (prefixLen > longestMatchPrefix) + { + // Yes: update the information about currently preferred match + longestMatchPrefix = prefixLen; + mostSpecificMatch = value; + mostSpecificMatchUri = key.UriPrefix; + } + } + } + + return mostSpecificMatch != null; + } + } +} diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 7db63948a28513..e4ffec94743f35 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -164,6 +164,8 @@ Link="Common\System\Text\ValueStringBuilder.AppendSpanFormattable.cs" /> + @@ -216,6 +218,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs index 639e0367ba5aa7..69f01907167c7e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs @@ -215,11 +215,12 @@ private static async ValueTask SendWithAuthAsync(HttpReques // If preauth is enabled and this isn't proxy auth, try to get a basic credential from the // preauth credentials cache, and if successful, set an auth header for it onto the request. // Currently we only support preauth for Basic. - bool performedBasicPreauth = false; + NetworkCredential? preAuthCredential = null; + Uri? preAuthCredentialUri = null; if (preAuthenticate) { Debug.Assert(pool.PreAuthCredentials != null); - NetworkCredential? credential; + (Uri uriPrefix, NetworkCredential credential)? preAuthCredentialPair; lock (pool.PreAuthCredentials) { // Just look for basic credentials. If in the future we support preauth @@ -227,13 +228,13 @@ private static async ValueTask SendWithAuthAsync(HttpReques Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NegotiateScheme) == null); Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, NtlmScheme) == null); Debug.Assert(pool.PreAuthCredentials.GetCredential(authUri, DigestScheme) == null); - credential = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme); + preAuthCredentialPair = pool.PreAuthCredentials.GetCredential(authUri, BasicScheme); } - if (credential != null) + if (preAuthCredentialPair != null) { - SetBasicAuthToken(request, credential, isProxyAuth); - performedBasicPreauth = true; + (preAuthCredentialUri, preAuthCredential) = preAuthCredentialPair.Value; + SetBasicAuthToken(request, preAuthCredential, isProxyAuth); } } @@ -265,13 +266,21 @@ await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isPro break; case AuthenticationType.Basic: - if (performedBasicPreauth) + if (preAuthCredential != null) { if (NetEventSource.Log.IsEnabled()) { NetEventSource.AuthenticationError(authUri, $"Pre-authentication with {(isProxyAuth ? "proxy" : "server")} failed."); } - break; + + if (challenge.Credential == preAuthCredential) + { + // Pre auth failed, and user supplied credentials are still same, we can stop there. + break; + } + + // Pre-auth credentials have changed, continue with the new ones. + // The old ones will be removed below. } response.Dispose(); @@ -293,6 +302,17 @@ await TrySetDigestAuthToken(request, challenge.Credential, digestResponse, isPro default: lock (pool.PreAuthCredentials!) { + // remove previously cached (failing) creds + if (preAuthCredentialUri != null) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(pool.PreAuthCredentials, $"Removing Basic credential from cache, uri={preAuthCredentialUri}, username={preAuthCredential!.UserName}"); + } + + pool.PreAuthCredentials.Remove(preAuthCredentialUri, BasicScheme); + } + try { if (NetEventSource.Log.IsEnabled()) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs index 1cff8e9ad39516..0d9f3a2a1b9537 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs @@ -59,7 +59,7 @@ internal sealed partial class HttpConnectionPool : IDisposable private SslClientAuthenticationOptions? _sslOptionsHttp3; private readonly SslClientAuthenticationOptions? _sslOptionsProxy; - private readonly CredentialCache? _preAuthCredentials; + private readonly PreAuthCredentialCache? _preAuthCredentials; /// Whether the pool has been used since the last time a cleanup occurred. private bool _usedSinceLastCleanup = true; @@ -237,7 +237,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK // Set up for PreAuthenticate. Access to this cache is guarded by a lock on the cache itself. if (_poolManager.Settings._preAuthenticate) { - _preAuthCredentials = new CredentialCache(); + _preAuthCredentials = new PreAuthCredentialCache(); } _http11RequestQueue = new RequestQueue(); @@ -296,7 +296,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.SslSocksTunnel; public Uri? ProxyUri => _proxyUri; public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials; - public CredentialCache? PreAuthCredentials => _preAuthCredentials; + public PreAuthCredentialCache? PreAuthCredentials => _preAuthCredentials; public bool IsDefaultPort => OriginAuthority.Port == (IsSecure ? DefaultHttpsPort : DefaultHttpPort); private bool DoProxyAuth => (_kind == HttpConnectionKind.Proxy || _kind == HttpConnectionKind.ProxyConnect); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/PreAuthCredentialCache.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/PreAuthCredentialCache.cs new file mode 100644 index 00000000000000..1f4dcc54fb0c2c --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/PreAuthCredentialCache.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace System.Net.Http +{ + internal sealed class PreAuthCredentialCache + { + private Dictionary? _cache; + + public void Add(Uri uriPrefix, string authType, NetworkCredential cred) + { + Debug.Assert(uriPrefix != null); + Debug.Assert(authType != null); + + var key = new CredentialCacheKey(uriPrefix, authType); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Adding key:[{key}], cred:[{cred.Domain}],[{cred.UserName}]"); + + _cache ??= new Dictionary(); + _cache.Add(key, cred); + } + + public void Remove(Uri uriPrefix, string authType) + { + Debug.Assert(uriPrefix != null); + Debug.Assert(authType != null); + + if (_cache == null) + { + return; + } + + var key = new CredentialCacheKey(uriPrefix, authType); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Removing key:[{key}]"); + _cache.Remove(key); + } + + public (Uri uriPrefix, NetworkCredential credential)? GetCredential(Uri uriPrefix, string authType) + { + Debug.Assert(uriPrefix != null); + Debug.Assert(authType != null); + + if (_cache == null) + { + return null; + } + + CredentialCacheHelper.TryGetCredential(_cache, uriPrefix, authType, out Uri? mostSpecificMatchUri, out NetworkCredential? mostSpecificMatch); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Returning {(mostSpecificMatch == null ? "null" : "(" + mostSpecificMatch.UserName + ":" + mostSpecificMatch.Domain + ")")}"); + + return mostSpecificMatch == null ? null : (mostSpecificMatchUri!, mostSpecificMatch!); + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.BasicAuth.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.BasicAuth.cs new file mode 100644 index 00000000000000..a6ac590fb6adcf --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.BasicAuth.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Net.Test.Common; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests +{ + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public class HttpClientHandlerTest_BasicAuth : HttpClientHandlerTestBase + { + public HttpClientHandlerTest_BasicAuth(ITestOutputHelper output) : base(output) + { + } + + protected override Version UseVersion => HttpVersion.Version20; + + [Fact] + public async Task RefreshesPreAuthCredentialsOnChange() + { + CredentialPlugin credentialPlugin = new CredentialPlugin(); + + using Http2LoopbackServer server = Http2LoopbackServer.CreateServer(); + server.AllowMultipleConnections = true; + + HttpClientHandler handler = CreateHttpClientHandler(); + handler.PreAuthenticate = true; + handler.Credentials = credentialPlugin; + using HttpClient client = CreateHttpClient(handler); + + Task sendTask = client.GetAsync(server.Address); + + async Task GetAuth(GenericLoopbackConnection connection) + { + HttpRequestData data = await connection.ReadRequestDataAsync(); + HttpHeaderData? header = data.Headers.FirstOrDefault(h => string.Equals(h.Name, "Authorization", StringComparison.OrdinalIgnoreCase)); + + if (header == null) + { + return ""; + } + + return Encoding.UTF8.GetString(Convert.FromBase64String(header.Value.Value.Replace("Basic", "", StringComparison.OrdinalIgnoreCase))); + } + + await server.HandleRequestAsync(HttpStatusCode.Unauthorized, headers: new[] { new HttpHeaderData("WWW-Authenticate", "Basic realm=\"test\"") }); + await server.AcceptConnectionAsync(async conn => + { + Assert.Equal("username:password", await GetAuth(conn)); + await conn.SendResponseAsync(HttpStatusCode.OK); + }).WaitAsync(TimeSpan.FromSeconds(30)); + + HttpResponseMessage response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // change password and try again + credentialPlugin.ChangePassword(); + sendTask = client.GetAsync(server.Address); + + // first one reuses the cached credentials -> 401 + await server.AcceptConnectionAsync(async conn => + { + Assert.Equal("username:password", await GetAuth(conn)); + await conn.SendResponseAsync(HttpStatusCode.Unauthorized, headers: new[] { new HttpHeaderData("WWW-Authenticate", "Basic realm=\"test\"") }); + }).WaitAsync(TimeSpan.FromSeconds(30)); + + // client should try again with correct credentials + await server.AcceptConnectionAsync(async conn => + { + Assert.Equal("username:password1", await GetAuth(conn)); + await conn.SendResponseAsync(HttpStatusCode.OK); + }).WaitAsync(TimeSpan.FromSeconds(30)); + + response = await sendTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + + internal class CredentialPlugin : ICredentials + { + public CredentialPlugin() + { + UserName = "username"; + counter = 0; + Password = "password"; + } + + private int counter; + public string UserName { get; private set; } + public string Password { get; private set; } + + public void ChangePassword() + { + counter++; + Password = "password" + counter; + } + + NetworkCredential? ICredentials.GetCredential(Uri uri, string authType) + { + if (authType == "Basic") + { + return new NetworkCredential(UserName, Password); + } + + return null; + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index fd09f7db0b2282..0256d1d53a4517 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -245,6 +245,7 @@ + diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index a659207964a3e8..e71aaa8222e300 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -82,6 +82,8 @@ Link="Common\System\Text\StringBuilderCache.cs" /> + diff --git a/src/libraries/System.Net.Primitives/src/System/Net/CredentialCache.cs b/src/libraries/System.Net.Primitives/src/System/Net/CredentialCache.cs index 8cb88371227919..8e46d4ab6c529d 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/CredentialCache.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/CredentialCache.cs @@ -13,7 +13,7 @@ namespace System.Net // name-password pairs and associates these with host/realm. public class CredentialCache : ICredentials, ICredentialsByHost, IEnumerable { - private Dictionary? _cache; + private Dictionary? _cache; private Dictionary? _cacheForHosts; private int _version; @@ -37,11 +37,11 @@ public void Add(Uri uriPrefix, string authType, NetworkCredential cred) ++_version; - var key = new CredentialKey(uriPrefix, authType); + var key = new CredentialCacheKey(uriPrefix, authType); if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Adding key:[{key}], cred:[{cred.Domain}],[{cred.UserName}]"); - _cache ??= new Dictionary(); + _cache ??= new Dictionary(); _cache.Add(key, cred); } @@ -88,7 +88,7 @@ public void Remove(Uri? uriPrefix, string? authType) ++_version; - var key = new CredentialKey(uriPrefix, authType); + var key = new CredentialCacheKey(uriPrefix, authType); if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Removing key:[{key}]"); @@ -135,28 +135,7 @@ public void Remove(string? host, int port, string? authenticationType) return null; } - int longestMatchPrefix = -1; - NetworkCredential? mostSpecificMatch = null; - - // Enumerate through every credential in the cache - foreach (KeyValuePair pair in _cache) - { - CredentialKey key = pair.Key; - - // Determine if this credential is applicable to the current Uri/AuthType - if (key.Match(uriPrefix, authType)) - { - int prefixLen = key.UriPrefixLength; - - // Check if the match is better than the current-most-specific match - if (prefixLen > longestMatchPrefix) - { - // Yes: update the information about currently preferred match - longestMatchPrefix = prefixLen; - mostSpecificMatch = pair.Value; - } - } - } + CredentialCacheHelper.TryGetCredential(_cache, uriPrefix, authType, out _ /*uri*/, out NetworkCredential? mostSpecificMatch); if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Returning {(mostSpecificMatch == null ? "null" : "(" + mostSpecificMatch.UserName + ":" + mostSpecificMatch.Domain + ")")}"); @@ -201,7 +180,7 @@ internal static CredentialEnumerator Create(CredentialCache cache) { return cache._cacheForHosts != null ? new DoubleTableCredentialEnumerator(cache) : - new SingleTableCredentialEnumerator(cache, cache._cache); + new SingleTableCredentialEnumerator(cache, cache._cache); } else { @@ -286,7 +265,7 @@ public override void Reset() } } - private sealed class DoubleTableCredentialEnumerator : SingleTableCredentialEnumerator + private sealed class DoubleTableCredentialEnumerator : SingleTableCredentialEnumerator { private Dictionary.ValueCollection.Enumerator _enumerator; // mutable struct field deliberately not readonly. private bool _onThisEnumerator; @@ -406,95 +385,4 @@ public override bool Equals([NotNullWhen(true)] object? obj) => public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"{Host}:{Port}:{AuthenticationType}"); } - - internal sealed class CredentialKey : IEquatable - { - public readonly Uri UriPrefix; - public readonly int UriPrefixLength = -1; - public readonly string AuthenticationType; - - internal CredentialKey(Uri uriPrefix, string authenticationType) - { - Debug.Assert(uriPrefix != null); - Debug.Assert(authenticationType != null); - - UriPrefix = uriPrefix; - UriPrefixLength = UriPrefix.ToString().Length; - AuthenticationType = authenticationType; - } - - internal bool Match(Uri uri, string authenticationType) - { - if (uri == null || authenticationType == null) - { - return false; - } - - // If the protocols don't match, this credential is not applicable for the given Uri. - if (!string.Equals(authenticationType, AuthenticationType, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Match({UriPrefix} & {uri})"); - - return IsPrefix(uri, UriPrefix); - } - - // IsPrefix (Uri) - // - // Determines whether is a prefix of this URI. A prefix - // match is defined as: - // - // scheme match - // + host match - // + port match, if any - // + path is a prefix of path, if any - // - // Returns: - // True if is a prefix of this URI - private static bool IsPrefix(Uri uri, Uri prefixUri) - { - Debug.Assert(uri != null); - Debug.Assert(prefixUri != null); - - if (prefixUri.Scheme != uri.Scheme || prefixUri.Host != uri.Host || prefixUri.Port != uri.Port) - { - return false; - } - - int prefixLen = prefixUri.AbsolutePath.LastIndexOf('/'); - if (prefixLen > uri.AbsolutePath.LastIndexOf('/')) - { - return false; - } - - return string.Compare(uri.AbsolutePath, 0, prefixUri.AbsolutePath, 0, prefixLen, StringComparison.OrdinalIgnoreCase) == 0; - } - - public override int GetHashCode() => - StringComparer.OrdinalIgnoreCase.GetHashCode(AuthenticationType) ^ - UriPrefix.GetHashCode(); - - public bool Equals([NotNullWhen(true)] CredentialKey? other) - { - if (other == null) - { - return false; - } - - bool equals = - string.Equals(AuthenticationType, other.AuthenticationType, StringComparison.OrdinalIgnoreCase) && - UriPrefix.Equals(other.UriPrefix); - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Equals({this},{other}) returns {equals}"); - - return equals; - } - - public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as CredentialKey); - - public override string ToString() => - string.Create(CultureInfo.InvariantCulture, $"[{UriPrefixLength}]:{UriPrefix}:{AuthenticationType}"); - } } diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/CredentialCacheTest.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/CredentialCacheTest.cs index a67967b36811c7..6895fabba4f1b9 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/CredentialCacheTest.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/CredentialCacheTest.cs @@ -272,6 +272,17 @@ public static void GetCredential_SimilarUriAuthenticationType_GetLongestUriPrefi Assert.Equal(nc, credential2); } + [Fact] + public static void GetCredential_LongerUriButShorterMatch() + { + CredentialCache cc = new CredentialCache(); + cc.Add(new Uri("http://microsoft:80/common/unique/something"), authenticationType1, credential2); + cc.Add(new Uri("http://microsoft:80/common/veryloooooongprefix"), authenticationType1, credential1); + + NetworkCredential nc = cc.GetCredential(new Uri("http://microsoft:80/common/unique/a"), authenticationType1); + Assert.Equal(nc, credential2); + } + [Fact] public static void GetCredential_UriAuthenticationType_Invalid() {