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

WinHttpHandler: Read HTTP/2 trailing headers (replacement PR) #48704

Merged
merged 28 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
df5dcd1
WinHttpHandler: Read trailing headers
JamesNK Dec 24, 2020
d943c1e
Merge remote-tracking branch 'JamesNK/jamesnk/winhttp-trailingheaders…
antonfirsov Feb 9, 2021
3e5f1cf
Merge branch 'master' into winhttp/trailing-headers
antonfirsov Feb 22, 2021
08d2f03
Merge branch 'master' into winhttp/trailing-headers
antonfirsov Feb 22, 2021
051d910
add "remote server" test
antonfirsov Feb 22, 2021
6f4449b
PlatformDetection.SupportsAlpn = false on .NET Framework
antonfirsov Feb 22, 2021
ba83606
make tests conditional
antonfirsov Feb 22, 2021
4ea3699
WinHttpTrailersHelper
antonfirsov Feb 23, 2021
46df0f1
add OS support check
antonfirsov Feb 23, 2021
cc4f74d
refactor step 1
antonfirsov Feb 23, 2021
8285ee6
refactor step 2
antonfirsov Feb 23, 2021
c5375e7
refactor step 3
antonfirsov Feb 23, 2021
6594f31
Add comments
antonfirsov Feb 24, 2021
1370e6d
simpify GetResponseHeaderCharBufferLength
antonfirsov Feb 24, 2021
3cceba8
fix build
antonfirsov Feb 24, 2021
e41eacb
Remove Http2GetAsyncResponseHeadersReadOption_RemoteServer_TrailingHe…
antonfirsov Feb 24, 2021
d48ae0a
fix WinHttpHandler.Unit.Tests
antonfirsov Feb 24, 2021
6635caf
fix unit tests
antonfirsov Feb 25, 2021
575cbd8
add comments on RequestMessagePropertyName
antonfirsov Feb 25, 2021
fdf4872
Merge branch 'winhttp/trailing-headers' of https://github.com/antonfi…
antonfirsov Feb 25, 2021
098356a
simplify GetTrailersSupported
antonfirsov Feb 25, 2021
01109e8
improve comments
antonfirsov Feb 25, 2021
cf13ba0
make code shorter in GetResponseHeaderCharBufferLength
antonfirsov Feb 25, 2021
29969b4
HttpClientHandlerTestBase.AllowAllCertificates should not be static
antonfirsov Feb 26, 2021
0332b0e
revert previous attempt
antonfirsov Feb 26, 2021
05b85ab
AllowAllCertificates: use parameter instead of static property
antonfirsov Mar 1, 2021
a3e2350
Merge branch 'winhttp/fix-HttpClientHandlerTestBase' into winhttp/tra…
antonfirsov Mar 2, 2021
4604e47
Merge branch 'main' into winhttp/trailing-headers
antonfirsov Mar 2, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ internal partial class WinHttp
public const uint WINHTTP_QUERY_STATUS_TEXT = 20;
public const uint WINHTTP_QUERY_RAW_HEADERS = 21;
public const uint WINHTTP_QUERY_RAW_HEADERS_CRLF = 22;
public const uint WINHTTP_QUERY_FLAG_TRAILERS = 0x02000000;
public const uint WINHTTP_QUERY_CONTENT_ENCODING = 29;
public const uint WINHTTP_QUERY_SET_COOKIE = 43;
public const uint WINHTTP_QUERY_CUSTOM = 65535;
Expand Down Expand Up @@ -164,6 +165,7 @@ internal partial class WinHttp
public const uint WINHTTP_OPTION_WEB_SOCKET_SEND_BUFFER_SIZE = 123;

public const uint WINHTTP_OPTION_TCP_KEEPALIVE = 152;
public const uint WINHTTP_OPTION_STREAM_ERROR_CODE = 159;

