Skip to content

Follow redirections to relative URLs - #2390

Merged
jonpryor merged 1 commit into
dotnet:masterfrom
grendello:issue1923
Nov 28, 2018
Merged

jonpryor merged 1 commit into
dotnet:masterfrom
grendello:issue1923

Conversation

@grendello

Copy link
Copy Markdown
Contributor

Fixes: #1923

When a Location header contains an URL that is relative or lacks schema/host, we
now use the original request URL to construct the destination URL. Additionally,
if the original URL contains a fragment but the destination does not, we append
the fragment to the destination.

Comment thread src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs Outdated

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}'");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Ditto"; instead of Logger.Log() calls ~every time you manipulate the URL, could we "get away with" just one message, after line 625 (redirectState.NewUrl = ...), which prints the originating URL and the new URL? Or would that one message not provide enough inference information?

Granted, Logger.LogNet will be disabled by default, but if anyone enables it, this looks like it could be quite verbose, depending on how often the app hits a URL...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of the messages is to show steps which are under-defined/problematic and may effect the outcome. The raw location is printed because we instantiate an Uri later which has issues with the way it parses URIs (e.g. it will bomb on URI like g:h or it will turn /location/test into file:///location/test) and this information is actually useful when debugging problems in this code. For those reasons I left the messages in the form, and where, they are.

Comment thread src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs Outdated
@dominik-weber

Copy link
Copy Markdown
Contributor

I'm getting mixed results with the new implementation:

            var uri1 = testuri("http://google.com/test?a=b#foo", "/asd");
            // -> http://google.com/asd#foo OK

            var uri2 = testuri("http://google.com/test?a=b#foo", "//google.de/asd");
            // -> http://google.de/asd#foo OK

            var uri3 = testuri("http://google.com/test?a=b#foo", "/asd?c=d#bar");
            // -> http://google.com/asd%3Fc=d%23bar#foo NOT OK

            var uri4 = testuri("http://google.com/test?a=b#foo", "//google.de/asd?c=d#bar");
            // -> http://google.de/asd%3Fc=d%23bar#foo NOT OK

Am I missing something? Do you get different results with these parameters?

Have you considered using the public Uri (Uri baseUri, string relativeUri); constructor (https://docs.microsoft.com/en-us/dotnet/api/system.uri.-ctor?view=netframework-4.7.2#System_Uri__ctor_System_Uri_System_String_) to do the heavy lifting here? It seems to handle all my examples correctly, apart from the result not having the fragment of the original request URI.

I have found this source code using the same method, for reference:
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs#L111

Comment thread src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs Outdated
Comment thread src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs Outdated
Fixes: dotnet#1923

When a Location header contains an URL that is relative or lacks schema/host, we
now use the original request URL to construct the destination URL. Additionally,
if the original URL contains a fragment but the destination does not, we append
the fragment to the destination.
@grendello

Copy link
Copy Markdown
Contributor Author

@dominik-weber please review the latest changes, it should work correctly for your cases now. The only thing that will not work (because of a bug in the Uri class, well, I consider it a bug at least - see comments in the diff) is when Location redirects to URL of the a:b form, but this code is not where the issue can/should be solved.

@dominik-weber

Copy link
Copy Markdown
Contributor

@grendello awesome, thanks for the update! Actually, I think you can even remove the if (location [0] == '/') check alltogether. From the MS Docs:

If relativeUri is an absolute URI (containing a scheme, host name, and optionally a port number), the Uri instance is created using only relativeUri.

https://docs.microsoft.com/en-us/dotnet/api/system.uri.-ctor?view=netframework-4.7.2#System_Uri__ctor_System_Uri_System_String_

So just redirectUrl = new Uri (baseUrl, location); should work in all cases, no matter if location is relative or not. Apart from that, the changes look good to me 👍 I can however confirm your findings with a:b uris where a is just a single letter.

@grendello

Copy link
Copy Markdown
Contributor Author

@grendello awesome, thanks for the update! Actually, I think you can even remove the if (location [0] == '/') check alltogether. From the MS Docs:

The check avoids having to deal with this:

csharp> var u = new Uri ("/stuff", UriKind.RelativeOrAbsolute)
csharp> u
file:///stuff

It's simply faster and cleaner to do it this way rather than to check for file scheme in the else case and then create the uri using the base location :)

If relativeUri is an absolute URI (containing a scheme, host name, and optionally a port number), the Uri instance is created using only relativeUri.

https://docs.microsoft.com/en-us/dotnet/api/system.uri.-ctor?view=netframework-4.7.2#System_Uri__ctor_System_Uri_System_String_

So just redirectUrl = new Uri (baseUrl, location); should work in all cases, no matter if location is relative or not. Apart from that, the changes look good to me +1 I can however confirm your findings with a:b uris where a is just a single letter.

Yeah, a:b is a bummer with no solution for now :(

@jonpryor
jonpryor merged commit 79b220a into dotnet:master Nov 28, 2018
@grendello
grendello deleted the issue1923 branch November 28, 2018 21:07
@dominik-weber

Copy link
Copy Markdown
Contributor

The check avoids having to deal with this:

csharp> var u = new Uri ("/stuff", UriKind.RelativeOrAbsolute)
csharp> u
file:///stuff

It's simply faster and cleaner to do it this way rather than to check for file scheme in the else case and then create the uri using the base location :)

Maybe you misunderstood me there. I think if we used redirectUrl = new Uri (baseUrl, location); we wouldn't need to convert location to Uri at all, thus avoiding the file:// case alltogether. For example:

// baseUrl will always be a valid absolute url since we already sent a request to it
Uri baseUrl = new Uri("http://google.com/foo/bar");
Uri redirectUrl1 = new Uri(baseUrl, "/test"); // http://google.com/test
Uri redirectUrl2 = new Uri(baseUrl, "https://example.com"); // https://example.com

Seems like the Uri(baseUri, relativeUri) constructor was made just for http redirects... ;)

jonpryor pushed a commit that referenced this pull request Dec 6, 2018
Fixes: #1923

Consider this code:

	var client = new HttpClient(new AndroidClientHandler());
	var res = await client.GetStringAsync("https://nghttp2.org/httpbin/redirect-to?url=/httpbin/get");

Accessing `https://nghttp2.org/httpbin/redirect-to?url=/httpbin/get`
results in an HTTP-302 (redirect) to the location `/httpbin/get`:

	$ curl -D - https://nghttp2.org/httpbin/redirect-to?url=/httpbin/get
	HTTP/2 302 
	date: Wed, 28 Nov 2018 20:52:01 GMT
	content-type: text/html; charset=utf-8
	content-length: 0
	location: /httpbin/get
	...

Expected behavior within Xamarin.Android is that this should work:
the HTTP-302 should be followed, with `httpClient.GetStringAsync()`
returning the contents of `https://nghttp2.org/httpbin/get`.

What instead happens is that an exception is thrown:

	System.InvalidCastException: Unable to convert instance of type 'Java.Net.URLConnectionInvoker' to type 'Java.Net.HttpURLConnection'

Oops.

Update `AndroidClientHandler.HandleRedirect()` to improve its support
for the `Location` header, such that if the `Location` header value
contains a URL that is relative or lacks schema/host, we now use the
original request URL to construct the destination URL.  Additionally,
if the original URL contains a fragment but the destination does not,
we append the fragment to the destination.
@github-actions github-actions Bot locked and limited conversation to collaborators Feb 2, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants