Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak AndroidMessageHandler behavior for WCF support #7785

Merged
merged 23 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d6d8ced
Tweak AndroidMessageHandler behavior for WCF support
grendello Feb 10, 2023
cf3ec47
Add test for compression methods
grendello Feb 13, 2023
af2d360
Expand the decompression test + fix build
grendello Feb 13, 2023
570d0d5
use `#if NET` instead
grendello Feb 13, 2023
f6cf2c5
Update apkdesc
grendello Feb 13, 2023
a599aff
Merge branch 'main' into http-compression-fix
grendello Feb 13, 2023
3aed98d
Merge branch 'main' into http-compression-fix
grendello Feb 14, 2023
3cd503a
Fix build
grendello Feb 17, 2023
876c32e
Merge branch 'main' into http-compression-fix
grendello Feb 17, 2023
f155b28
Merge branch 'main' into http-compression-fix
grendello Feb 22, 2023
25f4575
Merge branch 'main' into http-compression-fix
grendello Feb 24, 2023
b2fd01e
Revert "Bump to dotnet/installer/main@d25a3bb 8.0.100-preview.2.23105…
grendello Feb 24, 2023
2f75cb0
Merge branch 'main' into http-compression-fix
grendello Feb 24, 2023
1c1dfce
Cannot check whether Content-Length header was removed
grendello Feb 24, 2023
3caa331
Merge branch 'main' into http-compression-fix
grendello Feb 27, 2023
e86954b
Update test
grendello Feb 27, 2023
ddcfb04
Doh
grendello Feb 27, 2023
c010550
Adjust the test parameters to match responses from httpbin.org
grendello Feb 27, 2023
bd862d9
Merge branch 'main' into http-compression-fix
grendello Feb 28, 2023
eb92c4d
Merge branch 'main' into http-compression-fix
grendello Feb 28, 2023
7b2e172
Tweak the test to retry up to 5 times and dump the retrieved content
grendello Feb 28, 2023
168fd22
Merge branch 'main' into http-compression-fix
grendello Feb 28, 2023
36d0eb4
Update apkdesc
grendello Feb 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 122 additions & 32 deletions src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,46 @@ sealed class RequestRedirectionState
public bool MethodChanged;
}

/// <summary>
/// Some requests require modification to the set of headers returned from the native client.
/// However, the headers collection in it is immutable, so we need to perform the adjustments
/// in CopyHeaders. This class describes the necessary operations.
/// </summary>
sealed class ContentState
{
public bool? RemoveContentLengthHeader;

/// <summary>
/// If this is `true`, then `NewContentEncodingHeaderValue` is entirely ignored
/// </summary>
public bool? RemoveContentEncodingHeader;

/// <summary>
/// New 'Content-Encoding' header value. Ignored if not null and empty.
/// </summary>
public List<string>? NewContentEncodingHeaderValue;

/// <summary>
/// Reset the class to values that indicate there's no action to take. MUST be
/// called BEFORE any of the class members are assigned values and AFTER the state
/// modification is applied
/// </summary>
public void Reset ()
{
RemoveContentEncodingHeader = null;
RemoveContentLengthHeader = null;
NewContentEncodingHeaderValue = null;
}
}

internal const string LOG_APP = "monodroid-net";

const string GZIP_ENCODING = "gzip";
const string DEFLATE_ENCODING = "deflate";
const string BROTLI_ENCODING = "br";
const string IDENTITY_ENCODING = "identity";
const string ContentEncodingHeaderName = "Content-Encoding";
const string ContentLengthHeaderName = "Content-Length";

static readonly IDictionary<string, string> headerSeparators = new Dictionary<string, string> {
["User-Agent"] = " ",
Expand All @@ -82,9 +117,9 @@ sealed class RequestRedirectionState
static readonly HashSet <string> known_content_headers = new HashSet <string> (StringComparer.OrdinalIgnoreCase) {
"Allow",
"Content-Disposition",
"Content-Encoding",
ContentEncodingHeaderName,
"Content-Language",
"Content-Length",
ContentLengthHeaderName,
"Content-Location",
"Content-MD5",
"Content-Range",
Expand Down Expand Up @@ -571,6 +606,7 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
CancellationTokenRegistration cancelRegistration = default (CancellationTokenRegistration);
HttpStatusCode statusCode = HttpStatusCode.OK;
Uri? connectionUri = null;
var contentState = new ContentState ();

try {
cancelRegistration = cancellationToken.Register (() => {
Expand Down Expand Up @@ -608,13 +644,13 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
if (!IsErrorStatusCode (statusCode)) {
if (Logger.LogNet)
Logger.Log (LogLevel.Info, LOG_APP, $"Reading...");
ret.Content = GetContent (httpConnection, httpConnection.InputStream!);
ret.Content = GetContent (httpConnection, httpConnection.InputStream!, contentState);
} 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));
ret.Content = GetErrorContent (httpConnection, new StringContent (String.Empty, Encoding.ASCII), contentState);
}

bool disposeRet;
Expand All @@ -633,7 +669,7 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
}
}

