-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix HttpClient redirection logic on UAP #22702
Conversation
This PR changes the implementation of sending requests in HttpClient for UAP when dealing with redirection. In the past, we would let the WinRT layer handle auto redirection logic. However, due to #9003, the cookies were getting lost on 3xx responses since the .NET layer didn't see them. So, this PR implements the redirection ourselves. One important part of redirection is that we need to drop credentials. The WinRT layer did this for us. However, we are unable to use a single WinRT HttpBaseProtocolFilter object since we need to remove the credential after the first redirect. So, we need to maintain a second filter that uses no credentials and keep it in sync regarding all the other properties of the primary filter. With this PR, the behavior of other aspects (such as controlling max number of redirects, etc.) now matches .NET Framework. So, some tests were adjusted to remove the UAP specific behavior checks. Fixes #9003 Fixes #22191
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build |
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build |
internal HttpHandlerToFilter( | ||
RTHttpBaseProtocolFilter filter, | ||
RTHttpBaseProtocolFilter filterWithNoCredentials, | ||
HttpClientHandler handler) | ||
{ | ||
if (filter == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're testing that filter is non-null here, should we also check filterWithNoCredentials? Or are they related enough that one being alright implies that the other is too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. But now that I am using Lazy pattern, there no longer is the filterWithNoCredentials in the handler object.
@dotnet-bot Test Outerloop Windows x64 Debug Build |
Unrelated failures in Outerloop Linux: |
|
||
_allowAutoRedirect = true; | ||
_maxAutomaticRedirections = 50; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally hard-coded to 10, with a comment about the WinRT implementation via wininet. Why not 10 here? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not 10 because it was only 10 due to using auto redirection with WinRT. And that 10 was not settable...it was hard coded within WinRT.
But now that we do it ourselves,, we can have the default be the same as the rest of .NET Framework, .NET Core. #Closed
{ | ||
redirectUri = new Uri(string.Format("{0}://{1}:{2}{3}", | ||
request.RequestUri.Scheme, | ||
request.RequestUri.Host, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Host [](start = 43, length = 4)
If an IPv6 address, will this already have the square brackets?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have no tests for this currently. But, if I change this logic as per @CIPop suggestion below:
In that case would the ctor Uri(Uri, string) work better?
then it becomes less of a concern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the code to remove the manually formatting of the uri. So, any IPv6 behavior would be done internally by the Uri ctor itself.
if (!redirectUri.IsAbsoluteUri)
{
redirectUri = new Uri(request.RequestUri, redirectUri.OriginalString);
}
} | ||
|
||
if (response.StatusCode != HttpStatusCode.RedirectKeepVerb && | ||
requestHttpMethod == HttpMethod.Post) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you mention the RFC for why 307 is different? RFC 7231, section 6.4.7.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added comment.
requestHttpMethod == HttpMethod.Post) | ||
{ | ||
requestHttpMethod = HttpMethod.Get; | ||
skipRequestContentIfPresent = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it would be nice to have a comment referencing the Post/Redirect/Get pattern since it's not obvious why a Post becomes a Get.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added comment.
{ | ||
if (redirects > 0) | ||
{ | ||
rtResponse = await _filterWithNoCredentials.SendRequestAsync(rtRequest).AsTask(cancel).ConfigureAwait(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we create the filter right here as a clone of the original filter but leaving out the credentials (instead of keeping 2 filter objects just in case there is a redirection - which in most cases I believe is not true).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose I could use a Lazy pattern here and only create the rtFilterWithNoCredentials as a "clone, minus credentials" when we first need it. At that point, since the first operation on the rtFilter has happened, the original rtFilter properties are considered locked. Therefore, we don't have to constantly re-create the rtFilterWithNoCredentials.
I thought it was simpler to just create it and update properties but perhaps I can revise that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a new commit to use Lazy pattern and create a copy (minus creds) of the filter.
|
||
if (!redirectUri.IsAbsoluteUri) | ||
{ | ||
redirectUri = new Uri(string.Format("{0}://{1}:{2}{3}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use UriBuilder instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried using UriBuilder. It doesn't work well. It doesn't have the same properties. For example, there is a Uri.PathAndQuery but UriBuilder only has .Path and .Query. There were other problems as well with getting things to work properly. So, this logic ended up working and all tests pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case would the ctor Uri(Uri, string)
work better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I tried that and it didn't work. I can investigate again.
The problem is that the original URI is a full URI with path and possible querystring, fragment, etc. pieces. So, will this ctor actually "replace" the path and subsequent parts of the original URI with this relative URI piece?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: I checked again and I can see that the ctor overload works. I changed that code and tests still pass.
if (!redirectUri.IsAbsoluteUri)
{
redirectUri = new Uri(request.RequestUri, redirectUri.OriginalString);
}
_rtFilterWithNoCredentials.IgnorableServerCertificateErrors.Add(RTChainValidationResult.RevocationInformationMissing); | ||
_rtFilterWithNoCredentials.IgnorableServerCertificateErrors.Add(RTChainValidationResult.Untrusted); | ||
_rtFilterWithNoCredentials.IgnorableServerCertificateErrors.Add(RTChainValidationResult.WrongUsage); | ||
_rtFilterWithNoCredentials.ServerCustomValidationRequested += RTServerCertificateCallback; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If for some reason both filters are active, this would cause the callback to be called several times causing race conditions that could corrupt the application state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, that callback can be called several times even without redirection involved. For example, start multiple parallel requests to the same or different servers. There is only one callback defined per filter. So, it is already something that a developer needs to handle (i.e. handle multiple calls on the callback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this pattern is also the same for just the .NET layer itself. We have a TLS/SSL certificate validator callback defined per handler. So, multiple requests (at the same time perhaps) can happen against the single callback method.
if (redirectUri.Scheme != Uri.UriSchemeHttp && | ||
redirectUri.Scheme != Uri.UriSchemeHttps) | ||
{ | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is http->https not allowed in .Net Framework?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This particular check makes sure that we don't try to redirect to a scheme like FTP or FILE or WS. So, the only possible destination schemes need to be HTTP or HTTPS.
if (request.RequestUri.Scheme == Uri.UriSchemeHttps && | ||
redirectUri.Scheme == Uri.UriSchemeHttp) | ||
{ | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be a security exception so that the app understands that even if a redirect was requested, it was not honored because it would cause a https->http transition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current .NET Core behavior is not to throw an exception. But simply to not follow the redirect.
See this test that already is there:
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs#L544
FWIW, the .NET Framework behavior allows HTTPS -> HTTP
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build |
I pushed a new commit to address all outstanding feedback. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Outerloop tests for .NET Core and UWP are failing on this test:
It looks like the www.microsoft.com server is not using HTTP/2.0 protocol today. Our tests assume that this server always uses HTTP/2 protocol. Don't know if this is temporary problem. We might need to switch to a different HTTP/2 test server. I also confirmed using the Chrome browser that www.microsoft.com is not using HTTP/2. cc: @stephentoub @CIPop |
And FWIW, the HTTP/2 test failing on Windows doesn't run on Linux. There is another HTTP/2 test that does run on Linux but it allows HTTP/2 or HTTP/1.1 for a response. |
I added a new issue #22735 and will disable that test for now. |
@dotnet-bot Test Outerloop UWP CoreCLR x64 Debug Build |
@dotnet-bot Test Outerloop NETFX x86 Release Build |
Unrelated CI failures in Linux and UWP System.IO.Tests.FileStream_LockUnlock |
[release/uwp6.0] Fix HttpClient redirection logic on UAP (#22702)
* Address delayed feedback Address delayed feedback from PR #22702 and release/uwp6.0 port PR #22759 * Remove active issue on Uap TRACE test Uap platform doesn't support the 'TRACE' verb. After discussion with Wininet team, the method is explicitly not allowed. So, removing the activeissue and just skipping that part of the test. Fixes #22161
* Fix HttpClient redirection logic on UAP This PR changes the implementation of sending requests in HttpClient for UAP when dealing with redirection. In the past, we would let the WinRT layer handle auto redirection logic. However, due to dotnet/corefx#9003, the cookies were getting lost on 3xx responses since the .NET layer didn't see them. So, this PR implements the redirection ourselves. One important part of redirection is that we need to drop credentials. The WinRT layer did this for us. However, we are unable to use a single WinRT HttpBaseProtocolFilter object since we need to remove the credential after the first redirect. So, we need to maintain a second filter that uses no credentials and keep it in sync regarding all the other properties of the primary filter. With this PR, the behavior of other aspects (such as controlling max number of redirects, etc.) now matches .NET Framework. So, some tests were adjusted to remove the UAP specific behavior checks. Fixes dotnet/corefx#9003 Fixes dotnet/corefx#22191 * Disable test on Linux * Address PR feedback * Disable failing HTTP/2 test Commit migrated from dotnet/corefx@f2308f8
* Address delayed feedback Address delayed feedback from PR dotnet/corefx#22702 and release/uwp6.0 port PR dotnet/corefx#22759 * Remove active issue on Uap TRACE test Uap platform doesn't support the 'TRACE' verb. After discussion with Wininet team, the method is explicitly not allowed. So, removing the activeissue and just skipping that part of the test. Fixes dotnet/corefx#22161 Commit migrated from dotnet/corefx@f7bc8b4
This PR changes the implementation of sending requests in HttpClient for UAP when
dealing with redirection. In the past, we would let the WinRT layer handle auto
redirection logic. However, due to #9003, the cookies were getting lost on 3xx responses
since the .NET layer didn't see them. So, this PR implements the redirection ourselves.
One important part of redirection is that we need to drop credentials. The WinRT layer
did this for us. However, we are unable to use a single WinRT HttpBaseProtocolFilter object
since we need to remove the credential after the first redirect. So, we need to maintain
a second filter that uses no credentials and keep it in sync regarding all the other properties
of the primary filter.
With this PR, the behavior of other aspects (such as controlling max number of redirects, etc.)
now matches .NET Framework. So, some tests were adjusted to remove the UAP specific behavior
checks.
Fixes #9003
Fixes #22191