From a4a186867462c04174cb09075d3f99d51cda4ce8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 31 May 2022 11:48:51 +0200 Subject: [PATCH] Refactor NTAuthenticationHelper into NTAuthenticationHandler --- src/Mono.Android/Mono.Android.csproj | 3 +- .../AndroidMessageHandler.cs | 30 +- .../NTAuthenticationHandler.cs | 281 ++++++++++++++++++ .../NTAuthenticationHelper.cs | 193 ------------ .../NTAuthenticationProxy.cs | 86 ------ .../Mono.Android.NET-Tests.csproj | 2 + ...roidMessageHandlerNTAuthenticationTests.cs | 115 +++++++ 7 files changed, 426 insertions(+), 284 deletions(-) create mode 100644 src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHandler.cs delete mode 100644 src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHelper.cs delete mode 100644 src/Mono.Android/Xamarin.Android.Net/NTAuthenticationProxy.cs create mode 100644 tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerNTAuthenticationTests.cs diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index cb0a677268c..080cac698e9 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -368,9 +368,8 @@ - - + diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index 57f417185ae..f917db2bb16 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -139,11 +139,33 @@ public CookieContainer CookieContainer public bool UseProxy { get; set; } = true; - private bool CouldHaveNTCredentials => Proxy != null || Credentials != null; +#if !MONOANDROID1_0 + IWebProxy? _proxy; + ICredentials? _credentials; + NTAuthenticationHandler.Helper? _ntAuthHelper; + + public IWebProxy? Proxy + { + get => _proxy; + set { + _proxy = value; + _ntAuthHelper ??= new NTAuthenticationHandler.Helper (this); + } + } + public ICredentials? Credentials + { + get => _credentials; + set { + _credentials = value; + _ntAuthHelper ??= new NTAuthenticationHandler.Helper (this); + } + } +#else public IWebProxy? Proxy { get; set; } public ICredentials? Credentials { get; set; } +#endif public bool AllowAutoRedirect { get; set; } = true; @@ -338,8 +360,10 @@ string EncodeUrl (Uri url) var response = await DoSendAsync (request, cancellationToken).ConfigureAwait (false); #if !MONOANDROID1_0 - if (CouldHaveNTCredentials && RequestNeedsAuthorization && NTAuthenticationHelper.TryGetSupportedAuthMethod (this, request, out var auth, out var credentials)) { - response = await NTAuthenticationHelper.SendAsync (this, request, response, auth, credentials, cancellationToken).ConfigureAwait (false); + if (RequestNeedsAuthorization && _ntAuthHelper != null && _ntAuthHelper.RequestNeedsNTAuthentication (request, out var ntAuth)) { + var authenticatedResponse = await ntAuth.ResendRequestWithAuthAsync (cancellationToken).ConfigureAwait (false); + if (authenticatedResponse != null) + return authenticatedResponse; } #endif diff --git a/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHandler.cs b/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHandler.cs new file mode 100644 index 00000000000..ec9c6de9b44 --- /dev/null +++ b/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHandler.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Security.Authentication.ExtendedProtection; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Net +{ + // This code is heavily inspired by System.Net.Http.AuthenticationHelper + internal sealed class NTAuthenticationHandler + { + const int MaxRequests = 10; + + internal sealed class Helper + { + readonly AndroidMessageHandler _handler; + + internal Helper (AndroidMessageHandler handler) + { + _handler = handler; + } + + internal bool RequestNeedsNTAuthentication (HttpRequestMessage request, [NotNullWhen (true)] out NTAuthenticationHandler? ntAuthHandler) + { + IEnumerable requestedAuthentication = _handler.RequestedAuthentication ?? Enumerable.Empty (); + foreach (var auth in requestedAuthentication) { + if (TryGetSupportedAuthType (auth.Challenge, out var authType)) { + var credentials = auth.UseProxyAuthentication ? _handler.Proxy?.Credentials : _handler.Credentials; + var correspondingCredentials = credentials?.GetCredential (request.RequestUri, authType); + + if (correspondingCredentials != null) { + ntAuthHandler = new NTAuthenticationHandler (_handler, request, authType, auth.UseProxyAuthentication, correspondingCredentials); + return true; + } + } + } + + ntAuthHandler = null; + return false; + } + + static bool TryGetSupportedAuthType (string challenge, out string authType) + { + var spaceIndex = challenge.IndexOf (' '); + authType = spaceIndex == -1 ? challenge : challenge.Substring (0, spaceIndex); + + return authType.Equals ("NTLM", StringComparison.OrdinalIgnoreCase) || + authType.Equals ("Negotiate", StringComparison.OrdinalIgnoreCase); + } + } + + readonly AndroidMessageHandler _handler; + readonly HttpRequestMessage _request; + readonly string _authType; + readonly bool _isProxyAuth; + readonly NetworkCredential _credentials; + + private NTAuthenticationHandler ( + AndroidMessageHandler handler, + HttpRequestMessage request, + string authType, + bool isProxyAuth, + NetworkCredential credentials) + { + _handler = handler; + _request = request; + _authType = authType; + _isProxyAuth = isProxyAuth; + _credentials = credentials; + } + + internal async Task ResendRequestWithAuthAsync (CancellationToken cancellationToken) + { + var authContext = new NTAuthentication ( + isServer: false, + _authType, + _credentials, + spn: await GetSpn (cancellationToken).ConfigureAwait (false), + requestedContextFlags: GetRequestedContextFlags (), + channelBinding: null); + + // we need to make sure that the handler doesn't override the authorization header + // with the user defined pre-authentication data + var originalPreAuthenticate = _handler.PreAuthenticate; + _handler.PreAuthenticate = false; + + try { + return await DoResendRequestWithAuthAsync (authContext, cancellationToken); + } finally { + _handler.PreAuthenticate = originalPreAuthenticate; + authContext.CloseContext (); + } + } + + async Task DoResendRequestWithAuthAsync (NTAuthentication authContext, CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + + string? challenge = null; + int requestCounter = 0; + + while (requestCounter++ < MaxRequests) { + var challengeResponse = authContext.GetOutgoingBlob (challenge); + if (challengeResponse == null) { + // response indicated denial even after login, so stop processing + // and return current response + break; + } + + var headerValue = new AuthenticationHeaderValue (_authType, challengeResponse); + if (_isProxyAuth) { + _request.Headers.ProxyAuthorization = headerValue; + } else { + _request.Headers.Authorization = headerValue; + } + + response = await _handler.DoSendAsync (_request, cancellationToken).ConfigureAwait (false); + + // we need to drain the content otherwise the next request + // won't reuse the same TCP socket and persistent auth won't work + await response.Content.LoadIntoBufferAsync ().ConfigureAwait (false); + + if (authContext.IsCompleted || !TryGetChallenge (response, out challenge)) { + break; + } + + if (!IsAuthenticationChallenge (response)) { + // Tail response for Negotiate on successful authentication. + // Validate it before we proceed. + authContext.GetOutgoingBlob (challenge); + break; + } + } + + return response; + } + + async Task GetSpn (CancellationToken cancellationToken) + { + var hostName = await GetHostName (cancellationToken); + return $"HTTP/{hostName}"; + } + + async Task GetHostName (CancellationToken cancellationToken) + { + // Calculate SPN (Service Principal Name) using the host name of the request. + // Use the request's 'Host' header if available. Otherwise, use the request uri. + // Ignore the 'Host' header if this is proxy authentication since we need to use + // the host name of the proxy itself for SPN calculation. + if (!_isProxyAuth && _request.Headers.Host != null) { + // Use the host name without any normalization. + return _request.Headers.Host; + } + + var requestUri = _request.RequestUri!; + var authUri = _isProxyAuth ? _handler.Proxy?.GetProxy (requestUri) ?? requestUri : requestUri; + + // Need to use FQDN normalized host so that CNAME's are traversed. + // Use DNS to do the forward lookup to an A (host) record. + // But skip DNS lookup on IP literals. Otherwise, we would end up + // doing an unintended reverse DNS lookup. + if (authUri.HostNameType == UriHostNameType.IPv6 || authUri.HostNameType == UriHostNameType.IPv4) { + return authUri.IdnHost; + } else { + IPHostEntry result = await Dns.GetHostEntryAsync (authUri.IdnHost, cancellationToken).ConfigureAwait (false); + return result.HostName; + } + } + + int GetRequestedContextFlags () + { + // the ContextFlagsPal is internal type in dotnet/runtime and we can't + // use it directly here so we have to use ints directly + int contextFlags = 0x00000800; // ContextFlagsPal.Connection + + // When connecting to proxy server don't enforce the integrity to avoid + // compatibility issues. The assumption is that the proxy server comes + // from a trusted source. + if (!_isProxyAuth) { + contextFlags |= 0x00010000; // ContextFlagsPal.InitIntegrity + } + + return contextFlags; + } + + bool TryGetChallenge (HttpResponseMessage response, [NotNullWhen (true)] out string? challenge) + { + var responseHeaderValues = _isProxyAuth ? response.Headers.ProxyAuthenticate : response.Headers.WwwAuthenticate; + challenge = responseHeaderValues?.FirstOrDefault (headerValue => headerValue.Scheme == _authType)?.Parameter; + return !string.IsNullOrEmpty (challenge); + } + + bool IsAuthenticationChallenge (HttpResponseMessage response) + => _isProxyAuth + ? response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired + : response.StatusCode == HttpStatusCode.Unauthorized; + + private sealed class NTAuthentication + { + const string AssemblyName = "System.Net.Http"; + const string TypeName = "System.Net.NTAuthentication"; + const string ContextFlagsPalTypeName = "System.Net.ContextFlagsPal"; + + const string ConstructorDescription = "#ctor(System.Boolean,System.String,System.Net.NetworkCredential,System.String,System.Net.ContextFlagsPal,System.Security.Authentication.ExtendedProtection.ChannelBinding)"; + const string IsCompletedPropertyName = "IsCompleted"; + const string GetOutgoingBlobMethodName = "GetOutgoingBlob"; + const string CloseContextMethodName = "CloseContext"; + + const BindingFlags InstanceBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + static Lazy s_NTAuthenticationType = new (() => FindType (TypeName, AssemblyName)); + static Lazy s_NTAuthenticationConstructorInfo = new (() => GetNTAuthenticationConstructor ()); + static Lazy s_IsCompletedPropertyInfo = new (() => GetProperty (IsCompletedPropertyName)); + static Lazy s_GetOutgoingBlobMethodInfo = new (() => GetMethod (GetOutgoingBlobMethodName)); + static Lazy s_CloseContextMethodInfo = new (() => GetMethod (CloseContextMethodName)); + + static Type FindType (string typeName, string assemblyName) + => Type.GetType ($"{typeName}, {assemblyName}", throwOnError: true)!; + + static ConstructorInfo GetNTAuthenticationConstructor () + { + var contextFlagsPalType = FindType (ContextFlagsPalTypeName, AssemblyName); + var paramTypes = new[] { + typeof (bool), + typeof (string), + typeof (NetworkCredential), + typeof (string), + contextFlagsPalType, + typeof (ChannelBinding) + }; + + return s_NTAuthenticationType.Value.GetConstructor (InstanceBindingFlags, paramTypes) + ?? throw new MissingMemberException (TypeName, ConstructorInfo.ConstructorName); + } + + static PropertyInfo GetProperty (string name) + => s_NTAuthenticationType.Value.GetProperty (name, InstanceBindingFlags) + ?? throw new MissingMemberException (TypeName, name); + + static MethodInfo GetMethod (string name) + => s_NTAuthenticationType.Value.GetMethod (name, InstanceBindingFlags) + ?? throw new MissingMemberException (TypeName, name); + + object _instance; + + [DynamicDependency (ConstructorDescription, TypeName, AssemblyName)] + internal NTAuthentication ( + bool isServer, + string package, + NetworkCredential credential, + string? spn, + int requestedContextFlags, + ChannelBinding? channelBinding) + { + var constructorParams = new object?[] { isServer, package, credential, spn, requestedContextFlags, channelBinding }; + _instance = s_NTAuthenticationConstructorInfo.Value.Invoke (constructorParams); + } + + public bool IsCompleted + => GetIsCompleted (); + + [DynamicDependency ($"get_{IsCompletedPropertyName}", TypeName, AssemblyName)] + bool GetIsCompleted () + => (bool)(s_IsCompletedPropertyInfo.Value.GetValue (_instance) ?? false); + + [DynamicDependency (GetOutgoingBlobMethodName, TypeName, AssemblyName)] + public string? GetOutgoingBlob (string? incomingBlob) + => (string?)s_GetOutgoingBlobMethodInfo.Value.Invoke (_instance, new object?[] { incomingBlob }); + + [DynamicDependency (CloseContextMethodName, TypeName, AssemblyName)] + public void CloseContext () + => s_CloseContextMethodInfo.Value.Invoke (_instance, null); + } + } +} diff --git a/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHelper.cs b/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHelper.cs deleted file mode 100644 index 4a1ed91bae0..00000000000 --- a/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationHelper.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Authentication.ExtendedProtection; -using System.Threading; -using System.Threading.Tasks; - -namespace Xamarin.Android.Net -{ - // This code is heavily inspired by System.Net.Http.AuthenticationHelper - internal static class NTAuthenticationHelper - { - const int MaxRequests = 10; - - internal static bool TryGetSupportedAuthMethod ( - AndroidMessageHandler handler, - HttpRequestMessage request, - [NotNullWhen (true)] out AuthenticationData? supportedAuth, - [NotNullWhen (true)] out NetworkCredential? suitableCredentials) - { - IEnumerable requestedAuthentication = handler.RequestedAuthentication ?? Enumerable.Empty (); - foreach (var auth in requestedAuthentication) { - if (TryGetSupportedAuthType (auth.Challenge, out var authType)) { - var credentials = auth.UseProxyAuthentication ? handler.Proxy?.Credentials : handler.Credentials; - suitableCredentials = credentials?.GetCredential (request.RequestUri, authType); - - if (suitableCredentials != null) { - supportedAuth = auth; - return true; - } - } - } - - supportedAuth = null; - suitableCredentials = null; - return false; - } - - internal static async Task SendAsync ( - AndroidMessageHandler handler, - HttpRequestMessage request, - HttpResponseMessage response, - AuthenticationData auth, - NetworkCredential credentials, - CancellationToken cancellationToken) - { - var authType = GetSupportedAuthType (auth.Challenge); - var isProxyAuth = auth.UseProxyAuthentication; - var authContext = new NTAuthenticationProxy ( - isServer: false, - authType, - credentials, - spn: await GetSpn (handler, request, isProxyAuth, cancellationToken).ConfigureAwait (false), - requestedContextFlags: GetRequestedContextFlags (isProxyAuth), - channelBinding: null); - - // we need to make sure that the handler doesn't override the authorization header - // with the user defined pre-authentication data - var originalPreAuthenticate = handler.PreAuthenticate; - handler.PreAuthenticate = false; - - try { - string? challenge = null; - int requestCounter = 0; - - while (requestCounter++ < MaxRequests) { - var challengeResponse = authContext.GetOutgoingBlob (challenge); - if (challengeResponse == null) { - // response indicated denial even after login, so stop processing - // and return current response - break; - } - - var headerValue = new AuthenticationHeaderValue (authType, challengeResponse); - if (auth.UseProxyAuthentication) { - request.Headers.ProxyAuthorization = headerValue; - } else { - request.Headers.Authorization = headerValue; - } - - response = await handler.DoSendAsync (request, cancellationToken).ConfigureAwait (false); - - // we need to drain the content otherwise the next request - // won't reuse the same TCP socket and persistent auth won't work - await response.Content.LoadIntoBufferAsync ().ConfigureAwait (false); - - if (authContext.IsCompleted || !TryGetChallenge (response, authType, isProxyAuth, out challenge)) { - break; - } - - if (!IsAuthenticationChallenge (response, isProxyAuth)) { - // Tail response for Negotiate on successful authentication. - // Validate it before we proceed. - authContext.GetOutgoingBlob (challenge); - break; - } - } - - return response; - } finally { - handler.PreAuthenticate = originalPreAuthenticate; - authContext.CloseContext (); - } - } - - static string GetSupportedAuthType (string challenge) - { - if (!TryGetSupportedAuthType (challenge, out var authType)) { - throw new InvalidOperationException ($"Authenticaton scheme {authType} is not supported by {nameof (NTAuthenticationHelper)}."); - } - - return authType; - } - - static bool TryGetSupportedAuthType (string challenge, out string authType) - { - var spaceIndex = challenge.IndexOf (' '); - authType = spaceIndex == -1 ? challenge : challenge.Substring (0, spaceIndex); - - return authType.Equals ("NTLM", StringComparison.OrdinalIgnoreCase) || - authType.Equals ("Negotiate", StringComparison.OrdinalIgnoreCase); - } - - static async Task GetSpn ( - AndroidMessageHandler handler, - HttpRequestMessage request, - bool isProxyAuth, - CancellationToken cancellationToken) - { - // Calculate SPN (Service Principal Name) using the host name of the request. - // Use the request's 'Host' header if available. Otherwise, use the request uri. - // Ignore the 'Host' header if this is proxy authentication since we need to use - // the host name of the proxy itself for SPN calculation. - string hostName; - if (!isProxyAuth && request.Headers.Host != null) { - // Use the host name without any normalization. - hostName = request.Headers.Host; - } else { - var requestUri = request.RequestUri!; - var authUri = isProxyAuth ? handler.Proxy?.GetProxy (requestUri) ?? requestUri : requestUri; - - // Need to use FQDN normalized host so that CNAME's are traversed. - // Use DNS to do the forward lookup to an A (host) record. - // But skip DNS lookup on IP literals. Otherwise, we would end up - // doing an unintended reverse DNS lookup. - if (authUri.HostNameType == UriHostNameType.IPv6 || authUri.HostNameType == UriHostNameType.IPv4) { - hostName = authUri.IdnHost; - } else { - IPHostEntry result = await Dns.GetHostEntryAsync (authUri.IdnHost, cancellationToken).ConfigureAwait (false); - hostName = result.HostName; - } - } - - return $"HTTP/{hostName}"; - } - - static int GetRequestedContextFlags (bool isProxyAuth) - { - // the ContextFlagsPal is internal type in dotnet/runtime and we can't - // use it directly here so we have to use ints directly - int contextFlags = 0x00000800; // ContextFlagsPal.Connection - - // When connecting to proxy server don't enforce the integrity to avoid - // compatibility issues. The assumption is that the proxy server comes - // from a trusted source. - if (!isProxyAuth) { - contextFlags |= 0x00010000; // ContextFlagsPal.InitIntegrity - } - - return contextFlags; - } - - static bool TryGetChallenge ( - HttpResponseMessage response, - string authType, - bool isProxyAuth, - [NotNullWhen (true)] out string? challenge) - { - var responseHeaderValues = isProxyAuth ? response.Headers.ProxyAuthenticate : response.Headers.WwwAuthenticate; - challenge = responseHeaderValues?.FirstOrDefault (headerValue => headerValue.Scheme == authType)?.Parameter; - return !string.IsNullOrEmpty (challenge); - } - - static bool IsAuthenticationChallenge (HttpResponseMessage response, bool isProxyAuth) - => isProxyAuth - ? response.StatusCode == HttpStatusCode.ProxyAuthenticationRequired - : response.StatusCode == HttpStatusCode.Unauthorized; - } -} diff --git a/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationProxy.cs b/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationProxy.cs deleted file mode 100644 index 8e4c4514583..00000000000 --- a/src/Mono.Android/Xamarin.Android.Net/NTAuthenticationProxy.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Reflection; -using System.Runtime; -using System.Security.Authentication.ExtendedProtection; - -namespace Xamarin.Android.Net -{ - internal sealed class NTAuthenticationProxy - { - const string AssemblyName = "System.Net.Http"; - const string TypeName = "System.Net.NTAuthentication"; - const string ContextFlagsPalTypeName = "System.Net.ContextFlagsPal"; - - const string ConstructorDescription = "#ctor(System.Boolean,System.String,System.Net.NetworkCredential,System.String,System.Net.ContextFlagsPal,System.Security.Authentication.ExtendedProtection.ChannelBinding)"; - const string IsCompletedPropertyName = "IsCompleted"; - const string GetOutgoingBlobMethodName = "GetOutgoingBlob"; - const string CloseContextMethodName = "CloseContext"; - - const BindingFlags InstanceBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - - static Lazy s_NTAuthenticationType = new (() => FindType (TypeName, AssemblyName)); - static Lazy s_NTAuthenticationConstructorInfo = new (() => GetNTAuthenticationConstructor ()); - static Lazy s_IsCompletedPropertyInfo = new (() => GetProperty (IsCompletedPropertyName)); - static Lazy s_GetOutgoingBlobMethodInfo = new (() => GetMethod (GetOutgoingBlobMethodName)); - static Lazy s_CloseContextMethodInfo = new (() => GetMethod (CloseContextMethodName)); - - static Type FindType (string typeName, string assemblyName) - => Type.GetType ($"{typeName}, {assemblyName}", throwOnError: true)!; - - static ConstructorInfo GetNTAuthenticationConstructor () - { - var contextFlagsPalType = FindType (ContextFlagsPalTypeName, AssemblyName); - var paramTypes = new[] { - typeof (bool), - typeof (string), - typeof (NetworkCredential), - typeof (string), - contextFlagsPalType, - typeof (ChannelBinding) - }; - - return s_NTAuthenticationType.Value.GetConstructor (InstanceBindingFlags, paramTypes) - ?? throw new MissingMemberException (TypeName, ConstructorInfo.ConstructorName); - } - - static PropertyInfo GetProperty (string name) - => s_NTAuthenticationType.Value.GetProperty (name, InstanceBindingFlags) - ?? throw new MissingMemberException (TypeName, name); - - static MethodInfo GetMethod (string name) - => s_NTAuthenticationType.Value.GetMethod (name, InstanceBindingFlags) - ?? throw new MissingMemberException (TypeName, name); - - object _instance; - - [DynamicDependency (ConstructorDescription, TypeName, AssemblyName)] - internal NTAuthenticationProxy ( - bool isServer, - string package, - NetworkCredential credential, - string? spn, - int requestedContextFlags, - ChannelBinding? channelBinding) - { - var constructorParams = new object?[] { isServer, package, credential, spn, requestedContextFlags, channelBinding }; - _instance = s_NTAuthenticationConstructorInfo.Value.Invoke (constructorParams); - } - - public bool IsCompleted - => GetIsCompleted (); - - [DynamicDependency ($"get_{IsCompletedPropertyName}", TypeName, AssemblyName)] - bool GetIsCompleted () - => (bool)s_IsCompletedPropertyInfo.Value.GetValue (_instance); - - [DynamicDependency (GetOutgoingBlobMethodName, TypeName, AssemblyName)] - public string? GetOutgoingBlob (string? incomingBlob) - => (string?)s_GetOutgoingBlobMethodInfo.Value.Invoke (_instance, new object?[] { incomingBlob }); - - [DynamicDependency (CloseContextMethodName, TypeName, AssemblyName)] - public void CloseContext () - => s_CloseContextMethodInfo.Value.Invoke (_instance, null); - } -} diff --git a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj index 4bf8b2ef988..9aafe7ec29c 100644 --- a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj @@ -40,6 +40,8 @@ + + diff --git a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerNTAuthenticationTests.cs b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerNTAuthenticationTests.cs new file mode 100644 index 00000000000..d68ff12c0a7 --- /dev/null +++ b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerNTAuthenticationTests.cs @@ -0,0 +1,115 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xamarin.Android.Net; +using NUnit.Framework; + +namespace Xamarin.Android.NetTests { + [TestFixture] + public sealed class AndroidMessageHandlerNTAuthenticationTests + { + [Test] + public async Task RequestWithoutCredentialsFails () + { + using var server = new FakeNtlmServer (port: 47662); + var handler = new AndroidMessageHandler (); + var client = new HttpClient (handler); + + var response = await client.GetAsync (server.Uri); + + Assert.IsFalse (response.IsSuccessStatusCode); + Assert.AreEqual (HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Test] + public async Task RequestWithCredentialsSucceeds () + { + using var server = new FakeNtlmServer (port: 47663); + var cache = new CredentialCache (); + cache.Add (server.Uri, "NTLM", FakeNtlmServer.Credentials); + var handler = new AndroidMessageHandler { Credentials = cache }; + var client = new HttpClient (handler); + + var response = await client.GetAsync (server.Uri); + var content = await response.Content.ReadAsStringAsync (); + + Assert.IsTrue (response.IsSuccessStatusCode); + Assert.AreEqual (FakeNtlmServer.SecretContent, content); + } + + sealed class FakeNtlmServer : IDisposable + { + public static readonly NetworkCredential Credentials = new NetworkCredential ("User", "Password", "Domain"); + public static readonly string SecretContent = "SECRET"; + + HttpListener? _listener = new HttpListener (); + Task? _loop; + + public FakeNtlmServer (int port) + { + Uri = new Uri ($"http://localhost:{port}/"); + + _listener.Prefixes.Add ($"http://+:{port}/"); + _listener.Start (); + _loop = Task.Run (Loop); + } + + public Uri Uri { get; } + + public void Dispose () + { + _listener?.Close (); + _listener = null; + + _loop?.GetAwaiter ().GetResult (); + _loop = null; + } + + async Task Loop () + { + try { + while (true) { + var ctx = await _listener!.GetContextAsync (); + var authorization = ctx.Request.Headers.Get ("Authorization"); + var fakeResponse = Handle (authorization); + fakeResponse.ConfigureAndClose (ctx.Response); + } + } catch (ObjectDisposedException) { + // this exception is expected when the listener is closed + } catch (HttpListenerException) { + // shut down the listener + } + } + + const string ntlm = "NTLM"; + const string initiation = "NTLM TlRMTVNTUAABAAAAFYKIYgAAAAAAAAAAAAAAAAAAAAAGAbAdAAAADw=="; + const string challenge = "NTLM TlRMTVNTUAACAAAADAAMADgAAAAVgoliASNFZ4mrze8AAAAAAAAAADAAMABEAAAABgBwFwAAAA9EAG8AbQBhAGkAbgACAAwARABvAG0AYQBpAG4AAQAMAFMAZQByAHYAZQByAAcACADffWrlcGTYAQAAAAA="; + const string challengeResponsePrefix = "NTLM TlRMTVNTUAADAAAAGAAYAFgAAACcAJwAcAAAAAwADAAUAQAACAAIAAwBAAASABIAIAEAABAAEAAyAQAAFYKIYgYBsB0AAAAP"; + + // 1. the client makes an unauthenticated request + // -> the server responds to with the "WWW-Authenticate: NTLM" header + // 2. the client sends a request with the "Authorization: NTLM " header + // -> the server responds with the "WWW-Authenticate: NTLM " header + // 3. the client responds with the "Authorization: NTLM " header + // -> the server returns 200 + static FakeResponse Handle (string? authorization) + => authorization switch { + initiation => new (HttpStatusCode.Unauthorized, challenge, string.Empty), + string challengeResponse when challengeResponse.StartsWith (challengeResponsePrefix) => new (HttpStatusCode.OK, null, SecretContent), + _ => new (HttpStatusCode.Unauthorized, ntlm, string.Empty) + }; + + record FakeResponse (HttpStatusCode statusCode, string? header, string body) + { + public void ConfigureAndClose (HttpListenerResponse res) + { + res.StatusCode = (int)statusCode; + if (header != null) res.AddHeader ("WWW-Authenticate", header); + res.Close (Encoding.UTF8.GetBytes (body), false); + } + } + } + } +}