From c8c00b2c5d4aadb4f4d075e26a8762804fd09bc2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 22 Feb 2018 11:17:32 -0500 Subject: [PATCH 1/2] Fix fragment handling in HttpClient SocketsHttpHandler isn't sending fragments, nor is it properly inheriting the fragment from the original request URI into the redirect location URI when the original URI had one and the redirect URI did not, even though RFC 7231 says it must. This commit fixes that for SocketsHttpHandler. WinHttpHandler also isn't handling this inheritance according to the RFC. It appears that the logic for WinHttpHandler would actually need to be changed in WINHTTP itself, or else WinHttpHandler would need to be changed to do the redirects itself. Neither CurlHandler or NetFxHandler send fragments at all. This commit also fixes the test to correctly compare the expected and actual Uris... apparently Uri equality doesn't factor in fragments, so they're first converted to strings. It also updates the test to also validate that the server received the URI with the fragment included. --- .../Http/SocketsHttpHandler/HttpConnection.cs | 2 +- .../SocketsHttpHandler/RedirectHandler.cs | 15 ++++- .../FunctionalTests/HttpClientHandlerTest.cs | 55 ++++++++++++++++--- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 8b602e593a6c..9fac94f7ff45 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -278,7 +278,7 @@ public async Task SendAsync(HttpRequestMessage request, Can await WriteAsciiStringAsync(request.RequestUri.IdnHost).ConfigureAwait(false); } - await WriteStringAsync(request.RequestUri.PathAndQuery).ConfigureAwait(false); + await WriteStringAsync(request.RequestUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped)).ConfigureAwait(false); // Fall back to 1.1 for all versions other than 1.0 Debug.Assert(request.Version.Major >= 0 && request.Version.Minor >= 0); // guaranteed by Version class diff --git a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs index f17332a13dce..a1f4e5f1d3c8 100644 --- a/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs +++ b/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs @@ -81,17 +81,30 @@ private Uri GetUriForRedirect(Uri requestUri, HttpResponseMessage response) return null; } + // Ensure the redirect location is an absolute URI. if (!location.IsAbsoluteUri) { location = new Uri(requestUri, location); } + // Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a + // fragment should inherit the fragment from the original URI. + string requestFragment = requestUri.Fragment; + if (!string.IsNullOrEmpty(requestFragment)) + { + string redirectFragment = location.Fragment; + if (string.IsNullOrEmpty(redirectFragment)) + { + location = new UriBuilder(location) { Fragment = requestFragment }.Uri; + } + } + // Disallow automatic redirection from secure to non-secure schemes if (HttpUtilities.IsSupportedSecureScheme(requestUri.Scheme) && !HttpUtilities.IsSupportedSecureScheme(location.Scheme)) { if (NetEventSource.IsEnabled) { - NetEventSource.Info(this, $"Insecure https to http redirect from {requestUri} to {location} blocked."); + NetEventSource.Info(this, $"Insecure https to http redirect from '{requestUri}' to '{location}' blocked."); } return null; diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs index ccf19eba8257..be32f4a52615 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs @@ -912,40 +912,77 @@ await TestHelper.WhenAllCompletedOrAnyFailed( } [OuterLoop] // TODO: Issue #11345 - [ConditionalTheory(nameof(IsNotWindows7))] // Skip test on Win7 since WinHTTP has bugs w/ fragments. + [Theory] [InlineData("#origFragment", "", "#origFragment", false)] [InlineData("#origFragment", "", "#origFragment", true)] [InlineData("", "#redirFragment", "#redirFragment", false)] [InlineData("", "#redirFragment", "#redirFragment", true)] [InlineData("#origFragment", "#redirFragment", "#redirFragment", false)] [InlineData("#origFragment", "#redirFragment", "#redirFragment", true)] - [ActiveIssue(27217)] public async Task GetAsync_AllowAutoRedirectTrue_RetainsOriginalFragmentIfAppropriate( string origFragment, string redirFragment, string expectedFragment, bool useRelativeRedirect) { + if (IsCurlHandler) + { + // Starting with libcurl 7.20, "fragment part of URLs are no longer sent to the server". + // So CurlHandler doesn't send fragments. + return; + } + + if (IsNetfxHandler) + { + // Similarly, netfx doesn't send fragments at all. + return; + } + + if (IsWinHttpHandler && + (PlatformDetection.IsWindows7 || (!string.IsNullOrEmpty(origFragment) && string.IsNullOrEmpty(redirFragment)))) + { + // According to https://tools.ietf.org/html/rfc7231#section-7.1.2, + // "If the Location value provided in a 3xx (Redirection) response does + // not have a fragment component, a user agent MUST process the + // redirection as if the value inherits the fragment component of the + // URI reference used to generate the request target(i.e., the + // redirection inherits the original reference's fragment, if any)." + // WINHTTP is not doing this, and thus neither is WinHttpHandler. + // It also has issues with fragments on Windows 7. + return; + } + HttpClientHandler handler = CreateHttpClientHandler(); handler.AllowAutoRedirect = true; using (var client = new HttpClient(handler)) { await LoopbackServer.CreateServerAsync(async (origServer, origUrl) => { - origUrl = new Uri(origUrl.ToString() + origFragment); - Uri redirectUrl = useRelativeRedirect ? - new Uri(origUrl.PathAndQuery + redirFragment, UriKind.Relative) : - new Uri(origUrl.ToString() + redirFragment); - Uri expectedUrl = new Uri(origUrl.ToString() + expectedFragment); + origUrl = new UriBuilder(origUrl) { Fragment = origFragment }.Uri; + Uri redirectUrl = new UriBuilder(origUrl) { Fragment = redirFragment }.Uri; + if (useRelativeRedirect) + { + redirectUrl = new Uri(redirectUrl.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.SafeUnescaped), UriKind.Relative); + } + Uri expectedUrl = new UriBuilder(origUrl) { Fragment = expectedFragment }.Uri; + // Make and receive the first request that'll be redirected. Task getResponse = client.GetAsync(origUrl); Task firstRequest = origServer.AcceptConnectionSendResponseAndCloseAsync(HttpStatusCode.Found, $"Location: {redirectUrl}\r\n"); Assert.Equal(firstRequest, await Task.WhenAny(firstRequest, getResponse)); - Task secondRequest = origServer.AcceptConnectionSendResponseAndCloseAsync(); + // Receive the second request. + Task> secondRequest = origServer.AcceptConnectionSendResponseAndCloseAsync(); await TestHelper.WhenAllCompletedOrAnyFailed(secondRequest, getResponse); + // Make sure the server received the second request for the right Uri. + Assert.NotEmpty(secondRequest.Result); + string[] statusLineParts = secondRequest.Result[0].Split(' '); + Assert.Equal(3, statusLineParts.Length); + Assert.Equal(expectedUrl.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.SafeUnescaped), statusLineParts[1]); + + // Make sure the request message was updated with the correct redirected location. using (HttpResponseMessage response = await getResponse) { Assert.Equal(200, (int)response.StatusCode); - Assert.Equal(expectedUrl, response.RequestMessage.RequestUri); + Assert.Equal(expectedUrl.ToString(), response.RequestMessage.RequestUri.ToString()); } }); } From e117da1f351db5163cdc50989aece6d11b21bf6b Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 22 Feb 2018 13:39:12 -0500 Subject: [PATCH 2/2] Disable the fragments test on WinHttpHandler Apparently it sometimes doesn't do the "right thing" even in other cases. --- .../tests/FunctionalTests/HttpClientHandlerTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs index be32f4a52615..744dece8d02f 100644 --- a/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs +++ b/src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs @@ -935,8 +935,7 @@ public async Task GetAsync_AllowAutoRedirectTrue_RetainsOriginalFragmentIfApprop return; } - if (IsWinHttpHandler && - (PlatformDetection.IsWindows7 || (!string.IsNullOrEmpty(origFragment) && string.IsNullOrEmpty(redirFragment)))) + if (IsWinHttpHandler) { // According to https://tools.ietf.org/html/rfc7231#section-7.1.2, // "If the Location value provided in a 3xx (Redirection) response does @@ -945,7 +944,8 @@ public async Task GetAsync_AllowAutoRedirectTrue_RetainsOriginalFragmentIfApprop // URI reference used to generate the request target(i.e., the // redirection inherits the original reference's fragment, if any)." // WINHTTP is not doing this, and thus neither is WinHttpHandler. - // It also has issues with fragments on Windows 7. + // It also sometimes doesn't include the fragments for redirects + // even in other cases. return; }