From 74395f26821d8265c38fd922dbdda73e3d9b6a34 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Fri, 2 Jul 2021 10:37:18 -0400 Subject: [PATCH 01/21] Create AndroidMessageHandler and restructure AndroidClientHander to call into it --- .../AndroidClientHandler.cs | 794 +------------ .../AndroidMessageHandler.cs | 1016 +++++++++++++++++ 2 files changed, 1066 insertions(+), 744 deletions(-) create mode 100644 src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs index 9271e7eda2d..c33c1086b6e 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs @@ -58,50 +58,21 @@ 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 () + { + if (IsSocketHandler) + { + // maybe some other exception + throw new InvalidOperationException("SocketsHttpHandler is not supported as an underlying handler by AndroidClientHandler"); + } + + _underlyingHander = (AndroidMessageHandler) GetUnderlyingHandler (); + } /// /// @@ -116,7 +87,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 +104,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 +141,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 +163,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 +183,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 +203,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 +226,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); - } - } + 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 _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]); - } + return _underlyingHander.WriteRequestContentToOutput (request, httpConnection, cancellationToken); } /// @@ -762,8 +249,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 +263,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti { AssertSelf (); - return keyStore; + return _underlyingHander.ConfigureKeyStore (keyStore); } /// @@ -793,7 +279,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti { AssertSelf (); - return null; + return _underlyingHander.ConfigureKeyManagerFactory (keyStore); } /// @@ -810,71 +296,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 +312,7 @@ 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); - } - - 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); + return _underlyingHander.ConfigureCustomSSLSocketFactory (connection); } } } 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..3912de34417 --- /dev/null +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -0,0 +1,1016 @@ +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 bool UseCookies { get; set; } + + public DecompressionMethods AutomaticDecompression + { + get => _decompressionMethods; + set => _decompressionMethods = value; + } + + public CookieContainer CookieContainer + { + get => _cookieContainer; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _cookieContainer = value; + } + } + + public bool PreAuthenticate { get; set; } + + public bool UseProxy { get; set; } + + public ICredentials? Credentials { get; set; } + + public bool AllowAutoRedirect { get; set; } + + public int MaxAutomaticRedirections { get; set; } + + /// + /// + /// 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) { + 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); + } + + 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 From 75640feb2d7b6134b7b13fb1fbff332e5e1ea23d Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Mon, 5 Jul 2021 12:15:26 +0000 Subject: [PATCH 02/21] Update dependencies from https://github.com/dotnet/installer build 20210704.4 Microsoft.Dotnet.Sdk.Internal From Version 6.0.100-preview.7.21327.2 -> To Version 6.0.100-preview.7.21354.4 Dependency coherency updates Microsoft.NET.ILLink.Tasks,Microsoft.NETCore.App.Ref From Version 6.0.100-preview.6.21322.1 -> To Version 6.0.100-preview.6.21330.1 (parent: Microsoft.Dotnet.Sdk.Internal --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7d167e55418..4875ec233fa 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,16 +1,16 @@ - + https://github.com/dotnet/installer - e8b3b6bea1e37086869ba9aeafe65caa298537e7 + bd5653f30e86a0bf8069bfc976b267c9eeb2257b - + https://github.com/mono/linker - a07cab7b71a1321a9e68571c0b6095144a177b4e + f574448d16af45f7ac2c4b89d71dea73dec86726 - + https://github.com/dotnet/runtime - 02f70d0b903422282cd7ba8037de6b66ea0b7a2d + 0605bb3aba533702f234c907906ef076a97131fe diff --git a/eng/Versions.props b/eng/Versions.props index 5045d899556..a47e9801470 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,11 +1,11 @@ - 6.0.100-preview.7.21327.2 - 6.0.100-preview.6.21322.1 + 6.0.100-preview.7.21354.4 + 6.0.100-preview.6.21330.1 5.0.0-beta.20181.7 6.0.0-beta.21212.6 - 6.0.0-preview.7.21326.8 + 6.0.0-preview.7.21352.16 From fbb785bfe3bc18638b03caa29fc4c0d1b91b9a13 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 6 Jul 2021 10:59:58 -0500 Subject: [PATCH 03/21] Update .apkdesc files --- .../BuildReleaseArm64SimpleDotNet.apkdesc | 16 +-- .../BuildReleaseArm64XFormsDotNet.apkdesc | 102 +++++++++--------- 2 files changed, 59 insertions(+), 59 deletions(-) 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..89e05169b47 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,31 +8,31 @@ "Size": 54406 }, "assemblies/Mono.Android.dll": { - "Size": 78829 + "Size": 78831 }, "assemblies/rc.bin": { "Size": 802 }, "assemblies/System.Linq.dll": { - "Size": 10150 + "Size": 10154 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 495560 + "Size": 495899 }, "assemblies/System.Runtime.dll": { - "Size": 2262 + "Size": 2264 }, "assemblies/UnnamedProject.dll": { - "Size": 3173 + "Size": 3171 }, "classes.dex": { "Size": 316792 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 337816 + "Size": 341376 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3269936 + "Size": 3171840 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { "Size": 776216 @@ -77,5 +77,5 @@ "Size": 1724 } }, - "PackageSize": 2692947 + "PackageSize": 2643795 } \ 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..b2b7cae78b3 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 @@ -11,160 +11,160 @@ "Size": 61256 }, "assemblies/Microsoft.Win32.Primitives.dll": { - "Size": 3645 + "Size": 3651 }, "assemblies/Mono.Android.dll": { - "Size": 398294 + "Size": 398300 }, "assemblies/mscorlib.dll": { - "Size": 3832 + "Size": 3830 }, "assemblies/netstandard.dll": { - "Size": 5521 + "Size": 5523 }, "assemblies/rc.bin": { "Size": 802 }, "assemblies/System.Collections.Concurrent.dll": { - "Size": 11579 + "Size": 11584 }, "assemblies/System.Collections.dll": { - "Size": 19185 + "Size": 19142 }, "assemblies/System.Collections.NonGeneric.dll": { - "Size": 8471 + "Size": 8478 }, "assemblies/System.ComponentModel.dll": { - "Size": 2002 + "Size": 2006 }, "assemblies/System.ComponentModel.Primitives.dll": { - "Size": 2607 + "Size": 2612 }, "assemblies/System.ComponentModel.TypeConverter.dll": { - "Size": 6993 + "Size": 7003 }, "assemblies/System.Console.dll": { - "Size": 5839 + "Size": 5843 }, "assemblies/System.Core.dll": { - "Size": 1968 + "Size": 1970 }, "assemblies/System.Diagnostics.TraceSource.dll": { - "Size": 6800 + "Size": 6806 }, "assemblies/System.dll": { - "Size": 2315 + "Size": 2318 }, "assemblies/System.Drawing.dll": { - "Size": 2000 + "Size": 2003 }, "assemblies/System.Drawing.Primitives.dll": { - "Size": 12154 + "Size": 12158 }, "assemblies/System.Formats.Asn1.dll": { "Size": 26856 }, "assemblies/System.IO.Compression.Brotli.dll": { - "Size": 11461 + "Size": 11464 }, "assemblies/System.IO.Compression.dll": { - "Size": 18810 + "Size": 18818 }, "assemblies/System.IO.FileSystem.dll": { "Size": 1964 }, "assemblies/System.IO.IsolatedStorage.dll": { - "Size": 10628 + "Size": 10630 }, "assemblies/System.Linq.dll": { - "Size": 19504 + "Size": 19510 }, "assemblies/System.Linq.Expressions.dll": { "Size": 181323 }, "assemblies/System.Net.Http.dll": { - "Size": 211166 + "Size": 211322 }, "assemblies/System.Net.NameResolution.dll": { - "Size": 9924 + "Size": 9931 }, "assemblies/System.Net.NetworkInformation.dll": { - "Size": 17325 + "Size": 17336 }, "assemblies/System.Net.Primitives.dll": { - "Size": 41156 + "Size": 41162 }, "assemblies/System.Net.Quic.dll": { - "Size": 43453 + "Size": 43606 }, "assemblies/System.Net.Security.dll": { - "Size": 57317 + "Size": 57359 }, "assemblies/System.Net.Sockets.dll": { - "Size": 54458 + "Size": 54449 }, "assemblies/System.ObjectModel.dll": { - "Size": 11316 + "Size": 12003 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 698582 + "Size": 753071 }, "assemblies/System.Private.DataContractSerialization.dll": { - "Size": 193066 + "Size": 193083 }, "assemblies/System.Private.Uri.dll": { - "Size": 43183 + "Size": 43206 }, "assemblies/System.Private.Xml.dll": { - "Size": 251151 + "Size": 251183 }, "assemblies/System.Private.Xml.Linq.dll": { - "Size": 15060 + "Size": 15067 }, "assemblies/System.Runtime.CompilerServices.Unsafe.dll": { - "Size": 1342 + "Size": 1341 }, "assemblies/System.Runtime.dll": { - "Size": 2461 + "Size": 2464 }, "assemblies/System.Runtime.InteropServices.RuntimeInformation.dll": { - "Size": 2915 + "Size": 2919 }, "assemblies/System.Runtime.Numerics.dll": { - "Size": 21157 + "Size": 21158 }, "assemblies/System.Runtime.Serialization.dll": { - "Size": 1938 + "Size": 1941 }, "assemblies/System.Runtime.Serialization.Formatters.dll": { - "Size": 2675 + "Size": 2679 }, "assemblies/System.Runtime.Serialization.Primitives.dll": { - "Size": 3978 + "Size": 3985 }, "assemblies/System.Security.Cryptography.Algorithms.dll": { - "Size": 42389 + "Size": 42491 }, "assemblies/System.Security.Cryptography.Encoding.dll": { - "Size": 13812 + "Size": 13832 }, "assemblies/System.Security.Cryptography.Primitives.dll": { - "Size": 8844 + "Size": 8848 }, "assemblies/System.Security.Cryptography.X509Certificates.dll": { - "Size": 76400 + "Size": 76406 }, "assemblies/System.Text.RegularExpressions.dll": { - "Size": 76502 + "Size": 76547 }, "assemblies/System.Threading.Channels.dll": { - "Size": 16782 + "Size": 16789 }, "assemblies/System.Xml.dll": { - "Size": 1822 + "Size": 1826 }, "assemblies/UnnamedProject.dll": { - "Size": 117075 + "Size": 117076 }, "assemblies/Xamarin.AndroidX.Activity.dll": { "Size": 6067 @@ -236,10 +236,10 @@ "Size": 3455324 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 337816 + "Size": 341376 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3269936 + "Size": 3171840 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { "Size": 776216 @@ -2003,5 +2003,5 @@ "Size": 341040 } }, - "PackageSize": 8459870 + "PackageSize": 8463966 } \ No newline at end of file From f7498baee515cfbe868e01575a275cd4b101a29e Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 8 Jul 2021 15:48:04 -0400 Subject: [PATCH 04/21] Use reflection to get underlying handler from AndroidClientHandler --- src/Mono.Android/Mono.Android.csproj | 1 + .../AndroidClientHandler.cs | 21 ++++++++++--------- .../AndroidMessageHandler.cs | 2 ++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 877a0665c65..4168759e7b6 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -349,6 +349,7 @@ + diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs index c33c1086b6e..6648291aa7f 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs @@ -6,6 +6,7 @@ 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; @@ -65,13 +66,7 @@ public class AndroidClientHandler : HttpClientHandler public AndroidClientHandler () { - if (IsSocketHandler) - { - // maybe some other exception - throw new InvalidOperationException("SocketsHttpHandler is not supported as an underlying handler by AndroidClientHandler"); - } - - _underlyingHander = (AndroidMessageHandler) GetUnderlyingHandler (); + _underlyingHander = GetUnderlyingHandler () as AndroidMessageHandler ?? throw new InvalidOperationException ("Unknown underlying handler. Only AndroidMessageHandler is supported for AndroidClientHandler"); } /// @@ -226,17 +221,17 @@ protected void AssertSelf () protected override async Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) { AssertSelf (); - base.SendAsync (request, cancellationToken); + return await base.SendAsync (request, cancellationToken); } protected virtual async Task GetJavaProxy (Uri destination, CancellationToken cancellationToken) { - return _underlyingHander.GetJavaProxy (destination, cancellationToken); + return await _underlyingHander.GetJavaProxy (destination, cancellationToken); } protected virtual async Task WriteRequestContentToOutput (HttpRequestMessage request, HttpURLConnection httpConnection, CancellationToken cancellationToken) { - return _underlyingHander.WriteRequestContentToOutput (request, httpConnection, cancellationToken); + await _underlyingHander.WriteRequestContentToOutput (request, httpConnection, cancellationToken); } /// @@ -314,5 +309,11 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti { return _underlyingHander.ConfigureCustomSSLSocketFactory (connection); } + + object GetUnderlyingHandler () + { + var handler = GetType().BaseType.GetField("_underlyingHandler", BindingFlags.Instance | BindingFlags.NonPublic); + return handler.GetValue(this); + } } } diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index 3912de34417..dfb21e6b3b4 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -100,6 +100,8 @@ public CookieContainer CookieContainer public bool UseProxy { get; set; } + public IWebProxy? Proxy { get; set; } + public ICredentials? Credentials { get; set; } public bool AllowAutoRedirect { get; set; } From 5118b6ef61133405c0e39af9992012c492b0b474 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 8 Jul 2021 20:29:20 -0400 Subject: [PATCH 05/21] Split AndroidClientHandler between net6 and legacy. Excluded CompilerGeneratedAttribute from api compat check --- src/Mono.Android/Mono.Android.csproj | 3 +- .../AndroidClientHandler.Legacy.cs | 1012 +++++++++++++++++ ...soft.Android.Sdk.DefaultProperties.targets | 3 +- .../api-compat-exclude-attributes.txt | 1 + 4 files changed, 1017 insertions(+), 2 deletions(-) create mode 100644 src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 4168759e7b6..6794465fca4 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -348,7 +348,8 @@ - + + 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..9271e7eda2d --- /dev/null +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs @@ -0,0 +1,1012 @@ +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) { + 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); + } + + 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/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 205c6414c5d..ccd6b568d82 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 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 From 5062dd884d2527c2fb10695f592b007e6768b035 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 8 Jul 2021 20:55:56 -0400 Subject: [PATCH 06/21] [tests] Make it easier to repro dotnet/runtime#55375 Context: https://github.com/dotnet/runtime/issues/55375 There are lots of tests in `tests/Mono.Android-Tests`, but only two tests are failing. Add a `[Category("dotnet-runtime-55375")]` to the failing tests so that we can more easily execute *just* the tests in question: adb shell am instrument -e include dotnet-runtime-55375 -e loglevel Verbose -w Mono.Android.NET_Tests/xamarin.android.runtimetests.NUnitInstrumentation --- .../Android.Runtime/XmlReaderPullParserTest.cs | 1 + .../Android.Runtime/XmlReaderResourceParserTest.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs index 0ed7315f60e..eb577718ee7 100644 --- a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs +++ b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs @@ -13,6 +13,7 @@ namespace Android.RuntimeTests { public class XmlReaderPullParserTest { [Test] + [Category ("dotnet-runtime-55375")] public void ToLocalJniHandle () { var p = Application.Context.Resources.GetXml (MyResource.Xml.XmlReaderResourceParser); diff --git a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs index 2fc6e740c67..ccba72b2b2e 100644 --- a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs +++ b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs @@ -13,6 +13,7 @@ namespace Android.RuntimeTests { public class XmlReaderResourceParserTest { [Test] + [Category ("dotnet-runtime-55375")] public void ToLocalJniHandle () { var p = Application.Context.Resources.GetXml (MyResource.Xml.XmlReaderResourceParser); From 004725d4de8efd09226050d430bda0ab141ff50c Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 8 Jul 2021 21:03:29 -0400 Subject: [PATCH 07/21] [tests] Disable linking on Mono.Android.NET-Tests.csproj Context: https://github.com/xamarin/xamarin-android/pull/6072 Context: https://github.com/dotnet/runtime/issues/55375 Context: https://discord.com/channels/732297728826277939/732297837953679412/862829757460512788 @vargaz wondered if dotnet/runtime#55375 is because of the linker. Disable the linker, and lets see what happens! --- .../Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj index 296d426a057..4d34c760c2c 100644 --- a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj @@ -21,6 +21,8 @@ -$(TestsFlavor)NET6 DotNetIgnore + + None From 23310da918210453eccde27ce4b6aa3ec952978d Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 9 Jul 2021 15:01:05 -0400 Subject: [PATCH 08/21] Revert "[tests] Disable linking on Mono.Android.NET-Tests.csproj" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/dotnet/runtime/issues/55375#issuecomment-877059216 This reverts commit 004725d4de8efd09226050d430bda0ab141ff50c. > Disable the linker, and lets see what happens! What happens is…all the tests pass! Which suggests that this *is* a linker-related (adjacent?) issue, *not* a JIT issue, as I had previously believed. (I still don't understand *how* it's linker-related, and now I have to wonder if I can't "believe" the output of `ikdasm`, which is an equally troubling thought…) Re-enable the linker, so that tests once again *fail*. (I'm also not able to repro the crash locally, so if I want to further explore things, exploring CI output is the only path forward.) --- .../Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj index 4d34c760c2c..296d426a057 100644 --- a/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Runtime-Microsoft.Android.Sdk/Mono.Android.NET-Tests.csproj @@ -21,8 +21,6 @@ -$(TestsFlavor)NET6 DotNetIgnore - - None From cd67d24d2add19fac5df92b3e5a5869a13bfc971 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 9 Jul 2021 16:32:07 -0400 Subject: [PATCH 09/21] Revert "[tests] Make it easier to repro dotnet/runtime#55375" This reverts commit 5062dd884d2527c2fb10695f592b007e6768b035. Context: https://devdiv.visualstudio.com/DevDiv/_build/results?buildId=4963171&view=results Context: https://github.com/xamarin/xamarin-android/pull/6072#issuecomment-876685378 I *thought* that re-enabling the linker (23310da9) would cause the unit tests to once again start failing. The **APKs .NET** tests are *passing*. Meaning `Android.RuntimeTests.XmlReaderPullParserTest.ToLocalJniHandle()` is now *passing*. I had thought this was due to the linker, but could it instead be due to commit 5062dd88? Revert 5062dd88; let's see if it once again breaks. *Locally*, `XmlReaderPullParserTest.ToLocalJniHandle()` fails when commit 5062dd88is not applied. --- .../Android.Runtime/XmlReaderPullParserTest.cs | 1 - .../Android.Runtime/XmlReaderResourceParserTest.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs index eb577718ee7..0ed7315f60e 100644 --- a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs +++ b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderPullParserTest.cs @@ -13,7 +13,6 @@ namespace Android.RuntimeTests { public class XmlReaderPullParserTest { [Test] - [Category ("dotnet-runtime-55375")] public void ToLocalJniHandle () { var p = Application.Context.Resources.GetXml (MyResource.Xml.XmlReaderResourceParser); diff --git a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs index ccba72b2b2e..2fc6e740c67 100644 --- a/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs +++ b/tests/Mono.Android-Tests/Android.Runtime/XmlReaderResourceParserTest.cs @@ -13,7 +13,6 @@ namespace Android.RuntimeTests { public class XmlReaderResourceParserTest { [Test] - [Category ("dotnet-runtime-55375")] public void ToLocalJniHandle () { var p = Application.Context.Resources.GetXml (MyResource.Xml.XmlReaderResourceParser); From ac1385fed19fe790164b57bb9f2a96dc3f7fc5b7 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Mon, 12 Jul 2021 12:18:35 +0000 Subject: [PATCH 10/21] Update dependencies from https://github.com/dotnet/installer build 20210710.1 Microsoft.Dotnet.Sdk.Internal From Version 6.0.100-preview.7.21327.2 -> To Version 6.0.100-preview.7.21360.1 Dependency coherency updates Microsoft.NET.ILLink.Tasks,Microsoft.NETCore.App.Ref From Version 6.0.100-preview.6.21322.1 -> To Version 6.0.100-preview.6.21358.3 (parent: Microsoft.Dotnet.Sdk.Internal --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 4875ec233fa..602cad9dd21 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,16 +1,16 @@ - + https://github.com/dotnet/installer - bd5653f30e86a0bf8069bfc976b267c9eeb2257b + f284d88024417c9def2c84752a6ccaeeee914921 - + https://github.com/mono/linker - f574448d16af45f7ac2c4b89d71dea73dec86726 + b9501922637806f4135df09a9922d5540e203858 - + https://github.com/dotnet/runtime - 0605bb3aba533702f234c907906ef076a97131fe + 739218439bb6d3b224e240c012f37c516ccd29c9 diff --git a/eng/Versions.props b/eng/Versions.props index a47e9801470..f170c2f919a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,11 +1,11 @@ - 6.0.100-preview.7.21354.4 - 6.0.100-preview.6.21330.1 + 6.0.100-preview.7.21360.1 + 6.0.100-preview.6.21358.3 5.0.0-beta.20181.7 6.0.0-beta.21212.6 - 6.0.0-preview.7.21352.16 + 6.0.0-preview.7.21360.1 From b6bdde3961ddd672bb4fd4463a9a35cdea002ee4 Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" Date: Mon, 19 Jul 2021 12:38:59 +0000 Subject: [PATCH 11/21] Update dependencies from https://github.com/dotnet/installer build 20210719.3 Microsoft.Dotnet.Sdk.Internal From Version 6.0.100-preview.7.21327.2 -> To Version 6.0.100-rc.1.21369.3 Dependency coherency updates Microsoft.NET.ILLink.Tasks,Microsoft.NETCore.App.Ref From Version 6.0.100-preview.6.21322.1 -> To Version 6.0.100-preview.6.21366.2 (parent: Microsoft.Dotnet.Sdk.Internal --- eng/Version.Details.xml | 12 ++++++------ eng/Versions.props | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 602cad9dd21..616eda3f7d7 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,16 +1,16 @@ - + https://github.com/dotnet/installer - f284d88024417c9def2c84752a6ccaeeee914921 + 9c463710a333a48301a211fbd7b8ca3b15d4f1f7 - + https://github.com/mono/linker - b9501922637806f4135df09a9922d5540e203858 + 460dd6ddb329a5588d9e4399f4257ce28dfadaca - + https://github.com/dotnet/runtime - 739218439bb6d3b224e240c012f37c516ccd29c9 + 96ce6b35359b3c159ef3e685dd67cf30bb46769b diff --git a/eng/Versions.props b/eng/Versions.props index f170c2f919a..dc0b0cc5411 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,11 +1,11 @@ - 6.0.100-preview.7.21360.1 - 6.0.100-preview.6.21358.3 + 6.0.100-rc.1.21369.3 + 6.0.100-preview.6.21366.2 5.0.0-beta.20181.7 6.0.0-beta.21212.6 - 6.0.0-preview.7.21360.1 + 6.0.0-rc.1.21368.1 From 813104fc9300f9bf57ebddcd8eba297f42f09ac0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 19 Jul 2021 10:24:01 -0500 Subject: [PATCH 12/21] [xaprepare] always delete ~/android-toolchain/dotnet I'm seeing failures on CI such as: C:\a\_work\2\s\build-tools\xaprepare\xaprepare\package-download.proj : warning MSB4242: The SDK resolver "Microsoft.DotNet.MSBuildWorkloadSdkResolver" failed to run. Inconsistency in workload manifest 'microsoft.net.workload.mono.toolchain': missing dependency 'Microsoft.NET.Workload.Emscripten' C:\Users\cloudtest\android-toolchain\dotnet\sdk\6.0.100-preview.7.21369.2\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.ImportWorkloads.props(14,3): warning MSB4242: The SDK resolver "Microsoft.DotNet.MSBuildWorkloadSdkResolver" failed to run. Inconsistency in workload manifest 'microsoft.net.workload.mono.toolchain': missing dependency 'Microsoft.NET.Workload.Emscripten' C:\Users\cloudtest\android-toolchain\dotnet\sdk\6.0.100-preview.7.21369.2\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.ImportWorkloads.props(14,38): error MSB4236: The SDK 'Microsoft.NET.SDK.WorkloadAutoImportPropsLocator' specified could not be found. [C:\a\_work\2\s\build-tools\xaprepare\xaprepare\package-download.proj] Error: dotnet restore C:\a\_work\2\s\build-tools\xaprepare\xaprepare\package-download.proj failed. Step Xamarin.Android.Prepare.Step_InstallDotNetPreview failed System.InvalidOperationException: Step Xamarin.Android.Prepare.Step_InstallDotNetPreview failed at Xamarin.Android.Prepare.Scenario.d__26.MoveNext() in C:\a\_work\2\s\build-tools\xaprepare\xaprepare\Application\Scenario.cs:line 50 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Xamarin.Android.Prepare.Context.d__210.MoveNext() in C:\a\_work\2\s\build-tools\xaprepare\xaprepare\Application\Context.cs:line 827 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Xamarin.Android.Prepare.App.d__3.MoveNext() in C:\a\_work\2\s\build-tools\xaprepare\xaprepare\Main.cs:line 171 I saw the same thing locally, and noticed this folder was out of date: ~/android-toolchain/dotnet/sdk-manifests/6.0.100/microsoft.net.workload.mono.toolchain If we have an outdated `WorkloadManifest.json` or `WorkloadManifest.targets`, the build can get in a state where this step can't succeed. I manually deleted this folder to solve the problem: ~/android-toolchain/dotnet/ Going forward, let's make the `Prepare` step do this every time. This should simplify our .NET 6 provisioning & upgrade process. --- .../Steps/Step_InstallDotNetPreview.cs | 61 +------------------ 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs b/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs index 641f56c7cd9..f75058b714c 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."); From 4d3682a6d1e178f3fc42b378e4c75cca5e551300 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 19 Jul 2021 15:57:44 -0500 Subject: [PATCH 13/21] Remove $(SelfContained) = true by default Builds were failing with: error NETSDK1031: It is not supported to build or publish a self-contained application without specifying a RuntimeIdentifier. You must either specify a RuntimeIdentifier or set SelfContained to false. This seemed to be caused by reordering such as: https://github.com/dotnet/sdk/pull/18639 In the past, we weren't actually overwriting `$(SelfContained)` *at all*, and with these changes we now are. I set this value in the days of .NET 5. We should just remove where we set `$(SelfContained)` and let the dotnet/sdk manage this value. --- .../targets/Microsoft.Android.Sdk.DefaultProperties.targets | 1 - 1 file changed, 1 deletion(-) 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 10e831ac3dc..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 @@ -56,7 +56,6 @@ false false - true true true SdkOnly From 3faed48589101654795b876f3733839f58dbc45b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 19 Jul 2021 16:41:27 -0500 Subject: [PATCH 14/21] Bump to xamarin/java.interop/main@4fb7c147 Changes: https://github.com/xamarin/java.interop/compare/a5ed8919...4fb7c147 We need "[build] set $(DisableImplicitNamespaceImports) by default". --- external/Java.Interop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d2211bab2eb55c988f4c2b5c0a7cb842df3a1cc8 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Mon, 19 Jul 2021 22:09:47 -0500 Subject: [PATCH 15/21] Update .apkdesc files --- .../BuildReleaseArm64SimpleDotNet.apkdesc | 20 +-- .../BuildReleaseArm64XFormsDotNet.apkdesc | 125 +++++++++--------- 2 files changed, 74 insertions(+), 71 deletions(-) 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 89e05169b47..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": 78831 + "Size": 78850 }, "assemblies/rc.bin": { - "Size": 802 + "Size": 863 }, "assemblies/System.Linq.dll": { - "Size": 10154 + "Size": 10132 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 495899 + "Size": 507773 }, "assemblies/System.Runtime.dll": { - "Size": 2264 + "Size": 2246 }, "assemblies/UnnamedProject.dll": { - "Size": 3171 + "Size": 3175 }, "classes.dex": { "Size": 316792 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 341376 + "Size": 337896 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3171840 + "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": 2643795 + "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 b2b7cae78b3..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,160 +8,163 @@ "Size": 7236 }, "assemblies/Java.Interop.dll": { - "Size": 61256 + "Size": 61383 }, "assemblies/Microsoft.Win32.Primitives.dll": { - "Size": 3651 + "Size": 3818 }, "assemblies/Mono.Android.dll": { - "Size": 398300 + "Size": 415847 }, "assemblies/mscorlib.dll": { - "Size": 3830 + "Size": 3818 }, "assemblies/netstandard.dll": { - "Size": 5523 + "Size": 5504 }, "assemblies/rc.bin": { - "Size": 802 + "Size": 863 }, "assemblies/System.Collections.Concurrent.dll": { - "Size": 11584 + "Size": 11566 }, "assemblies/System.Collections.dll": { - "Size": 19142 + "Size": 19021 }, "assemblies/System.Collections.NonGeneric.dll": { - "Size": 8478 + "Size": 8461 }, "assemblies/System.ComponentModel.dll": { - "Size": 2006 + "Size": 1984 }, "assemblies/System.ComponentModel.Primitives.dll": { - "Size": 2612 + "Size": 2588 }, "assemblies/System.ComponentModel.TypeConverter.dll": { - "Size": 7003 + "Size": 5996 }, "assemblies/System.Console.dll": { - "Size": 5843 + "Size": 5824 }, "assemblies/System.Core.dll": { - "Size": 1970 + "Size": 1951 + }, + "assemblies/System.Diagnostics.DiagnosticSource.dll": { + "Size": 3015 }, "assemblies/System.Diagnostics.TraceSource.dll": { - "Size": 6806 + "Size": 6779 }, "assemblies/System.dll": { - "Size": 2318 + "Size": 2298 }, "assemblies/System.Drawing.dll": { - "Size": 2003 + "Size": 1985 }, "assemblies/System.Drawing.Primitives.dll": { - "Size": 12158 + "Size": 12228 }, "assemblies/System.Formats.Asn1.dll": { - "Size": 26856 + "Size": 26839 }, "assemblies/System.IO.Compression.Brotli.dll": { - "Size": 11464 + "Size": 11443 }, "assemblies/System.IO.Compression.dll": { - "Size": 18818 - }, - "assemblies/System.IO.FileSystem.dll": { - "Size": 1964 + "Size": 18802 }, "assemblies/System.IO.IsolatedStorage.dll": { - "Size": 10630 + "Size": 10780 }, "assemblies/System.Linq.dll": { - "Size": 19510 + "Size": 19492 }, "assemblies/System.Linq.Expressions.dll": { - "Size": 181323 + "Size": 181671 }, "assemblies/System.Net.Http.dll": { - "Size": 211322 + "Size": 216071 }, "assemblies/System.Net.NameResolution.dll": { - "Size": 9931 + "Size": 13102 }, "assemblies/System.Net.NetworkInformation.dll": { - "Size": 17336 + "Size": 17566 }, "assemblies/System.Net.Primitives.dll": { - "Size": 41162 + "Size": 41353 }, "assemblies/System.Net.Quic.dll": { - "Size": 43606 + "Size": 44610 + }, + "assemblies/System.Net.Requests.dll": { + "Size": 3271 }, "assemblies/System.Net.Security.dll": { - "Size": 57359 + "Size": 57683 }, "assemblies/System.Net.Sockets.dll": { - "Size": 54449 + "Size": 54590 }, "assemblies/System.ObjectModel.dll": { - "Size": 12003 + "Size": 11988 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 753071 + "Size": 772013 }, "assemblies/System.Private.DataContractSerialization.dll": { - "Size": 193083 + "Size": 193651 }, "assemblies/System.Private.Uri.dll": { - "Size": 43206 + "Size": 43326 }, "assemblies/System.Private.Xml.dll": { - "Size": 251183 + "Size": 251560 }, "assemblies/System.Private.Xml.Linq.dll": { - "Size": 15067 + "Size": 17087 }, "assemblies/System.Runtime.CompilerServices.Unsafe.dll": { - "Size": 1341 + "Size": 1339 }, "assemblies/System.Runtime.dll": { - "Size": 2464 + "Size": 2447 }, "assemblies/System.Runtime.InteropServices.RuntimeInformation.dll": { - "Size": 2919 + "Size": 2901 }, "assemblies/System.Runtime.Numerics.dll": { - "Size": 21158 + "Size": 24128 }, "assemblies/System.Runtime.Serialization.dll": { - "Size": 1941 + "Size": 1921 }, "assemblies/System.Runtime.Serialization.Formatters.dll": { - "Size": 2679 + "Size": 2652 }, "assemblies/System.Runtime.Serialization.Primitives.dll": { - "Size": 3985 + "Size": 3966 }, "assemblies/System.Security.Cryptography.Algorithms.dll": { - "Size": 42491 + "Size": 42462 }, "assemblies/System.Security.Cryptography.Encoding.dll": { - "Size": 13832 + "Size": 13908 }, "assemblies/System.Security.Cryptography.Primitives.dll": { - "Size": 8848 + "Size": 8832 }, "assemblies/System.Security.Cryptography.X509Certificates.dll": { - "Size": 76406 + "Size": 76749 }, "assemblies/System.Text.RegularExpressions.dll": { - "Size": 76547 + "Size": 76721 }, "assemblies/System.Threading.Channels.dll": { - "Size": 16789 + "Size": 16835 }, "assemblies/System.Xml.dll": { - "Size": 1826 + "Size": 1807 }, "assemblies/UnnamedProject.dll": { "Size": 117076 @@ -236,22 +239,22 @@ "Size": 3455324 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 341376 + "Size": 337896 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3171840 + "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": 8463966 + "PackageSize": 8521405 } \ No newline at end of file From 6ff64d613df79ccf4d259feeb83ceaf404cadd18 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 20 Jul 2021 10:33:14 -0500 Subject: [PATCH 16/21] Fixes for AndroidClientHandler.GetUnderlyingHandler() --- .../Xamarin.Android.Net/AndroidClientHandler.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs index 6648291aa7f..6ffa88ee09a 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs @@ -312,8 +312,13 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti object GetUnderlyingHandler () { - var handler = GetType().BaseType.GetField("_underlyingHandler", BindingFlags.Instance | BindingFlags.NonPublic); - return handler.GetValue(this); + 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}'."); + } + return field.GetValue (this); } } } From 3929b6644708320e51a076cb5eab7a7f2bdcc1f4 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 20 Jul 2021 13:41:36 -0500 Subject: [PATCH 17/21] Add [DynamicDependency] for AndroidClientHandler.GetUnderlyingHandler() --- src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs index 6ffa88ee09a..e616cbfb18f 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; @@ -310,6 +311,7 @@ protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnecti return _underlyingHander.ConfigureCustomSSLSocketFactory (connection); } + [DynamicDependency (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof (AndroidMessageHandler))] object GetUnderlyingHandler () { var fieldName = "_nativeHandler"; From f3095316e5e69bebe24e74aa72658efc46f0862d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 20 Jul 2021 14:02:26 -0500 Subject: [PATCH 18/21] [tests] System.IO.FileSystem.dll no longer exists We don't need to assert for the presence of this file anyway. --- tests/MSBuildDeviceIntegration/Tests/BundleToolTests.cs | 2 -- 1 file changed, 2 deletions(-) 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"); From f462738838a692180abacd45c0d463132305b3ee Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 20 Jul 2021 14:49:24 -0500 Subject: [PATCH 19/21] Address PR comments * Use `$(TargetFramework)` and `monoandroid10` checks, so they will continue to work in a future .NET 7. * Fixed mixed tabs/spaces in `AndroidMessageHandler` * Removed `goto done` where it could simply be `return proxy`. --- src/Mono.Android/Mono.Android.csproj | 8 ++++---- .../Xamarin.Android.Net/AndroidClientHandler.Legacy.cs | 5 ++--- .../Xamarin.Android.Net/AndroidMessageHandler.cs | 7 +++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 6794465fca4..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,8 +348,8 @@ - - + + @@ -379,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 index 9271e7eda2d..3e1efe01910 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.Legacy.cs @@ -308,12 +308,12 @@ string EncodeUrl (Uri url) var proxy = Java.Net.Proxy.NoProxy; if (destination == null || Proxy == null) { - goto done; + return proxy; } Uri puri = Proxy.GetProxy (destination); if (puri == null) { - goto done; + return proxy; } proxy = await Task .Run (() => { @@ -322,7 +322,6 @@ string EncodeUrl (Uri url) return new Java.Net.Proxy (Java.Net.Proxy.Type.Http, addr); }, cancellationToken); - done: return proxy; } diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index dfb21e6b3b4..9591ac78990 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -20,7 +20,7 @@ namespace Xamarin.Android.Net { - public class AndroidMessageHandler : HttpMessageHandler + public class AndroidMessageHandler : HttpMessageHandler { sealed class RequestRedirectionState { @@ -313,12 +313,12 @@ string EncodeUrl (Uri url) var proxy = Java.Net.Proxy.NoProxy; if (destination == null || Proxy == null) { - goto done; + return proxy; } Uri puri = Proxy.GetProxy (destination); if (puri == null) { - goto done; + return proxy; } proxy = await Task .Run (() => { @@ -327,7 +327,6 @@ string EncodeUrl (Uri url) return new Java.Net.Proxy (Java.Net.Proxy.Type.Http, addr); }, cancellationToken); - done: return proxy; } From f957c82b9ab471bb3dab14a149d081e88db5232d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 20 Jul 2021 16:24:43 -0500 Subject: [PATCH 20/21] Add default settings for AndroidMessageHandler Context: https://github.com/xamarin/xamarin-android/blob/0b9395a7786ef36a17c2bfd6dd2f00dd3e65301c/tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidClientHandlerTests.cs#L360-L377 Context: https://github.com/dotnet/runtime/blob/ccfe21882e4a2206ce49cd5b32d3eb3cab3e530f/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs Some tests that check 301 redirects were failing with: System.Net.Http.HttpRequestException : net_http_message_not_success_statuscode, 301, Moved Permanently at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode() in System.Net.Http.dll:token 0x6000298+0x39 at Xamarin.Android.NetTests.AndroidClientHandlerTests.Redirect_Without_Protocol_Works() in Mono.Android.NET-Tests.dll:token 0x600002e+0x7b at System.Reflection.RuntimeMethodInfo.Invoke(Object , BindingFlags , Binder , Object[] , CultureInfo ) in System.Private.CoreLib.dll:token 0x600281b+0x6a We originally were inheriting some default values from `AndroidClientHandler`. Now that we have `AndroidMessageHandler` in .NET 6, we need to specify these defaults ourselves. --- .../Xamarin.Android.Net/AndroidMessageHandler.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index 9591ac78990..cd220456df6 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -96,17 +96,20 @@ public CookieContainer CookieContainer } } - public bool PreAuthenticate { get; set; } + // NOTE: defaults here are based on: + // https://github.com/dotnet/runtime/blob/ccfe21882e4a2206ce49cd5b32d3eb3cab3e530f/src/libraries/Common/src/System/Net/Http/HttpHandlerDefaults.cs - public bool UseProxy { get; set; } + 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; } + public bool AllowAutoRedirect { get; set; } = true; - public int MaxAutomaticRedirections { get; set; } + public int MaxAutomaticRedirections { get; set; } = 50; /// /// From 04b67b43670ed7fb3392bd52dadf3f20e0fd505d Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 21 Jul 2021 08:45:03 -0500 Subject: [PATCH 21/21] [tests] changes for AndroidClientHandlerTests to pass * MaxAutomaticRedirections should throw if <= 0 * UseCookies defaults to true * CookieContainer creates a new instance on first call * Catch TargetInvocationException, see: https://github.com/dotnet/runtime/issues/56089 * Catch PlatformNotSupportedException for ClientCertificateOptions --- .../AndroidMessageHandler.cs | 22 ++++++++++++++----- .../AndroidClientHandlerTests.cs | 10 ++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs index cd220456df6..949b87b4618 100644 --- a/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs +++ b/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs @@ -74,8 +74,6 @@ sealed class RequestRedirectionState internal const bool SupportsProxy = true; internal const bool SupportsRedirectConfiguration = true; - public bool UseCookies { get; set; } - public DecompressionMethods AutomaticDecompression { get => _decompressionMethods; @@ -84,13 +82,11 @@ public DecompressionMethods AutomaticDecompression public CookieContainer CookieContainer { - get => _cookieContainer; + get => _cookieContainer ?? (_cookieContainer = new CookieContainer ()); set { if (value == null) - { throw new ArgumentNullException(nameof(value)); - } _cookieContainer = value; } @@ -99,6 +95,8 @@ public CookieContainer CookieContainer // 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; @@ -109,7 +107,19 @@ public CookieContainer CookieContainer public bool AllowAutoRedirect { get; set; } = true; - public int MaxAutomaticRedirections { get; set; } = 50; + 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; + } + } /// /// 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 } }