Skip to content

Commit

Permalink
WinHttpHandler: Read HTTP/2 trailing headers
Browse files Browse the repository at this point in the history
  • Loading branch information
antonfirsov authored Mar 10, 2021
1 parent 016ec29 commit 423f5e1
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 19 deletions.
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 @@ -137,7 +137,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 @@ -94,6 +94,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

0 comments on commit 423f5e1

Please sign in to comment.