diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs index 4d04d945a2..23f76ff8c9 100644 --- a/src/Servers/HttpSys/src/FeatureContext.cs +++ b/src/Servers/HttpSys/src/FeatureContext.cs @@ -36,7 +36,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys IHttpMaxRequestBodySizeFeature, IHttpBodyControlFeature, IHttpSysRequestInfoFeature, - IHttpResponseTrailersFeature + IHttpResponseTrailersFeature, + IHttpResetFeature { private RequestContext _requestContext; private IFeatureCollection _features; @@ -350,7 +351,16 @@ namespace Microsoft.AspNetCore.Server.HttpSys internal IHttpResponseTrailersFeature GetResponseTrailersFeature() { - if (Request.ProtocolVersion >= HttpVersion.Version20 && ComNetOS.SupportsTrailers) + if (Request.ProtocolVersion >= HttpVersion.Version20 && HttpApi.SupportsTrailers) + { + return this; + } + return null; + } + + internal IHttpResetFeature GetResetFeature() + { + if (Request.ProtocolVersion >= HttpVersion.Version20 && HttpApi.SupportsReset) { return this; } @@ -455,6 +465,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys Task IHttpResponseBodyFeature.CompleteAsync() => CompleteAsync(); + void IHttpResetFeature.Reset(int errorCode) + { + _requestContext.SetResetCode(errorCode); + _requestContext.Abort(); + } + internal async Task CompleteAsync() { if (!_responseStarted) diff --git a/src/Servers/HttpSys/src/MessagePump.cs b/src/Servers/HttpSys/src/MessagePump.cs index b2ec094968..c6a2ca8419 100644 --- a/src/Servers/HttpSys/src/MessagePump.cs +++ b/src/Servers/HttpSys/src/MessagePump.cs @@ -228,6 +228,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys _application.DisposeContext(context, ex); if (requestContext.Response.HasStarted) { + // HTTP/2 INTERNAL_ERROR = 0x2 https://tools.ietf.org/html/rfc7540#section-7 + // Otherwise the default is Cancel = 0x8. + requestContext.SetResetCode(2); requestContext.Abort(); } else diff --git a/src/Servers/HttpSys/src/NativeInterop/ComNetOS.cs b/src/Servers/HttpSys/src/NativeInterop/ComNetOS.cs index f02282f589..f2b693c1d1 100644 --- a/src/Servers/HttpSys/src/NativeInterop/ComNetOS.cs +++ b/src/Servers/HttpSys/src/NativeInterop/ComNetOS.cs @@ -11,15 +11,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys // Minimum support for Windows 7 is assumed. internal static readonly bool IsWin8orLater; - internal static readonly bool SupportsTrailers; - static ComNetOS() { var win8Version = new Version(6, 2); IsWin8orLater = (Environment.OSVersion.Version >= win8Version); - - SupportsTrailers = Environment.OSVersion.Version >= new Version(10, 0, 19505); } } } diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index c4e23d3b3d..6781465bc2 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -70,6 +70,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys [DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)] internal static extern unsafe uint HttpCloseRequestQueue(IntPtr pReqQueueHandle); + internal delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped); private static HTTPAPI_VERSION version; @@ -106,6 +107,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys } } + internal static SafeLibraryHandle HttpApiModule { get; private set; } + internal static HttpSetRequestPropertyInvoker HttpSetRequestProperty { get; private set; } + internal static bool SupportsTrailers { get; private set; } + internal static bool SupportsReset { get; private set; } + static HttpApi() { InitHttpApi(2, 0); @@ -119,6 +125,16 @@ namespace Microsoft.AspNetCore.Server.HttpSys var statusCode = HttpInitialize(version, (uint)HTTP_FLAGS.HTTP_INITIALIZE_SERVER, null); supported = statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS; + + if (supported) + { + HttpApiModule = SafeLibraryHandle.Open(HTTPAPI); + HttpSetRequestProperty = HttpApiModule.GetProcAddress("HttpSetRequestProperty", throwIfNotFound: false); + + SupportsReset = HttpSetRequestProperty != null; + // Trailers support was added in the same release as Reset, but there's no method we can export to check it directly. + SupportsTrailers = SupportsReset; + } } private static volatile bool supported; diff --git a/src/Servers/HttpSys/src/NativeInterop/SafeLibraryHandle.cs b/src/Servers/HttpSys/src/NativeInterop/SafeLibraryHandle.cs new file mode 100644 index 0000000000..bf3254c6d6 --- /dev/null +++ b/src/Servers/HttpSys/src/NativeInterop/SafeLibraryHandle.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.AspNetCore.Server.HttpSys +{ + /// + /// Represents a handle to a Windows module (DLL). + /// + internal unsafe sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + // Called by P/Invoke when returning SafeHandles + private SafeLibraryHandle() + : base(ownsHandle: true) + { + } + + /// + /// Returns a value stating whether the library exports a given proc. + /// + public bool DoesProcExist(string lpProcName) + { + IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName); + return (pfnProc != IntPtr.Zero); + } + + /// + /// Gets a delegate pointing to a given export from this library. + /// + public TDelegate GetProcAddress(string lpProcName, bool throwIfNotFound = true) where TDelegate : class + { + IntPtr pfnProc = UnsafeNativeMethods.GetProcAddress(this, lpProcName); + if (pfnProc == IntPtr.Zero) + { + if (throwIfNotFound) + { + UnsafeNativeMethods.ThrowExceptionForLastWin32Error(); + } + else + { + return null; + } + } + + return Marshal.GetDelegateForFunctionPointer(pfnProc); + } + + /// + /// Opens a library. If 'filename' is not a fully-qualified path, the default search path is used. + /// + public static SafeLibraryHandle Open(string filename) + { + const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800U; // from libloaderapi.h + + SafeLibraryHandle handle = UnsafeNativeMethods.LoadLibraryEx(filename, IntPtr.Zero, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (handle == null || handle.IsInvalid) + { + UnsafeNativeMethods.ThrowExceptionForLastWin32Error(); + } + return handle; + } + + // Do not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle for you. + protected override bool ReleaseHandle() + { + return UnsafeNativeMethods.FreeLibrary(handle); + } + + [SuppressUnmanagedCodeSecurity] + private static class UnsafeNativeMethods + { + // http://msdn.microsoft.com/en-us/library/ms683152(v=vs.85).aspx + [return: MarshalAs(UnmanagedType.Bool)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode)] + internal static extern bool FreeLibrary(IntPtr hModule); + + // http://msdn.microsoft.com/en-us/library/ms683212(v=vs.85).aspx + [DllImport("kernel32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true)] + internal static extern IntPtr GetProcAddress( + [In] SafeLibraryHandle hModule, + [In, MarshalAs(UnmanagedType.LPStr)] string lpProcName); + + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms684179(v=vs.85).aspx + [DllImport("kernel32.dll", EntryPoint = "LoadLibraryExW", CallingConvention = CallingConvention.Winapi, SetLastError = true)] + internal static extern SafeLibraryHandle LoadLibraryEx( + [In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, + [In] IntPtr hFile, + [In] uint dwFlags); + + internal static void ThrowExceptionForLastWin32Error() + { + int hr = Marshal.GetHRForLastWin32Error(); + Marshal.ThrowExceptionForHR(hr); + } + } + } +} diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 89405cf538..151486d2c3 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Security.Authentication.ExtendedProtection; using System.Security.Claims; using System.Security.Principal; @@ -218,5 +219,25 @@ namespace Microsoft.AspNetCore.Server.HttpSys // RequestQueueHandle may have been closed } } + + // You must still call ForceCancelRequest after this. + internal unsafe void SetResetCode(int errorCode) + { + if (!HttpApi.SupportsReset) + { + return; + } + + try + { + var streamError = new HttpApiTypes.HTTP_REQUEST_PROPERTY_STREAM_ERROR() { ErrorCode = (uint)errorCode }; + var statusCode = HttpApi.HttpSetRequestProperty(Server.RequestQueue.Handle, Request.RequestId, HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertyStreamError, (void*)&streamError, + (uint)sizeof(HttpApiTypes.HTTP_REQUEST_PROPERTY_STREAM_ERROR), IntPtr.Zero); + } + catch (ObjectDisposedException) + { + // RequestQueueHandle may have been closed + } + } } } diff --git a/src/Servers/HttpSys/src/RequestProcessing/Response.cs b/src/Servers/HttpSys/src/RequestProcessing/Response.cs index 6d6b061331..3f8dd86f7e 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Response.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Response.cs @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys // Trailers are supported on this OS, it's HTTP/2, and the app added a Trailer response header to announce trailers were intended. // Needed to delay the completion of Content-Length responses. internal bool TrailersExpected => HasTrailers - || (ComNetOS.SupportsTrailers && Request.ProtocolVersion >= HttpVersion.Version20 + || (HttpApi.SupportsTrailers && Request.ProtocolVersion >= HttpVersion.Version20 && Headers.ContainsKey(HttpKnownHeaderNames.Trailer)); internal long ExpectedBodyLength diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index 567f640fd1..304b39b070 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys { typeof(IHttpBodyControlFeature), _identityFunc }, { typeof(IHttpSysRequestInfoFeature), _identityFunc }, { typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() }, + { typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() }, }; private readonly FeatureContext _featureContext; diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs b/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs index 990d35a647..4995c2ef3c 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs @@ -1,7 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.IO; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http2Cat; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Testing; @@ -16,7 +22,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests { [ConditionalFact(Skip = "https://github.com/aspnet/AspNetCore/issues/17420")] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] - [MaximumOSVersion(OperatingSystems.Windows, "10.0.18362.9999", SkipReason = "This is last version without GoAway support")] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_19H1, SkipReason = "This is last version without GoAway support")] public async Task ConnectionClose_NoOSSupport_NoGoAway() { using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => @@ -34,38 +40,23 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); - var headersFrame = await h2Connection.ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); - Assert.True((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_HEADERS) != 0); - Assert.True((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_STREAM) != 0); - - h2Connection.Logger.LogInformation("Received headers in a single frame."); - - var decodedHeaders = h2Connection.DecodeHeaders(headersFrame); - - // HTTP/2 filters out the connection header - Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); - Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + await h2Connection.ReceiveHeadersAsync(1, endStream: true, decodedHeaders => + { + // HTTP/2 filters out the connection header + Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); // Send and receive a second request to ensure there is no GoAway frame on the wire yet. await h2Connection.StartStreamAsync(3, Http2Utilities.BrowserRequestHeaders, endStream: true); - headersFrame = await h2Connection.ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); - Assert.True((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_HEADERS) != 0); - Assert.True((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_STREAM) != 0); - - h2Connection.Logger.LogInformation("Received headers in a single frame."); - - h2Connection.ResetHeaders(); - decodedHeaders = h2Connection.DecodeHeaders(headersFrame); - - // HTTP/2 filters out the connection header - Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); - Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + await h2Connection.ReceiveHeadersAsync(3, endStream: true, decodedHeaders => + { + // HTTP/2 filters out the connection header + Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); await h2Connection.StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); @@ -96,23 +87,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests var goAwayFrame = await h2Connection.ReceiveFrameAsync(); h2Connection.VerifyGoAway(goAwayFrame, int.MaxValue, Http2ErrorCode.NO_ERROR); - var headersFrame = await h2Connection.ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); - Assert.Equal(Http2HeadersFrameFlags.END_HEADERS, headersFrame.HeadersFlags); - - h2Connection.Logger.LogInformation("Received headers in a single frame."); - - var decodedHeaders = h2Connection.DecodeHeaders(headersFrame); - - // HTTP/2 filters out the connection header - Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); - Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + // HTTP/2 filters out the connection header + Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); var dataFrame = await h2Connection.ReceiveFrameAsync(); - Assert.Equal(Http2FrameType.DATA, dataFrame.Type); - Assert.Equal(Http2DataFrameFlags.END_STREAM, dataFrame.DataFlags); - Assert.Equal(0, dataFrame.PayloadLength); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); // Http.Sys doesn't send a final GoAway unless we ignore the first one and send 200 additional streams. @@ -144,26 +127,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests var goAwayFrame = await h2Connection.ReceiveFrameAsync(); h2Connection.VerifyGoAway(goAwayFrame, int.MaxValue, Http2ErrorCode.NO_ERROR); - var headersFrame = await h2Connection.ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); - Assert.Equal(Http2HeadersFrameFlags.END_HEADERS, headersFrame.HeadersFlags); - Assert.Equal(streamId, headersFrame.StreamId); - - h2Connection.Logger.LogInformation("Received headers in a single frame."); - - var decodedHeaders = h2Connection.DecodeHeaders(headersFrame); - - // HTTP/2 filters out the connection header - Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); - Assert.Equal("200", decodedHeaders[HeaderNames.Status]); - h2Connection.ResetHeaders(); + await h2Connection.ReceiveHeadersAsync(streamId, decodedHeaders => + { + // HTTP/2 filters out the connection header + Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); var dataFrame = await h2Connection.ReceiveFrameAsync(); - Assert.Equal(Http2FrameType.DATA, dataFrame.Type); - Assert.Equal(Http2DataFrameFlags.END_STREAM, dataFrame.DataFlags); - Assert.Equal(0, dataFrame.PayloadLength); - Assert.Equal(streamId, dataFrame.StreamId); + Http2Utilities.VerifyDataFrame(dataFrame, streamId, endOfStream: true, length: 0); // Http.Sys doesn't send a final GoAway unless we ignore the first one and send 200 additional streams. @@ -172,26 +144,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests streamId = 1 + (i * 2); // Odds. await h2Connection.StartStreamAsync(streamId, Http2Utilities.BrowserRequestHeaders, endStream: true); - headersFrame = await h2Connection.ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); - Assert.Equal(Http2HeadersFrameFlags.END_HEADERS, headersFrame.HeadersFlags); - Assert.Equal(streamId, headersFrame.StreamId); - - h2Connection.Logger.LogInformation("Received headers in a single frame."); - - decodedHeaders = h2Connection.DecodeHeaders(headersFrame); - - // HTTP/2 filters out the connection header - Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); - Assert.Equal("200", decodedHeaders[HeaderNames.Status]); - h2Connection.ResetHeaders(); + await h2Connection.ReceiveHeadersAsync(streamId, decodedHeaders => + { + // HTTP/2 filters out the connection header + Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); dataFrame = await h2Connection.ReceiveFrameAsync(); - Assert.Equal(Http2FrameType.DATA, dataFrame.Type); - Assert.Equal(Http2DataFrameFlags.END_STREAM, dataFrame.DataFlags); - Assert.Equal(0, dataFrame.PayloadLength); - Assert.Equal(streamId, dataFrame.StreamId); + Http2Utilities.VerifyDataFrame(dataFrame, streamId, endOfStream: true, length: 0); } streamId = 1 + (200 * 2); // Odds. @@ -202,26 +163,495 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests h2Connection.VerifyGoAway(goAwayFrame, streamId, Http2ErrorCode.NO_ERROR); // Normal response - headersFrame = await h2Connection.ReceiveFrameAsync(); - - Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); - Assert.Equal(Http2HeadersFrameFlags.END_HEADERS, headersFrame.HeadersFlags); - Assert.Equal(streamId, headersFrame.StreamId); - - h2Connection.Logger.LogInformation("Received headers in a single frame."); - - decodedHeaders = h2Connection.DecodeHeaders(headersFrame); - - // HTTP/2 filters out the connection header - Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); - Assert.Equal("200", decodedHeaders[HeaderNames.Status]); - h2Connection.ResetHeaders(); + await h2Connection.ReceiveHeadersAsync(streamId, decodedHeaders => + { + // HTTP/2 filters out the connection header + Assert.False(decodedHeaders.ContainsKey(HeaderNames.Connection)); + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); dataFrame = await h2Connection.ReceiveFrameAsync(); - Assert.Equal(Http2FrameType.DATA, dataFrame.Type); - Assert.Equal(Http2DataFrameFlags.END_STREAM, dataFrame.DataFlags); - Assert.Equal(0, dataFrame.PayloadLength); - Assert.Equal(streamId, dataFrame.StreamId); + Http2Utilities.VerifyDataFrame(dataFrame, streamId, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task AppException_BeforeHeaders_500() + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + throw new Exception("Application exception"); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("500", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H1, SkipReason = "This is last version without custom Reset support")] + public async Task AppException_AfterHeaders_PriorOSVersions_ResetCancel() + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + await httpContext.Response.Body.FlushAsync(); + throw new Exception("Application exception"); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, Http2ErrorCode.CANCEL); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Custom Reset support was added in Win10_20H2.")] + public async Task AppException_AfterHeaders_ResetInternalError() + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + await httpContext.Response.Body.FlushAsync(); + throw new Exception("Application exception"); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, Http2ErrorCode.INTERNAL_ERROR); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + public async Task Reset_Http1_NotSupported() + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + Assert.Equal("HTTP/1.1", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.Null(feature); + return httpContext.Response.WriteAsync("Hello World"); + }); + + var handler = new HttpClientHandler(); + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + using HttpClient client = new HttpClient(handler); + client.DefaultRequestVersion = HttpVersion.Version11; + var response = await client.GetStringAsync(address); + Assert.Equal("Hello World", response); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H1, SkipReason = "This is last version without Reset support")] + public async Task Reset_PriorOSVersions_NotSupported() + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.Null(feature); + return httpContext.Response.WriteAsync("Hello World"); + }); + + var handler = new HttpClientHandler(); + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + using HttpClient client = new HttpClient(handler); + client.DefaultRequestVersion = HttpVersion.Version20; + var response = await client.GetStringAsync(address); + Assert.Equal("Hello World", response); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_BeforeResponse_Resets() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + feature.Reset(1111); // Custom + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + return Task.FromResult(0); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, expectedErrorCode: (Http2ErrorCode)1111); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_AfterResponseHeaders_Resets() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + await httpContext.Response.Body.FlushAsync(); + feature.Reset(1111); // Custom + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, expectedErrorCode: (Http2ErrorCode)1111); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_DurringResponseBody_Resets() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + await httpContext.Response.WriteAsync("Hello World"); + feature.Reset(1111); // Custom + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 11); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, expectedErrorCode: (Http2ErrorCode)1111); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_AfterCompleteAsync_NoReset() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + await httpContext.Response.WriteAsync("Hello World"); + await httpContext.Response.CompleteAsync(); + // The request and response are fully complete, the reset doesn't get sent. + feature.Reset(1111); + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.BrowserRequestHeaders, endStream: true); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 11); + + dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_BeforeRequestBody_Resets() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + var readTask = httpContext.Request.Body.ReadAsync(new byte[10], 0, 10); + + feature.Reset(1111); + + await Assert.ThrowsAsync(() => readTask); + + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.PostRequestHeaders, endStream: false); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, expectedErrorCode: (Http2ErrorCode)1111); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_DurringRequestBody_Resets() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + + var read = await httpContext.Request.Body.ReadAsync(new byte[10], 0, 10); + Assert.Equal(10, read); + + var readTask = httpContext.Request.Body.ReadAsync(new byte[10], 0, 10); + feature.Reset(1111); + await Assert.ThrowsAsync(() => readTask); + + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.PostRequestHeaders, endStream: false); + await h2Connection.SendDataAsync(1, new byte[10], endStream: false); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, expectedErrorCode: (Http2ErrorCode)1111); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalFact] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Reset support was added in Win10_20H2.")] + public async Task Reset_CompleteAsyncDurringRequestBody_Resets() + { + var appResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var server = Utilities.CreateDynamicHttpsServer(out var address, async httpContext => + { + try + { + Assert.Equal("HTTP/2", httpContext.Request.Protocol); + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + + var read = await httpContext.Request.Body.ReadAsync(new byte[10], 0, 10); + Assert.Equal(10, read); + + var readTask = httpContext.Request.Body.ReadAsync(new byte[10], 0, 10); + await httpContext.Response.CompleteAsync(); + feature.Reset((int)Http2ErrorCode.NO_ERROR); // GRPC does this + await Assert.ThrowsAsync(() => readTask); + + appResult.SetResult(0); + } + catch (Exception ex) + { + appResult.SetException(ex); + } + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + await h2Connection.StartStreamAsync(1, Http2Utilities.PostRequestHeaders, endStream: false); + await h2Connection.SendDataAsync(1, new byte[10], endStream: false); + + // Any app errors? + Assert.Equal(0, await appResult.Task.DefaultTimeout()); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + var resetFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyResetFrame(resetFrame, expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.NO_ERROR); h2Connection.Logger.LogInformation("Connection stopped."); }) diff --git a/src/Servers/HttpSys/test/FunctionalTests/ResponseTrailersTests.cs b/src/Servers/HttpSys/test/FunctionalTests/ResponseTrailersTests.cs index f161bcb8c5..637220a990 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/ResponseTrailersTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/ResponseTrailersTests.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_HTTP2_TrailersAvailable() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_ProhibitedTrailers_Blocked() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_NoBody_TrailersSent() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => @@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_WithBody_TrailersSent() { using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext => @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_WithContentLengthBody_TrailersNotSent() { var body = "Hello World"; @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_WithTrailersBeforeContentLengthBody_TrailersSent() { var body = "Hello World"; @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_WithContentLengthBodyAndDeclared_TrailersSent() { var body = "Hello World"; @@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_WithContentLengthBodyAndDeclaredButMissingTrailers_Completes() { var body = "Hello World"; @@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_CompleteAsyncNoBody_TrailersSent() { var trailersReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_CompleteAsyncWithBody_TrailersSent() { var trailersReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -265,7 +265,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_MultipleValues_SentAsSeperateHeaders() { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => @@ -285,7 +285,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalFact] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_LargeTrailers_Success() { var values = new[] { @@ -313,7 +313,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys } [ConditionalTheory, MemberData(nameof(NullHeaderData))] - [MinimumOSVersion(OperatingSystems.Windows, "10.0.19505", SkipReason = "Requires HTTP/2 Trailers support.")] + [MinimumOSVersion(OperatingSystems.Windows, "10.0.19529", SkipReason = "Requires HTTP/2 Trailers support.")] public async Task ResponseTrailers_NullValues_Ignored(string headerName, StringValues headerValue, StringValues expectedValue) { using (Utilities.CreateDynamicHttpsServer(out var address, httpContext => diff --git a/src/Shared/Http2cat/Http2Utilities.cs b/src/Shared/Http2cat/Http2Utilities.cs index eace29386f..14fe26a601 100644 --- a/src/Shared/Http2cat/Http2Utilities.cs +++ b/src/Shared/Http2cat/Http2Utilities.cs @@ -10,6 +10,7 @@ using System.IO.Pipelines; using System.Linq; using System.Net.Http; using System.Net.Http.HPack; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -938,6 +939,31 @@ namespace Microsoft.AspNetCore.Http2Cat return WaitForConnectionErrorAsync(ignoreNonGoAwayFrames, expectedLastStreamId, Http2ErrorCode.NO_ERROR); } + internal Task ReceiveHeadersAsync(int expectedStreamId, Action> verifyHeaders = null) + => ReceiveHeadersAsync(expectedStreamId, endStream: false, verifyHeaders); + + internal async Task ReceiveHeadersAsync(int expectedStreamId, bool endStream = false, Action> verifyHeaders = null) + { + var headersFrame = await ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.HEADERS, headersFrame.Type); + Assert.Equal(expectedStreamId, headersFrame.StreamId); + Assert.True((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_HEADERS) != 0); + Assert.Equal(endStream, (headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_STREAM) != 0); + Logger.LogInformation("Received headers in a single frame."); + + ResetHeaders(); + DecodeHeaders(headersFrame); + verifyHeaders?.Invoke(_decodedHeaders); + } + + internal static void VerifyDataFrame(Http2Frame frame, int expectedStreamId, bool endOfStream, int length) + { + Assert.Equal(Http2FrameType.DATA, frame.Type); + Assert.Equal(expectedStreamId, frame.StreamId); + Assert.Equal(endOfStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE, frame.DataFlags); + Assert.Equal(length, frame.PayloadLength); + } + internal void VerifyGoAway(Http2Frame frame, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) { Assert.Equal(Http2FrameType.GOAWAY, frame.Type); @@ -948,6 +974,15 @@ namespace Microsoft.AspNetCore.Http2Cat Assert.Equal(expectedErrorCode, frame.GoAwayErrorCode); } + internal static void VerifyResetFrame(Http2Frame frame, int expectedStreamId, Http2ErrorCode expectedErrorCode) + { + Assert.Equal(Http2FrameType.RST_STREAM, frame.Type); + Assert.Equal(expectedStreamId, frame.StreamId); + Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode); + Assert.Equal(4, frame.PayloadLength); + Assert.Equal(0, frame.Flags); + } + internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) where TException : Exception { diff --git a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs index f1e515c204..2e69d7bc40 100644 --- a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs +++ b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs @@ -51,6 +51,16 @@ namespace Microsoft.AspNetCore.HttpSys.Internal HttpResponseInfoTypeQosProperty, } + internal enum HTTP_REQUEST_PROPERTY + { + HttpRequestPropertyIsb, + HttpRequestPropertyTcpInfoV0, + HttpRequestPropertyQuicStats, + HttpRequestPropertyTcpInfoV1, + HttpRequestPropertySni, + HttpRequestPropertyStreamError, + } + internal enum HTTP_TIMEOUT_TYPE { EntityBody, @@ -61,6 +71,11 @@ namespace Microsoft.AspNetCore.HttpSys.Internal MinSendRate, } + internal struct HTTP_REQUEST_PROPERTY_STREAM_ERROR + { + internal uint ErrorCode; + } + internal const int MaxTimeout = 6; [StructLayout(LayoutKind.Sequential)]