Skip to content

Commit df5dcd1

Browse files
committed
WinHttpHandler: Read trailing headers
1 parent f3c6cb0 commit df5dcd1

File tree

8 files changed

+328
-32
lines changed

8 files changed

+328
-32
lines changed

src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ internal partial class WinHttp
6969
public const uint WINHTTP_QUERY_STATUS_TEXT = 20;
7070
public const uint WINHTTP_QUERY_RAW_HEADERS = 21;
7171
public const uint WINHTTP_QUERY_RAW_HEADERS_CRLF = 22;
72+
public const uint WINHTTP_QUERY_FLAG_TRAILERS = 0x02000000;
7273
public const uint WINHTTP_QUERY_CONTENT_ENCODING = 29;
7374
public const uint WINHTTP_QUERY_SET_COOKIE = 43;
7475
public const uint WINHTTP_QUERY_CUSTOM = 65535;

src/libraries/Common/src/System/Net/SecurityProtocol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace System.Net
88
internal static class SecurityProtocol
99
{
1010
public const SslProtocols DefaultSecurityProtocols =
11-
#if !NETSTANDARD2_0 && !NETFRAMEWORK
11+
#if !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETFRAMEWORK
1212
SslProtocols.Tls13 |
1313
#endif
1414
SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;

src/libraries/System.Net.Http.WinHttpHandler/src/System.Net.Http.WinHttpHandler.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<IncludeDllSafeSearchPathAttribute>true</IncludeDllSafeSearchPathAttribute>
4-
<TargetFrameworks>netstandard2.0-windows;netstandard2.0;net461-windows</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0-windows;netstandard2.0;netstandard2.1-windows;netstandard2.1;net461-windows</TargetFrameworks>
55
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
66
<Nullable>enable</Nullable>
77
</PropertyGroup>

src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public static HttpResponseMessage CreateResponseMessage(
2929
// Create a single buffer to use for all subsequent WinHttpQueryHeaders string interop calls.
3030
// This buffer is the length needed for WINHTTP_QUERY_RAW_HEADERS_CRLF, which includes the status line
3131
// and all headers separated by CRLF, so it should be large enough for any individual status line or header queries.
32-
int bufferLength = GetResponseHeaderCharBufferLength(requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF);
32+
int bufferLength = GetResponseHeaderCharBufferLength(requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF, isTrailingHeaders: false);
3333
char[] buffer = ArrayPool<char>.Shared.Rent(bufferLength);
3434
try
3535
{
@@ -58,7 +58,7 @@ public static HttpResponseMessage CreateResponseMessage(
5858
string.Empty;
5959

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

@@ -93,7 +93,7 @@ public static HttpResponseMessage CreateResponseMessage(
9393
response.RequestMessage = request;
9494

9595
// Parse raw response headers and place them into response message.
96-
ParseResponseHeaders(requestHandle, response, buffer, stripEncodingHeaders);
96+
ParseResponseHeaders(requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF, response, buffer, stripEncodingHeaders, isTrailers: false);
9797

9898
if (response.RequestMessage.Method != HttpMethod.Head)
9999
{
@@ -223,7 +223,7 @@ private static unsafe int GetResponseHeader(SafeWinHttpHandle requestHandle, uin
223223
/// <summary>
224224
/// Returns the size of the char array buffer.
225225
/// </summary>
226-
private static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, uint infoLevel)
226+
public static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle requestHandle, uint infoLevel, bool isTrailingHeaders)
227227
{
228228
char* buffer = null;
229229
int bufferLength = 0;
@@ -233,11 +233,21 @@ private static unsafe int GetResponseHeaderCharBufferLength(SafeWinHttpHandle re
233233
{
234234
int lastError = Marshal.GetLastWin32Error();
235235

236-
Debug.Assert(lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND);
236+
if (!isTrailingHeaders)
237+
{
238+
Debug.Assert(lastError != Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND);
237239

238-
if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER)
240+
if (lastError != Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER)
241+
{
242+
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
243+
}
244+
}
245+
else
239246
{
240-
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
247+
if (!(lastError == Interop.WinHttp.ERROR_INSUFFICIENT_BUFFER || lastError == Interop.WinHttp.ERROR_WINHTTP_HEADER_NOT_FOUND))
248+
{
249+
throw WinHttpException.CreateExceptionUsingError(lastError, nameof(Interop.WinHttp.WinHttpQueryHeaders));
250+
}
241251
}
242252
}
243253

@@ -286,47 +296,69 @@ private static string GetReasonPhrase(HttpStatusCode statusCode, char[] buffer,
286296
new string(buffer, 0, bufferLength);
287297
}
288298

289-
private static void ParseResponseHeaders(
299+
private class HttpResponseTrailers : HttpHeaders
300+
{
301+
}
302+
303+
public static void ParseResponseHeaders(
290304
SafeWinHttpHandle requestHandle,
305+
uint infoLevel,
291306
HttpResponseMessage response,
292307
char[] buffer,
293-
bool stripEncodingHeaders)
308+
bool stripEncodingHeaders,
309+
bool isTrailers)
294310
{
295311
HttpResponseHeaders responseHeaders = response.Headers;
296312
HttpContentHeaders contentHeaders = response.Content.Headers;
313+
#if NETSTANDARD2_1
314+
HttpResponseHeaders responseTrailers = response.TrailingHeaders;
315+
#else
316+
HttpResponseTrailers responseTrailers = new HttpResponseTrailers();
317+
response.RequestMessage.Properties["__ResponseTrailers"] = responseTrailers;
318+
#endif
297319

298320
int bufferLength = GetResponseHeader(
299321
requestHandle,
300-
Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF,
322+
infoLevel,
301323
buffer);
302324

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

305-
// Skip the first line which contains status code, etc. information that we already parsed.
306-
reader.ReadLine();
327+
if (!isTrailers)
328+
{
329+
// Skip the first line which contains status code, etc. information that we already parsed.
330+
reader.ReadLine();
331+
}
307332

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

312337
while (reader.ReadHeader(out headerName, out headerValue))
313338
{
314-
if (!responseHeaders.TryAddWithoutValidation(headerName, headerValue))
339+
if (!isTrailers)
315340
{
316-
if (stripEncodingHeaders)
341+
if (!responseHeaders.TryAddWithoutValidation(headerName, headerValue))
317342
{
318-
// Remove Content-Length and Content-Encoding headers if we are
319-
// decompressing the response stream in the handler (due to
320-
// WINHTTP not supporting it in a particular downlevel platform).
321-
// This matches the behavior of WINHTTP when it does decompression itself.
322-
if (string.Equals(HttpKnownHeaderNames.ContentLength, headerName, StringComparison.OrdinalIgnoreCase) ||
323-
string.Equals(HttpKnownHeaderNames.ContentEncoding, headerName, StringComparison.OrdinalIgnoreCase))
343+
if (stripEncodingHeaders)
324344
{
325-
continue;
345+
// Remove Content-Length and Content-Encoding headers if we are
346+
// decompressing the response stream in the handler (due to
347+
// WINHTTP not supporting it in a particular downlevel platform).
348+
// This matches the behavior of WINHTTP when it does decompression itself.
349+
if (string.Equals(HttpKnownHeaderNames.ContentLength, headerName, StringComparison.OrdinalIgnoreCase) ||
350+
string.Equals(HttpKnownHeaderNames.ContentEncoding, headerName, StringComparison.OrdinalIgnoreCase))
351+
{
352+
continue;
353+
}
326354
}
327-
}
328355

329-
contentHeaders.TryAddWithoutValidation(headerName, headerValue);
356+
contentHeaders.TryAddWithoutValidation(headerName, headerValue);
357+
}
358+
}
359+
else
360+
{
361+
responseTrailers.TryAddWithoutValidation(headerName, headerValue);
330362
}
331363
}
332364
}

src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ internal sealed class WinHttpResponseStream : Stream
1616
{
1717
private volatile bool _disposed;
1818
private readonly WinHttpRequestState _state;
19+
private readonly HttpResponseMessage _responseMessage;
1920
private SafeWinHttpHandle _requestHandle;
21+
private bool _readTrailingHeaders;
2022

21-
internal WinHttpResponseStream(SafeWinHttpHandle requestHandle, WinHttpRequestState state)
23+
internal WinHttpResponseStream(SafeWinHttpHandle requestHandle, WinHttpRequestState state, HttpResponseMessage responseMessage)
2224
{
2325
_state = state;
26+
_responseMessage = responseMessage;
2427
_requestHandle = requestHandle;
2528
}
2629

@@ -126,6 +129,7 @@ private async Task CopyToAsyncCore(Stream destination, byte[] buffer, Cancellati
126129
int bytesAvailable = await _state.LifecycleAwaitable;
127130
if (bytesAvailable == 0)
128131
{
132+
ReadResponseTrailers();
129133
break;
130134
}
131135
Debug.Assert(bytesAvailable > 0);
@@ -142,12 +146,17 @@ private async Task CopyToAsyncCore(Stream destination, byte[] buffer, Cancellati
142146
int bytesRead = await _state.LifecycleAwaitable;
143147
if (bytesRead == 0)
144148
{
149+
ReadResponseTrailers();
145150
break;
146151
}
147152
Debug.Assert(bytesRead > 0);
148153

149154
// Write that data out to the output stream
155+
#if NETSTANDARD2_1
156+
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
157+
#else
150158
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
159+
#endif
151160
}
152161
}
153162
finally
@@ -240,7 +249,14 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc
240249
}
241250
}
242251

243-
return await _state.LifecycleAwaitable;
252+
int bytesRead = await _state.LifecycleAwaitable;
253+
254+
if (bytesRead == 0)
255+
{
256+
ReadResponseTrailers();
257+
}
258+
259+
return bytesRead;
244260
}
245261
finally
246262
{
@@ -249,6 +265,37 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc
249265
}
250266
}
251267

268+
private void ReadResponseTrailers()
269+
{
270+
// Only load response trailers if:
271+
// 1. HTTP/2 or later
272+
// 2. Response trailers not already loaded
273+
if (_readTrailingHeaders || _responseMessage.Version < WinHttpHandler.HttpVersion20)
274+
{
275+
return;
276+
}
277+
278+
_readTrailingHeaders = true;
279+
280+
var bufferLength = WinHttpResponseParser.GetResponseHeaderCharBufferLength(
281+
_requestHandle,
282+
Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF | Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS,
283+
isTrailingHeaders: true);
284+
285+
if (bufferLength != 0)
286+
{
287+
char[] trailersBuffer = ArrayPool<char>.Shared.Rent(bufferLength);
288+
try
289+
{
290+
WinHttpResponseParser.ParseResponseHeaders(_requestHandle, Interop.WinHttp.WINHTTP_QUERY_RAW_HEADERS_CRLF | Interop.WinHttp.WINHTTP_QUERY_FLAG_TRAILERS, _responseMessage, trailersBuffer, stripEncodingHeaders: false, isTrailers: true);
291+
}
292+
finally
293+
{
294+
ArrayPool<char>.Shared.Return(trailersBuffer);
295+
}
296+
}
297+
}
298+
252299
public override int Read(byte[] buffer, int offset, int count)
253300
{
254301
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();

src/libraries/System.Net.Http.WinHttpHandler/tests/FunctionalTests/System.Net.Http.WinHttpHandler.Functional.Tests.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
<Compile Include="BaseCertificateTest.cs" />
1515
<Compile Include="ServerCertificateTest.cs" />
1616
<Compile Include="WinHttpHandlerTest.cs" />
17-
<Compile Include="XunitTestAssemblyAtrributes.cs" />
17+
<Compile Include="XunitTestAssemblyAtrributes.cs" />
1818
<Compile Include="$(CommonPath)\System\Net\Http\HttpHandlerDefaults.cs"
1919
Link="Common\System\Net\Http\HttpHandlerDefaults.cs" />
2020
<Compile Include="$(CommonTestPath)System\IO\DelegateStream.cs"
21-
Link="Common\System\IO\DelegateStream.cs" />
21+
Link="Common\System\IO\DelegateStream.cs" />
2222
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.cs"
2323
Link="Common\System\Net\Configuration.Certificates.cs" />
2424
<Compile Include="$(CommonTestPath)System\Net\Configuration.Security.cs"
@@ -126,7 +126,7 @@
126126
<Compile Include="$(CommonTestPath)System\Net\Http\RepeatedFlushContent.cs"
127127
Link="Common\System\Net\Http\RepeatedFlushContent.cs" />
128128
<Compile Include="$(CommonTestPath)System\Net\Http\ResponseStreamTest.cs"
129-
Link="Common\System\Net\Http\ResponseStreamTest.cs" />
129+
Link="Common\System\Net\Http\ResponseStreamTest.cs" />
130130
<Compile Include="$(CommonTestPath)System\Net\Http\SchSendAuxRecordHttpTest.cs"
131131
Link="Common\System\Net\Http\SchSendAuxRecordHttpTest.cs" />
132132
<Compile Include="$(CommonTestPath)System\Net\Http\SyncBlockingContent.cs"
@@ -141,6 +141,7 @@
141141
<Compile Include="WinHttpClientHandler.cs" />
142142
<Compile Include="PlatformHandlerTest.cs" />
143143
<Compile Include="ClientCertificateTest.cs" />
144+
<Compile Include="TrailingHeadersTest.cs" />
144145
</ItemGroup>
145146
<ItemGroup>
146147
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />

0 commit comments

Comments
 (0)