public enum WINHTTP_WEB_SOCKET_BUFFER_TYPE
{
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/Common/src/System/Net/SecurityProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace System.Net
internal static class SecurityProtocol
{
public const SslProtocols DefaultSecurityProtocols =
#if !NETSTANDARD2_0 && !NETFRAMEWORK
#if !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETFRAMEWORK
SslProtocols.Tls13 |
#endif
SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static bool IsNonZeroLowerBoundArraySupported
// OSX - SecureTransport doesn't expose alpn APIs. TODO https://github.com/dotnet/runtime/issues/27727
public static bool IsOpenSslSupported => IsLinux || IsFreeBSD || Isillumos || IsSolaris;

public static bool SupportsAlpn => (IsWindows && !IsWindows7) ||
public static bool SupportsAlpn => (IsWindows && !IsWindows7 && !IsNetFramework) ||
(IsOpenSslSupported &&
(OpenSslVersion.Major >= 1 && (OpenSslVersion.Minor >= 1 || OpenSslVersion.Build >= 2)));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IncludeDllSafeSearchPathAttribute>true</IncludeDllSafeSearchPathAttribute>
<TargetFrameworks>netstandard2.0-windows;netstandard2.0;net461-windows</TargetFrameworks>
<TargetFrameworks>netstandard2.0-windows;netstandard2.0;netstandard2.1-windows;netstandard2.1;net461-windows</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down Expand Up @@ -91,6 +91,7 @@
<Compile Include="System\Net\Http\WinHttpResponseParser.cs" />
<Compile Include="System\Net\Http\WinHttpResponseStream.cs" />
<Compile Include="System\Net\Http\WinHttpTraceHelper.cs" />
<Compile Include="System\Net\Http\WinHttpTrailersHelper.cs" />
<Compile Include="System\Net\Http\WinHttpTransportContext.cs" />
<Compile Include="$(CommonPath)System\IO\StreamHelpers.CopyValidation.cs"
Link="Common\System\IO\StreamHelpers.CopyValidation.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static HttpResponseMessage CreateResponseMessage(
// Create a single buffer to use for all subsequent WinHttpQueryHeaders string interop calls.
// This buffer is the length needed for WINHTTP_QUERY_RAW_HEADERS_CRLF, which includes the status line
// and all headers separated by CRLF, so it should be large enough for any individual status line or header queries.
int bufferLength = GetResponseHeaderCharBufferLength(requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF);
int bufferLength = GetResponseHeaderCharBufferLength(requestHandle, isTrailingHeaders: false);
char[] buffer = ArrayPool<char>.Shared.Rent(bufferLength);
try
{
Expand Down Expand Up @@ -58,7 +58,7 @@ public static HttpResponseMessage CreateResponseMessage(
string.Empty;

// Create response stream and wrap it in a StreamContent object.
var responseStream = new WinHttpResponseStream(requestHandle, state);
var responseStream = new WinHttpResponseStream(requestHandle, state, response);
state.RequestHandle = null; // ownership successfully transfered to WinHttpResponseStram.
Stream decompressedStream = responseStream;

Expand Down Expand Up @@ -223,19 +223,26 @@ private static unsafe int GetResponseHeader(SafeWinHttpHandle requestHandle, uin
/// <summary>
/// Returns the size of the char array buffer.
/// </summary>
private static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, uint infoLevel)
public static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, bool isTrailingHeaders)
{
char* buffer = null;
int bufferLength = 0;
uint index = 0;

uint infoLevel = Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF;
if (isTrailingHeaders)
{
infoLevel |= Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS;
}

if (!QueryHeaders(requestHandle, infoLevel, buffer, ref bufferLength, ref index))
{
int lastError = Marshal.GetLastWin32Error();

Debug.Assert(lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND);
Debug.Assert(isTrailingHeaders || lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND);

if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER)
if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER &&
(!isTrailingHeaders || lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND))
{
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
}
Expand Down Expand Up @@ -306,10 +313,7 @@ private static void ParseResponseHeaders(
reader.ReadLine();

// Parse the array of headers and split them between Content headers and Response headers.
string headerName;
string headerValue;

while (reader.ReadHeader(out headerName, out headerValue))
while (reader.ReadHeader(out string headerName, out string headerValue))
{
if (!responseHeaders.TryAddWithoutValidation(headerName, headerValue))
{
Expand All @@ -331,6 +335,27 @@ private static void ParseResponseHeaders(
}
}

public static void ParseResponseTrailers(
SafeWinHttpHandle requestHandle,
HttpResponseMessage response,
char[] buffer)
{
HttpHeaders responseTrailers = WinHttpTrailersHelper.GetResponseTrailers(response);

int bufferLength = GetResponseHeader(
requestHandle,
Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF | Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS,
buffer);

var reader = new WinHttpResponseHeaderReader(buffer, 0, bufferLength);

// Parse the array of headers and split them between Content headers and Response headers.
while (reader.ReadHeader(out string headerName, out string headerValue))
{
responseTrailers.TryAddWithoutValidation(headerName, headerValue);
}
}

private static bool IsResponseHttp2(SafeWinHttpHandle requestHandle)
{
uint data = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ internal sealed class WinHttpResponseStream : Stream
{
private volatile bool _disposed;
private readonly WinHttpRequestState _state;
private readonly HttpResponseMessage _responseMessage;
private SafeWinHttpHandle _requestHandle;
private bool _readTrailingHeaders;

internal WinHttpResponseStream(SafeWinHttpHandle requestHandle, WinHttpRequestState state)
internal WinHttpResponseStream(SafeWinHttpHandle requestHandle, WinHttpRequestState state, HttpResponseMessage responseMessage)
{
_state = state;
_responseMessage = responseMessage;
_requestHandle = requestHandle;
}

Expand Down Expand Up @@ -126,6 +129,7 @@ private async Task CopyToAsyncCore(Stream destination, byte[] buffer, Cancellati
int bytesAvailable = await _state.LifecycleAwaitable;
if (bytesAvailable == 0)
{
ReadResponseTrailers();
break;
}
Debug.Assert(bytesAvailable > 0);
Expand All @@ -142,12 +146,17 @@ private async Task CopyToAsyncCore(Stream destination, byte[] buffer, Cancellati
int bytesRead = await _state.LifecycleAwaitable;
if (bytesRead == 0)
{
ReadResponseTrailers();
break;
}
Debug.Assert(bytesRead > 0);

// Write that data out to the output stream
#if NETSTANDARD2_1
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
#else
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
#endif
}
}
finally
Expand Down Expand Up @@ -240,7 +249,14 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc
}
}

return await _state.LifecycleAwaitable;
int bytesRead = await _state.LifecycleAwaitable;

if (bytesRead == 0)
{
ReadResponseTrailers();
}

return bytesRead;
}
finally
{
Expand All @@ -249,6 +265,35 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc
}
}

private void ReadResponseTrailers()
{
// Only load response trailers if:
// 1. WINHTTP_QUERY_FLAG_TRAILERS is supported by the OS
// 2. HTTP/2 or later (WINHTTP_QUERY_FLAG_TRAILERS does not work with HTTP/1.1)
// 3. Response trailers not already loaded
if (!WinHttpTrailersHelper.OsSupportsTrailers || _responseMessage.Version < WinHttpHandler.HttpVersion20 || _readTrailingHeaders)
{
return;
}

_readTrailingHeaders = true;

var bufferLength = WinHttpResponseParser.GetResponseHeaderCharBufferLength(_requestHandle, isTrailingHeaders: true);

if (bufferLength != 0)
{
char[] trailersBuffer = ArrayPool<char>.Shared.Rent(bufferLength);
try
{
WinHttpResponseParser.ParseResponseTrailers(_requestHandle, _responseMessage, trailersBuffer);
}
finally
{
ArrayPool<char>.Shared.Return(trailersBuffer);
}
}
}

public override int Read(byte[] buffer, int offset, int count)
{
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using SafeWinHttpHandle = Interop.WinHttp.SafeWinHttpHandle;

namespace System.Net.Http
{
internal static class WinHttpTrailersHelper
{
// UNITTEST is true when building against WinHttpHandler.Unit.Tests, which includes the source file.
#if !NETSTANDARD2_1 && !UNITTEST
// Trailer property name was chosen to be descriptive and be unlikely to collide with a user set property.
// Apps and libraries will use this key so it shouldn't change.
private const string RequestMessagePropertyName = "__ResponseTrailers";
private class HttpResponseTrailers : HttpHeaders
{
}
#endif
private static Lazy<bool> s_trailersSupported = new Lazy<bool>(GetTrailersSupported);
public static bool OsSupportsTrailers => s_trailersSupported.Value;

public static HttpHeaders GetResponseTrailers(HttpResponseMessage response)
{
#if NETSTANDARD2_1 || UNITTEST
return response.TrailingHeaders;
#else
HttpResponseTrailers responseTrailers = new HttpResponseTrailers();
response.RequestMessage.Properties[RequestMessagePropertyName] = responseTrailers;
return responseTrailers;
#endif
}

// There is no way to verify if WINHTTP_QUERY_FLAG_TRAILERS is supported by the OS without creating a request.
// Instead, the WinHTTP team recommended to check if WINHTTP_OPTION_STREAM_ERROR_CODE is recognized by the OS.
// Both features were introduced in Manganese and are planned to be backported to older Windows versions together.
private static bool GetTrailersSupported()
{
using SafeWinHttpHandle sessionHandle = Interop.WinHttp.WinHttpOpen(
IntPtr.Zero,
Interop.WinHttp.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
Interop.WinHttp.WINHTTP_NO_PROXY_NAME,
Interop.WinHttp.WINHTTP_NO_PROXY_BYPASS,
(int)Interop.WinHttp.WINHTTP_FLAG_ASYNC);

if (sessionHandle.IsInvalid) return false;
uint buffer = 0;
uint bufferSize = sizeof(uint);
if (Interop.WinHttp.WinHttpQueryOption(sessionHandle, Interop.WinHttp.WINHTTP_OPTION_STREAM_ERROR_CODE, ref buffer, ref bufferSize))
{
Debug.Fail("Querying WINHTTP_OPTION_STREAM_ERROR_CODE on a session handle should never succeed.");
return false;
}

int lastError = Marshal.GetLastWin32Error();

// New Windows builds are expected to fail with ERROR_WINHTTP_INCORRECT_HANDLE_TYPE,
// when querying WINHTTP_OPTION_STREAM_ERROR_CODE on a session handle.
return lastError != Interop.WinHttp.ERROR_INVALID_PARAMETER;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
<Compile Include="BaseCertificateTest.cs" />
<Compile Include="ServerCertificateTest.cs" />
<Compile Include="WinHttpHandlerTest.cs" />
<Compile Include="XunitTestAssemblyAtrributes.cs" />
<Compile Include="XunitTestAssemblyAtrributes.cs" />
<Compile Include="$(CommonPath)\System\Net\Http\HttpHandlerDefaults.cs"
Link="Common\System\Net\Http\HttpHandlerDefaults.cs" />
<Compile Include="$(CommonTestPath)System\IO\DelegateStream.cs"
Link="Common\System\IO\DelegateStream.cs" />
Link="Common\System\IO\DelegateStream.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.cs"
Link="Common\System\Net\Configuration.Certificates.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Security.cs"
Expand Down Expand Up @@ -128,7 +128,7 @@
<Compile Include="$(CommonTestPath)System\Net\Http\RepeatedFlushContent.cs"
Link="Common\System\Net\Http\RepeatedFlushContent.cs" />
<Compile Include="$(CommonTestPath)System\Net\Http\ResponseStreamTest.cs"
Link="Common\System\Net\Http\ResponseStreamTest.cs" />
Link="Common\System\Net\Http\ResponseStreamTest.cs" />
<Compile Include="$(CommonTestPath)System\Net\Http\SchSendAuxRecordHttpTest.cs"
Link="Common\System\Net\Http\SchSendAuxRecordHttpTest.cs" />
<Compile Include="$(CommonTestPath)System\Net\Http\SyncBlockingContent.cs"
Expand All @@ -143,6 +143,7 @@
<Compile Include="WinHttpClientHandler.cs" />
<Compile Include="PlatformHandlerTest.cs" />
<Compile Include="ClientCertificateTest.cs" />
<Compile Include="TrailingHeadersTest.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
Expand Down
Loading