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