diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index fd92f494e..1ae8ecabb 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -215,6 +215,7 @@
+
..\..\packages\Validation.2.3.7\lib\dotnet\Validation.dll
True
@@ -303,6 +304,7 @@
+
@@ -311,6 +313,7 @@
+
diff --git a/src/Android/CustomAndroidClientHandler.cs b/src/Android/CustomAndroidClientHandler.cs
new file mode 100644
index 000000000..654d85bfa
--- /dev/null
+++ b/src/Android/CustomAndroidClientHandler.cs
@@ -0,0 +1,774 @@
+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.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 CustomAndroidClientHandler : HttpClientHandler
+ {
+ sealed class RequestRedirectionState
+ {
+ public Uri NewUrl;
+ public int RedirectCounter;
+ public HttpMethod Method;
+ }
+
+ internal const string LOG_APP = "monodroid-net";
+
+ const string GZIP_ENCODING = "gzip";
+ const string DEFLATE_ENCODING = "deflate";
+ const string IDENTITY_ENCODING = "identity";
+
+ 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 (),
+ // COMMENTED OUT: Kyle
+ //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; }
+
+ 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;
+
+ if(String.IsNullOrEmpty(url.Query))
+ return Uri.EscapeUriString(url.ToString());
+
+ // 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();
+ }
+
+ ///
+ /// 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 = java_url.OpenConnection();
+ HttpURLConnection httpConnection = await SetupRequestInternal(request, java_connection);
+ HttpResponseMessage response = await ProcessRequest(request, java_url, httpConnection, cancellationToken, redirectState);
+ if(response != null)
+ return response;
+
+ if(redirectState.NewUrl == null)
+ throw new InvalidOperationException("Request redirected but no new URI specified");
+ request.Method = redirectState.Method;
+ }
+ }
+
+ 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);
+ }
+
+ async Task DoProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState)
+ {
+ if(cancellationToken.IsCancellationRequested)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ try
+ {
+ await Task.WhenAny(
+ httpConnection.ConnectAsync(),
+ Task.Run(() => { cancellationToken.WaitHandle.WaitOne(); }))
+ .ConfigureAwait(false);
+ }
+ catch(Java.Net.ConnectException 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)
+ {
+ httpConnection.Disconnect();
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+ cancellationToken.Register(httpConnection.Disconnect);
+
+ if(httpConnection.DoOutput)
+ {
+ using(var stream = await request.Content.ReadAsStreamAsync())
+ {
+ await stream.CopyToAsync(httpConnection.OutputStream, 4096, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+
+ if(cancellationToken.IsCancellationRequested)
+ {
+ httpConnection.Disconnect();
+ cancellationToken.ThrowIfCancellationRequested();
+ }
+
+ var statusCode = await Task.Run(() => (HttpStatusCode)httpConnection.ResponseCode).ConfigureAwait(false);
+ var connectionUri = new Uri(httpConnection.URL.ToString());
+
+ // 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,
+ };
+
+ bool disposeRet;
+ if(HandleRedirect(statusCode, httpConnection, redirectState, out disposeRet))
+ {
+ if(disposeRet)
+ {
+ ret.Dispose();
+ ret = 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);
+ }
+
+ // COMMENTED OUT: Kyle
+ //ret.RequestedAuthentication = RequestedAuthentication;
+ return ret;
+ }
+
+ if(!IsErrorStatusCode(statusCode))
+ {
+ ret.Content = GetContent(httpConnection, httpConnection.InputStream);
+ }
+ else
+ {
+ // 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));
+ }
+
+ CopyHeaders(httpConnection, ret);
+
+ IEnumerable cookieHeaderValue;
+ if(!UseCookies || CookieContainer == null || !ret.Headers.TryGetValues("Set-Cookie", out cookieHeaderValue) || cookieHeaderValue == null)
+ {
+ return ret;
+ }
+
+ try
+ {
+ 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.
+ }
+
+ 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)
+ {
+ string[] 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;
+ switch(redirectCode)
+ {
+ case HttpStatusCode.MultipleChoices: // 300
+ break;
+
+ case HttpStatusCode.Moved: // 301
+ case HttpStatusCode.Redirect: // 302
+ case HttpStatusCode.SeeOther: // 303
+ redirectState.Method = HttpMethod.Get;
+ break;
+
+ 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;
+ }
+
+ IDictionary> headers = httpConnection.HeaderFields;
+ IList locationHeader;
+ if(!headers.TryGetValue("Location", out locationHeader) || locationHeader == null || locationHeader.Count == 0)
+ throw new InvalidOperationException($"HTTP connection redirected with code {redirectCode} ({(int)redirectCode}) but no Location header found in response");
+
+
+ redirectState.RedirectCounter++;
+ if(redirectState.RedirectCounter >= MaxAutomaticRedirections)
+ throw new WebException($"Maximum automatic redirections exceeded (allowed {MaxAutomaticRedirections}, redirected {redirectState.RedirectCounter} times)");
+
+ Uri location = new Uri(locationHeader[0], UriKind.Absolute);
+ redirectState.NewUrl = location;
+
+ 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),
+ // COMMENTED OUT: Kyle
+ //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 CopyHeaders(HttpURLConnection httpConnection, HttpResponseMessage response)
+ {
+ IDictionary> headers = httpConnection.HeaderFields;
+ foreach(string key in headers.Keys)
+ {
+ if(key == null) // First header entry has null key, it corresponds to the response message
+ continue;
+
+ HttpHeaders item_headers;
+ string kind;
+ if(known_content_headers.Contains(key))
+ {
+ kind = "content";
+ item_headers = response.Content.Headers;
+ }
+ else
+ {
+ kind = "response";
+ 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)
+ {
+ return Task.Factory.StartNew(AssertSelf);
+ }
+
+ ///
+ /// 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}");
+
+ httpConnection.RequestMethod = request.Method.ToString();
+
+ // 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);
+ SetupRequestBody(httpConnection, request);
+
+ return httpConnection;
+ }
+
+ void SetupSSL(HttpsURLConnection httpsConnection)
+ {
+ if(httpsConnection == null)
+ return;
+
+ KeyStore 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);
+ KeyManagerFactory kmf = ConfigureKeyManagerFactory(keyStore);
+ TrustManagerFactory 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);
+ }
+
+ SSLContext context = SSLContext.GetInstance("TLS");
+ context.Init(kmf?.GetKeyManagers(), tmf.GetTrustManagers(), null);
+ httpsConnection.SSLSocketFactory = context.SocketFactory;
+ }
+
+ void HandlePreAuthentication(HttpURLConnection httpConnection)
+ {
+ AuthenticationData data = PreAuthenticationData;
+ if(!PreAuthenticate || data == null)
+ return;
+
+ ICredentials creds = data.UseProxyAuthentication ? Proxy?.Credentials : Credentials;
+ if(creds == null)
+ {
+ return;
+ }
+
+ IAndroidAuthenticationModule auth = data.Scheme == AuthenticationScheme.Unsupported ? data.AuthModule : authModules.Find(m => m?.Scheme == data.Scheme);
+ if(auth == null)
+ {
+ return;
+ }
+
+ Authorization authorization = auth.Authenticate(data.Challenge, httpConnection, creds);
+ if(authorization == null)
+ {
+ return;
+ }
+
+ httpConnection.SetRequestProperty(data.UseProxyAuthentication ? "Proxy-Authorization" : "Authorization", authorization.Message);
+ }
+
+ void AddHeaders(HttpURLConnection conn, HttpHeaders headers)
+ {
+ if(headers == null)
+ return;
+
+ foreach(KeyValuePair> header in headers)
+ {
+ conn.SetRequestProperty(header.Key, header.Value != null ? String.Join(",", 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);
+ }
+ }
+
+ sealed class AuthModuleBasic : IAndroidAuthenticationModule
+ {
+ public AuthenticationScheme Scheme { get; } = AuthenticationScheme.Basic;
+ public string AuthenticationType { get; } = "Basic";
+ public bool CanPreAuthenticate { get; } = true;
+
+ public Authorization Authenticate(string challenge, HttpURLConnection request, ICredentials credentials)
+ {
+ string header = challenge?.Trim();
+ if(credentials == null || String.IsNullOrEmpty(header))
+ return null;
+
+ if(header.IndexOf("basic", StringComparison.OrdinalIgnoreCase) == -1)
+ return null;
+
+ return InternalAuthenticate(request, credentials);
+ }
+
+ public Authorization PreAuthenticate(HttpURLConnection request, ICredentials credentials)
+ {
+ return InternalAuthenticate(request, credentials);
+ }
+
+ Authorization InternalAuthenticate(HttpURLConnection request, ICredentials credentials)
+ {
+ if(request == null || credentials == null)
+ return null;
+
+ NetworkCredential cred = credentials.GetCredential(new Uri(request.URL.ToString()), AuthenticationType.ToLowerInvariant());
+ if(cred == null)
+ return null;
+
+ if(String.IsNullOrEmpty(cred.UserName))
+ return null;
+
+ string domain = cred.Domain?.Trim();
+ string response = String.Empty;
+
+ // If domain is set, MS sends "domain\user:password".
+ if(!String.IsNullOrEmpty(domain))
+ response = domain + "\\";
+ response += cred.UserName + ":" + cred.Password;
+
+ return new Authorization($"{AuthenticationType} {Convert.ToBase64String(Encoding.ASCII.GetBytes(response))}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs
index 801fb5e25..7aa789235 100644
--- a/src/Android/MainApplication.cs
+++ b/src/Android/MainApplication.cs
@@ -209,6 +209,7 @@ namespace Bit.Android
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
+ .RegisterType(new ContainerControlledLifetimeManager())
// Repositories
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
diff --git a/src/Android/Services/HttpService.cs b/src/Android/Services/HttpService.cs
new file mode 100644
index 000000000..8879b70b4
--- /dev/null
+++ b/src/Android/Services/HttpService.cs
@@ -0,0 +1,12 @@
+using System;
+using Xamarin.Android.Net;
+using Bit.App;
+using Bit.App.Abstractions;
+
+namespace Bit.Android.Services
+{
+ public class HttpService : IHttpService
+ {
+ public ApiHttpClient Client => new ApiHttpClient(new CustomAndroidClientHandler());
+ }
+}
diff --git a/src/App/Abstractions/Services/IHttpService.cs b/src/App/Abstractions/Services/IHttpService.cs
new file mode 100644
index 000000000..85c38f6f9
--- /dev/null
+++ b/src/App/Abstractions/Services/IHttpService.cs
@@ -0,0 +1,7 @@
+namespace Bit.App.Abstractions
+{
+ public interface IHttpService
+ {
+ ApiHttpClient Client { get; }
+ }
+}
diff --git a/src/App/App.csproj b/src/App/App.csproj
index 979eb8b0c..672fc443c 100644
--- a/src/App/App.csproj
+++ b/src/App/App.csproj
@@ -37,6 +37,7 @@
+
diff --git a/src/App/Repositories/AccountsApiRepository.cs b/src/App/Repositories/AccountsApiRepository.cs
index 0028817b2..b8536c6c7 100644
--- a/src/App/Repositories/AccountsApiRepository.cs
+++ b/src/App/Repositories/AccountsApiRepository.cs
@@ -10,8 +10,10 @@ namespace Bit.App.Repositories
{
public class AccountsApiRepository : BaseApiRepository, IAccountsApiRepository
{
- public AccountsApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public AccountsApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
protected override string ApiRoute => "accounts";
@@ -23,7 +25,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
@@ -55,7 +57,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
diff --git a/src/App/Repositories/ApiRepository.cs b/src/App/Repositories/ApiRepository.cs
index bfa074545..6e916c354 100644
--- a/src/App/Repositories/ApiRepository.cs
+++ b/src/App/Repositories/ApiRepository.cs
@@ -15,8 +15,10 @@ namespace Bit.App.Repositories
where TRequest : class
where TResponse : class
{
- public ApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public ApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
public virtual async Task> GetByIdAsync(TId id)
@@ -26,7 +28,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
@@ -60,7 +62,7 @@ namespace Bit.App.Repositories
return HandledNotConnected>();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
@@ -94,7 +96,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
@@ -128,7 +130,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
@@ -162,7 +164,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
diff --git a/src/App/Repositories/AuthApiRepository.cs b/src/App/Repositories/AuthApiRepository.cs
index 709960de9..7e8fca602 100644
--- a/src/App/Repositories/AuthApiRepository.cs
+++ b/src/App/Repositories/AuthApiRepository.cs
@@ -11,8 +11,10 @@ namespace Bit.App.Repositories
{
public class AuthApiRepository : BaseApiRepository, IAuthApiRepository
{
- public AuthApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public AuthApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
protected override string ApiRoute => "auth";
@@ -24,7 +26,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
@@ -44,7 +46,7 @@ namespace Bit.App.Repositories
var responseObj = JsonConvert.DeserializeObject(responseContent);
return ApiResult.Success(responseObj, response.StatusCode);
}
- catch(WebException)
+ catch(WebException e)
{
return HandledWebException();
}
@@ -58,7 +60,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
diff --git a/src/App/Repositories/BaseApiRepository.cs b/src/App/Repositories/BaseApiRepository.cs
index 717094b38..c7e63ee06 100644
--- a/src/App/Repositories/BaseApiRepository.cs
+++ b/src/App/Repositories/BaseApiRepository.cs
@@ -5,17 +5,20 @@ using System.Threading.Tasks;
using Bit.App.Models.Api;
using Newtonsoft.Json;
using Plugin.Connectivity.Abstractions;
+using Bit.App.Abstractions;
namespace Bit.App.Repositories
{
public abstract class BaseApiRepository
{
- public BaseApiRepository(IConnectivity connectivity)
+ public BaseApiRepository(IConnectivity connectivity, IHttpService httpService)
{
Connectivity = connectivity;
+ HttpService = httpService;
}
protected IConnectivity Connectivity { get; private set; }
+ protected IHttpService HttpService { get; private set; }
protected abstract string ApiRoute { get; }
protected ApiResult HandledNotConnected()
diff --git a/src/App/Repositories/CipherApiRepository.cs b/src/App/Repositories/CipherApiRepository.cs
index 66fa6f51d..cd4483681 100644
--- a/src/App/Repositories/CipherApiRepository.cs
+++ b/src/App/Repositories/CipherApiRepository.cs
@@ -12,8 +12,10 @@ namespace Bit.App.Repositories
{
public class CipherApiRepository : BaseApiRepository, ICipherApiRepository
{
- public CipherApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public CipherApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
protected override string ApiRoute => "ciphers";
@@ -25,7 +27,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
@@ -59,7 +61,7 @@ namespace Bit.App.Repositories
return HandledNotConnected>();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
@@ -93,7 +95,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
diff --git a/src/App/Repositories/DeviceApiRepository.cs b/src/App/Repositories/DeviceApiRepository.cs
index 4272f9453..95c5a7031 100644
--- a/src/App/Repositories/DeviceApiRepository.cs
+++ b/src/App/Repositories/DeviceApiRepository.cs
@@ -11,8 +11,10 @@ namespace Bit.App.Repositories
{
public class DeviceApiRepository : ApiRepository, IDeviceApiRepository
{
- public DeviceApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public DeviceApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
protected override string ApiRoute => "devices";
@@ -24,7 +26,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage(request)
{
@@ -56,7 +58,7 @@ namespace Bit.App.Repositories
return HandledNotConnected();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage
{
diff --git a/src/App/Repositories/FolderApiRepository.cs b/src/App/Repositories/FolderApiRepository.cs
index 34af4fbd0..2bf528a04 100644
--- a/src/App/Repositories/FolderApiRepository.cs
+++ b/src/App/Repositories/FolderApiRepository.cs
@@ -12,8 +12,10 @@ namespace Bit.App.Repositories
{
public class FolderApiRepository : ApiRepository, IFolderApiRepository
{
- public FolderApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public FolderApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
protected override string ApiRoute => "folders";
@@ -25,7 +27,7 @@ namespace Bit.App.Repositories
return HandledNotConnected>();
}
- using(var client = new ApiHttpClient())
+ using(var client = HttpService.Client)
{
var requestMessage = new TokenHttpRequestMessage()
{
diff --git a/src/App/Repositories/SiteApiRepository.cs b/src/App/Repositories/SiteApiRepository.cs
index 735191a0f..f93882e69 100644
--- a/src/App/Repositories/SiteApiRepository.cs
+++ b/src/App/Repositories/SiteApiRepository.cs
@@ -11,8 +11,10 @@ namespace Bit.App.Repositories
{
public class SiteApiRepository : ApiRepository, ISiteApiRepository
{
- public SiteApiRepository(IConnectivity connectivity)
- : base(connectivity)
+ public SiteApiRepository(
+ IConnectivity connectivity,
+ IHttpService httpService)
+ : base(connectivity, httpService)
{ }
protected override string ApiRoute => "sites";
diff --git a/src/App/Utilities/ApiHttpClient.cs b/src/App/Utilities/ApiHttpClient.cs
index 3a3706ebf..498cae7d6 100644
--- a/src/App/Utilities/ApiHttpClient.cs
+++ b/src/App/Utilities/ApiHttpClient.cs
@@ -7,6 +7,17 @@ namespace Bit.App
public class ApiHttpClient : HttpClient
{
public ApiHttpClient()
+ {
+ Init();
+ }
+
+ public ApiHttpClient(HttpMessageHandler handler)
+ : base(handler)
+ {
+ Init();
+ }
+
+ private void Init()
{
BaseAddress = new Uri("https://api.bitwarden.com");
DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
diff --git a/src/iOS.Core/Services/HttpService.cs b/src/iOS.Core/Services/HttpService.cs
new file mode 100644
index 000000000..8d7f5217e
--- /dev/null
+++ b/src/iOS.Core/Services/HttpService.cs
@@ -0,0 +1,11 @@
+using System;
+using Bit.App;
+using Bit.App.Abstractions;
+
+namespace Bit.iOS.Core.Services
+{
+ public class HttpService : IHttpService
+ {
+ public ApiHttpClient Client => new ApiHttpClient();
+ }
+}
diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj
index b4a98ff6c..5a57f1355 100644
--- a/src/iOS.Core/iOS.Core.csproj
+++ b/src/iOS.Core/iOS.Core.csproj
@@ -106,6 +106,7 @@
+
diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs
index aab8d4457..a9216d09c 100644
--- a/src/iOS.Extension/LoadingViewController.cs
+++ b/src/iOS.Extension/LoadingViewController.cs
@@ -279,6 +279,7 @@ namespace Bit.iOS.Extension
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
+ .RegisterType(new ContainerControlledLifetimeManager())
// Repositories
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs
index be52eafa1..830a2d0f6 100644
--- a/src/iOS/AppDelegate.cs
+++ b/src/iOS/AppDelegate.cs
@@ -262,6 +262,7 @@ namespace Bit.iOS
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
.RegisterType(new ContainerControlledLifetimeManager())
+ .RegisterType(new ContainerControlledLifetimeManager())
// Repositories
// Repositories
.RegisterType(new ContainerControlledLifetimeManager())