CopyHeaders (httpConnection, ret);
CopyHeaders (httpConnection, ret, contentState);
ParseCookies (ret, connectionUri);

if (disposeRet) {
Expand Down Expand Up @@ -661,8 +697,8 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
// 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);
ret.Content = GetErrorContent (httpConnection, new StringContent ("Unauthorized", Encoding.ASCII), contentState);
CopyHeaders (httpConnection, ret, contentState);

if (ret.Headers.WwwAuthenticate != null) {
ProxyAuthenticationRequested = false;
Expand All @@ -676,37 +712,65 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
return ret;
}

CopyHeaders (httpConnection, ret);
CopyHeaders (httpConnection, ret, contentState);
ParseCookies (ret, connectionUri);

if (Logger.LogNet)
Logger.Log (LogLevel.Info, LOG_APP, $"Returning");
return ret;
}

HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent)
HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent, ContentState contentState)
{
var contentStream = httpConnection.ErrorStream;

if (contentStream != null) {
return GetContent (httpConnection, contentStream);
return GetContent (httpConnection, contentStream, contentState);
}

return fallbackContent;
}

HttpContent GetContent (URLConnection httpConnection, Stream contentStream)
Stream GetDecompressionWrapper (URLConnection httpConnection, Stream inputStream, ContentState contentState)
{
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);
contentState.Reset ();
if (!decompress_here || String.IsNullOrEmpty (httpConnection.ContentEncoding)) {
return inputStream;
}

var encodings = new HashSet<string> (httpConnection.ContentEncoding?.Split (','), StringComparer.OrdinalIgnoreCase);
Stream? ret = null;
string? supportedEncoding = null;
if (encodings.Contains (GZIP_ENCODING)) {
supportedEncoding = GZIP_ENCODING;
ret = new GZipStream (inputStream, CompressionMode.Decompress);
} else if (encodings.Contains (DEFLATE_ENCODING)) {
supportedEncoding = DEFLATE_ENCODING;
ret = new DeflateStream (inputStream, CompressionMode.Decompress);
}
#if NETCOREAPP
else if (encodings.Contains (BROTLI_ENCODING)) {
supportedEncoding = BROTLI_ENCODING;
ret = new BrotliStream (inputStream, CompressionMode.Decompress);
}
#endif
if (!String.IsNullOrEmpty (supportedEncoding)) {
contentState.RemoveContentLengthHeader = true;

encodings.Remove (supportedEncoding!);
if (encodings.Count == 0) {
contentState.RemoveContentEncodingHeader = true;
} else {
contentState.NewContentEncodingHeaderValue = new List<string> (encodings);
}
}

return ret ?? inputStream;
}

HttpContent GetContent (URLConnection httpConnection, Stream contentStream, ContentState contentState)
{
Stream inputStream = GetDecompressionWrapper (httpConnection, new BufferedStream (contentStream), contentState);
return new StreamContent (inputStream);
}

Expand Down Expand Up @@ -881,9 +945,13 @@ void ParseCookies (AndroidHttpResponseMessage ret, Uri connectionUri)
}
}

void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response)
void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response, ContentState contentState)
{
var headers = httpConnection.HeaderFields;
bool removeContentLength = contentState.RemoveContentLengthHeader ?? false;
bool removeContentEncoding = contentState.RemoveContentEncodingHeader ?? false;
bool setNewContentEncodingValue = !removeContentEncoding && contentState.NewContentEncodingHeaderValue != null && contentState.NewContentEncodingHeaderValue.Count > 0;

foreach (var key in headers!.Keys) {
if (key == null) // First header entry has null key, it corresponds to the response message
continue;
Expand All @@ -895,8 +963,25 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response
} else {
item_headers = response.Headers;
}
item_headers.TryAddWithoutValidation (key, headers [key]);

IEnumerable<string> values = headers [key];
if (removeContentLength && String.Compare (ContentLengthHeaderName, key, StringComparison.OrdinalIgnoreCase) == 0) {
removeContentLength = false;
continue;
}

if ((removeContentEncoding || setNewContentEncodingValue) && String.Compare (ContentEncodingHeaderName, key, StringComparison.OrdinalIgnoreCase) == 0) {
if (removeContentEncoding) {
removeContentEncoding = false;
continue;
}

setNewContentEncodingValue = false;
values = contentState.NewContentEncodingHeaderValue!;
}
item_headers.TryAddWithoutValidation (key, values);
}
contentState.Reset ();
}

/// <summary>
Expand Down Expand Up @@ -1006,19 +1091,24 @@ void AppendEncoding (string encoding, ref List <string>? list)
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
} else {
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 NETCOREAPP
if ((AutomaticDecompression & DecompressionMethods.Brotli) != 0) {
AppendEncoding (BROTLI_ENCODING, ref accept_encoding);
decompress_here = true;
}
#endif
}

if (accept_encoding?.Count > 0)
Expand Down
Loading