diff --git a/build-tools/automation/azure-pipelines.yaml b/build-tools/automation/azure-pipelines.yaml
index d5d00bf91ee..aa8585666cb 100644
--- a/build-tools/automation/azure-pipelines.yaml
+++ b/build-tools/automation/azure-pipelines.yaml
@@ -171,6 +171,8 @@ stages:
- template: yaml-templates\clean.yaml
+ - template: yaml-templates\delete-platform-31.yaml
+
- script: |
echo ##vso[task.setvariable variable=JI_JAVA_HOME]%USERPROFILE%\android-toolchain\$(XA.Jdk11.Folder)
displayName: set JI_JAVA_HOME
@@ -291,6 +293,8 @@ stages:
- template: yaml-templates\clean.yaml
+ - template: yaml-templates\delete-platform-31.yaml
+
- script: |
echo ##vso[task.setvariable variable=JI_JAVA_HOME]%USERPROFILE%\android-toolchain\$(XA.Jdk11.Folder)
displayName: set JI_JAVA_HOME
@@ -1258,6 +1262,18 @@ stages:
usePipelineArtifactTasks: true
condition: eq(variables['MicroBuildSignType'], 'Real')
+ # Check - "Xamarin.Android (.NET 6 Preview Installers Convert NuGet to MSI)"
+ - template: nuget-msi-convert/job/v1.yml@yaml
+ parameters:
+ yamlResourceName: yaml
+ dependsOn: signing
+ artifactName: nuget-signed
+ artifactPatterns: |
+ !*Darwin*
+ propsArtifactName: nuget-unsigned
+ signType: $(MicroBuildSignType)
+ condition: eq(variables['MicroBuildSignType'], 'Real')
+
# Check - "Xamarin.Android (.NET 6 Preview Installers Create .pkg)"
- job: dotnet_create_pkg
displayName: Create .pkg
@@ -1454,6 +1470,14 @@ stages:
solution: build-tools\Xamarin.Android.Tools.BootstrapTasks\Xamarin.Android.Tools.BootstrapTasks.csproj
artifactName: Build Results - .NET 6 Preview .msi
+# .NET 6 VS Insertion Stage
+# Check - "Xamarin.Android (VS Insertion - Wait For Approval)"
+# Check - "Xamarin.Android (VS Insertion - Create VS Drop and Open PR)"
+- template: vs-insertion/stage/v1.yml@yaml
+ parameters:
+ dependsOn: dotnet_installers
+ condition: eq(variables['MicroBuildSignType'], 'Real')
+
- stage: finalize_installers
displayName: Finalize Installers
dependsOn: mac_build
diff --git a/build-tools/automation/yaml-templates/delete-platform-31.yaml b/build-tools/automation/yaml-templates/delete-platform-31.yaml
new file mode 100644
index 00000000000..f38011393eb
--- /dev/null
+++ b/build-tools/automation/yaml-templates/delete-platform-31.yaml
@@ -0,0 +1,10 @@
+# NOTE: temporary workaround to delete platform-31
+steps:
+
+- script: rm -rfv "$HOME/Library/Android/sdk/platforms/android-31"
+ displayName: delete API 31
+ condition: and(succeeded(), eq(variables['agent.os'], 'Darwin'))
+
+- script: if exist "%USERPROFILE%\android-toolchain\sdk\platforms\android-31" rmdir /s /q "%USERPROFILE%\android-toolchain\sdk\platforms\android-31"
+ displayName: delete API 31
+ condition: and(succeeded(), eq(variables['agent.os'], 'Windows_NT'))
diff --git a/build-tools/automation/yaml-templates/setup-test-environment.yaml b/build-tools/automation/yaml-templates/setup-test-environment.yaml
index c8978481cd3..1c2b31fa81e 100644
--- a/build-tools/automation/yaml-templates/setup-test-environment.yaml
+++ b/build-tools/automation/yaml-templates/setup-test-environment.yaml
@@ -19,6 +19,8 @@ steps:
parameters:
provisionExtraArgs: ${{ parameters.provisionExtraArgs }}
+- template: delete-platform-31.yaml
+
- script: |
echo "##vso[task.setvariable variable=JI_JAVA_HOME]$HOME/Library/Android/${{ parameters.jdkTestFolder }}"
echo "##vso[task.setvariable variable=DOTNET_TOOL_PATH]$HOME/Library/Android/dotnet/dotnet"
diff --git a/build-tools/create-packs/Directory.Build.targets b/build-tools/create-packs/Directory.Build.targets
index ad697aa34e0..a7a0ab1c742 100644
--- a/build-tools/create-packs/Directory.Build.targets
+++ b/build-tools/create-packs/Directory.Build.targets
@@ -11,6 +11,7 @@
+
<_MonoAndroidNETOutputDir>$(XAInstallPrefix)xbuild-frameworks\Microsoft.Android\net6.0\
@@ -56,7 +57,7 @@
+ DependsOnTargets="DeleteExtractedWorkloadPacks;_SetGlobalProperties;GetXAVersionInfo">
@@ -69,6 +70,11 @@
+
+
+
+ Microsoft.NET.Sdk.Android.Workload
+
+
+
+
+ @PACK_VERSION_SHORT@
+
+
+
+
+
+
+
diff --git a/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.cs b/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.cs
index 1e51f1bf1eb..148d4372e04 100644
--- a/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.cs
+++ b/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.cs
@@ -15,7 +15,7 @@ namespace Xamarin.Android.Prepare
//
partial class Configurables
{
- const string BinutilsVersion = "2.36.1-XA.1";
+ const string BinutilsVersion = "2.37-XA.1";
const string MicrosoftOpenJDK11Version = "11.0.10";
const string MicrosoftOpenJDK11Release = "9.1";
diff --git a/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs b/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs
index 641f56c7cd9..540f38c817a 100644
--- a/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs
+++ b/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs
@@ -21,65 +21,8 @@ protected override async Task Execute (Context context)
var dotnetPreviewVersion = context.Properties.GetRequiredValue (KnownProperties.MicrosoftDotnetSdkInternalPackageVersion);
var dotnetTestRuntimeVersion = Configurables.Defaults.DotNetTestRuntimeVersion;
- // Delete any custom Microsoft.Android packs that may have been installed by test runs. Other ref/runtime packs will be ignored.
- var packsPath = Path.Combine (dotnetPath, "packs");
- if (Directory.Exists (packsPath)) {
- foreach (var packToRemove in Directory.EnumerateDirectories (packsPath)) {
- var info = new DirectoryInfo (packToRemove);
- if (info.Name.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1) {
- Log.StatusLine ($"Removing Android pack: {packToRemove}");
- Utilities.DeleteDirectory (packToRemove);
- }
- }
- }
-
- // Delete Workload manifests, such as sdk-manifests/6.0.100/Microsoft.NET.Sdk.Android
- var sdkManifestsPath = Path.Combine (dotnetPath, "sdk-manifests");
- if (Directory.Exists (sdkManifestsPath)) {
- foreach (var versionBand in Directory.EnumerateDirectories (sdkManifestsPath)) {
- foreach (var workloadManifestDirectory in Directory.EnumerateDirectories (versionBand)) {
- var info = new DirectoryInfo (workloadManifestDirectory);
- if (info.Name.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1) {
- Log.StatusLine ($"Removing Android manifest directory: {workloadManifestDirectory}");
- Utilities.DeleteDirectory (workloadManifestDirectory);
- }
- }
- }
- }
-
- // Delete any unnecessary SDKs if they exist.
- var sdkPath = Path.Combine (dotnetPath, "sdk");
- if (Directory.Exists (sdkPath)) {
- foreach (var sdkToRemove in Directory.EnumerateDirectories (sdkPath).Where (s => new DirectoryInfo (s).Name != dotnetPreviewVersion)) {
- Log.StatusLine ($"Removing out of date SDK: {sdkToRemove}");
- Utilities.DeleteDirectory (sdkToRemove);
- }
- }
-
- // Delete Android template-packs
- var templatePacksPath = Path.Combine (dotnetPath, "template-packs");
- if (Directory.Exists (templatePacksPath)) {
- foreach (var templateToRemove in Directory.EnumerateFiles (templatePacksPath)) {
- var name = Path.GetFileName (templateToRemove);
- if (name.IndexOf ("Android", StringComparison.OrdinalIgnoreCase) != -1) {
- Log.StatusLine ($"Removing Android template: {templateToRemove}");
- Utilities.DeleteFile (templateToRemove);
- }
- }
- }
-
- // Delete the metadata folder, which contains old workload data
- var metadataPath = Path.Combine (dotnetPath, "metadata");
- if (Directory.Exists (metadataPath)) {
- Utilities.DeleteDirectory (metadataPath);
- }
-
- if (File.Exists (dotnetTool)) {
- if (!TestDotNetSdk (dotnetTool)) {
- Log.WarningLine ($"Attempt to run `dotnet --version` failed, reinstalling the SDK.");
- Utilities.DeleteDirectory (dotnetPath);
- }
- }
+ // Always delete the ~/android-toolchain/dotnet/ directory
+ Utilities.DeleteDirectory (dotnetPath);
if (!await InstallDotNetAsync (context, dotnetPath, dotnetPreviewVersion)) {
Log.ErrorLine ($"Installation of dotnet SDK {dotnetPreviewVersion} failed.");
@@ -108,12 +51,6 @@ protected override async Task Execute (Context context)
Utilities.CopyFileToDir (file, destination);
}
- // Install the microsoft-net-runtime-android workload
- if (!Utilities.RunCommand (dotnetTool, BuildPaths.XamarinAndroidSourceRoot, ignoreEmptyArguments: false, new [] { "workload", "install", "microsoft-net-runtime-android", "--skip-manifest-update", "--verbosity", "diag" })) {
- Log.ErrorLine ($"dotnet workload install failed.");
- return false;
- }
-
return true;
}
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 7d167e55418..aa5774f5c82 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -1,16 +1,16 @@
-
+
https://github.com/dotnet/installer
- e8b3b6bea1e37086869ba9aeafe65caa298537e7
+ 19943da31a3470cf59f363f46956edd8a8451af8
-
+
https://github.com/mono/linker
- a07cab7b71a1321a9e68571c0b6095144a177b4e
+ 6eae01980dc694107bdee0bc723d75a0dd601f0e
-
+
https://github.com/dotnet/runtime
- 02f70d0b903422282cd7ba8037de6b66ea0b7a2d
+ 14d6532f388903f425f0a3b9dea697c5a28293d3
diff --git a/eng/Versions.props b/eng/Versions.props
index 5045d899556..6fc0904faf1 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-rc.1.21372.10
+ 6.0.100-preview.6.21370.1
5.0.0-beta.20181.7
6.0.0-beta.21212.6
- 6.0.0-preview.7.21326.8
+ 6.0.0-rc.1.21372.1
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.Export/CallbackCode.cs b/src/Mono.Android.Export/CallbackCode.cs
index 2aedbdb8004..dafd6fba0d7 100644
--- a/src/Mono.Android.Export/CallbackCode.cs
+++ b/src/Mono.Android.Export/CallbackCode.cs
@@ -407,7 +407,7 @@ public CodeExpression FromNative (CodeExpression arg)
}
// Ignore ToNative() overload that takes generic instancing mapping. The reflected method should have nothing to do with it.
- [DynamicDependency ("ToLocalJniHandle", "Android.Runtime.JavaArray", "Mono.Android")]
+ [DynamicDependency ("ToLocalJniHandle", "Android.Runtime.JavaArray`1", "Mono.Android")]
[DynamicDependency ("ToLocalJniHandle", "Android.Runtime.JavaCollection", "Mono.Android")]
[DynamicDependency ("ToLocalJniHandle", "Android.Runtime.JavaCollection`1", "Mono.Android")]
[DynamicDependency ("ToLocalJniHandle", "Android.Runtime.JavaDictionary", "Mono.Android")]
diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs
index 5dc17a24f49..6cdd1545626 100644
--- a/src/Mono.Android/Android.Runtime/JNIEnv.cs
+++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs
@@ -247,7 +247,9 @@ static void ManualJavaObjectDispose (Java.Lang.Object obj)
internal static Action mono_unhandled_exception = null!;
#endif // NETCOREAPP
+#if !NETCOREAPP
static Action AppDomain_DoUnhandledException = null!;
+#endif // ndef NETCOREAPP
static void Initialize ()
{
@@ -258,6 +260,7 @@ static void Initialize ()
mono_unhandled_exception = (Action) Delegate.CreateDelegate (typeof(Action), mono_UnhandledException);
}
+#if !NETCOREAPP
if (AppDomain_DoUnhandledException == null) {
var ad_due = typeof (AppDomain)
.GetMethod ("DoUnhandledException",
@@ -270,8 +273,14 @@ static void Initialize ()
typeof (Action), ad_due);
}
}
+#endif // ndef NETCOREAPP
}
+#if NETCOREAPP
+ [MethodImplAttribute(MethodImplOptions.InternalCall)]
+ extern static void monodroid_unhandled_exception (Exception javaException);
+#endif // def NETCOREAPP
+
internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr)
{
if (!PropagateExceptions)
@@ -292,14 +301,18 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt
try {
var jltp = javaException as JavaProxyThrowable;
Exception? innerException = jltp?.InnerException;
- var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
Logger.Log (LogLevel.Info, "MonoDroid", "UNHANDLED EXCEPTION:");
Logger.Log (LogLevel.Info, "MonoDroid", javaException.ToString ());
+#if !NETCOREAPP
+ var args = new UnhandledExceptionEventArgs (innerException ?? javaException, isTerminating: true);
// Disabled until Linker error surfaced in https://github.com/xamarin/xamarin-android/pull/4302#issuecomment-596400025 is resolved
//AppDomain.CurrentDomain.DoUnhandledException (args);
AppDomain_DoUnhandledException?.Invoke (AppDomain.CurrentDomain, args);
+#else // ndef NETCOREAPP
+ monodroid_unhandled_exception (innerException ?? javaException);
+#endif // def NETCOREAPP
} catch (Exception e) {
Logger.Log (LogLevel.Error, "monodroid", "Exception thrown while raising AppDomain.UnhandledException event: " + e.ToString ());
}
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/Microsoft.NET.Sdk.Android/WorkloadManifest.in.json b/src/Xamarin.Android.Build.Tasks/Microsoft.NET.Sdk.Android/WorkloadManifest.in.json
index 71732354a8b..b33670471fb 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.NET.Sdk.Android/WorkloadManifest.in.json
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.NET.Sdk.Android/WorkloadManifest.in.json
@@ -2,7 +2,7 @@
"version": "@WORKLOAD_VERSION@",
"workloads": {
"android": {
- "description": "Microsoft Android SDK for .NET",
+ "description": ".NET SDK Workload for building Android applications.",
"packs": [
"Microsoft.Android.Sdk",
"Microsoft.Android.Sdk.BundleTool",
@@ -16,7 +16,7 @@
"extends" : [ "microsoft-net-runtime-android" ]
},
"android-aot": {
- "description": "Microsoft Android SDK for .NET with AOT support",
+ "description": ".NET SDK Workload for building Android applications with AOT support.",
"extends" : [ "android", "microsoft-net-runtime-android-aot" ]
}
},
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..d9ef8b4ffef 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": 78850
},
"assemblies/rc.bin": {
- "Size": 802
+ "Size": 863
},
"assemblies/System.Linq.dll": {
- "Size": 10150
+ "Size": 10132
},
"assemblies/System.Private.CoreLib.dll": {
- "Size": 495560
+ "Size": 507773
},
"assemblies/System.Runtime.dll": {
- "Size": 2262
+ "Size": 2246
},
"assemblies/UnnamedProject.dll": {
- "Size": 3173
+ "Size": 3175
},
"classes.dex": {
"Size": 316792
},
"lib/arm64-v8a/libmonodroid.so": {
- "Size": 337816
+ "Size": 337896
},
"lib/arm64-v8a/libmonosgen-2.0.so": {
- "Size": 3269936
+ "Size": 3167776
},
"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..889b22a8c09 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": 3818
},
"assemblies/Mono.Android.dll": {
- "Size": 398294
+ "Size": 415847
},
"assemblies/mscorlib.dll": {
- "Size": 3832
+ "Size": 3818
},
"assemblies/netstandard.dll": {
- "Size": 5521
+ "Size": 5504
},
"assemblies/rc.bin": {
- "Size": 802
+ "Size": 863
},
"assemblies/System.Collections.Concurrent.dll": {
- "Size": 11579
+ "Size": 11566
},
"assemblies/System.Collections.dll": {
- "Size": 19185
+ "Size": 19021
},
"assemblies/System.Collections.NonGeneric.dll": {
- "Size": 8471
+ "Size": 8461
},
"assemblies/System.ComponentModel.dll": {
- "Size": 2002
+ "Size": 1984
},
"assemblies/System.ComponentModel.Primitives.dll": {
- "Size": 2607
+ "Size": 2588
},
"assemblies/System.ComponentModel.TypeConverter.dll": {
- "Size": 6993
+ "Size": 5996
},
"assemblies/System.Console.dll": {
- "Size": 5839
+ "Size": 5824
},
"assemblies/System.Core.dll": {
- "Size": 1968
+ "Size": 1951
+ },
+ "assemblies/System.Diagnostics.DiagnosticSource.dll": {
+ "Size": 3015
},
"assemblies/System.Diagnostics.TraceSource.dll": {
- "Size": 6800
+ "Size": 6779
},
"assemblies/System.dll": {
- "Size": 2315
+ "Size": 2298
},
"assemblies/System.Drawing.dll": {
- "Size": 2000
+ "Size": 1985
},
"assemblies/System.Drawing.Primitives.dll": {
- "Size": 12154
+ "Size": 12228
},
"assemblies/System.Formats.Asn1.dll": {
- "Size": 26856
+ "Size": 26839
},
"assemblies/System.IO.Compression.Brotli.dll": {
- "Size": 11461
+ "Size": 11443
},
"assemblies/System.IO.Compression.dll": {
- "Size": 18810
- },
- "assemblies/System.IO.FileSystem.dll": {
- "Size": 1964
+ "Size": 18802
},
"assemblies/System.IO.IsolatedStorage.dll": {
- "Size": 10628
+ "Size": 10780
},
"assemblies/System.Linq.dll": {
- "Size": 19504
+ "Size": 19492
},
"assemblies/System.Linq.Expressions.dll": {
- "Size": 181323
+ "Size": 181671
},
"assemblies/System.Net.Http.dll": {
- "Size": 211166
+ "Size": 216071
},
"assemblies/System.Net.NameResolution.dll": {
- "Size": 9924
+ "Size": 13102
},
"assemblies/System.Net.NetworkInformation.dll": {
- "Size": 17325
+ "Size": 17566
},
"assemblies/System.Net.Primitives.dll": {
- "Size": 41156
+ "Size": 41353
},
"assemblies/System.Net.Quic.dll": {
- "Size": 43453
+ "Size": 44610
+ },
+ "assemblies/System.Net.Requests.dll": {
+ "Size": 3271
},
"assemblies/System.Net.Security.dll": {
- "Size": 57317
+ "Size": 57683
},
"assemblies/System.Net.Sockets.dll": {
- "Size": 54458
+ "Size": 54590
},
"assemblies/System.ObjectModel.dll": {
- "Size": 11316
+ "Size": 11988
},
"assemblies/System.Private.CoreLib.dll": {
- "Size": 698582
+ "Size": 772013
},
"assemblies/System.Private.DataContractSerialization.dll": {
- "Size": 193066
+ "Size": 193651
},
"assemblies/System.Private.Uri.dll": {
- "Size": 43183
+ "Size": 43326
},
"assemblies/System.Private.Xml.dll": {
- "Size": 251151
+ "Size": 251560
},
"assemblies/System.Private.Xml.Linq.dll": {
- "Size": 15060
+ "Size": 17087
},
"assemblies/System.Runtime.CompilerServices.Unsafe.dll": {
- "Size": 1342
+ "Size": 1339
},
"assemblies/System.Runtime.dll": {
- "Size": 2461
+ "Size": 2447
},
"assemblies/System.Runtime.InteropServices.RuntimeInformation.dll": {
- "Size": 2915
+ "Size": 2901
},
"assemblies/System.Runtime.Numerics.dll": {
- "Size": 21157
+ "Size": 24128
},
"assemblies/System.Runtime.Serialization.dll": {
- "Size": 1938
+ "Size": 1921
},
"assemblies/System.Runtime.Serialization.Formatters.dll": {
- "Size": 2675
+ "Size": 2652
},
"assemblies/System.Runtime.Serialization.Primitives.dll": {
- "Size": 3978
+ "Size": 3966
},
"assemblies/System.Security.Cryptography.Algorithms.dll": {
- "Size": 42389
+ "Size": 42462
},
"assemblies/System.Security.Cryptography.Encoding.dll": {
- "Size": 13812
+ "Size": 13908
},
"assemblies/System.Security.Cryptography.Primitives.dll": {
- "Size": 8844
+ "Size": 8832
},
"assemblies/System.Security.Cryptography.X509Certificates.dll": {
- "Size": 76400
+ "Size": 76749
},
"assemblies/System.Text.RegularExpressions.dll": {
- "Size": 76502
+ "Size": 76721
},
"assemblies/System.Threading.Channels.dll": {
- "Size": 16782
+ "Size": 16835
},
"assemblies/System.Xml.dll": {
- "Size": 1822
+ "Size": 1807
},
"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": 3167776
},
"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/src/monodroid/jni/monodroid-glue-internal.hh b/src/monodroid/jni/monodroid-glue-internal.hh
index b82c8e722f4..1e567f30a64 100644
--- a/src/monodroid/jni/monodroid-glue-internal.hh
+++ b/src/monodroid/jni/monodroid-glue-internal.hh
@@ -261,6 +261,8 @@ namespace xamarin::android::internal
}
#if defined (NET6)
+ static void monodroid_unhandled_exception (MonoObject *java_exception);
+
MonoClass* get_android_runtime_class ();
#else // def NET6
MonoClass* get_android_runtime_class (MonoDomain *domain);
diff --git a/src/monodroid/jni/monodroid-glue.cc b/src/monodroid/jni/monodroid-glue.cc
index 0484e598cb5..2b07d43cb8c 100644
--- a/src/monodroid/jni/monodroid-glue.cc
+++ b/src/monodroid/jni/monodroid-glue.cc
@@ -27,6 +27,7 @@
#include
#include
#include
+#include
#include
#include
@@ -1019,7 +1020,8 @@ MonodroidRuntime::init_android_runtime (
mono_add_internal_call ("Android.Runtime.JNIEnv::monodroid_typemap_managed_to_java", reinterpret_cast(typemap_managed_to_java));
#if defined (NET6)
mono_add_internal_call ("Android.Runtime.JNIEnv::monodroid_debugger_unhandled_exception", reinterpret_cast (monodroid_debugger_unhandled_exception));
-#endif
+ mono_add_internal_call ("Android.Runtime.JNIEnv::monodroid_unhandled_exception", reinterpret_cast(monodroid_unhandled_exception));
+#endif // def NET6
struct JnienvInitializeArgs init = {};
init.javaVm = osBridge.get_jvm ();
@@ -1837,6 +1839,14 @@ MonodroidRuntime::create_and_initialize_domain (JNIEnv* env, jclass runtimeClass
return domain;
}
+#if defined (NET6)
+void
+MonodroidRuntime::monodroid_unhandled_exception (MonoObject *java_exception)
+{
+ mono_unhandled_exception (java_exception);
+}
+#endif // def NET6
+
MonoReflectionType*
MonodroidRuntime::typemap_java_to_managed (MonoString *java_type_name)
{
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/MSBuildDeviceIntegration/Tests/UncaughtExceptionTests.cs b/tests/MSBuildDeviceIntegration/Tests/UncaughtExceptionTests.cs
new file mode 100644
index 00000000000..609ebbf6486
--- /dev/null
+++ b/tests/MSBuildDeviceIntegration/Tests/UncaughtExceptionTests.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.RegularExpressions;
+using NUnit.Framework;
+using Xamarin.ProjectTools;
+
+namespace Xamarin.Android.Build.Tests
+{
+ [TestFixture]
+ [Category ("UsesDevice")]
+ public class UncaughtExceptionTests : DeviceTest
+ {
+ class LogcatLine
+ {
+ public string Text;
+ public bool Found = false;
+ public int SequenceNumber = -1;
+ public int Count = 0;
+ };
+
+ [Test]
+ public void EnsureUncaughtExceptionWorks ()
+ {
+ AssertHasDevices ();
+
+ var lib = new XamarinAndroidBindingProject {
+ ProjectName = "Scratch.Try",
+ AndroidClassParser = "class-parse",
+ };
+
+ lib.Imports.Add (
+ new Import (() => "Directory.Build.targets") {
+ TextContent = () =>
+@"
+
+ 1.8
+ 1.8
+ javac
+ jar
+
+
+
+
+
+
+
+
+
+
+ <_Classes>$(IntermediateOutputPath)classes
+
+
+
+ '"%(Identity)"', ' ')"" />
+
+
+
+"
+ });
+
+ lib.Sources.Add (
+ new BuildItem.NoActionResource ("java\\testing\\Run.java") {
+ Encoding = new System.Text.UTF8Encoding (encoderShouldEmitUTF8Identifier: false),
+ TextContent = () =>
+@"package testing;
+
+public final class Run {
+ private Run() {
+ }
+
+ public static interface CatchThrowableHandler {
+ void onCatch(Throwable t);
+ }
+
+ public static final void tryCatchFinally (Runnable r, CatchThrowableHandler c, Runnable f) {
+ try {
+ r.run();
+ }
+ catch (Throwable t) {
+ c.onCatch(t);
+ }
+ finally {
+ f.run();
+ }
+ }
+}
+"
+ });
+
+ var app = new XamarinAndroidApplicationProject {
+ ProjectName = "Scratch.JMJMException",
+ };
+
+ app.SetDefaultTargetDevice ();
+ app.AddReference (lib);
+
+ app.Sources.Remove (app.GetItem ("MainActivity.cs"));
+
+ string mainActivityTemplate = @"using System;
+using Android.App;
+using Android.OS;
+using Android.Runtime;
+using Android.Views;
+using Android.Widget;
+using Testing;
+
+namespace Scratch.JMJMException
+{
+ [Register (""${JAVA_PACKAGENAME}.MainActivity""), Activity (Label = ""${PROJECT_NAME}"", MainLauncher = true, Icon = ""@drawable/icon"")]
+ public class MainActivity : Activity
+ {
+ protected override void OnCreate (Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ Button b = new Button (this) {
+ Text = ""Click Me!"",
+ };
+
+ Testing.Run.TryCatchFinally (
+ new Java.Lang.Runnable (() => {
+ Console.WriteLine (""#UET-1# jon: Should be in a Java > Managed [MainActivity.OnCreate] > Java [Run.tryCatchFinally] > Managed [Run] frame. Throwing an exception..."");
+ Console.WriteLine (new System.Diagnostics.StackTrace(fNeedFileInfo: true).ToString());
+ throw new Exception (""Should be in a Java > Managed [MainActivity.OnCreate] > Java [Run.tryCatchFinally] > Managed [Run] frame. Throwing an exception..."");
+ }),
+ new MyCatchHandler (),
+ new Java.Lang.Runnable (() => {
+ Console.WriteLine ($""#UET-3# jon: from Java finally block"");
+ })
+ );
+
+ SetContentView (b);
+ }
+ }
+
+ class MyCatchHandler : Java.Lang.Object, Run.ICatchThrowableHandler
+ {
+ public void OnCatch (Java.Lang.Throwable t)
+ {
+ Console.WriteLine ($""#UET-2# jon: MyCatchHandler.OnCatch: t={t.ToString()}"");
+ }
+ }
+}
+";
+ string mainActivity = app.ProcessSourceTemplate (mainActivityTemplate);
+ app.Sources.Add (
+ new BuildItem.Source ("MainActivity.cs") {
+ TextContent = () => mainActivity
+ }
+ );
+
+ var expectedLogLines = new LogcatLine[] {
+ new LogcatLine { Text = "#UET-1#" },
+ new LogcatLine { Text = "#UET-2#" },
+ new LogcatLine { Text = "#UET-3#" },
+ };
+
+ string path = Path.Combine ("temp", TestName);
+ using (var libBuilder = CreateDllBuilder (Path.Combine (path, lib.ProjectName)))
+ using (var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName))) {
+ Assert.True (libBuilder.Build (lib), "Library should have built.");
+ Assert.IsTrue (appBuilder.Install (app), "Install should have succeeded.");
+
+ ClearAdbLogcat ();
+
+ AdbStartActivity ($"{app.PackageName}/{app.JavaPackageName}.MainActivity");
+
+ string logcatPath = Path.Combine (Root, appBuilder.ProjectDirectory, "logcat.log");
+ int sequenceCounter = 0;
+ MonitorAdbLogcat (
+ (string line) => {
+ foreach (LogcatLine ll in expectedLogLines) {
+ if (line.IndexOf (ll.Text, StringComparison.Ordinal) < 0) {
+ continue;
+ }
+ ll.Found = true;
+ ll.Count++;
+ ll.SequenceNumber = sequenceCounter++;
+ break;
+ }
+ return false; // we must examine all the lines, and returning `true` aborts the monitoring process
+ }, logcatPath, 15);
+ }
+
+ AssertValidLine (0, 0);
+ AssertValidLine (1, 1);
+ AssertValidLine (2, 2);
+
+ void AssertValidLine (int idx, int expectedSequence)
+ {
+ LogcatLine ll = expectedLogLines [idx];
+ Assert.IsTrue (ll.Found, $"Logcat line {idx} was not found");
+ Assert.IsTrue (ll.Count == 1, $"Logcat line {idx} should have been found only once but it was found {ll.Count} times");
+ Assert.IsTrue (ll.SequenceNumber == expectedSequence, $"Logcat line {idx} sequence number should be {expectedSequence} but it was {ll.SequenceNumber}");
+ }
+ }
+ }
+}
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