From b10fe0933974b0c8636bb75100f5c4853aee78f0 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 21 Jul 2021 17:35:13 -0400 Subject: [PATCH] Bump to dotnet/installer/release/6.0.1xx-preview7@808795cc 6.0.100-preview.7.21369.5 (#6097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/xamarin/java.interop/issues/854 Changes: https://github.com/dotnet/installer/compare/e8b3b6bea1e37086869ba9aeafe65caa298537e7...808795cca8cfaa143ef556cac0dc40d15d2dc186 % git diff --shortstat e8b3b6be...808795cc 102 files changed, 2218 insertions(+), 2674 deletions(-) Changes: https://github.com/mono/linker/compare/a07cab7b71a1321a9e68571c0b6095144a177b4e...9ecf5bd2809f93d98c1dbea640790ca5a88d35ec % git diff --shortstat a07cab7b...9ecf5bd2 81 files changed, 2122 insertions(+), 246 deletions(-) Changes: https://github.com/dotnet/runtime/compare/02f70d0b903422282cd7ba8037de6b66ea0b7a2d...8d3afa3a4a42021be024ffa4a1dee77e35d33911 % git diff --shortstat 02f70d0b...8d3afa3a 2518 files changed, 122843 insertions(+), 33676 deletions(-) Changes: https://github.com/xamarin/java.interop/compare/a5ed8919fb2ec894cb8144e51ae7c29b4811ee2a...4fb7c147f8c6eb9bf94d9bfb8305c7d2a7a9fb33 * xamarin/java.interop@4fb7c147: [build] set $(DisableImplicitNamespaceImports) by default (#859) * xamarin/java.interop@855ecfa3: [generator] Don't generate unexpected NRT types like `void?` (#856) * xamarin/java.interop@4a02bc32: Revert "[Xamarin.Android.Tools.Bytecode] hide nested types (#827)" (#855) * xamarin/java.interop@95c9b79d: [generator] Avoid 'error (…):' construct in diagnostic messages (#851) * xamarin/java.interop@7c4f7db0: [build] Bump to Mono with MSBuild 16.10 (#848) * xamarin/java.interop@0227cdae: [generator] Gracefully handle BindingGeneratorException. (#845) * xamarin/java.interop@ce1750fd: Add SECURITY.md (#846) Context: https://github.com/dotnet/runtime/pull/55384 Context: https://github.com/dotnet/sdk/pull/18639 Updates: * Microsoft.Dotnet.Sdk.Internal: from 6.0.100-preview.7.21327.2 to 6.0.100-preview.7.21369.5 * Microsoft.NET.ILLink.Tasks: from 6.0.100-preview.6.21322.1 to 6.0.100-preview.6.21365.1 * Microsoft.NETCore.App.Ref: from 6.0.0-preview.7.21326.8 to 6.0.0-preview.7.21368.2 dotnet/runtime#55384 broke how .NET 6 interacts with `AndroidClientHandler`. Fix this by introducing a new `Xamarin.Android.Net.AndroidMessageHandler` type for use on .NET 6, and update the .NET 6 `AndroidClientHandler` to delegate to `AndroidMessageHandler`. `AndroidMessageHandler` doesn't exist on Legacy. Update `.apkdesc` files: * `BuildReleaseArm64SimpleDotNet` is ~37KB smaller * `BuildReleaseArm64XFormsDotNet` is ~62KB larger. Update `tests/api-compatibility/api-compat-exclude-attributes.txt` so that `T:System.Runtime.CompilerServices.CompilerGeneratedAttribute` is ignored. `[CompilerGeneratedAttribute]` is emitted as part of C# 3 "auto-props": public T Property { get; set; } Converting this into a "real" property: T value; public T Property { get => value; set => this.value = value; } results in the `_CheckApiCompatibility` target complaining about an API break. We don't care; ignore `[CompilerGeneratedAttribute]`. Remove `$(SelfContained)` property: early on in .NET 5 (yes 5) development, the Xamarin.Android SDK needed to specify `$(SelfContained)` by default in order to produce `.apk` files. After we became a proper workload, setting the value became unnecessary. It also didn't actually do anything because dotnet/sdk overwrote the value. Starting in dotnet/sdk#18639, `Microsoft.NET.RuntimeIdentifierInference.targets` is now being imported *after* a workload, which meant that dotnet/sdk no longer overwrote our `$(SelfContained)` value, which broke things: error NETSDK1031: It is not supported to build or publish a self-contained application without specifying a RuntimeIdentifier. You must either specify a RuntimeIdentifier or set SelfContained to false. We can simply remove `$(SelfContained)` now, and rely on dotnet/sdk to set this value. Co-authored-by: Jonathan Peppers Co-authored-by: Steve Pfister --- eng/Version.Details.xml | 12 +- eng/Versions.props | 6 +- external/Java.Interop | 2 +- src/Mono.Android/Mono.Android.csproj | 8 +- .../AndroidClientHandler.Legacy.cs | 1011 ++++++++++++++++ .../AndroidClientHandler.cs | 794 +------------ .../AndroidMessageHandler.cs | 1030 +++++++++++++++++ ...soft.Android.Sdk.DefaultProperties.targets | 4 +- .../BuildReleaseArm64SimpleDotNet.apkdesc | 20 +- .../BuildReleaseArm64XFormsDotNet.apkdesc | 125 +- .../Tests/BundleToolTests.cs | 2 - .../AndroidClientHandlerTests.cs | 10 +- .../api-compat-exclude-attributes.txt | 1 + 13 files changed, 2196 insertions(+), 829 deletions(-) create mode 100644 src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs create mode 100644 src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7d167e55418..691057819c5 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,16 +1,16 @@ - + https://github.com/dotnet/installer - e8b3b6bea1e37086869ba9aeafe65caa298537e7 + 808795cca8cfaa143ef556cac0dc40d15d2dc186 - + https://github.com/mono/linker - a07cab7b71a1321a9e68571c0b6095144a177b4e + 9ecf5bd2809f93d98c1dbea640790ca5a88d35ec - + https://github.com/dotnet/runtime - 02f70d0b903422282cd7ba8037de6b66ea0b7a2d + 8d3afa3a4a42021be024ffa4a1dee77e35d33911 diff --git a/eng/Versions.props b/eng/Versions.props index 5045d899556..f5e9a75e0e0 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,11 +1,11 @@ - 6.0.100-preview.7.21327.2 - 6.0.100-preview.6.21322.1 + 6.0.100-preview.7.21369.5 + 6.0.100-preview.6.21365.1 5.0.0-beta.20181.7 6.0.0-beta.21212.6 - 6.0.0-preview.7.21326.8 + 6.0.0-preview.7.21368.2 diff --git a/external/Java.Interop b/external/Java.Interop index a5ed8919fb2..4fb7c147f8c 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit a5ed8919fb2ec894cb8144e51ae7c29b4811ee2a +Subproject commit 4fb7c147f8c6eb9bf94d9bfb8305c7d2a7a9fb33 diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 877a0665c65..e31a43a70a2 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -42,7 +42,7 @@ $(XAInstallPrefix)xbuild-frameworks\MonoAndroid\$(AndroidFrameworkVersion)\ - + $(XAInstallPrefix)xbuild-frameworks\Microsoft.Android\$(TargetFramework)\ @@ -348,7 +348,9 @@ - + + + @@ -377,7 +379,7 @@ - + diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs new file mode 100644 index 00000000000..3e1efe01910 --- /dev/null +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs @@ -0,0 +1,1011 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Android.OS; +using Android.Runtime; +using Java.IO; +using Java.Net; +using Java.Security; +using Java.Security.Cert; +using Javax.Net.Ssl; + +namespace Xamarin.Android.Net +{ + /// + /// A custom implementation of which internally uses + /// (or its HTTPS incarnation) to send HTTP requests. + /// + /// + /// Instance of this class is used to configure instance + /// in the following way: + /// + /// + /// var handler = new AndroidClientHandler { + /// UseCookies = true, + /// AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, + /// }; + /// + /// var httpClient = new HttpClient (handler); + /// var response = httpClient.GetAsync ("http://example.com")?.Result as AndroidHttpResponseMessage; + /// + /// + /// The class supports pre-authentication of requests albeit in a slightly "manual" way. Namely, whenever a request to a server requiring authentication + /// is made and no authentication credentials are provided in the property (which is usually the case on the first + /// request), the property will return true and the property will + /// contain all the authentication information gathered from the server. The application must then fill in the blanks (i.e. the credentials) and re-send + /// the request configured to perform pre-authentication. The reason for this manual process is that the underlying Java HTTP client API supports only a + /// single, VM-wide, authentication handler which cannot be configured to handle credentials for several requests. AndroidClientHandler, therefore, implements + /// the authentication in managed .NET code. Message handler supports both Basic and Digest authentication. If an authentication scheme that's not supported + /// by AndroidClientHandler is requested by the server, the application can provide its own authentication module (, + /// ) to handle the protocol authorization. + /// AndroidClientHandler also supports requests to servers with "invalid" (e.g. self-signed) SSL certificates. Since this process is a bit convoluted using + /// the Java APIs, AndroidClientHandler defines two ways to handle the situation. First, easier, is to store the necessary certificates (either CA or server certificates) + /// in the collection or, after deriving a custom class from AndroidClientHandler, by overriding one or more methods provided for this purpose + /// (, and ). The former method should be sufficient + /// for most use cases, the latter allows the application to provide fully customized key store, trust manager and key manager, if needed. Note that the instance of + /// AndroidClientHandler configured to accept an "invalid" certificate from the particular server will most likely fail to validate certificates from other servers (even + /// if they use a certificate with a fully validated trust chain) unless you store the CA certificates from your Android system in along with + /// the self-signed certificate(s). + /// + public class AndroidClientHandler : HttpClientHandler + { + sealed class RequestRedirectionState + { + public Uri? NewUrl; + public int RedirectCounter; + public HttpMethod? Method; + public bool MethodChanged; + } + + internal const string LOG_APP = "monodroid-net"; + + const string GZIP_ENCODING = "gzip"; + const string DEFLATE_ENCODING = "deflate"; + const string IDENTITY_ENCODING = "identity"; + + static readonly IDictionary headerSeparators = new Dictionary { + ["User-Agent"] = " ", + }; + + static readonly HashSet known_content_headers = new HashSet (StringComparer.OrdinalIgnoreCase) { + "Allow", + "Content-Disposition", + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-MD5", + "Content-Range", + "Content-Type", + "Expires", + "Last-Modified" + }; + + static readonly List authModules = new List { + new AuthModuleBasic (), + new AuthModuleDigest () + }; + + bool disposed; + + // Now all hail Java developers! Get this... HttpURLClient defaults to accepting AND + // uncompressing the gzip content encoding UNLESS you set the Accept-Encoding header to ANY + // value. So if we set it to 'gzip' below we WILL get gzipped stream but HttpURLClient will NOT + // uncompress it any longer, doh. And they don't support 'deflate' so we need to handle it ourselves. + bool decompress_here; + + /// + /// + /// Gets or sets the pre authentication data for the request. This property must be set by the application + /// before the request is made. Generally the value can be taken from + /// after the initial request, without any authentication data, receives the authorization request from the + /// server. The application must then store credentials in instance of and + /// assign the instance to this propery before retrying the request. + /// + /// + /// The property is never set by AndroidClientHandler. + /// + /// + /// The pre authentication data. + public AuthenticationData? PreAuthenticationData { get; set; } + + /// + /// If the website requires authentication, this property will contain data about each scheme supported + /// by the server after the response. Note that unauthorized request will return a valid response - you + /// need to check the status code and and (re)configure AndroidClientHandler instance accordingly by providing + /// both the credentials and the authentication scheme by setting the + /// property. If AndroidClientHandler is not able to detect the kind of authentication scheme it will store an + /// instance of with its property + /// set to AuthenticationScheme.Unsupported and the application will be responsible for providing an + /// instance of which handles this kind of authorization scheme + /// ( + /// + public IList ? RequestedAuthentication { get; private set; } + + /// + /// Server authentication response indicates that the request to authorize comes from a proxy if this property is true. + /// All the instances of stored in the property will + /// have their preset to the same value as this property. + /// + public bool ProxyAuthenticationRequested { get; private set; } + + /// + /// If true then the server requested authorization and the application must use information + /// found in to set the value of + /// + public bool RequestNeedsAuthorization { + get { return RequestedAuthentication?.Count > 0; } + } + + /// + /// + /// If the request is to the server protected with a self-signed (or otherwise untrusted) SSL certificate, the request will + /// fail security chain verification unless the application provides either the CA certificate of the entity which issued the + /// server's certificate or, alternatively, provides the server public key. Whichever the case, the certificate(s) must be stored + /// in this property in order for AndroidClientHandler to configure the request to accept the server certificate. + /// AndroidClientHandler uses a custom and to configure the connection. + /// If, however, the application requires finer control over the SSL configuration (e.g. it implements its own TrustManager) then + /// it should leave this property empty and instead derive a custom class from AndroidClientHandler and override, as needed, the + /// , and methods + /// instead + /// + /// The trusted certs. + public IList ? TrustedCerts { get; set; } + + /// + /// + /// Specifies the connection read timeout. + /// + /// + /// Since there's no way for the handler to access + /// directly, this property should be set by the calling party to the same desired value. Value of this + /// property will be passed to the native Java HTTP client, unless it is set to + /// + /// + /// The default value is 24 hours, much higher than the documented value of and the same as the value of iOS-specific + /// NSUrlSessionHandler. + /// + /// + public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24); + + /// + /// + /// Specifies the connect timeout + /// + /// + /// The native Java client supports two separate timeouts - one for reading from the connection () and another for establishing the connection. This property sets the value of + /// the latter timeout, unless it is set to in which case the + /// native Java client defaults are used. + /// + /// + /// The default value is 120 seconds. + /// + /// + public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromHours (24); + + protected override void Dispose (bool disposing) + { + disposed = true; + + base.Dispose (disposing); + } + + protected void AssertSelf () + { + if (!disposed) + return; + throw new ObjectDisposedException (nameof (AndroidClientHandler)); + } + + string EncodeUrl (Uri url) + { + if (url == null) + return String.Empty; + + // UriBuilder takes care of encoding everything properly + var bldr = new UriBuilder (url); + if (url.IsDefaultPort) + bldr.Port = -1; // Avoids adding :80 or :443 to the host name in the result + + // bldr.Uri.ToString () would ruin the good job UriBuilder did + return bldr.ToString (); + } + + /// + /// Returns a custom host name verifier for a HTTPS connection. By default it returns null and + /// thus the connection uses whatever host name verification mechanism the operating system defaults to. + /// Override in your class to define custom host name verification behavior. The overriding class should + /// not set the property directly on the passed + /// + /// + /// Instance of IHostnameVerifier to be used for this HTTPS connection + /// HTTPS connection object. + protected virtual IHostnameVerifier? GetSSLHostnameVerifier (HttpsURLConnection connection) + { + return null; + } + + /// + /// Creates, configures and processes an asynchronous request to the indicated resource. + /// + /// Task in which the request is executed + /// Request provided by + /// Cancellation token. + protected override async Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) + { + AssertSelf (); + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (!request.RequestUri.IsAbsoluteUri) + throw new ArgumentException ("Must represent an absolute URI", "request"); + + var redirectState = new RequestRedirectionState { + NewUrl = request.RequestUri, + RedirectCounter = 0, + Method = request.Method + }; + while (true) { + URL java_url = new URL (EncodeUrl (redirectState.NewUrl)); + URLConnection? java_connection; + if (UseProxy) { + var javaProxy = await GetJavaProxy (redirectState.NewUrl, cancellationToken).ConfigureAwait (continueOnCapturedContext: false); + // When you use the parameter Java.Net.Proxy.NoProxy the system proxy is overriden. Leave the parameter out to respect the default settings. + java_connection = javaProxy == Java.Net.Proxy.NoProxy ? java_url.OpenConnection () : java_url.OpenConnection (javaProxy); + } else { + // In this case the consumer of this class has explicitly chosen to not use a proxy, so bypass the default proxy. The default value of UseProxy is true. + java_connection = java_url.OpenConnection (Java.Net.Proxy.NoProxy); + } + + var httpsConnection = java_connection as HttpsURLConnection; + if (httpsConnection != null) { + IHostnameVerifier? hnv = GetSSLHostnameVerifier (httpsConnection); + if (hnv != null) + httpsConnection.HostnameVerifier = hnv; + } + + if (ConnectTimeout != TimeSpan.Zero) + java_connection!.ConnectTimeout = checked ((int)ConnectTimeout.TotalMilliseconds); + + if (ReadTimeout != TimeSpan.Zero) + java_connection!.ReadTimeout = checked ((int)ReadTimeout.TotalMilliseconds); + + try { + HttpURLConnection httpConnection = await SetupRequestInternal (request, java_connection!).ConfigureAwait (continueOnCapturedContext: false); + HttpResponseMessage? response = await ProcessRequest (request, java_url, httpConnection, cancellationToken, redirectState).ConfigureAwait (continueOnCapturedContext: false); + if (response != null) + return response; + + if (redirectState.NewUrl == null) + throw new InvalidOperationException ("Request redirected but no new URI specified"); + request.Method = redirectState.Method; + } catch (Java.Net.SocketTimeoutException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.Timeout, null); + } catch (Java.Net.UnknownServiceException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); + } catch (Java.Lang.SecurityException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.SecureChannelFailure, null); + } catch (Java.IO.IOException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.UnknownError, null); + } + } + } + + protected virtual async Task GetJavaProxy (Uri destination, CancellationToken cancellationToken) + { + var proxy = Java.Net.Proxy.NoProxy; + + if (destination == null || Proxy == null) { + return proxy; + } + + Uri puri = Proxy.GetProxy (destination); + if (puri == null) { + return proxy; + } + + proxy = await Task .Run (() => { + // Let the Java code resolve the address, if necessary + var addr = new Java.Net.InetSocketAddress (puri.Host, puri.Port); + return new Java.Net.Proxy (Java.Net.Proxy.Type.Http, addr); + }, cancellationToken); + + return proxy; + } + + Task ProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) + { + cancellationToken.ThrowIfCancellationRequested (); + httpConnection.InstanceFollowRedirects = false; // We handle it ourselves + RequestedAuthentication = null; + ProxyAuthenticationRequested = false; + + return DoProcessRequest (request, javaUrl, httpConnection, cancellationToken, redirectState); + } + + Task DisconnectAsync (HttpURLConnection httpConnection) + { + return Task.Run (() => httpConnection?.Disconnect ()); + } + + Task ConnectAsync (HttpURLConnection httpConnection, CancellationToken ct) + { + return Task.Run (() => { + try { + using (ct.Register(() => DisconnectAsync(httpConnection).ContinueWith(t => { + if (t.Exception != null) Logger.Log(LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}"); + }, TaskScheduler.Default))) + httpConnection?.Connect (); + } catch (Exception ex) { + if (ct.IsCancellationRequested) { + Logger.Log (LogLevel.Info, LOG_APP, $"Exception caught while cancelling connection: {ex}"); + ct.ThrowIfCancellationRequested (); + } + throw; + } + }, ct); + } + + protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage request, HttpURLConnection httpConnection, CancellationToken cancellationToken) + { + using (var stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false)) { + await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); + + // + // Rewind the stream to beginning in case the HttpContent implementation + // will be accessed again (e.g. after redirect) and it keeps its stream + // open behind the scenes instead of recreating it on the next call to + // ReadAsStreamAsync. If we don't rewind it, the ReadAsStreamAsync + // call above will throw an exception as we'd be attempting to read an + // already "closed" stream (that is one whose Position is set to its + // end). + // + // This is not a perfect solution since the HttpContent may do weird + // things in its implementation, but it's better than copying the + // content into a buffer since we have no way of knowing how the data is + // read or generated and also we don't want to keep potentially large + // amounts of data in memory (which would happen if we read the content + // into a byte[] buffer and kept it cached for re-use on redirect). + // + // See https://bugzilla.xamarin.com/show_bug.cgi?id=55477 + // + if (stream.CanSeek) + stream.Seek (0, SeekOrigin.Begin); + } + } + + async Task DoProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) + { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"{this}.DoProcessRequest ()"); + + if (cancellationToken.IsCancellationRequested) { + if(Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, " cancelled"); + + cancellationToken.ThrowIfCancellationRequested (); + } + + try { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $" connecting"); + + await ConnectAsync (httpConnection, cancellationToken).ConfigureAwait (continueOnCapturedContext: false); + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $" connected"); + } catch (Java.Net.ConnectException ex) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Connection exception {ex}"); + // Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler + throw new WebException (ex.Message, ex, WebExceptionStatus.ConnectFailure, null); + } + + if (cancellationToken.IsCancellationRequested) { + if(Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, " cancelled"); + + await DisconnectAsync (httpConnection).ConfigureAwait (continueOnCapturedContext: false); + cancellationToken.ThrowIfCancellationRequested (); + } + + CancellationTokenRegistration cancelRegistration = default (CancellationTokenRegistration); + HttpStatusCode statusCode = HttpStatusCode.OK; + Uri? connectionUri = null; + + try { + cancelRegistration = cancellationToken.Register (() => { + DisconnectAsync (httpConnection).ContinueWith (t => { + if (t.Exception != null) + Logger.Log (LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}"); + }, TaskScheduler.Default); + }, useSynchronizationContext: false); + + if (httpConnection.DoOutput) + await WriteRequestContentToOutput (request, httpConnection, cancellationToken); + + statusCode = await Task.Run (() => (HttpStatusCode)httpConnection.ResponseCode, cancellationToken).ConfigureAwait (false); + connectionUri = new Uri (httpConnection.URL?.ToString ()!); + } finally { + cancelRegistration.Dispose (); + } + + if (cancellationToken.IsCancellationRequested) { + await DisconnectAsync (httpConnection).ConfigureAwait (continueOnCapturedContext: false); + cancellationToken.ThrowIfCancellationRequested(); + } + + // If the request was redirected we need to put the new URL in the request + request.RequestUri = connectionUri; + var ret = new AndroidHttpResponseMessage (javaUrl, httpConnection) { + RequestMessage = request, + ReasonPhrase = httpConnection.ResponseMessage, + StatusCode = statusCode, + }; + + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Status code: {statusCode}"); + + if (!IsErrorStatusCode (statusCode)) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Reading..."); + ret.Content = GetContent (httpConnection, httpConnection.InputStream!); + } else { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Status code is {statusCode}, reading..."); + // For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream. + // Instead we try to read the error stream and return an empty string if the error stream isn't readable. + ret.Content = GetErrorContent (httpConnection, new StringContent (String.Empty, Encoding.ASCII)); + } + + bool disposeRet; + if (HandleRedirect (statusCode, httpConnection, redirectState, out disposeRet)) { + if (redirectState.MethodChanged) { + // If a redirect uses GET but the original request used POST with content, then the redirected + // request will fail with an exception. + // There's also no way to send content using GET (except in the URL, of course), so discarding + // request.Content is what we should do. + // + // See https://github.com/xamarin/xamarin-android/issues/1282 + if (redirectState.Method == HttpMethod.Get) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Discarding content on redirect"); + request.Content = null; + } + } + + if (disposeRet) { + ret.Dispose (); + ret = null!; + } else { + CopyHeaders (httpConnection, ret); + ParseCookies (ret, connectionUri); + } + + // We don't want to pass the authorization header onto the next location + request.Headers.Authorization = null; + + return ret; + } + + switch (statusCode) { + case HttpStatusCode.Unauthorized: + case HttpStatusCode.ProxyAuthenticationRequired: + // We don't resend the request since that would require new set of credentials if the + // ones provided in Credentials are invalid (or null) and that, in turn, may require asking the + // user which is not something that should be taken care of by us and in this + // context. The application should be responsible for this. + // HttpClientHandler throws an exception in this instance, but I think it's not a good + // idea. We'll return the response message with all the information required by the + // application to fill in the blanks and provide the requested credentials instead. + // + // We return the body of the response too, but the Java client will throw + // a FileNotFound exception if we attempt to access the input stream. + // Instead we try to read the error stream and return an default message if the error stream isn't readable. + ret.Content = GetErrorContent (httpConnection, new StringContent ("Unauthorized", Encoding.ASCII)); + CopyHeaders (httpConnection, ret); + + if (ret.Headers.WwwAuthenticate != null) { + ProxyAuthenticationRequested = false; + CollectAuthInfo (ret.Headers.WwwAuthenticate); + } else if (ret.Headers.ProxyAuthenticate != null) { + ProxyAuthenticationRequested = true; + CollectAuthInfo (ret.Headers.ProxyAuthenticate); + } + + ret.RequestedAuthentication = RequestedAuthentication; + return ret; + } + + CopyHeaders (httpConnection, ret); + ParseCookies (ret, connectionUri); + + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Returning"); + return ret; + } + + HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent) + { + var contentStream = httpConnection.ErrorStream; + + if (contentStream != null) { + return GetContent (httpConnection, contentStream); + } + + return fallbackContent; + } + + HttpContent GetContent (URLConnection httpConnection, Stream contentStream) + { + Stream inputStream = new BufferedStream (contentStream); + if (decompress_here) { + var encodings = httpConnection.ContentEncoding?.Split (','); + if (encodings != null) { + if (encodings.Contains (GZIP_ENCODING, StringComparer.OrdinalIgnoreCase)) + inputStream = new GZipStream (inputStream, CompressionMode.Decompress); + else if (encodings.Contains (DEFLATE_ENCODING, StringComparer.OrdinalIgnoreCase)) + inputStream = new DeflateStream (inputStream, CompressionMode.Decompress); + } + } + return new StreamContent (inputStream); + } + + bool HandleRedirect (HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet) + { + if (!AllowAutoRedirect) { + disposeRet = false; + return true; // We shouldn't follow and there's no data to fetch, just return + } + disposeRet = true; + + redirectState.NewUrl = null; + redirectState.MethodChanged = false; + switch (redirectCode) { + case HttpStatusCode.MultipleChoices: // 300 + break; + + case HttpStatusCode.Moved: // 301 + case HttpStatusCode.Redirect: // 302 + case HttpStatusCode.SeeOther: // 303 + redirectState.MethodChanged = redirectState.Method != HttpMethod.Get; + redirectState.Method = HttpMethod.Get; + break; + + case HttpStatusCode.NotModified: // 304 + disposeRet = false; + return true; // Not much happening here, just return and let the client decide + // what to do with the response + + case HttpStatusCode.TemporaryRedirect: // 307 + break; + + default: + if ((int)redirectCode >= 300 && (int)redirectCode < 400) + throw new InvalidOperationException ($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported"); + return false; + } + + var headers = httpConnection.HeaderFields; + IList ? locationHeader = null; + string? location = null; + + if (headers?.TryGetValue ("Location", out locationHeader) == true && locationHeader != null && locationHeader.Count > 0) { + if (locationHeader.Count == 1) { + location = locationHeader [0]?.Trim (); + } else { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"More than one location header for HTTP {redirectCode} redirect. Will use the first non-empty one."); + + foreach (string l in locationHeader) { + location = l?.Trim (); + if (!String.IsNullOrEmpty (location)) + break; + } + } + } + + if (String.IsNullOrEmpty (location)) { + // As per https://tools.ietf.org/html/rfc7231#section-6.4.1 the reponse isn't required to contain the Location header and the + // client should act accordingly. Since it is not documented what the action in this case should be, we're following what + // Xamarin.iOS does and simply return the content of the request as if it wasn't a redirect. + // It is not clear what to do if there is a Location header but its value is empty, so + // we assume the same action here. + disposeRet = false; + return true; + } + + redirectState.RedirectCounter++; + if (redirectState.RedirectCounter >= MaxAutomaticRedirections) + throw new WebException ($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)"); + + Uri redirectUrl; + try { + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Raw redirect location: {location}"); + + var baseUrl = new Uri (httpConnection.URL?.ToString ()!); + if (location? [0] == '/') { + // Shortcut for the '/' and '//' cases, simplifies logic since URI won't treat + // such URLs as relative and we'd have to work around it in the `else` block + // below. + redirectUrl = new Uri (baseUrl, location); + } else { + // Special case (from https://tools.ietf.org/html/rfc3986#section-5.4.1) not + // handled by the Uri class: scheme:host + // + // This is a valid URI (should be treated as `scheme://host`) but URI throws an + // exception about DOS path being malformed IF the part before colon is just one + // character long... We could replace the scheme with the original request's one, but + // that would NOT be the right thing to do since it is not what the redirecting server + // meant. The fix doesn't belong here, but rather in the Uri class. So we'll throw... + + redirectUrl = new Uri (location!, UriKind.RelativeOrAbsolute); + if (!redirectUrl.IsAbsoluteUri) + redirectUrl = new Uri (baseUrl, location); + } + + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Cooked redirect location: {redirectUrl}"); + } catch (Exception ex) { + throw new WebException ($"Invalid redirect URI received: {location}", ex); + } + + UriBuilder? builder = null; + if (!String.IsNullOrEmpty (httpConnection.URL?.Ref) && String.IsNullOrEmpty (redirectUrl.Fragment)) { + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Appending fragment '{httpConnection.URL?.Ref}' to redirect URL '{redirectUrl}'"); + + builder = new UriBuilder (redirectUrl) { + Fragment = httpConnection.URL?.Ref + }; + } + + redirectState.NewUrl = builder == null ? redirectUrl : builder.Uri; + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Request redirected to {redirectState.NewUrl}"); + + return true; + } + + bool IsErrorStatusCode (HttpStatusCode statusCode) + { + return (int)statusCode >= 400 && (int)statusCode <= 599; + } + + void CollectAuthInfo (HttpHeaderValueCollection headers) + { + var authData = new List (headers.Count); + + foreach (AuthenticationHeaderValue ahv in headers) { + var data = new AuthenticationData { + Scheme = GetAuthScheme (ahv.Scheme), + Challenge = $"{ahv.Scheme} {ahv.Parameter}", + UseProxyAuthentication = ProxyAuthenticationRequested + }; + authData.Add (data); + } + + RequestedAuthentication = authData.AsReadOnly (); + } + + AuthenticationScheme GetAuthScheme (string scheme) + { + if (String.Compare ("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0) + return AuthenticationScheme.Basic; + if (String.Compare ("digest", scheme, StringComparison.OrdinalIgnoreCase) == 0) + return AuthenticationScheme.Digest; + + return AuthenticationScheme.Unsupported; + } + + void ParseCookies (AndroidHttpResponseMessage ret, Uri connectionUri) + { + IEnumerable cookieHeaderValue; + if (!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues ("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"No cookies"); + return; + } + + try { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Parsing cookies"); + CookieContainer.SetCookies (connectionUri, String.Join (",", cookieHeaderValue)); + } catch (Exception ex) { + // We don't want to terminate the response because of a bad cookie, hence just reporting + // the issue. We might consider adding a virtual method to let the user handle the + // issue, but not sure if it's really needed. Set-Cookie header will be part of the + // header collection so the user can always examine it if they spot an error. + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Failed to parse cookies in the server response. {ex.GetType ()}: {ex.Message}"); + } + } + + void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response) + { + var headers = httpConnection.HeaderFields; + foreach (var key in headers!.Keys) { + if (key == null) // First header entry has null key, it corresponds to the response message + continue; + + HttpHeaders item_headers; + + if (known_content_headers.Contains (key)) { + item_headers = response.Content.Headers; + } else { + item_headers = response.Headers; + } + item_headers.TryAddWithoutValidation (key, headers [key]); + } + } + + /// + /// Configure the before the request is sent. This method is meant to be overriden + /// by applications which need to perform some extra configuration steps on the connection. It is called with all + /// the request headers set, pre-authentication performed (if applicable) but before the request body is set + /// (e.g. for POST requests). The default implementation in AndroidClientHandler does nothing. + /// + /// Request data + /// Pre-configured connection instance + protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn) + { + Action a = AssertSelf; + return Task.Run (a); + } + + /// + /// Configures the key store. The parameter is set to instance of + /// created using the type and with populated with certificates provided in the + /// property. AndroidClientHandler implementation simply returns the instance passed in the parameter + /// + /// The key store. + /// Key store to configure. + protected virtual KeyStore? ConfigureKeyStore (KeyStore? keyStore) + { + AssertSelf (); + + return keyStore; + } + + /// + /// Create and configure an instance of . The parameter is set to the + /// return value of the method, so it might be null if the application overrode the method and provided + /// no key store. It will not be null when the default implementation is used. The application can return null here since + /// KeyManagerFactory is not required for the custom SSL configuration, but it might be used by the application to implement a more advanced + /// mechanism of key management. + /// + /// The key manager factory or null. + /// Key store. + protected virtual KeyManagerFactory? ConfigureKeyManagerFactory (KeyStore? keyStore) + { + AssertSelf (); + + return null; + } + + /// + /// Create and configure an instance of . The parameter is set to the + /// return value of the method, so it might be null if the application overrode the method and provided + /// no key store. It will not be null when the default implementation is used. The application can return null from this + /// method in which case AndroidClientHandler will create its own instance of the trust manager factory provided that the + /// list contains at least one valid certificate. If there are no valid certificates and this method returns null, no custom + /// trust manager will be created since that would make all the HTTPS requests fail. + /// + /// The trust manager factory. + /// Key store. + protected virtual TrustManagerFactory? ConfigureTrustManagerFactory (KeyStore? keyStore) + { + AssertSelf (); + + return null; + } + + void AppendEncoding (string encoding, ref List ? list) + { + if (list == null) + list = new List (); + if (list.Contains (encoding)) + return; + list.Add (encoding); + } + + async Task SetupRequestInternal (HttpRequestMessage request, URLConnection conn) + { + if (conn == null) + throw new ArgumentNullException (nameof (conn)); + var httpConnection = conn.JavaCast (); + if (httpConnection == null) + throw new InvalidOperationException ($"Unsupported URL scheme {conn.URL?.Protocol}"); + + try { + httpConnection.RequestMethod = request.Method.ToString (); + } catch (Java.Net.ProtocolException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); + } + + // SSL context must be set up as soon as possible, before adding any content or + // headers. Otherwise Java won't use the socket factory + SetupSSL (httpConnection as HttpsURLConnection); + if (request.Content != null) + AddHeaders (httpConnection, request.Content.Headers); + AddHeaders (httpConnection, request.Headers); + + List ? accept_encoding = null; + + decompress_here = false; + if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) { + AppendEncoding (GZIP_ENCODING, ref accept_encoding); + decompress_here = true; + } + + if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) { + AppendEncoding (DEFLATE_ENCODING, ref accept_encoding); + decompress_here = true; + } + + if (AutomaticDecompression == DecompressionMethods.None) { + accept_encoding?.Clear (); + AppendEncoding (IDENTITY_ENCODING, ref accept_encoding); // Turns off compression for the Java client + } + + if (accept_encoding?.Count > 0) + httpConnection.SetRequestProperty ("Accept-Encoding", String.Join (",", accept_encoding)); + + if (UseCookies && CookieContainer != null) { + string cookieHeaderValue = CookieContainer.GetCookieHeader (request.RequestUri); + if (!String.IsNullOrEmpty (cookieHeaderValue)) + httpConnection.SetRequestProperty ("Cookie", cookieHeaderValue); + } + + HandlePreAuthentication (httpConnection); + await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);; + SetupRequestBody (httpConnection, request); + + return httpConnection; + } + + /// + /// Configure and return a custom for the passed HTTPS . If the class overriding the method returns anything but the default + /// null, the SSL setup code will not call the nor the + /// methods used to configure a custom trust manager which is + /// then used to create a default socket factory. + /// Deriving class must perform all the key manager and trust manager configuration to ensure proper + /// operation of the returned socket factory. + /// + /// Instance of SSLSocketFactory ready to use with the HTTPS connection. + /// HTTPS connection to return socket factory for + protected virtual SSLSocketFactory? ConfigureCustomSSLSocketFactory (HttpsURLConnection connection) + { + return null; + } + + void SetupSSL (HttpsURLConnection? httpsConnection) + { + if (httpsConnection == null) + return; + + var socketFactory = ConfigureCustomSSLSocketFactory (httpsConnection); + if (socketFactory != null) { + httpsConnection.SSLSocketFactory = socketFactory; + return; + } + + // Context: https://github.com/xamarin/xamarin-android/issues/1615 + int apiLevel = (int)Build.VERSION.SdkInt; + if (apiLevel >= 16 && apiLevel <= 20) { + httpsConnection.SSLSocketFactory = new OldAndroidSSLSocketFactory (); + return; + } + + var keyStore = KeyStore.GetInstance (KeyStore.DefaultType); + keyStore?.Load (null, null); + bool gotCerts = TrustedCerts?.Count > 0; + if (gotCerts) { + for (int i = 0; i < TrustedCerts!.Count; i++) { + Certificate cert = TrustedCerts [i]; + if (cert == null) + continue; + keyStore?.SetCertificateEntry ($"ca{i}", cert); + } + } + keyStore = ConfigureKeyStore (keyStore); + var kmf = ConfigureKeyManagerFactory (keyStore); + var tmf = ConfigureTrustManagerFactory (keyStore); + + if (tmf == null) { + // If there are no certs and no trust manager factory, we can't use a custom manager + // because it will cause all the HTTPS requests to fail because of unverified trust + // chain + if (!gotCerts) + return; + + tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); + tmf?.Init (keyStore); + } + + var context = SSLContext.GetInstance ("TLS"); + context?.Init (kmf?.GetKeyManagers (), tmf?.GetTrustManagers (), null); + httpsConnection.SSLSocketFactory = context?.SocketFactory; + } + + void HandlePreAuthentication (HttpURLConnection httpConnection) + { + var data = PreAuthenticationData; + if (!PreAuthenticate || data == null) + return; + + var creds = data.UseProxyAuthentication ? Proxy?.Credentials : Credentials; + if (creds == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Authentication using scheme {data.Scheme} requested but no credentials found. No authentication will be performed"); + return; + } + + var auth = data.Scheme == AuthenticationScheme.Unsupported ? data.AuthModule : authModules.Find (m => m?.Scheme == data.Scheme); + if (auth == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Authentication module for scheme '{data.Scheme}' not found. No authentication will be performed"); + return; + } + + Authorization authorization = auth.Authenticate (data.Challenge!, httpConnection, creds); + if (authorization == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Authorization module {auth.GetType ()} for scheme {data.Scheme} returned no authorization"); + return; + } + + if (Logger.LogNet) { + var header = data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization"; + Logger.Log (LogLevel.Info, LOG_APP, $"Authentication header '{header}' will be set to '{authorization.Message}'"); + } + httpConnection.SetRequestProperty (data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization", authorization.Message); + } + + static string GetHeaderSeparator (string name) => headerSeparators.TryGetValue (name, out var value) ? value : ","; + + void AddHeaders (HttpURLConnection conn, HttpHeaders headers) + { + if (headers == null) + return; + + foreach (KeyValuePair> header in headers) { + conn.SetRequestProperty (header.Key, header.Value != null ? String.Join (GetHeaderSeparator (header.Key), header.Value) : String.Empty); + } + } + + void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage request) + { + if (request.Content == null) { + // Pilfered from System.Net.Http.HttpClientHandler:SendAync + if (HttpMethod.Post.Equals (request.Method) || HttpMethod.Put.Equals (request.Method) || HttpMethod.Delete.Equals (request.Method)) { + // Explicitly set this to make sure we're sending a "Content-Length: 0" header. + // This fixes the issue that's been reported on the forums: + // http://forums.xamarin.com/discussion/17770/length-required-error-in-http-post-since-latest-release + httpConnection.SetRequestProperty ("Content-Length", "0"); + } + return; + } + + httpConnection.DoOutput = true; + long? contentLength = request.Content.Headers.ContentLength; + if (contentLength != null) + httpConnection.SetFixedLengthStreamingMode ((int)contentLength); + else + httpConnection.SetChunkedStreamingMode (0); + } + } +} diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs index 9271e7eda2d..e616cbfb18f 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -58,50 +60,15 @@ namespace Xamarin.Android.Net /// public class AndroidClientHandler : HttpClientHandler { - sealed class RequestRedirectionState - { - public Uri? NewUrl; - public int RedirectCounter; - public HttpMethod? Method; - public bool MethodChanged; - } - internal const string LOG_APP = "monodroid-net"; - - const string GZIP_ENCODING = "gzip"; - const string DEFLATE_ENCODING = "deflate"; - const string IDENTITY_ENCODING = "identity"; - - static readonly IDictionary headerSeparators = new Dictionary { - ["User-Agent"] = " ", - }; - - static readonly HashSet known_content_headers = new HashSet (StringComparer.OrdinalIgnoreCase) { - "Allow", - "Content-Disposition", - "Content-Encoding", - "Content-Language", - "Content-Length", - "Content-Location", - "Content-MD5", - "Content-Range", - "Content-Type", - "Expires", - "Last-Modified" - }; - - static readonly List authModules = new List { - new AuthModuleBasic (), - new AuthModuleDigest () - }; + AndroidMessageHandler _underlyingHander; bool disposed; - // Now all hail Java developers! Get this... HttpURLClient defaults to accepting AND - // uncompressing the gzip content encoding UNLESS you set the Accept-Encoding header to ANY - // value. So if we set it to 'gzip' below we WILL get gzipped stream but HttpURLClient will NOT - // uncompress it any longer, doh. And they don't support 'deflate' so we need to handle it ourselves. - bool decompress_here; + public AndroidClientHandler () + { + _underlyingHander = GetUnderlyingHandler () as AndroidMessageHandler ?? throw new InvalidOperationException ("Unknown underlying handler. Only AndroidMessageHandler is supported for AndroidClientHandler"); + } /// /// @@ -116,7 +83,11 @@ sealed class RequestRedirectionState /// /// /// The pre authentication data. - public AuthenticationData? PreAuthenticationData { get; set; } + public AuthenticationData? PreAuthenticationData + { + get { return _underlyingHander.PreAuthenticationData; } + set { _underlyingHander.PreAuthenticationData = value; } + } /// /// If the website requires authentication, this property will contain data about each scheme supported @@ -129,21 +100,28 @@ sealed class RequestRedirectionState /// instance of which handles this kind of authorization scheme /// ( /// - public IList ? RequestedAuthentication { get; private set; } + public IList ? RequestedAuthentication + { + get { return _underlyingHander.RequestedAuthentication; } + } /// /// Server authentication response indicates that the request to authorize comes from a proxy if this property is true. /// All the instances of stored in the property will /// have their preset to the same value as this property. /// - public bool ProxyAuthenticationRequested { get; private set; } + public bool ProxyAuthenticationRequested + { + get { return _underlyingHander.ProxyAuthenticationRequested; } + } /// /// If true then the server requested authorization and the application must use information /// found in to set the value of /// - public bool RequestNeedsAuthorization { - get { return RequestedAuthentication?.Count > 0; } + public bool RequestNeedsAuthorization + { + get { return _underlyingHander.RequestNeedsAuthorization; } } /// @@ -159,7 +137,11 @@ public bool RequestNeedsAuthorization { /// instead /// /// The trusted certs. - public IList ? TrustedCerts { get; set; } + public IList ? TrustedCerts + { + get { return _underlyingHander.TrustedCerts; } + set { _underlyingHander.TrustedCerts = value; } + } /// /// @@ -177,7 +159,11 @@ public bool RequestNeedsAuthorization { /// NSUrlSessionHandler. /// /// - public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24); + public TimeSpan ReadTimeout + { + get { return _underlyingHander.ReadTimeout; } + set { _underlyingHander.ReadTimeout = value; } + } /// /// @@ -193,7 +179,11 @@ public bool RequestNeedsAuthorization { /// The default value is 120 seconds. /// /// - public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromHours (24); + public TimeSpan ConnectTimeout + { + get { return _underlyingHander.ConnectTimeout; } + set { _underlyingHander.ConnectTimeout = value; } + } protected override void Dispose (bool disposing) { @@ -209,20 +199,6 @@ protected void AssertSelf () throw new ObjectDisposedException (nameof (AndroidClientHandler)); } - string EncodeUrl (Uri url) - { - if (url == null) - return String.Empty; - - // UriBuilder takes care of encoding everything properly - var bldr = new UriBuilder (url); - if (url.IsDefaultPort) - bldr.Port = -1; // Avoids adding :80 or :443 to the host name in the result - - // bldr.Uri.ToString () would ruin the good job UriBuilder did - return bldr.ToString (); - } - /// /// Returns a custom host name verifier for a HTTPS connection. By default it returns null and /// thus the connection uses whatever host name verification mechanism the operating system defaults to. @@ -246,510 +222,17 @@ string EncodeUrl (Uri url) protected override async Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) { AssertSelf (); - if (request == null) - throw new ArgumentNullException (nameof (request)); - - if (!request.RequestUri.IsAbsoluteUri) - throw new ArgumentException ("Must represent an absolute URI", "request"); - - var redirectState = new RequestRedirectionState { - NewUrl = request.RequestUri, - RedirectCounter = 0, - Method = request.Method - }; - while (true) { - URL java_url = new URL (EncodeUrl (redirectState.NewUrl)); - URLConnection? java_connection; - if (UseProxy) { - var javaProxy = await GetJavaProxy (redirectState.NewUrl, cancellationToken).ConfigureAwait (continueOnCapturedContext: false); - // When you use the parameter Java.Net.Proxy.NoProxy the system proxy is overriden. Leave the parameter out to respect the default settings. - java_connection = javaProxy == Java.Net.Proxy.NoProxy ? java_url.OpenConnection () : java_url.OpenConnection (javaProxy); - } else { - // In this case the consumer of this class has explicitly chosen to not use a proxy, so bypass the default proxy. The default value of UseProxy is true. - java_connection = java_url.OpenConnection (Java.Net.Proxy.NoProxy); - } - - var httpsConnection = java_connection as HttpsURLConnection; - if (httpsConnection != null) { - IHostnameVerifier? hnv = GetSSLHostnameVerifier (httpsConnection); - if (hnv != null) - httpsConnection.HostnameVerifier = hnv; - } - - if (ConnectTimeout != TimeSpan.Zero) - java_connection!.ConnectTimeout = checked ((int)ConnectTimeout.TotalMilliseconds); - - if (ReadTimeout != TimeSpan.Zero) - java_connection!.ReadTimeout = checked ((int)ReadTimeout.TotalMilliseconds); - - try { - HttpURLConnection httpConnection = await SetupRequestInternal (request, java_connection!).ConfigureAwait (continueOnCapturedContext: false); - HttpResponseMessage? response = await ProcessRequest (request, java_url, httpConnection, cancellationToken, redirectState).ConfigureAwait (continueOnCapturedContext: false); - if (response != null) - return response; - - if (redirectState.NewUrl == null) - throw new InvalidOperationException ("Request redirected but no new URI specified"); - request.Method = redirectState.Method; - } catch (Java.Net.SocketTimeoutException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new WebException (ex.Message, ex, WebExceptionStatus.Timeout, null); - } catch (Java.Net.UnknownServiceException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); - } catch (Java.Lang.SecurityException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new WebException (ex.Message, ex, WebExceptionStatus.SecureChannelFailure, null); - } catch (Java.IO.IOException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new WebException (ex.Message, ex, WebExceptionStatus.UnknownError, null); - } - } + return await base.SendAsync (request, cancellationToken); } protected virtual async Task GetJavaProxy (Uri destination, CancellationToken cancellationToken) { - var proxy = Java.Net.Proxy.NoProxy; - - if (destination == null || Proxy == null) { - goto done; - } - - Uri puri = Proxy.GetProxy (destination); - if (puri == null) { - goto done; - } - - proxy = await Task .Run (() => { - // Let the Java code resolve the address, if necessary - var addr = new Java.Net.InetSocketAddress (puri.Host, puri.Port); - return new Java.Net.Proxy (Java.Net.Proxy.Type.Http, addr); - }, cancellationToken); - - done: - return proxy; - } - - Task ProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) - { - cancellationToken.ThrowIfCancellationRequested (); - httpConnection.InstanceFollowRedirects = false; // We handle it ourselves - RequestedAuthentication = null; - ProxyAuthenticationRequested = false; - - return DoProcessRequest (request, javaUrl, httpConnection, cancellationToken, redirectState); - } - - Task DisconnectAsync (HttpURLConnection httpConnection) - { - return Task.Run (() => httpConnection?.Disconnect ()); - } - - Task ConnectAsync (HttpURLConnection httpConnection, CancellationToken ct) - { - return Task.Run (() => { - try { - using (ct.Register(() => DisconnectAsync(httpConnection).ContinueWith(t => { - if (t.Exception != null) Logger.Log(LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}"); - }, TaskScheduler.Default))) - httpConnection?.Connect (); - } catch (Exception ex) { - if (ct.IsCancellationRequested) { - Logger.Log (LogLevel.Info, LOG_APP, $"Exception caught while cancelling connection: {ex}"); - ct.ThrowIfCancellationRequested (); - } - throw; - } - }, ct); + return await _underlyingHander.GetJavaProxy (destination, cancellationToken); } protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage request, HttpURLConnection httpConnection, CancellationToken cancellationToken) { - using (var stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false)) { - await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); - - // - // Rewind the stream to beginning in case the HttpContent implementation - // will be accessed again (e.g. after redirect) and it keeps its stream - // open behind the scenes instead of recreating it on the next call to - // ReadAsStreamAsync. If we don't rewind it, the ReadAsStreamAsync - // call above will throw an exception as we'd be attempting to read an - // already "closed" stream (that is one whose Position is set to its - // end). - // - // This is not a perfect solution since the HttpContent may do weird - // things in its implementation, but it's better than copying the - // content into a buffer since we have no way of knowing how the data is - // read or generated and also we don't want to keep potentially large - // amounts of data in memory (which would happen if we read the content - // into a byte[] buffer and kept it cached for re-use on redirect). - // - // See https://bugzilla.xamarin.com/show_bug.cgi?id=55477 - // - if (stream.CanSeek) - stream.Seek (0, SeekOrigin.Begin); - } - } - - async Task DoProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) - { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"{this}.DoProcessRequest ()"); - - if (cancellationToken.IsCancellationRequested) { - if(Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, " cancelled"); - - cancellationToken.ThrowIfCancellationRequested (); - } - - try { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $" connecting"); - - await ConnectAsync (httpConnection, cancellationToken).ConfigureAwait (continueOnCapturedContext: false); - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $" connected"); - } catch (Java.Net.ConnectException ex) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Connection exception {ex}"); - // Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler - throw new WebException (ex.Message, ex, WebExceptionStatus.ConnectFailure, null); - } - - if (cancellationToken.IsCancellationRequested) { - if(Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, " cancelled"); - - await DisconnectAsync (httpConnection).ConfigureAwait (continueOnCapturedContext: false); - cancellationToken.ThrowIfCancellationRequested (); - } - - CancellationTokenRegistration cancelRegistration = default (CancellationTokenRegistration); - HttpStatusCode statusCode = HttpStatusCode.OK; - Uri? connectionUri = null; - - try { - cancelRegistration = cancellationToken.Register (() => { - DisconnectAsync (httpConnection).ContinueWith (t => { - if (t.Exception != null) - Logger.Log (LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}"); - }, TaskScheduler.Default); - }, useSynchronizationContext: false); - - if (httpConnection.DoOutput) - await WriteRequestContentToOutput (request, httpConnection, cancellationToken); - - statusCode = await Task.Run (() => (HttpStatusCode)httpConnection.ResponseCode, cancellationToken).ConfigureAwait (false); - connectionUri = new Uri (httpConnection.URL?.ToString ()!); - } finally { - cancelRegistration.Dispose (); - } - - if (cancellationToken.IsCancellationRequested) { - await DisconnectAsync (httpConnection).ConfigureAwait (continueOnCapturedContext: false); - cancellationToken.ThrowIfCancellationRequested(); - } - - // If the request was redirected we need to put the new URL in the request - request.RequestUri = connectionUri; - var ret = new AndroidHttpResponseMessage (javaUrl, httpConnection) { - RequestMessage = request, - ReasonPhrase = httpConnection.ResponseMessage, - StatusCode = statusCode, - }; - - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Status code: {statusCode}"); - - if (!IsErrorStatusCode (statusCode)) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Reading..."); - ret.Content = GetContent (httpConnection, httpConnection.InputStream!); - } else { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Status code is {statusCode}, reading..."); - // For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream. - // Instead we try to read the error stream and return an empty string if the error stream isn't readable. - ret.Content = GetErrorContent (httpConnection, new StringContent (String.Empty, Encoding.ASCII)); - } - - bool disposeRet; - if (HandleRedirect (statusCode, httpConnection, redirectState, out disposeRet)) { - if (redirectState.MethodChanged) { - // If a redirect uses GET but the original request used POST with content, then the redirected - // request will fail with an exception. - // There's also no way to send content using GET (except in the URL, of course), so discarding - // request.Content is what we should do. - // - // See https://github.com/xamarin/xamarin-android/issues/1282 - if (redirectState.Method == HttpMethod.Get) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Discarding content on redirect"); - request.Content = null; - } - } - - if (disposeRet) { - ret.Dispose (); - ret = null!; - } else { - CopyHeaders (httpConnection, ret); - ParseCookies (ret, connectionUri); - } - - // We don't want to pass the authorization header onto the next location - request.Headers.Authorization = null; - - return ret; - } - - switch (statusCode) { - case HttpStatusCode.Unauthorized: - case HttpStatusCode.ProxyAuthenticationRequired: - // We don't resend the request since that would require new set of credentials if the - // ones provided in Credentials are invalid (or null) and that, in turn, may require asking the - // user which is not something that should be taken care of by us and in this - // context. The application should be responsible for this. - // HttpClientHandler throws an exception in this instance, but I think it's not a good - // idea. We'll return the response message with all the information required by the - // application to fill in the blanks and provide the requested credentials instead. - // - // We return the body of the response too, but the Java client will throw - // a FileNotFound exception if we attempt to access the input stream. - // Instead we try to read the error stream and return an default message if the error stream isn't readable. - ret.Content = GetErrorContent (httpConnection, new StringContent ("Unauthorized", Encoding.ASCII)); - CopyHeaders (httpConnection, ret); - - if (ret.Headers.WwwAuthenticate != null) { - ProxyAuthenticationRequested = false; - CollectAuthInfo (ret.Headers.WwwAuthenticate); - } else if (ret.Headers.ProxyAuthenticate != null) { - ProxyAuthenticationRequested = true; - CollectAuthInfo (ret.Headers.ProxyAuthenticate); - } - - ret.RequestedAuthentication = RequestedAuthentication; - return ret; - } - - CopyHeaders (httpConnection, ret); - ParseCookies (ret, connectionUri); - - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Returning"); - return ret; - } - - HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent) - { - var contentStream = httpConnection.ErrorStream; - - if (contentStream != null) { - return GetContent (httpConnection, contentStream); - } - - return fallbackContent; - } - - HttpContent GetContent (URLConnection httpConnection, Stream contentStream) - { - Stream inputStream = new BufferedStream (contentStream); - if (decompress_here) { - var encodings = httpConnection.ContentEncoding?.Split (','); - if (encodings != null) { - if (encodings.Contains (GZIP_ENCODING, StringComparer.OrdinalIgnoreCase)) - inputStream = new GZipStream (inputStream, CompressionMode.Decompress); - else if (encodings.Contains (DEFLATE_ENCODING, StringComparer.OrdinalIgnoreCase)) - inputStream = new DeflateStream (inputStream, CompressionMode.Decompress); - } - } - return new StreamContent (inputStream); - } - - bool HandleRedirect (HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet) - { - if (!AllowAutoRedirect) { - disposeRet = false; - return true; // We shouldn't follow and there's no data to fetch, just return - } - disposeRet = true; - - redirectState.NewUrl = null; - redirectState.MethodChanged = false; - switch (redirectCode) { - case HttpStatusCode.MultipleChoices: // 300 - break; - - case HttpStatusCode.Moved: // 301 - case HttpStatusCode.Redirect: // 302 - case HttpStatusCode.SeeOther: // 303 - redirectState.MethodChanged = redirectState.Method != HttpMethod.Get; - redirectState.Method = HttpMethod.Get; - break; - - case HttpStatusCode.NotModified: // 304 - disposeRet = false; - return true; // Not much happening here, just return and let the client decide - // what to do with the response - - case HttpStatusCode.TemporaryRedirect: // 307 - break; - - default: - if ((int)redirectCode >= 300 && (int)redirectCode < 400) - throw new InvalidOperationException ($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported"); - return false; - } - - var headers = httpConnection.HeaderFields; - IList ? locationHeader = null; - string? location = null; - - if (headers?.TryGetValue ("Location", out locationHeader) == true && locationHeader != null && locationHeader.Count > 0) { - if (locationHeader.Count == 1) { - location = locationHeader [0]?.Trim (); - } else { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"More than one location header for HTTP {redirectCode} redirect. Will use the first non-empty one."); - - foreach (string l in locationHeader) { - location = l?.Trim (); - if (!String.IsNullOrEmpty (location)) - break; - } - } - } - - if (String.IsNullOrEmpty (location)) { - // As per https://tools.ietf.org/html/rfc7231#section-6.4.1 the reponse isn't required to contain the Location header and the - // client should act accordingly. Since it is not documented what the action in this case should be, we're following what - // Xamarin.iOS does and simply return the content of the request as if it wasn't a redirect. - // It is not clear what to do if there is a Location header but its value is empty, so - // we assume the same action here. - disposeRet = false; - return true; - } - - redirectState.RedirectCounter++; - if (redirectState.RedirectCounter >= MaxAutomaticRedirections) - throw new WebException ($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)"); - - Uri redirectUrl; - try { - if (Logger.LogNet) - Logger.Log (LogLevel.Debug, LOG_APP, $"Raw redirect location: {location}"); - - var baseUrl = new Uri (httpConnection.URL?.ToString ()!); - if (location? [0] == '/') { - // Shortcut for the '/' and '//' cases, simplifies logic since URI won't treat - // such URLs as relative and we'd have to work around it in the `else` block - // below. - redirectUrl = new Uri (baseUrl, location); - } else { - // Special case (from https://tools.ietf.org/html/rfc3986#section-5.4.1) not - // handled by the Uri class: scheme:host - // - // This is a valid URI (should be treated as `scheme://host`) but URI throws an - // exception about DOS path being malformed IF the part before colon is just one - // character long... We could replace the scheme with the original request's one, but - // that would NOT be the right thing to do since it is not what the redirecting server - // meant. The fix doesn't belong here, but rather in the Uri class. So we'll throw... - - redirectUrl = new Uri (location!, UriKind.RelativeOrAbsolute); - if (!redirectUrl.IsAbsoluteUri) - redirectUrl = new Uri (baseUrl, location); - } - - if (Logger.LogNet) - Logger.Log (LogLevel.Debug, LOG_APP, $"Cooked redirect location: {redirectUrl}"); - } catch (Exception ex) { - throw new WebException ($"Invalid redirect URI received: {location}", ex); - } - - UriBuilder? builder = null; - if (!String.IsNullOrEmpty (httpConnection.URL?.Ref) && String.IsNullOrEmpty (redirectUrl.Fragment)) { - if (Logger.LogNet) - Logger.Log (LogLevel.Debug, LOG_APP, $"Appending fragment '{httpConnection.URL?.Ref}' to redirect URL '{redirectUrl}'"); - - builder = new UriBuilder (redirectUrl) { - Fragment = httpConnection.URL?.Ref - }; - } - - redirectState.NewUrl = builder == null ? redirectUrl : builder.Uri; - if (Logger.LogNet) - Logger.Log (LogLevel.Debug, LOG_APP, $"Request redirected to {redirectState.NewUrl}"); - - return true; - } - - bool IsErrorStatusCode (HttpStatusCode statusCode) - { - return (int)statusCode >= 400 && (int)statusCode <= 599; - } - - void CollectAuthInfo (HttpHeaderValueCollection headers) - { - var authData = new List (headers.Count); - - foreach (AuthenticationHeaderValue ahv in headers) { - var data = new AuthenticationData { - Scheme = GetAuthScheme (ahv.Scheme), - Challenge = $"{ahv.Scheme} {ahv.Parameter}", - UseProxyAuthentication = ProxyAuthenticationRequested - }; - authData.Add (data); - } - - RequestedAuthentication = authData.AsReadOnly (); - } - - AuthenticationScheme GetAuthScheme (string scheme) - { - if (String.Compare ("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0) - return AuthenticationScheme.Basic; - if (String.Compare ("digest", scheme, StringComparison.OrdinalIgnoreCase) == 0) - return AuthenticationScheme.Digest; - - return AuthenticationScheme.Unsupported; - } - - void ParseCookies (AndroidHttpResponseMessage ret, Uri connectionUri) - { - IEnumerable cookieHeaderValue; - if (!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues ("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"No cookies"); - return; - } - - try { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Parsing cookies"); - CookieContainer.SetCookies (connectionUri, String.Join (",", cookieHeaderValue)); - } catch (Exception ex) { - // We don't want to terminate the response because of a bad cookie, hence just reporting - // the issue. We might consider adding a virtual method to let the user handle the - // issue, but not sure if it's really needed. Set-Cookie header will be part of the - // header collection so the user can always examine it if they spot an error. - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Failed to parse cookies in the server response. {ex.GetType ()}: {ex.Message}"); - } - } - - void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response) - { - var headers = httpConnection.HeaderFields; - foreach (var key in headers!.Keys) { - if (key == null) // First header entry has null key, it corresponds to the response message - continue; - - HttpHeaders item_headers; - - if (known_content_headers.Contains (key)) { - item_headers = response.Content.Headers; - } else { - item_headers = response.Headers; - } - item_headers.TryAddWithoutValidation (key, headers [key]); - } + await _underlyingHander.WriteRequestContentToOutput (request, httpConnection, cancellationToken); } /// @@ -762,8 +245,7 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response /// Pre-configured connection instance protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn) { - Action a = AssertSelf; - return Task.Run (a); + return _underlyingHander.SetupRequest (request, conn); } /// @@ -777,7 +259,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti { AssertSelf (); - return keyStore; + return _underlyingHander.ConfigureKeyStore (keyStore); } /// @@ -793,7 +275,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti { AssertSelf (); - return null; + return _underlyingHander.ConfigureKeyManagerFactory (keyStore); } /// @@ -810,71 +292,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti { AssertSelf (); - return null; - } - - void AppendEncoding (string encoding, ref List ? list) - { - if (list == null) - list = new List (); - if (list.Contains (encoding)) - return; - list.Add (encoding); - } - - async Task SetupRequestInternal (HttpRequestMessage request, URLConnection conn) - { - if (conn == null) - throw new ArgumentNullException (nameof (conn)); - var httpConnection = conn.JavaCast (); - if (httpConnection == null) - throw new InvalidOperationException ($"Unsupported URL scheme {conn.URL?.Protocol}"); - - try { - httpConnection.RequestMethod = request.Method.ToString (); - } catch (Java.Net.ProtocolException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); - } - - // SSL context must be set up as soon as possible, before adding any content or - // headers. Otherwise Java won't use the socket factory - SetupSSL (httpConnection as HttpsURLConnection); - if (request.Content != null) - AddHeaders (httpConnection, request.Content.Headers); - AddHeaders (httpConnection, request.Headers); - - List ? accept_encoding = null; - - decompress_here = false; - if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) { - AppendEncoding (GZIP_ENCODING, ref accept_encoding); - decompress_here = true; - } - - if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) { - AppendEncoding (DEFLATE_ENCODING, ref accept_encoding); - decompress_here = true; - } - - if (AutomaticDecompression == DecompressionMethods.None) { - accept_encoding?.Clear (); - AppendEncoding (IDENTITY_ENCODING, ref accept_encoding); // Turns off compression for the Java client - } - - if (accept_encoding?.Count > 0) - httpConnection.SetRequestProperty ("Accept-Encoding", String.Join (",", accept_encoding)); - - if (UseCookies && CookieContainer != null) { - string cookieHeaderValue = CookieContainer.GetCookieHeader (request.RequestUri); - if (!String.IsNullOrEmpty (cookieHeaderValue)) - httpConnection.SetRequestProperty ("Cookie", cookieHeaderValue); - } - - HandlePreAuthentication (httpConnection); - await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);; - SetupRequestBody (httpConnection, request); - - return httpConnection; + return _underlyingHander.ConfigureTrustManagerFactory (keyStore); } /// @@ -890,123 +308,19 @@ void AppendEncoding (string encoding, ref List ? list) /// HTTPS connection to return socket factory for protected virtual SSLSocketFactory? ConfigureCustomSSLSocketFactory (HttpsURLConnection connection) { - return null; - } - - void SetupSSL (HttpsURLConnection? httpsConnection) - { - if (httpsConnection == null) - return; - - var socketFactory = ConfigureCustomSSLSocketFactory (httpsConnection); - if (socketFactory != null) { - httpsConnection.SSLSocketFactory = socketFactory; - return; - } - - // Context: https://github.com/xamarin/xamarin-android/issues/1615 - int apiLevel = (int)Build.VERSION.SdkInt; - if (apiLevel >= 16 && apiLevel <= 20) { - httpsConnection.SSLSocketFactory = new OldAndroidSSLSocketFactory (); - return; - } - - var keyStore = KeyStore.GetInstance (KeyStore.DefaultType); - keyStore?.Load (null, null); - bool gotCerts = TrustedCerts?.Count > 0; - if (gotCerts) { - for (int i = 0; i < TrustedCerts!.Count; i++) { - Certificate cert = TrustedCerts [i]; - if (cert == null) - continue; - keyStore?.SetCertificateEntry ($"ca{i}", cert); - } - } - keyStore = ConfigureKeyStore (keyStore); - var kmf = ConfigureKeyManagerFactory (keyStore); - var tmf = ConfigureTrustManagerFactory (keyStore); - - if (tmf == null) { - // If there are no certs and no trust manager factory, we can't use a custom manager - // because it will cause all the HTTPS requests to fail because of unverified trust - // chain - if (!gotCerts) - return; - - tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); - tmf?.Init (keyStore); - } - - var context = SSLContext.GetInstance ("TLS"); - context?.Init (kmf?.GetKeyManagers (), tmf?.GetTrustManagers (), null); - httpsConnection.SSLSocketFactory = context?.SocketFactory; - } - - void HandlePreAuthentication (HttpURLConnection httpConnection) - { - var data = PreAuthenticationData; - if (!PreAuthenticate || data == null) - return; - - var creds = data.UseProxyAuthentication ? Proxy?.Credentials : Credentials; - if (creds == null) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Authentication using scheme {data.Scheme} requested but no credentials found. No authentication will be performed"); - return; - } - - var auth = data.Scheme == AuthenticationScheme.Unsupported ? data.AuthModule : authModules.Find (m => m?.Scheme == data.Scheme); - if (auth == null) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Authentication module for scheme '{data.Scheme}' not found. No authentication will be performed"); - return; - } - - Authorization authorization = auth.Authenticate (data.Challenge!, httpConnection, creds); - if (authorization == null) { - if (Logger.LogNet) - Logger.Log (LogLevel.Info, LOG_APP, $"Authorization module {auth.GetType ()} for scheme {data.Scheme} returned no authorization"); - return; - } - - if (Logger.LogNet) { - var header = data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization"; - Logger.Log (LogLevel.Info, LOG_APP, $"Authentication header '{header}' will be set to '{authorization.Message}'"); - } - httpConnection.SetRequestProperty (data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization", authorization.Message); + return _underlyingHander.ConfigureCustomSSLSocketFactory (connection); } - static string GetHeaderSeparator (string name) => headerSeparators.TryGetValue (name, out var value) ? value : ","; - - void AddHeaders (HttpURLConnection conn, HttpHeaders headers) + [DynamicDependency (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof (AndroidMessageHandler))] + object GetUnderlyingHandler () { - if (headers == null) - return; - - foreach (KeyValuePair> header in headers) { - conn.SetRequestProperty (header.Key, header.Value != null ? String.Join (GetHeaderSeparator (header.Key), header.Value) : String.Empty); + var fieldName = "_nativeHandler"; + var baseType = GetType ().BaseType; + var field = baseType.GetField (fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + if (field == null) { + throw new InvalidOperationException ($"Field '{fieldName}' is missing from type '{baseType}'."); } - } - - void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage request) - { - if (request.Content == null) { - // Pilfered from System.Net.Http.HttpClientHandler:SendAync - if (HttpMethod.Post.Equals (request.Method) || HttpMethod.Put.Equals (request.Method) || HttpMethod.Delete.Equals (request.Method)) { - // Explicitly set this to make sure we're sending a "Content-Length: 0" header. - // This fixes the issue that's been reported on the forums: - // http://forums.xamarin.com/discussion/17770/length-required-error-in-http-post-since-latest-release - httpConnection.SetRequestProperty ("Content-Length", "0"); - } - return; - } - - httpConnection.DoOutput = true; - long? contentLength = request.Content.Headers.ContentLength; - if (contentLength != null) - httpConnection.SetFixedLengthStreamingMode ((int)contentLength); - else - httpConnection.SetChunkedStreamingMode (0); + return field.GetValue (this); } } } diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs new file mode 100644 index 00000000000..949b87b4618 --- /dev/null +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -0,0 +1,1030 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Android.OS; +using Android.Runtime; +using Java.IO; +using Java.Net; +using Java.Security; +using Java.Security.Cert; +using Javax.Net.Ssl; + +namespace Xamarin.Android.Net +{ + public class AndroidMessageHandler : HttpMessageHandler + { + sealed class RequestRedirectionState + { + public Uri? NewUrl; + public int RedirectCounter; + public HttpMethod? Method; + public bool MethodChanged; + } + + internal const string LOG_APP = "monodroid-net"; + + const string GZIP_ENCODING = "gzip"; + const string DEFLATE_ENCODING = "deflate"; + const string IDENTITY_ENCODING = "identity"; + + static readonly IDictionary headerSeparators = new Dictionary { + ["User-Agent"] = " ", + }; + + static readonly HashSet known_content_headers = new HashSet (StringComparer.OrdinalIgnoreCase) { + "Allow", + "Content-Disposition", + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-MD5", + "Content-Range", + "Content-Type", + "Expires", + "Last-Modified" + }; + + static readonly List authModules = new List { + new AuthModuleBasic (), + new AuthModuleDigest () + }; + + CookieContainer _cookieContainer; + DecompressionMethods _decompressionMethods; + + bool disposed; + + // Now all hail Java developers! Get this... HttpURLClient defaults to accepting AND + // uncompressing the gzip content encoding UNLESS you set the Accept-Encoding header to ANY + // value. So if we set it to 'gzip' below we WILL get gzipped stream but HttpURLClient will NOT + // uncompress it any longer, doh. And they don't support 'deflate' so we need to handle it ourselves. + bool decompress_here; + + internal const bool SupportsAutomaticDecompression = true; + internal const bool SupportsProxy = true; + internal const bool SupportsRedirectConfiguration = true; + + public DecompressionMethods AutomaticDecompression + { + get => _decompressionMethods; + set => _decompressionMethods = value; + } + + public CookieContainer CookieContainer + { + get => _cookieContainer ?? (_cookieContainer = new CookieContainer ()); + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + _cookieContainer = value; + } + } + + // NOTE: defaults here are based on: + // https://github.com/dotnet/runtime/blob/ccfe21882e4a2206ce49cd5b32d3eb3cab3e530f/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs + + public bool UseCookies { get; set; } = true; + + public bool PreAuthenticate { get; set; } = false; + + public bool UseProxy { get; set; } = true; + + public IWebProxy? Proxy { get; set; } + + public ICredentials? Credentials { get; set; } + + public bool AllowAutoRedirect { get; set; } = true; + + int maxAutomaticRedirections = 50; + + public int MaxAutomaticRedirections + { + get => maxAutomaticRedirections; + set { + // https://github.com/dotnet/runtime/blob/913facdca8b04cc674163e31a7650ef6868a7d5b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs#L142-L145 + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(value), value, "The specified value must be greater than 0"); + + maxAutomaticRedirections = value; + } + } + + /// + /// + /// Gets or sets the pre authentication data for the request. This property must be set by the application + /// before the request is made. Generally the value can be taken from + /// after the initial request, without any authentication data, receives the authorization request from the + /// server. The application must then store credentials in instance of and + /// assign the instance to this propery before retrying the request. + /// + /// + /// The property is never set by AndroidClientHandler. + /// + /// + /// The pre authentication data. + public AuthenticationData? PreAuthenticationData { get; set; } + + /// + /// If the website requires authentication, this property will contain data about each scheme supported + /// by the server after the response. Note that unauthorized request will return a valid response - you + /// need to check the status code and and (re)configure AndroidClientHandler instance accordingly by providing + /// both the credentials and the authentication scheme by setting the + /// property. If AndroidClientHandler is not able to detect the kind of authentication scheme it will store an + /// instance of with its property + /// set to AuthenticationScheme.Unsupported and the application will be responsible for providing an + /// instance of which handles this kind of authorization scheme + /// ( + /// + public IList ? RequestedAuthentication { get; private set; } + + /// + /// Server authentication response indicates that the request to authorize comes from a proxy if this property is true. + /// All the instances of stored in the property will + /// have their preset to the same value as this property. + /// + public bool ProxyAuthenticationRequested { get; private set; } + + /// + /// If true then the server requested authorization and the application must use information + /// found in to set the value of + /// + public bool RequestNeedsAuthorization { + get { return RequestedAuthentication?.Count > 0; } + } + + /// + /// + /// If the request is to the server protected with a self-signed (or otherwise untrusted) SSL certificate, the request will + /// fail security chain verification unless the application provides either the CA certificate of the entity which issued the + /// server's certificate or, alternatively, provides the server public key. Whichever the case, the certificate(s) must be stored + /// in this property in order for AndroidClientHandler to configure the request to accept the server certificate. + /// AndroidClientHandler uses a custom and to configure the connection. + /// If, however, the application requires finer control over the SSL configuration (e.g. it implements its own TrustManager) then + /// it should leave this property empty and instead derive a custom class from AndroidClientHandler and override, as needed, the + /// , and methods + /// instead + /// + /// The trusted certs. + public IList ? TrustedCerts { get; set; } + + /// + /// + /// Specifies the connection read timeout. + /// + /// + /// Since there's no way for the handler to access + /// directly, this property should be set by the calling party to the same desired value. Value of this + /// property will be passed to the native Java HTTP client, unless it is set to + /// + /// + /// The default value is 24 hours, much higher than the documented value of and the same as the value of iOS-specific + /// NSUrlSessionHandler. + /// + /// + public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24); + + /// + /// + /// Specifies the connect timeout + /// + /// + /// The native Java client supports two separate timeouts - one for reading from the connection () and another for establishing the connection. This property sets the value of + /// the latter timeout, unless it is set to in which case the + /// native Java client defaults are used. + /// + /// + /// The default value is 120 seconds. + /// + /// + public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromHours (24); + + protected override void Dispose (bool disposing) + { + disposed = true; + + base.Dispose (disposing); + } + + protected void AssertSelf () + { + if (!disposed) + return; + throw new ObjectDisposedException (nameof (AndroidMessageHandler)); + } + + string EncodeUrl (Uri url) + { + if (url == null) + return String.Empty; + + // UriBuilder takes care of encoding everything properly + var bldr = new UriBuilder (url); + if (url.IsDefaultPort) + bldr.Port = -1; // Avoids adding :80 or :443 to the host name in the result + + // bldr.Uri.ToString () would ruin the good job UriBuilder did + return bldr.ToString (); + } + + /// + /// Returns a custom host name verifier for a HTTPS connection. By default it returns null and + /// thus the connection uses whatever host name verification mechanism the operating system defaults to. + /// Override in your class to define custom host name verification behavior. The overriding class should + /// not set the property directly on the passed + /// + /// + /// Instance of IHostnameVerifier to be used for this HTTPS connection + /// HTTPS connection object. + internal virtual IHostnameVerifier? GetSSLHostnameVerifier (HttpsURLConnection connection) + { + return null; + } + + /// + /// Creates, configures and processes an asynchronous request to the indicated resource. + /// + /// Task in which the request is executed + /// Request provided by + /// Cancellation token. + protected override async Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) + { + AssertSelf (); + if (request == null) + throw new ArgumentNullException (nameof (request)); + + if (!request.RequestUri.IsAbsoluteUri) + throw new ArgumentException ("Must represent an absolute URI", "request"); + + var redirectState = new RequestRedirectionState { + NewUrl = request.RequestUri, + RedirectCounter = 0, + Method = request.Method + }; + while (true) { + URL java_url = new URL (EncodeUrl (redirectState.NewUrl)); + URLConnection? java_connection; + if (UseProxy) { + var javaProxy = await GetJavaProxy (redirectState.NewUrl, cancellationToken).ConfigureAwait (continueOnCapturedContext: false); + // When you use the parameter Java.Net.Proxy.NoProxy the system proxy is overriden. Leave the parameter out to respect the default settings. + java_connection = javaProxy == Java.Net.Proxy.NoProxy ? java_url.OpenConnection () : java_url.OpenConnection (javaProxy); + } else { + // In this case the consumer of this class has explicitly chosen to not use a proxy, so bypass the default proxy. The default value of UseProxy is true. + java_connection = java_url.OpenConnection (Java.Net.Proxy.NoProxy); + } + + var httpsConnection = java_connection as HttpsURLConnection; + if (httpsConnection != null) { + IHostnameVerifier? hnv = GetSSLHostnameVerifier (httpsConnection); + if (hnv != null) + httpsConnection.HostnameVerifier = hnv; + } + + if (ConnectTimeout != TimeSpan.Zero) + java_connection!.ConnectTimeout = checked ((int)ConnectTimeout.TotalMilliseconds); + + if (ReadTimeout != TimeSpan.Zero) + java_connection!.ReadTimeout = checked ((int)ReadTimeout.TotalMilliseconds); + + try { + HttpURLConnection httpConnection = await SetupRequestInternal (request, java_connection!).ConfigureAwait (continueOnCapturedContext: false); + HttpResponseMessage? response = await ProcessRequest (request, java_url, httpConnection, cancellationToken, redirectState).ConfigureAwait (continueOnCapturedContext: false); + if (response != null) + return response; + + if (redirectState.NewUrl == null) + throw new InvalidOperationException ("Request redirected but no new URI specified"); + request.Method = redirectState.Method; + } catch (Java.Net.SocketTimeoutException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.Timeout, null); + } catch (Java.Net.UnknownServiceException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); + } catch (Java.Lang.SecurityException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.SecureChannelFailure, null); + } catch (Java.IO.IOException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.UnknownError, null); + } + } + } + + internal virtual async Task GetJavaProxy (Uri destination, CancellationToken cancellationToken) + { + var proxy = Java.Net.Proxy.NoProxy; + + if (destination == null || Proxy == null) { + return proxy; + } + + Uri puri = Proxy.GetProxy (destination); + if (puri == null) { + return proxy; + } + + proxy = await Task .Run (() => { + // Let the Java code resolve the address, if necessary + var addr = new Java.Net.InetSocketAddress (puri.Host, puri.Port); + return new Java.Net.Proxy (Java.Net.Proxy.Type.Http, addr); + }, cancellationToken); + + return proxy; + } + + Task ProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) + { + cancellationToken.ThrowIfCancellationRequested (); + httpConnection.InstanceFollowRedirects = false; // We handle it ourselves + RequestedAuthentication = null; + ProxyAuthenticationRequested = false; + + return DoProcessRequest (request, javaUrl, httpConnection, cancellationToken, redirectState); + } + + Task DisconnectAsync (HttpURLConnection httpConnection) + { + return Task.Run (() => httpConnection?.Disconnect ()); + } + + Task ConnectAsync (HttpURLConnection httpConnection, CancellationToken ct) + { + return Task.Run (() => { + try { + using (ct.Register(() => DisconnectAsync(httpConnection).ContinueWith(t => { + if (t.Exception != null) Logger.Log(LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}"); + }, TaskScheduler.Default))) + httpConnection?.Connect (); + } catch (Exception ex) { + if (ct.IsCancellationRequested) { + Logger.Log (LogLevel.Info, LOG_APP, $"Exception caught while cancelling connection: {ex}"); + ct.ThrowIfCancellationRequested (); + } + throw; + } + }, ct); + } + + internal virtual async Task WriteRequestContentToOutput (HttpRequestMessage request, HttpURLConnection httpConnection, CancellationToken cancellationToken) + { + using (var stream = await request.Content.ReadAsStreamAsync ().ConfigureAwait (false)) { + await stream.CopyToAsync(httpConnection.OutputStream!, 4096, cancellationToken).ConfigureAwait(false); + + // + // Rewind the stream to beginning in case the HttpContent implementation + // will be accessed again (e.g. after redirect) and it keeps its stream + // open behind the scenes instead of recreating it on the next call to + // ReadAsStreamAsync. If we don't rewind it, the ReadAsStreamAsync + // call above will throw an exception as we'd be attempting to read an + // already "closed" stream (that is one whose Position is set to its + // end). + // + // This is not a perfect solution since the HttpContent may do weird + // things in its implementation, but it's better than copying the + // content into a buffer since we have no way of knowing how the data is + // read or generated and also we don't want to keep potentially large + // amounts of data in memory (which would happen if we read the content + // into a byte[] buffer and kept it cached for re-use on redirect). + // + // See https://bugzilla.xamarin.com/show_bug.cgi?id=55477 + // + if (stream.CanSeek) + stream.Seek (0, SeekOrigin.Begin); + } + } + + async Task DoProcessRequest (HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) + { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"{this}.DoProcessRequest ()"); + + if (cancellationToken.IsCancellationRequested) { + if(Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, " cancelled"); + + cancellationToken.ThrowIfCancellationRequested (); + } + + try { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $" connecting"); + + await ConnectAsync (httpConnection, cancellationToken).ConfigureAwait (continueOnCapturedContext: false); + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $" connected"); + } catch (Java.Net.ConnectException ex) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Connection exception {ex}"); + // Wrap it nicely in a "standard" exception so that it's compatible with HttpClientHandler + throw new WebException (ex.Message, ex, WebExceptionStatus.ConnectFailure, null); + } + + if (cancellationToken.IsCancellationRequested) { + if(Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, " cancelled"); + + await DisconnectAsync (httpConnection).ConfigureAwait (continueOnCapturedContext: false); + cancellationToken.ThrowIfCancellationRequested (); + } + + CancellationTokenRegistration cancelRegistration = default (CancellationTokenRegistration); + HttpStatusCode statusCode = HttpStatusCode.OK; + Uri? connectionUri = null; + + try { + cancelRegistration = cancellationToken.Register (() => { + DisconnectAsync (httpConnection).ContinueWith (t => { + if (t.Exception != null) + Logger.Log (LogLevel.Info, LOG_APP, $"Disconnection exception: {t.Exception}"); + }, TaskScheduler.Default); + }, useSynchronizationContext: false); + + if (httpConnection.DoOutput) + await WriteRequestContentToOutput (request, httpConnection, cancellationToken); + + statusCode = await Task.Run (() => (HttpStatusCode)httpConnection.ResponseCode, cancellationToken).ConfigureAwait (false); + connectionUri = new Uri (httpConnection.URL?.ToString ()!); + } finally { + cancelRegistration.Dispose (); + } + + if (cancellationToken.IsCancellationRequested) { + await DisconnectAsync (httpConnection).ConfigureAwait (continueOnCapturedContext: false); + cancellationToken.ThrowIfCancellationRequested(); + } + + // If the request was redirected we need to put the new URL in the request + request.RequestUri = connectionUri; + var ret = new AndroidHttpResponseMessage (javaUrl, httpConnection) { + RequestMessage = request, + ReasonPhrase = httpConnection.ResponseMessage, + StatusCode = statusCode, + }; + + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Status code: {statusCode}"); + + if (!IsErrorStatusCode (statusCode)) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Reading..."); + ret.Content = GetContent (httpConnection, httpConnection.InputStream!); + } else { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Status code is {statusCode}, reading..."); + // For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream. + // Instead we try to read the error stream and return an empty string if the error stream isn't readable. + ret.Content = GetErrorContent (httpConnection, new StringContent (String.Empty, Encoding.ASCII)); + } + + bool disposeRet; + if (HandleRedirect (statusCode, httpConnection, redirectState, out disposeRet)) { + if (redirectState.MethodChanged) { + // If a redirect uses GET but the original request used POST with content, then the redirected + // request will fail with an exception. + // There's also no way to send content using GET (except in the URL, of course), so discarding + // request.Content is what we should do. + // + // See https://github.com/xamarin/xamarin-android/issues/1282 + if (redirectState.Method == HttpMethod.Get) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Discarding content on redirect"); + request.Content = null; + } + } + + if (disposeRet) { + ret.Dispose (); + ret = null!; + } else { + CopyHeaders (httpConnection, ret); + ParseCookies (ret, connectionUri); + } + + // We don't want to pass the authorization header onto the next location + request.Headers.Authorization = null; + + return ret; + } + + switch (statusCode) { + case HttpStatusCode.Unauthorized: + case HttpStatusCode.ProxyAuthenticationRequired: + // We don't resend the request since that would require new set of credentials if the + // ones provided in Credentials are invalid (or null) and that, in turn, may require asking the + // user which is not something that should be taken care of by us and in this + // context. The application should be responsible for this. + // HttpClientHandler throws an exception in this instance, but I think it's not a good + // idea. We'll return the response message with all the information required by the + // application to fill in the blanks and provide the requested credentials instead. + // + // We return the body of the response too, but the Java client will throw + // a FileNotFound exception if we attempt to access the input stream. + // Instead we try to read the error stream and return an default message if the error stream isn't readable. + ret.Content = GetErrorContent (httpConnection, new StringContent ("Unauthorized", Encoding.ASCII)); + CopyHeaders (httpConnection, ret); + + if (ret.Headers.WwwAuthenticate != null) { + ProxyAuthenticationRequested = false; + CollectAuthInfo (ret.Headers.WwwAuthenticate); + } else if (ret.Headers.ProxyAuthenticate != null) { + ProxyAuthenticationRequested = true; + CollectAuthInfo (ret.Headers.ProxyAuthenticate); + } + + ret.RequestedAuthentication = RequestedAuthentication; + return ret; + } + + CopyHeaders (httpConnection, ret); + ParseCookies (ret, connectionUri); + + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Returning"); + return ret; + } + + HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent) + { + var contentStream = httpConnection.ErrorStream; + + if (contentStream != null) { + return GetContent (httpConnection, contentStream); + } + + return fallbackContent; + } + + HttpContent GetContent (URLConnection httpConnection, Stream contentStream) + { + Stream inputStream = new BufferedStream (contentStream); + if (decompress_here) { + var encodings = httpConnection.ContentEncoding?.Split (','); + if (encodings != null) { + if (encodings.Contains (GZIP_ENCODING, StringComparer.OrdinalIgnoreCase)) + inputStream = new GZipStream (inputStream, CompressionMode.Decompress); + else if (encodings.Contains (DEFLATE_ENCODING, StringComparer.OrdinalIgnoreCase)) + inputStream = new DeflateStream (inputStream, CompressionMode.Decompress); + } + } + return new StreamContent (inputStream); + } + + bool HandleRedirect (HttpStatusCode redirectCode, HttpURLConnection httpConnection, RequestRedirectionState redirectState, out bool disposeRet) + { + if (!AllowAutoRedirect) { + disposeRet = false; + return true; // We shouldn't follow and there's no data to fetch, just return + } + disposeRet = true; + + redirectState.NewUrl = null; + redirectState.MethodChanged = false; + switch (redirectCode) { + case HttpStatusCode.MultipleChoices: // 300 + break; + + case HttpStatusCode.Moved: // 301 + case HttpStatusCode.Redirect: // 302 + case HttpStatusCode.SeeOther: // 303 + redirectState.MethodChanged = redirectState.Method != HttpMethod.Get; + redirectState.Method = HttpMethod.Get; + break; + + case HttpStatusCode.NotModified: // 304 + disposeRet = false; + return true; // Not much happening here, just return and let the client decide + // what to do with the response + + case HttpStatusCode.TemporaryRedirect: // 307 + break; + + default: + if ((int)redirectCode >= 300 && (int)redirectCode < 400) + throw new InvalidOperationException ($"HTTP Redirection status code {redirectCode} ({(int)redirectCode}) not supported"); + return false; + } + + var headers = httpConnection.HeaderFields; + IList ? locationHeader = null; + string? location = null; + + if (headers?.TryGetValue ("Location", out locationHeader) == true && locationHeader != null && locationHeader.Count > 0) { + if (locationHeader.Count == 1) { + location = locationHeader [0]?.Trim (); + } else { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"More than one location header for HTTP {redirectCode} redirect. Will use the first non-empty one."); + + foreach (string l in locationHeader) { + location = l?.Trim (); + if (!String.IsNullOrEmpty (location)) + break; + } + } + } + + if (String.IsNullOrEmpty (location)) { + // As per https://tools.ietf.org/html/rfc7231#section-6.4.1 the reponse isn't required to contain the Location header and the + // client should act accordingly. Since it is not documented what the action in this case should be, we're following what + // Xamarin.iOS does and simply return the content of the request as if it wasn't a redirect. + // It is not clear what to do if there is a Location header but its value is empty, so + // we assume the same action here. + disposeRet = false; + return true; + } + + redirectState.RedirectCounter++; + if (redirectState.RedirectCounter >= MaxAutomaticRedirections) + throw new WebException ($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)"); + + Uri redirectUrl; + try { + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Raw redirect location: {location}"); + + var baseUrl = new Uri (httpConnection.URL?.ToString ()!); + if (location? [0] == '/') { + // Shortcut for the '/' and '//' cases, simplifies logic since URI won't treat + // such URLs as relative and we'd have to work around it in the `else` block + // below. + redirectUrl = new Uri (baseUrl, location); + } else { + // Special case (from https://tools.ietf.org/html/rfc3986#section-5.4.1) not + // handled by the Uri class: scheme:host + // + // This is a valid URI (should be treated as `scheme://host`) but URI throws an + // exception about DOS path being malformed IF the part before colon is just one + // character long... We could replace the scheme with the original request's one, but + // that would NOT be the right thing to do since it is not what the redirecting server + // meant. The fix doesn't belong here, but rather in the Uri class. So we'll throw... + + redirectUrl = new Uri (location!, UriKind.RelativeOrAbsolute); + if (!redirectUrl.IsAbsoluteUri) + redirectUrl = new Uri (baseUrl, location); + } + + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Cooked redirect location: {redirectUrl}"); + } catch (Exception ex) { + throw new WebException ($"Invalid redirect URI received: {location}", ex); + } + + UriBuilder? builder = null; + if (!String.IsNullOrEmpty (httpConnection.URL?.Ref) && String.IsNullOrEmpty (redirectUrl.Fragment)) { + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Appending fragment '{httpConnection.URL?.Ref}' to redirect URL '{redirectUrl}'"); + + builder = new UriBuilder (redirectUrl) { + Fragment = httpConnection.URL?.Ref + }; + } + + redirectState.NewUrl = builder == null ? redirectUrl : builder.Uri; + if (Logger.LogNet) + Logger.Log (LogLevel.Debug, LOG_APP, $"Request redirected to {redirectState.NewUrl}"); + + return true; + } + + bool IsErrorStatusCode (HttpStatusCode statusCode) + { + return (int)statusCode >= 400 && (int)statusCode <= 599; + } + + void CollectAuthInfo (HttpHeaderValueCollection headers) + { + var authData = new List (headers.Count); + + foreach (AuthenticationHeaderValue ahv in headers) { + var data = new AuthenticationData { + Scheme = GetAuthScheme (ahv.Scheme), + Challenge = $"{ahv.Scheme} {ahv.Parameter}", + UseProxyAuthentication = ProxyAuthenticationRequested + }; + authData.Add (data); + } + + RequestedAuthentication = authData.AsReadOnly (); + } + + AuthenticationScheme GetAuthScheme (string scheme) + { + if (String.Compare ("basic", scheme, StringComparison.OrdinalIgnoreCase) == 0) + return AuthenticationScheme.Basic; + if (String.Compare ("digest", scheme, StringComparison.OrdinalIgnoreCase) == 0) + return AuthenticationScheme.Digest; + + return AuthenticationScheme.Unsupported; + } + + void ParseCookies (AndroidHttpResponseMessage ret, Uri connectionUri) + { + IEnumerable cookieHeaderValue; + if (!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues ("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"No cookies"); + return; + } + + try { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Parsing cookies"); + CookieContainer.SetCookies (connectionUri, String.Join (",", cookieHeaderValue)); + } catch (Exception ex) { + // We don't want to terminate the response because of a bad cookie, hence just reporting + // the issue. We might consider adding a virtual method to let the user handle the + // issue, but not sure if it's really needed. Set-Cookie header will be part of the + // header collection so the user can always examine it if they spot an error. + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Failed to parse cookies in the server response. {ex.GetType ()}: {ex.Message}"); + } + } + + void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response) + { + var headers = httpConnection.HeaderFields; + foreach (var key in headers!.Keys) { + if (key == null) // First header entry has null key, it corresponds to the response message + continue; + + HttpHeaders item_headers; + + if (known_content_headers.Contains (key)) { + item_headers = response.Content.Headers; + } else { + item_headers = response.Headers; + } + item_headers.TryAddWithoutValidation (key, headers [key]); + } + } + + /// + /// Configure the before the request is sent. This method is meant to be overriden + /// by applications which need to perform some extra configuration steps on the connection. It is called with all + /// the request headers set, pre-authentication performed (if applicable) but before the request body is set + /// (e.g. for POST requests). The default implementation in AndroidClientHandler does nothing. + /// + /// Request data + /// Pre-configured connection instance + internal virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn) + { + Action a = AssertSelf; + return Task.Run (a); + } + + /// + /// Configures the key store. The parameter is set to instance of + /// created using the type and with populated with certificates provided in the + /// property. AndroidClientHandler implementation simply returns the instance passed in the parameter + /// + /// The key store. + /// Key store to configure. + internal virtual KeyStore? ConfigureKeyStore (KeyStore? keyStore) + { + AssertSelf (); + + return keyStore; + } + + /// + /// Create and configure an instance of . The parameter is set to the + /// return value of the method, so it might be null if the application overrode the method and provided + /// no key store. It will not be null when the default implementation is used. The application can return null here since + /// KeyManagerFactory is not required for the custom SSL configuration, but it might be used by the application to implement a more advanced + /// mechanism of key management. + /// + /// The key manager factory or null. + /// Key store. + internal virtual KeyManagerFactory? ConfigureKeyManagerFactory (KeyStore? keyStore) + { + AssertSelf (); + + return null; + } + + /// + /// Create and configure an instance of . The parameter is set to the + /// return value of the method, so it might be null if the application overrode the method and provided + /// no key store. It will not be null when the default implementation is used. The application can return null from this + /// method in which case AndroidClientHandler will create its own instance of the trust manager factory provided that the + /// list contains at least one valid certificate. If there are no valid certificates and this method returns null, no custom + /// trust manager will be created since that would make all the HTTPS requests fail. + /// + /// The trust manager factory. + /// Key store. + internal virtual TrustManagerFactory? ConfigureTrustManagerFactory (KeyStore? keyStore) + { + AssertSelf (); + + return null; + } + + void AppendEncoding (string encoding, ref List ? list) + { + if (list == null) + list = new List (); + if (list.Contains (encoding)) + return; + list.Add (encoding); + } + + async Task SetupRequestInternal (HttpRequestMessage request, URLConnection conn) + { + if (conn == null) + throw new ArgumentNullException (nameof (conn)); + var httpConnection = conn.JavaCast (); + if (httpConnection == null) + throw new InvalidOperationException ($"Unsupported URL scheme {conn.URL?.Protocol}"); + + try { + httpConnection.RequestMethod = request.Method.ToString (); + } catch (Java.Net.ProtocolException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { + throw new WebException (ex.Message, ex, WebExceptionStatus.ProtocolError, null); + } + + // SSL context must be set up as soon as possible, before adding any content or + // headers. Otherwise Java won't use the socket factory + SetupSSL (httpConnection as HttpsURLConnection); + if (request.Content != null) + AddHeaders (httpConnection, request.Content.Headers); + AddHeaders (httpConnection, request.Headers); + + List ? accept_encoding = null; + + decompress_here = false; + if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) { + AppendEncoding (GZIP_ENCODING, ref accept_encoding); + decompress_here = true; + } + + if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) { + AppendEncoding (DEFLATE_ENCODING, ref accept_encoding); + decompress_here = true; + } + + if (AutomaticDecompression == DecompressionMethods.None) { + accept_encoding?.Clear (); + AppendEncoding (IDENTITY_ENCODING, ref accept_encoding); // Turns off compression for the Java client + } + + if (accept_encoding?.Count > 0) + httpConnection.SetRequestProperty ("Accept-Encoding", String.Join (",", accept_encoding)); + + if (UseCookies && CookieContainer != null) { + string cookieHeaderValue = CookieContainer.GetCookieHeader (request.RequestUri); + if (!String.IsNullOrEmpty (cookieHeaderValue)) + httpConnection.SetRequestProperty ("Cookie", cookieHeaderValue); + } + + HandlePreAuthentication (httpConnection); + await SetupRequest (request, httpConnection).ConfigureAwait (continueOnCapturedContext: false);; + SetupRequestBody (httpConnection, request); + + return httpConnection; + } + + /// + /// Configure and return a custom for the passed HTTPS . If the class overriding the method returns anything but the default + /// null, the SSL setup code will not call the nor the + /// methods used to configure a custom trust manager which is + /// then used to create a default socket factory. + /// Deriving class must perform all the key manager and trust manager configuration to ensure proper + /// operation of the returned socket factory. + /// + /// Instance of SSLSocketFactory ready to use with the HTTPS connection. + /// HTTPS connection to return socket factory for + internal virtual SSLSocketFactory? ConfigureCustomSSLSocketFactory (HttpsURLConnection connection) + { + return null; + } + + void SetupSSL (HttpsURLConnection? httpsConnection) + { + if (httpsConnection == null) + return; + + var socketFactory = ConfigureCustomSSLSocketFactory (httpsConnection); + if (socketFactory != null) { + httpsConnection.SSLSocketFactory = socketFactory; + return; + } + + // Context: https://github.com/xamarin/xamarin-android/issues/1615 + int apiLevel = (int)Build.VERSION.SdkInt; + if (apiLevel >= 16 && apiLevel <= 20) { + httpsConnection.SSLSocketFactory = new OldAndroidSSLSocketFactory (); + return; + } + + var keyStore = KeyStore.GetInstance (KeyStore.DefaultType); + keyStore?.Load (null, null); + bool gotCerts = TrustedCerts?.Count > 0; + if (gotCerts) { + for (int i = 0; i < TrustedCerts!.Count; i++) { + Certificate cert = TrustedCerts [i]; + if (cert == null) + continue; + keyStore?.SetCertificateEntry ($"ca{i}", cert); + } + } + keyStore = ConfigureKeyStore (keyStore); + var kmf = ConfigureKeyManagerFactory (keyStore); + var tmf = ConfigureTrustManagerFactory (keyStore); + + if (tmf == null) { + // If there are no certs and no trust manager factory, we can't use a custom manager + // because it will cause all the HTTPS requests to fail because of unverified trust + // chain + if (!gotCerts) + return; + + tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); + tmf?.Init (keyStore); + } + + var context = SSLContext.GetInstance ("TLS"); + context?.Init (kmf?.GetKeyManagers (), tmf?.GetTrustManagers (), null); + httpsConnection.SSLSocketFactory = context?.SocketFactory; + } + + void HandlePreAuthentication (HttpURLConnection httpConnection) + { + var data = PreAuthenticationData; + if (!PreAuthenticate || data == null) + return; + + var creds = data.UseProxyAuthentication ? Proxy?.Credentials : Credentials; + if (creds == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Authentication using scheme {data.Scheme} requested but no credentials found. No authentication will be performed"); + return; + } + + var auth = data.Scheme == AuthenticationScheme.Unsupported ? data.AuthModule : authModules.Find (m => m?.Scheme == data.Scheme); + if (auth == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Authentication module for scheme '{data.Scheme}' not found. No authentication will be performed"); + return; + } + + Authorization authorization = auth.Authenticate (data.Challenge!, httpConnection, creds); + if (authorization == null) { + if (Logger.LogNet) + Logger.Log (LogLevel.Info, LOG_APP, $"Authorization module {auth.GetType ()} for scheme {data.Scheme} returned no authorization"); + return; + } + + if (Logger.LogNet) { + var header = data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization"; + Logger.Log (LogLevel.Info, LOG_APP, $"Authentication header '{header}' will be set to '{authorization.Message}'"); + } + httpConnection.SetRequestProperty (data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization", authorization.Message); + } + + static string GetHeaderSeparator (string name) => headerSeparators.TryGetValue (name, out var value) ? value : ","; + + void AddHeaders (HttpURLConnection conn, HttpHeaders headers) + { + if (headers == null) + return; + + foreach (KeyValuePair> header in headers) { + conn.SetRequestProperty (header.Key, header.Value != null ? String.Join (GetHeaderSeparator (header.Key), header.Value) : String.Empty); + } + } + + void SetupRequestBody (HttpURLConnection httpConnection, HttpRequestMessage request) + { + if (request.Content == null) { + // Pilfered from System.Net.Http.HttpClientHandler:SendAync + if (HttpMethod.Post.Equals (request.Method) || HttpMethod.Put.Equals (request.Method) || HttpMethod.Delete.Equals (request.Method)) { + // Explicitly set this to make sure we're sending a "Content-Length: 0" header. + // This fixes the issue that's been reported on the forums: + // http://forums.xamarin.com/discussion/17770/length-required-error-in-http-post-since-latest-release + httpConnection.SetRequestProperty ("Content-Length", "0"); + } + return; + } + + httpConnection.DoOutput = true; + long? contentLength = request.Content.Headers.ContentLength; + if (contentLength != null) + httpConnection.SetFixedLengthStreamingMode ((int)contentLength); + else + httpConnection.SetChunkedStreamingMode (0); + } + } + +} \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets index 8dcefaf317b..b8412b4b9bb 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets @@ -9,7 +9,8 @@ Resource $(MonoAndroidResourcePrefix)\$(_AndroidResourceDesigner) true - Xamarin.Android.Net.AndroidClientHandler + Xamarin.Android.Net.AndroidMessageHandler + Xamarin.Android.Net.AndroidClientHandler true false false @@ -55,7 +56,6 @@ false false - true true true SdkOnly diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc index bf601e7a946..c2f9be66add 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc @@ -8,37 +8,37 @@ "Size": 54406 }, "assemblies/Mono.Android.dll": { - "Size": 78829 + "Size": 78883 }, "assemblies/rc.bin": { - "Size": 802 + "Size": 863 }, "assemblies/System.Linq.dll": { - "Size": 10150 + "Size": 10149 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 495560 + "Size": 507488 }, "assemblies/System.Runtime.dll": { - "Size": 2262 + "Size": 2257 }, "assemblies/UnnamedProject.dll": { - "Size": 3173 + "Size": 3174 }, "classes.dex": { "Size": 316792 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 337816 + "Size": 337896 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3269936 + "Size": 3171872 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { "Size": 776216 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 79968 + "Size": 88160 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { "Size": 150024 @@ -77,5 +77,5 @@ "Size": 1724 } }, - "PackageSize": 2692947 + "PackageSize": 2656083 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc index ec9dc6b0c0c..b4eb9b9eca3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc @@ -8,163 +8,166 @@ "Size": 7236 }, "assemblies/Java.Interop.dll": { - "Size": 61256 + "Size": 61383 }, "assemblies/Microsoft.Win32.Primitives.dll": { - "Size": 3645 + "Size": 3829 }, "assemblies/Mono.Android.dll": { - "Size": 398294 + "Size": 415875 }, "assemblies/mscorlib.dll": { - "Size": 3832 + "Size": 3827 }, "assemblies/netstandard.dll": { - "Size": 5521 + "Size": 5518 }, "assemblies/rc.bin": { - "Size": 802 + "Size": 863 }, "assemblies/System.Collections.Concurrent.dll": { - "Size": 11579 + "Size": 11577 }, "assemblies/System.Collections.dll": { - "Size": 19185 + "Size": 19028 }, "assemblies/System.Collections.NonGeneric.dll": { - "Size": 8471 + "Size": 8469 }, "assemblies/System.ComponentModel.dll": { - "Size": 2002 + "Size": 1998 }, "assemblies/System.ComponentModel.Primitives.dll": { - "Size": 2607 + "Size": 2603 }, "assemblies/System.ComponentModel.TypeConverter.dll": { - "Size": 6993 + "Size": 6008 }, "assemblies/System.Console.dll": { - "Size": 5839 + "Size": 5834 }, "assemblies/System.Core.dll": { - "Size": 1968 + "Size": 1964 + }, + "assemblies/System.Diagnostics.DiagnosticSource.dll": { + "Size": 3028 }, "assemblies/System.Diagnostics.TraceSource.dll": { - "Size": 6800 + "Size": 6795 }, "assemblies/System.dll": { - "Size": 2315 + "Size": 2311 }, "assemblies/System.Drawing.dll": { - "Size": 2000 + "Size": 1997 }, "assemblies/System.Drawing.Primitives.dll": { - "Size": 12154 + "Size": 12245 }, "assemblies/System.Formats.Asn1.dll": { - "Size": 26856 + "Size": 26852 }, "assemblies/System.IO.Compression.Brotli.dll": { - "Size": 11461 + "Size": 11456 }, "assemblies/System.IO.Compression.dll": { - "Size": 18810 - }, - "assemblies/System.IO.FileSystem.dll": { - "Size": 1964 + "Size": 18812 }, "assemblies/System.IO.IsolatedStorage.dll": { - "Size": 10628 + "Size": 10791 }, "assemblies/System.Linq.dll": { - "Size": 19504 + "Size": 19501 }, "assemblies/System.Linq.Expressions.dll": { - "Size": 181323 + "Size": 181683 }, "assemblies/System.Net.Http.dll": { - "Size": 211166 + "Size": 216080 }, "assemblies/System.Net.NameResolution.dll": { - "Size": 9924 + "Size": 13111 }, "assemblies/System.Net.NetworkInformation.dll": { - "Size": 17325 + "Size": 17580 }, "assemblies/System.Net.Primitives.dll": { - "Size": 41156 + "Size": 41361 }, "assemblies/System.Net.Quic.dll": { - "Size": 43453 + "Size": 44623 + }, + "assemblies/System.Net.Requests.dll": { + "Size": 3289 }, "assemblies/System.Net.Security.dll": { - "Size": 57317 + "Size": 57687 }, "assemblies/System.Net.Sockets.dll": { - "Size": 54458 + "Size": 54604 }, "assemblies/System.ObjectModel.dll": { - "Size": 11316 + "Size": 11995 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 698582 + "Size": 771706 }, "assemblies/System.Private.DataContractSerialization.dll": { - "Size": 193066 + "Size": 193601 }, "assemblies/System.Private.Uri.dll": { - "Size": 43183 + "Size": 43208 }, "assemblies/System.Private.Xml.dll": { - "Size": 251151 + "Size": 251455 }, "assemblies/System.Private.Xml.Linq.dll": { - "Size": 15060 + "Size": 17099 }, "assemblies/System.Runtime.CompilerServices.Unsafe.dll": { "Size": 1342 }, "assemblies/System.Runtime.dll": { - "Size": 2461 + "Size": 2457 }, "assemblies/System.Runtime.InteropServices.RuntimeInformation.dll": { - "Size": 2915 + "Size": 2913 }, "assemblies/System.Runtime.Numerics.dll": { - "Size": 21157 + "Size": 24134 }, "assemblies/System.Runtime.Serialization.dll": { - "Size": 1938 + "Size": 1934 }, "assemblies/System.Runtime.Serialization.Formatters.dll": { - "Size": 2675 + "Size": 2673 }, "assemblies/System.Runtime.Serialization.Primitives.dll": { - "Size": 3978 + "Size": 3974 }, "assemblies/System.Security.Cryptography.Algorithms.dll": { - "Size": 42389 + "Size": 42474 }, "assemblies/System.Security.Cryptography.Encoding.dll": { - "Size": 13812 + "Size": 13922 }, "assemblies/System.Security.Cryptography.Primitives.dll": { - "Size": 8844 + "Size": 8840 }, "assemblies/System.Security.Cryptography.X509Certificates.dll": { - "Size": 76400 + "Size": 76763 }, "assemblies/System.Text.RegularExpressions.dll": { - "Size": 76502 + "Size": 76678 }, "assemblies/System.Threading.Channels.dll": { - "Size": 16782 + "Size": 16845 }, "assemblies/System.Xml.dll": { - "Size": 1822 + "Size": 1819 }, "assemblies/UnnamedProject.dll": { - "Size": 117075 + "Size": 117076 }, "assemblies/Xamarin.AndroidX.Activity.dll": { "Size": 6067 @@ -236,22 +239,22 @@ "Size": 3455324 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 337816 + "Size": 337896 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3269936 + "Size": 3171872 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { "Size": 776216 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 79968 + "Size": 88160 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { "Size": 150024 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 126744 + "Size": 126816 }, "META-INF/android.support.design_material.version": { "Size": 12 @@ -260,7 +263,7 @@ "Size": 1213 }, "META-INF/ANDROIDD.SF": { - "Size": 80418 + "Size": 80539 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -371,7 +374,7 @@ "Size": 10 }, "META-INF/MANIFEST.MF": { - "Size": 80291 + "Size": 80412 }, "META-INF/proguard/androidx-annotations.pro": { "Size": 339 @@ -2003,5 +2006,5 @@ "Size": 341040 } }, - "PackageSize": 8459870 + "PackageSize": 8521405 } \ No newline at end of file diff --git a/tests/MSBuildDeviceIntegration/Tests/BundleToolTests.cs b/tests/MSBuildDeviceIntegration/Tests/BundleToolTests.cs index fe03147f91b..fb4f4b541df 100644 --- a/tests/MSBuildDeviceIntegration/Tests/BundleToolTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/BundleToolTests.cs @@ -138,7 +138,6 @@ public void BaseZip () }; if (Builder.UseDotNet) { expectedFiles.Add ("root/assemblies/System.Console.dll"); - expectedFiles.Add ("root/assemblies/System.IO.FileSystem.dll"); expectedFiles.Add ("root/assemblies/System.Linq.dll"); expectedFiles.Add ("root/assemblies/System.Net.Http.dll"); @@ -202,7 +201,6 @@ public void AppBundle () }; if (Builder.UseDotNet) { expectedFiles.Add ("base/root/assemblies/System.Console.dll"); - expectedFiles.Add ("base/root/assemblies/System.IO.FileSystem.dll"); expectedFiles.Add ("base/root/assemblies/System.Linq.dll"); expectedFiles.Add ("base/root/assemblies/System.Net.Http.dll"); diff --git a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidClientHandlerTests.cs b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidClientHandlerTests.cs index c249382a315..9b504a033e6 100644 --- a/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidClientHandlerTests.cs +++ b/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidClientHandlerTests.cs @@ -88,7 +88,11 @@ public void Properties_Defaults () Assert.IsTrue (h.UseCookies, "#12"); Assert.IsFalse (h.UseDefaultCredentials, "#13"); Assert.IsTrue (h.UseProxy, "#14"); - Assert.AreEqual (ClientCertificateOption.Manual, h.ClientCertificateOptions, "#15"); + try { + Assert.AreEqual (ClientCertificateOption.Manual, h.ClientCertificateOptions, "#15"); + } catch (PlatformNotSupportedException) { + // https://github.com/dotnet/runtime/blob/07336810acf3b4e7bdd0fb7da87b54920ea9c382/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.AnyMobile.cs#L310-L314 + } } [Test] @@ -99,12 +103,16 @@ public void Properties_Invalid () h.MaxAutomaticRedirections = 0; Assert.Fail ("#1"); } catch (ArgumentOutOfRangeException) { + } catch (TargetInvocationException) { + // See: https://github.com/dotnet/runtime/issues/56089 } try { h.MaxRequestContentBufferSize = -1; Assert.Fail ("#2"); } catch (ArgumentOutOfRangeException) { + } catch (TargetInvocationException) { + // See: https://github.com/dotnet/runtime/issues/56089 } } diff --git a/tests/api-compatibility/api-compat-exclude-attributes.txt b/tests/api-compatibility/api-compat-exclude-attributes.txt index 26482ab497b..3e1e8a5db81 100644 --- a/tests/api-compatibility/api-compat-exclude-attributes.txt +++ b/tests/api-compatibility/api-compat-exclude-attributes.txt @@ -8,6 +8,7 @@ T:System.Diagnostics.DebuggerStepThroughAttribute T:System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute T:System.Runtime.CompilerServices.AsyncStateMachineAttribute +T:System.Runtime.CompilerServices.CompilerGeneratedAttribute T:System.Runtime.CompilerServices.IteratorStateMachineAttribute T:System.Runtime.CompilerServices.NullableAttribute