Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
999 lines (862 sloc) 42.3 KB
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
{
/// <summary>
/// A custom implementation of <see cref="System.Net.Http.HttpClientHandler"/> which internally uses <see cref="Java.Net.HttpURLConnection"/>
/// (or its HTTPS incarnation) to send HTTP requests.
/// </summary>
/// <remarks>
/// <para>Instance of this class is used to configure <see cref="System.Net.Http.HttpClient"/> instance
/// in the following way:
///
/// <example>
/// 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;
/// </example></para>
/// <para>
/// 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 <see cref="PreAuthenticationData"/> property (which is usually the case on the first
/// request), the <see cref="RequestNeedsAuthorization"/> property will return <c>true</c> and the <see cref="RequestedAuthentication"/> 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 (<see cref="AuthenticationData"/>,
/// <see cref="PreAuthenticationData"/>) to handle the protocol authorization.</para>
/// <para>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 <see cref="TrustedCerts"/> collection or, after deriving a custom class from AndroidClientHandler, by overriding one or more methods provided for this purpose
/// (<see cref="ConfigureTrustManagerFactory"/>, <see cref="ConfigureKeyManagerFactory"/> and <see cref="ConfigureKeyStore"/>). 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 <see cref="TrustedCerts"/> along with
/// the self-signed certificate(s).</para>
/// </remarks>
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<string, string> headerSeparators = new Dictionary<string, string> {
["User-Agent"] = " ",
};
static readonly HashSet <string> known_content_headers = new HashSet <string> (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 <IAndroidAuthenticationModule> authModules = new List <IAndroidAuthenticationModule> {
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;
/// <summary>
/// <para>
/// 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 <see cref="RequestedAuthentication"/>
/// after the initial request, without any authentication data, receives the authorization request from the
/// server. The application must then store credentials in instance of <see cref="AuthenticationData"/> and
/// assign the instance to this propery before retrying the request.
/// </para>
/// <para>
/// The property is never set by AndroidClientHandler.
/// </para>
/// </summary>
/// <value>The pre authentication data.</value>
public AuthenticationData PreAuthenticationData { get; set; }
/// <summary>
/// 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 <see cref="PreAuthenticationData"/>
/// property. If AndroidClientHandler is not able to detect the kind of authentication scheme it will store an
/// instance of <see cref="AuthenticationData"/> with its <see cref="AuthenticationData.Scheme"/> property
/// set to <c>AuthenticationScheme.Unsupported</c> and the application will be responsible for providing an
/// instance of <see cref="IAndroidAuthenticationModule"/> which handles this kind of authorization scheme
/// (<see cref="AuthenticationData.AuthModule"/>
/// </summary>
public IList <AuthenticationData> RequestedAuthentication { get; private set; }
/// <summary>
/// Server authentication response indicates that the request to authorize comes from a proxy if this property is <c>true</c>.
/// All the instances of <see cref="AuthenticationData"/> stored in the <see cref="RequestedAuthentication"/> property will
/// have their <see cref="AuthenticationData.UseProxyAuthentication"/> preset to the same value as this property.
/// </summary>
public bool ProxyAuthenticationRequested { get; private set; }
/// <summary>
/// If <c>true</c> then the server requested authorization and the application must use information
/// found in <see cref="RequestedAuthentication"/> to set the value of <see cref="PreAuthenticationData"/>
/// </summary>
public bool RequestNeedsAuthorization {
get { return RequestedAuthentication?.Count > 0; }
}
/// <summary>
/// <para>
/// 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.</para>
/// <para>AndroidClientHandler uses a custom <see cref="KeyStore"/> and <see cref="TrustManagerFactory"/> 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
/// <see cref="ConfigureTrustManagerFactory"/>, <see cref="ConfigureKeyManagerFactory"/> and <see cref="ConfigureKeyStore"/> methods
/// instead</para>
/// </summary>
/// <value>The trusted certs.</value>
public IList <Certificate> TrustedCerts { get; set; }
/// <summary>
/// <para>
/// Specifies the connection read timeout.
/// </para>
/// <para>
/// Since there's no way for the handler to access <see cref="t:System.Net.Http.HttpClient.Timeout"/>
/// 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 <see
/// cref="t:System.TimeSpan.Zero"/>
/// </para>
/// <para>
/// The default value is <c>24</c> hours, much higher than the documented value of <see
/// cref="t:System.Net.Http.HttpClient.Timeout"/> and the same as the value of iOS-specific
/// NSUrlSessionHandler.
/// </para>
/// </summary>
public TimeSpan ReadTimeout { get; set; } = TimeSpan.FromHours (24);
/// <summary>
/// <para>
/// Specifies the connect timeout
/// </para>
/// <para>
/// The native Java client supports two separate timeouts - one for reading from the connection (<see
/// cref="ReadTimeout"/>) and another for establishing the connection. This property sets the value of
/// the latter timeout, unless it is set to <see cref="t:System.TimeSpan.Zero"/> in which case the
/// native Java client defaults are used.
/// </para>
/// <para>
/// The default value is <c>120</c> seconds.
/// </para>
/// </summary>
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 ();
}
/// <summary>
/// Returns a custom host name verifier for a HTTPS connection. By default it returns <c>null</c> 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 <see cref="m:HttpsURLConnection.HostnameVerifier"/> property directly on the passed
/// <paramref name="connection"/>
/// </summary>
/// <returns>Instance of IHostnameVerifier to be used for this HTTPS connection</returns>
/// <param name="connection">HTTPS connection object.</param>
protected virtual IHostnameVerifier GetSSLHostnameVerifier (HttpsURLConnection connection)
{
return null;
}
/// <summary>
/// Creates, configures and processes an asynchronous request to the indicated resource.
/// </summary>
/// <returns>Task in which the request is executed</returns>
/// <param name="request">Request provided by <see cref="System.Net.Http.HttpClient"/></param>
/// <param name="cancellationToken">Cancellation token.</param>
protected override async Task <HttpResponseMessage> 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);
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;
}
}
protected virtual async Task <Java.Net.Proxy> GetJavaProxy (Uri destination, CancellationToken cancellationToken)
{
Java.Net.Proxy 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 <Java.Net.Proxy>.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 <HttpResponseMessage> 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 {
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 <HttpResponseMessage> 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).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) {
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;
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;
}
IDictionary <string, IList <string>> headers = httpConnection.HeaderFields;
IList <string> locationHeader;
string location = null;
if (headers.TryGetValue ("Location", out locationHeader) && 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 <AuthenticationHeaderValue> headers)
{
var authData = new List <AuthenticationData> (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 <string> 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)
{
IDictionary <string, IList <string>> 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]);
}
}
/// <summary>
/// Configure the <see cref="HttpURLConnection"/> 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.
/// </summary>
/// <param name="request">Request data</param>
/// <param name="conn">Pre-configured connection instance</param>
protected virtual Task SetupRequest (HttpRequestMessage request, HttpURLConnection conn)
{
Action a = AssertSelf;
return Task.Run (a);
}
/// <summary>
/// Configures the key store. The <paramref name="keyStore"/> parameter is set to instance of <see cref="KeyStore"/>
/// created using the <see cref="KeyStore.DefaultType"/> type and with populated with certificates provided in the <see cref="TrustedCerts"/>
/// property. AndroidClientHandler implementation simply returns the instance passed in the <paramref name="keyStore"/> parameter
/// </summary>
/// <returns>The key store.</returns>
/// <param name="keyStore">Key store to configure.</param>
protected virtual KeyStore ConfigureKeyStore (KeyStore keyStore)
{
AssertSelf ();
return keyStore;
}
/// <summary>
/// Create and configure an instance of <see cref="KeyManagerFactory"/>. The <paramref name="keyStore"/> parameter is set to the
/// return value of the <see cref="ConfigureKeyStore"/> method, so it might be null if the application overrode the method and provided
/// no key store. It will not be <c>null</c> when the default implementation is used. The application can return <c>null</c> 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.
/// </summary>
/// <returns>The key manager factory or <c>null</c>.</returns>
/// <param name="keyStore">Key store.</param>
protected virtual KeyManagerFactory ConfigureKeyManagerFactory (KeyStore keyStore)
{
AssertSelf ();
return null;
}
/// <summary>
/// Create and configure an instance of <see cref="TrustManagerFactory"/>. The <paramref name="keyStore"/> parameter is set to the
/// return value of the <see cref="ConfigureKeyStore"/> method, so it might be null if the application overrode the method and provided
/// no key store. It will not be <c>null</c> when the default implementation is used. The application can return <c>null</c> from this
/// method in which case AndroidClientHandler will create its own instance of the trust manager factory provided that the <see cref="TrustCerts"/>
/// list contains at least one valid certificate. If there are no valid certificates and this method returns <c>null</c>, no custom
/// trust manager will be created since that would make all the HTTPS requests fail.
/// </summary>
/// <returns>The trust manager factory.</returns>
/// <param name="keyStore">Key store.</param>
protected virtual TrustManagerFactory ConfigureTrustManagerFactory (KeyStore keyStore)
{
AssertSelf ();
return null;
}
void AppendEncoding (string encoding, ref List <string> list)
{
if (list == null)
list = new List <string> ();
if (list.Contains (encoding))
return;
list.Add (encoding);
}
async Task <HttpURLConnection> SetupRequestInternal (HttpRequestMessage request, URLConnection conn)
{
if (conn == null)
throw new ArgumentNullException (nameof (conn));
var httpConnection = conn.JavaCast <HttpURLConnection> ();
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 <string> 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;
}
/// <summary>
/// Configure and return a custom <see cref="t:SSLSocketFactory"/> for the passed HTTPS <paramref
/// name="connection"/>. If the class overriding the method returns anything but the default
/// <c>null</c>, the SSL setup code will not call the <see cref="ConfigureKeyManagerFactory"/> nor the
/// <see cref="ConfigureTrustManagerFactory"/> 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.
/// </summary>
/// <returns>Instance of SSLSocketFactory ready to use with the HTTPS connection.</returns>
/// <param name="connection">HTTPS connection to return socket factory for</param>
protected virtual SSLSocketFactory ConfigureCustomSSLSocketFactory (HttpsURLConnection connection)
{
return null;
}
void SetupSSL (HttpsURLConnection httpsConnection)
{
if (httpsConnection == null)
return;
SSLSocketFactory 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;
}
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) {
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;
}
IAndroidAuthenticationModule 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<string, IEnumerable<string>> 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);
}
}
}
You can’t perform that action at this time.