diff --git a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
index 8386402a78..b14fd2e14f 100644
--- a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
+++ b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
@@ -48,5 +49,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { }
public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { }
public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) { }
+ public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason) { }
}
}
diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx
index 7ef91a6b83..6ef16c78f0 100644
--- a/src/Kestrel.Core/CoreStrings.resx
+++ b/src/Kestrel.Core/CoreStrings.resx
@@ -545,4 +545,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
The request stream was aborted.
+
+ CONNECT requests must not send :scheme or :path headers.
+
+
+ The Method '{method}' is invalid.
+
+
+ The request :path is invalid: '{path}'
+
+
+ The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.
+
\ No newline at end of file
diff --git a/src/Kestrel.Core/Internal/Http/Http1Connection.cs b/src/Kestrel.Core/Internal/Http/Http1Connection.cs
index 849b3b1ce8..af32fdf01d 100644
--- a/src/Kestrel.Core/Internal/Http/Http1Connection.cs
+++ b/src/Kestrel.Core/Internal/Http/Http1Connection.cs
@@ -204,6 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Debug.Assert(HttpVersion != null, "HttpVersion was not set");
}
+ // Compare with Http2Stream.TryValidatePseudoHeaders
private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded)
{
Debug.Assert(target[0] == ByteForwardSlash, "Should only be called when path starts with /");
@@ -213,59 +214,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
- string requestUrlPath = null;
- string rawTarget = null;
try
{
// Read raw target before mutating memory.
- rawTarget = target.GetAsciiStringNonNullCharacters();
-
- if (pathEncoded)
- {
- // URI was encoded, unescape and then parse as UTF-8
- // Disabling warning temporary
- var pathLength = UrlDecoder.Decode(path, path);
-
- // Removing dot segments must be done after unescaping. From RFC 3986:
- //
- // URI producing applications should percent-encode data octets that
- // correspond to characters in the reserved set unless these characters
- // are specifically allowed by the URI scheme to represent data in that
- // component. If a reserved character is found in a URI component and
- // no delimiting role is known for that character, then it must be
- // interpreted as representing the data octet corresponding to that
- // character's encoding in US-ASCII.
- //
- // https://tools.ietf.org/html/rfc3986#section-2.2
- pathLength = PathNormalizer.RemoveDotSegments(path.Slice(0, pathLength));
-
- requestUrlPath = GetUtf8String(path.Slice(0, pathLength));
- }
- else
- {
- var pathLength = PathNormalizer.RemoveDotSegments(path);
-
- if (path.Length == pathLength && query.Length == 0)
- {
- // If no decoding was required, no dot segments were removed and
- // there is no query, the request path is the same as the raw target
- requestUrlPath = rawTarget;
- }
- else
- {
- requestUrlPath = path.Slice(0, pathLength).GetAsciiStringNonNullCharacters();
- }
- }
+ RawTarget = target.GetAsciiStringNonNullCharacters();
+ QueryString = query.GetAsciiStringNonNullCharacters();
+ Path = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
}
catch (InvalidOperationException)
{
ThrowRequestTargetRejected(target);
}
-
- QueryString = query.GetAsciiStringNonNullCharacters();
- RawTarget = rawTarget;
- Path = requestUrlPath;
}
private void OnAuthorityFormTarget(HttpMethod method, Span target)
@@ -346,16 +306,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
QueryString = query.GetAsciiStringNonNullCharacters();
}
- private static unsafe string GetUtf8String(Span path)
- {
- // .NET 451 doesn't have pointer overloads for Encoding.GetString so we
- // copy to an array
- fixed (byte* pointer = &MemoryMarshal.GetReference(path))
- {
- return Encoding.UTF8.GetString(pointer, path.Length);
- }
- }
-
internal void EnsureHostHeaderExists()
{
// https://tools.ietf.org/html/rfc7230#section-5.4
@@ -383,10 +333,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Tail call
ValidateNonOrginHostHeader(hostText);
}
- else
+ else if (!HttpUtilities.IsHostHeaderValid(hostText))
{
- // Tail call
- HttpUtilities.ValidateHostHeader(hostText);
+ BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
}
}
@@ -418,8 +367,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
- // Tail call
- HttpUtilities.ValidateHostHeader(hostText);
+ if (!HttpUtilities.IsHostHeaderValid(hostText))
+ {
+ BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ }
}
protected override void OnReset()
diff --git a/src/Kestrel.Core/Internal/Http/PathNormalizer.cs b/src/Kestrel.Core/Internal/Http/PathNormalizer.cs
index 68cdddf7ce..135a55ab25 100644
--- a/src/Kestrel.Core/Internal/Http/PathNormalizer.cs
+++ b/src/Kestrel.Core/Internal/Http/PathNormalizer.cs
@@ -4,6 +4,9 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
+using System.Text;
+using Microsoft.AspNetCore.Connections.Abstractions;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
@@ -12,6 +15,53 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private const byte ByteSlash = (byte)'/';
private const byte ByteDot = (byte)'.';
+ public static string DecodePath(Span path, bool pathEncoded, string rawTarget, int queryLength)
+ {
+ int pathLength;
+ if (pathEncoded)
+ {
+ // URI was encoded, unescape and then parse as UTF-8
+ // Disabling warning temporary
+ pathLength = UrlDecoder.DecodeInPlace(path);
+
+ // Removing dot segments must be done after unescaping. From RFC 3986:
+ //
+ // URI producing applications should percent-encode data octets that
+ // correspond to characters in the reserved set unless these characters
+ // are specifically allowed by the URI scheme to represent data in that
+ // component. If a reserved character is found in a URI component and
+ // no delimiting role is known for that character, then it must be
+ // interpreted as representing the data octet corresponding to that
+ // character's encoding in US-ASCII.
+ //
+ // https://tools.ietf.org/html/rfc3986#section-2.2
+ pathLength = RemoveDotSegments(path.Slice(0, pathLength));
+
+ return GetUtf8String(path.Slice(0, pathLength));
+ }
+
+ pathLength = RemoveDotSegments(path);
+
+ if (path.Length == pathLength && queryLength == 0)
+ {
+ // If no decoding was required, no dot segments were removed and
+ // there is no query, the request path is the same as the raw target
+ return rawTarget;
+ }
+
+ return path.Slice(0, pathLength).GetAsciiStringNonNullCharacters();
+ }
+
+ private static unsafe string GetUtf8String(Span path)
+ {
+ // .NET 451 doesn't have pointer overloads for Encoding.GetString so we
+ // copy to an array
+ fixed (byte* pointer = &MemoryMarshal.GetReference(path))
+ {
+ return Encoding.UTF8.GetString(pointer, path.Length);
+ }
+ }
+
// In-place implementation of the algorithm from https://tools.ietf.org/html/rfc3986#section-5.2.4
public static unsafe int RemoveDotSegments(Span input)
{
diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
index 5b52546f20..85cb88d5da 100644
--- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
+++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
@@ -3,13 +3,18 @@
using System;
using System.Buffers;
+using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
+using System.Runtime.InteropServices;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Abstractions;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
@@ -55,71 +60,192 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
protected override MessageBody CreateMessageBody()
=> Http2MessageBody.For(HttpRequestHeaders, this);
+ // Compare to Http1Connection.OnStartLine
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
{
// We don't need any of the parameters because we don't implement BeginRead to actually
// do the reading from a pipeline, nor do we use endConnection to report connection-level errors.
+ endConnection = !TryValidatePseudoHeaders();
+ return true;
+ }
+
+ private bool TryValidatePseudoHeaders()
+ {
+ // The initial pseudo header validation takes place in Http2Connection.ValidateHeader and StartStream
+ // They make sure the right fields are at least present (except for Connect requests) exactly once.
_httpVersion = Http.HttpVersion.Http2;
- var methodText = RequestHeaders[HeaderNames.Method];
- Method = HttpUtilities.GetKnownMethod(methodText);
- _methodText = methodText;
- if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase))
+
+ if (!TryValidateMethod())
{
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
+ return false;
}
- var path = RequestHeaders[HeaderNames.Path].ToString();
- var queryIndex = path.IndexOf('?');
+ if (!TryValidateAuthorityAndHost(out var hostText))
+ {
+ return false;
+ }
- Path = queryIndex == -1 ? path : path.Substring(0, queryIndex);
- QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex);
+ // CONNECT - :scheme and :path must be excluded
+ if (Method == HttpMethod.Connect)
+ {
+ if (!String.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !String.IsNullOrEmpty(RequestHeaders[HeaderNames.Path]))
+ {
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
+
+ RawTarget = hostText;
+
+ return true;
+ }
+
+ // :scheme https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+ // ":scheme" is not restricted to "http" and "https" schemed URIs. A
+ // proxy or gateway can translate requests for non - HTTP schemes,
+ // enabling the use of HTTP to interact with non - HTTP services.
+
+ // - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right?
+ // - For now we'll restrict it to http/s and require it match the transport.
+ // - We'll need to find some concrete scenarios to warrant unblocking this.
+ if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase))
+ {
+ ResetAndAbort(new ConnectionAbortedException(
+ CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
+
+ // :path (and query) - Required
+ // Must start with / except may be * for OPTIONS
+ var path = RequestHeaders[HeaderNames.Path].ToString();
RawTarget = path;
+ // OPTIONS - https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+ // This pseudo-header field MUST NOT be empty for "http" or "https"
+ // URIs; "http" or "https" URIs that do not contain a path component
+ // MUST include a value of '/'. The exception to this rule is an
+ // OPTIONS request for an "http" or "https" URI that does not include
+ // a path component; these MUST include a ":path" pseudo-header field
+ // with a value of '*'.
+ if (Method == HttpMethod.Options && path.Length == 1 && path[0] == '*')
+ {
+ // * is stored in RawTarget only since HttpRequest expects Path to be empty or start with a /.
+ Path = string.Empty;
+ QueryString = string.Empty;
+ return true;
+ }
+
+ var queryIndex = path.IndexOf('?');
+ QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex);
+
+ var pathSegment = queryIndex == -1 ? path.AsSpan() : path.AsSpan(0, queryIndex);
+
+ return TryValidatePath(pathSegment);
+ }
+
+ private bool TryValidateMethod()
+ {
+ // :method
+ _methodText = RequestHeaders[HeaderNames.Method].ToString();
+ Method = HttpUtilities.GetKnownMethod(_methodText);
+
+ if (Method == HttpMethod.None)
+ {
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
+
+ if (Method == HttpMethod.Custom)
+ {
+ if (HttpCharacters.IndexOfInvalidTokenChar(_methodText) >= 0)
+ {
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private bool TryValidateAuthorityAndHost(out string hostText)
+ {
+ // :authority (optional)
+ // Prefer this over Host
+
+ var authority = RequestHeaders[HeaderNames.Authority];
+ var host = HttpRequestHeaders.HeaderHost;
+ if (!StringValues.IsNullOrEmpty(authority))
+ {
+ // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+ // Clients that generate HTTP/2 requests directly SHOULD use the ":authority"
+ // pseudo - header field instead of the Host header field.
+ // An intermediary that converts an HTTP/2 request to HTTP/1.1 MUST
+ // create a Host header field if one is not present in a request by
+ // copying the value of the ":authority" pseudo - header field.
+
+ // We take this one step further, we don't want mismatched :authority
+ // and Host headers, replace Host if :authority is defined. The application
+ // will operate on the Host header.
+ HttpRequestHeaders.HeaderHost = authority;
+ host = authority;
+ }
+
// https://tools.ietf.org/html/rfc7230#section-5.4
// A server MUST respond with a 400 (Bad Request) status code to any
// HTTP/1.1 request message that lacks a Host header field and to any
// request message that contains more than one Host header field or a
// Host header field with an invalid field-value.
-
- var authority = RequestHeaders[HeaderNames.Authority];
- var host = HttpRequestHeaders.HeaderHost;
- if (authority.Count > 0)
+ hostText = host.ToString();
+ if (host.Count > 1 || !HttpUtilities.IsHostHeaderValid(hostText))
{
- // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
- // An intermediary that converts an HTTP/2 request to HTTP/1.1 MUST
- // create a Host header field if one is not present in a request by
- // copying the value of the ":authority" pseudo - header field.
- //
- // We take this one step further, we don't want mismatched :authority
- // and Host headers, replace Host if :authority is defined.
- HttpRequestHeaders.HeaderHost = authority;
- host = authority;
+ // RST replaces 400
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
}
- // TODO: OPTIONS * requests?
- // To ensure that the HTTP / 1.1 request line can be reproduced
- // accurately, this pseudo - header field MUST be omitted when
- // translating from an HTTP/ 1.1 request that has a request target in
- // origin or asterisk form(see[RFC7230], Section 5.3).
- // https://tools.ietf.org/html/rfc7230#section-5.3
-
- if (host.Count <= 0)
- {
- BadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader);
- }
- else if (host.Count > 1)
- {
- BadHttpRequestException.Throw(RequestRejectionReason.MultipleHostHeaders);
- }
-
- var hostText = host.ToString();
- HttpUtilities.ValidateHostHeader(hostText);
-
- endConnection = false;
return true;
}
+ private bool TryValidatePath(ReadOnlySpan pathSegment)
+ {
+ // Must start with a leading slash
+ if (pathSegment.Length == 0 || pathSegment[0] != '/')
+ {
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
+
+ var pathEncoded = pathSegment.IndexOf('%') >= 0;
+
+ // Compare with Http1Connection.OnOriginFormTarget
+
+ // URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
+ // Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
+ // then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
+
+ try
+ {
+ // The decoder operates only on raw bytes
+ var pathBuffer = new byte[pathSegment.Length].AsSpan();
+ for (int i = 0; i < pathSegment.Length; i++)
+ {
+ var ch = pathSegment[i];
+ // The header parser should already be checking this
+ Debug.Assert(32 < ch && ch < 127);
+ pathBuffer[i] = (byte)ch;
+ }
+
+ Path = PathNormalizer.DecodePath(pathBuffer, pathEncoded, RawTarget, QueryString.Length);
+
+ return true;
+ }
+ catch (InvalidOperationException)
+ {
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
+ }
+
public async Task OnDataAsync(ArraySegment data, bool endStream)
{
// TODO: content-length accounting
@@ -164,7 +290,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
protected override void ApplicationAbort()
{
- Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier);
var abortReason = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication);
ResetAndAbort(abortReason, Http2ErrorCode.CANCEL);
}
@@ -176,6 +301,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return;
}
+ Log.Http2StreamResetAbort(TraceIdentifier, error, abortReason);
+
// Don't block on IO. This never faults.
_ = _http2Output.WriteRstStreamAsync(error);
diff --git a/src/Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs b/src/Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs
index d4d807aa13..6ee60b5018 100644
--- a/src/Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs
+++ b/src/Kestrel.Core/Internal/Infrastructure/HttpUtilities.cs
@@ -191,13 +191,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
// Called by http/2
if (value == null)
{
- throw new ArgumentNullException(nameof(value));
+ return HttpMethod.None;
}
var length = value.Length;
if (length == 0)
{
- throw new ArgumentException(nameof(value));
+ return HttpMethod.None;
}
// Start with custom and assign if known method is found
@@ -395,39 +395,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
}
}
- public static void ValidateHostHeader(string hostText)
+ public static bool IsHostHeaderValid(string hostText)
{
if (string.IsNullOrEmpty(hostText))
{
// The spec allows empty values
- return;
+ return true;
}
var firstChar = hostText[0];
if (firstChar == '[')
{
// Tail call
- ValidateIPv6Host(hostText);
+ return IsIPv6HostValid(hostText);
}
else
{
if (firstChar == ':')
{
// Only a port
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ return false;
}
var invalid = HttpCharacters.IndexOfInvalidHostChar(hostText);
if (invalid >= 0)
{
// Tail call
- ValidateHostPort(hostText, invalid);
+ return IsHostPortValid(hostText, invalid);
}
+
+ return true;
}
}
// The lead '[' was already checked
- private static void ValidateIPv6Host(string hostText)
+ private static bool IsIPv6HostValid(string hostText)
{
for (var i = 1; i < hostText.Length; i++)
{
@@ -437,43 +439,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
// [::1] is the shortest valid IPv6 host
if (i < 4)
{
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ return false;
}
else if (i + 1 < hostText.Length)
{
// Tail call
- ValidateHostPort(hostText, i + 1);
+ return IsHostPortValid(hostText, i + 1);
}
- return;
+ return true;
}
if (!IsHex(ch) && ch != ':' && ch != '.')
{
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ return false;
}
}
// Must contain a ']'
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ return false;
}
- private static void ValidateHostPort(string hostText, int offset)
+ private static bool IsHostPortValid(string hostText, int offset)
{
var firstChar = hostText[offset];
offset++;
if (firstChar != ':' || offset == hostText.Length)
{
// Must have at least one number after the colon if present.
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ return false;
}
for (var i = offset; i < hostText.Length; i++)
{
if (!IsNumeric(hostText[i]))
{
- BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
+ return false;
}
}
+
+ return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
index caaf66cfae..d29b1b43d8 100644
--- a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
+++ b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.Extensions.Logging;
@@ -58,6 +59,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void Http2StreamError(string connectionId, Http2StreamErrorException ex);
+ void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason);
+
void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex);
}
}
diff --git a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
index 3ef1aa1721..8f4a22539e 100644
--- a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
+++ b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -86,6 +87,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static readonly Action _applicationAbortedConnection =
LoggerMessage.Define(LogLevel.Information, new EventId(34, nameof(RequestBodyDrainTimedOut)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application aborted the connection.");
+ private static readonly Action _http2StreamResetError =
+ LoggerMessage.Define(LogLevel.Debug, new EventId(35, nameof(Http2StreamResetAbort)),
+ @"Trace id ""{TraceIdentifier}"": HTTP/2 stream error ""{error}"". A Reset is being sent to the stream.");
+
protected readonly ILogger _logger;
public KestrelTrace(ILogger logger)
@@ -213,6 +218,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_http2StreamError(_logger, connectionId, ex);
}
+ public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason)
+ {
+ _http2StreamResetError(_logger, traceIdentifier, error, abortReason);
+ }
+
public virtual void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex)
{
_hpackDecodingError(_logger, connectionId, streamId, ex);
diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
index 20c07bf02b..60354b61ea 100644
--- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
+++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
@@ -2002,6 +2002,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2StreamAborted()
=> GetString("Http2StreamAborted");
+ ///
+ /// CONNECT requests must not send :scheme or :path headers.
+ ///
+ internal static string Http2ErrorConnectMustNotSendSchemeOrPath
+ {
+ get => GetString("Http2ErrorConnectMustNotSendSchemeOrPath");
+ }
+
+ ///
+ /// CONNECT requests must not send :scheme or :path headers.
+ ///
+ internal static string FormatHttp2ErrorConnectMustNotSendSchemeOrPath()
+ => GetString("Http2ErrorConnectMustNotSendSchemeOrPath");
+
+ ///
+ /// The Method '{method}' is invalid.
+ ///
+ internal static string Http2ErrorMethodInvalid
+ {
+ get => GetString("Http2ErrorMethodInvalid");
+ }
+
+ ///
+ /// The Method '{method}' is invalid.
+ ///
+ internal static string FormatHttp2ErrorMethodInvalid(object method)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Http2ErrorMethodInvalid", "method"), method);
+
+ ///
+ /// The request :path is invalid: '{path}'
+ ///
+ internal static string Http2StreamErrorPathInvalid
+ {
+ get => GetString("Http2StreamErrorPathInvalid");
+ }
+
+ ///
+ /// The request :path is invalid: '{path}'
+ ///
+ internal static string FormatHttp2StreamErrorPathInvalid(object path)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Http2StreamErrorPathInvalid", "path"), path);
+
+ ///
+ /// The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.
+ ///
+ internal static string Http2StreamErrorSchemeMismatch
+ {
+ get => GetString("Http2StreamErrorSchemeMismatch");
+ }
+
+ ///
+ /// The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.
+ ///
+ internal static string FormatHttp2StreamErrorSchemeMismatch(object requestScheme, object transportScheme)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Http2StreamErrorSchemeMismatch", "requestScheme", "transportScheme"), requestScheme, transportScheme);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs
index 56ae2c8095..d584b8e5f9 100644
--- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs
+++ b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs
@@ -116,7 +116,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private readonly object _abortedStreamIdsLock = new object();
private readonly RequestDelegate _noopApplication;
- private readonly RequestDelegate _echoHost;
private readonly RequestDelegate _readHeadersApplication;
private readonly RequestDelegate _readTrailersApplication;
private readonly RequestDelegate _bufferingApplication;
@@ -151,13 +150,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_noopApplication = context => Task.CompletedTask;
- _echoHost = context =>
- {
- context.Response.Headers[HeaderNames.Host] = context.Request.Headers[HeaderNames.Host];
-
- return Task.CompletedTask;
- };
-
_readHeadersApplication = context =>
{
foreach (var header in context.Request.Headers)
@@ -1495,311 +1487,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
- [Fact]
- public async Task HEADERS_Received_InvalidAuthority_400Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, "local=host:80"),
- };
- await InitializeConnectionAsync(_noopApplication);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 55,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(3, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("400", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders["content-length"]);
- }
-
- [Fact]
- public async Task HEADERS_Received_MissingAuthority_400Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- };
- await InitializeConnectionAsync(_noopApplication);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 55,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(3, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("400", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders["content-length"]);
- }
-
- [Fact]
- public async Task HEADERS_Received_TwoHosts_400Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair("Host", "host1"),
- new KeyValuePair("Host", "host2"),
- };
- await InitializeConnectionAsync(_noopApplication);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 55,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(3, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("400", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders["content-length"]);
- }
-
- [Fact]
- public async Task HEADERS_Received_EmptyAuthority_200Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, ""),
- };
- await InitializeConnectionAsync(_noopApplication);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 55,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(3, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders["content-length"]);
- }
-
- [Fact]
- public async Task HEADERS_Received_EmptyAuthorityOverridesHost_200Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, ""),
- new KeyValuePair("Host", "abc"),
- };
- await InitializeConnectionAsync(_echoHost);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 62,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(4, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
- Assert.Equal("", _decodedHeaders[HeaderNames.Host]);
- }
-
- [Fact]
- public async Task HEADERS_Received_AuthorityOverridesHost_200Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, "def"),
- new KeyValuePair("Host", "abc"),
- };
- await InitializeConnectionAsync(_echoHost);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 65,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(4, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
- Assert.Equal("def", _decodedHeaders[HeaderNames.Host]);
- }
-
- [Fact]
- public async Task HEADERS_Received_MissingAuthorityFallsBackToHost_200Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair("Host", "abc"),
- };
- await InitializeConnectionAsync(_echoHost);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 65,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(4, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
- Assert.Equal("abc", _decodedHeaders[HeaderNames.Host]);
- }
-
- [Fact]
- public async Task HEADERS_Received_AuthorityOverridesInvalidHost_200Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, "def"),
- new KeyValuePair("Host", "a=bc"),
- };
- await InitializeConnectionAsync(_echoHost);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 65,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(4, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
- Assert.Equal("def", _decodedHeaders[HeaderNames.Host]);
- }
-
- [Fact]
- public async Task HEADERS_Received_InvalidAuthorityWithValidHost_400Status()
- {
- var headers = new[]
- {
- new KeyValuePair(HeaderNames.Method, "GET"),
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, "d=ef"),
- new KeyValuePair("Host", "abc"),
- };
- await InitializeConnectionAsync(_echoHost);
-
- await StartStreamAsync(1, headers, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 55,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 0,
- withFlags: (byte)Http2DataFrameFlags.END_STREAM,
- withStreamId: 1);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
-
- _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
-
- Assert.Equal(3, _decodedHeaders.Count);
- Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- Assert.Equal("400", _decodedHeaders[HeaderNames.Status]);
- Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
- }
-
[Fact]
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
{
@@ -1873,45 +1560,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamSelfDependency(Http2FrameType.PRIORITY, 1));
}
- [Fact]
- public async Task RST_STREAM_Received_AbortsStream()
- {
- await InitializeConnectionAsync(_waitForAbortApplication);
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- await SendRstStreamAsync(1);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
- [Fact]
- public async Task RST_STREAM_Received_AbortsStream_FlushedHeadersNotSent()
- {
- await InitializeConnectionAsync(_waitForAbortFlushingApplication);
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- await SendRstStreamAsync(1);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
- [Fact]
- public async Task RST_STREAM_Received_AbortsStream_FlushedDataNotSent()
- {
- await InitializeConnectionAsync(_waitForAbortWithDataApplication);
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- await SendRstStreamAsync(1);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
[Fact]
public async Task RST_STREAM_Received_RelievesConnectionBackpressure()
{
@@ -2116,92 +1764,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Contains(5, _abortedStreamIds);
}
- [Fact]
- public async Task RST_STREAM_WaitingForRequestBody_RequestBodyThrows()
- {
- var sem = new SemaphoreSlim(0);
- await InitializeConnectionAsync(async context =>
- {
- var streamIdFeature = context.Features.Get();
-
- try
- {
- var readTask = context.Request.Body.ReadAsync(new byte[100], 0, 100).DefaultTimeout();
- sem.Release();
- await readTask;
-
- _runningStreams[streamIdFeature.StreamId].TrySetException(new Exception("ReadAsync was expected to throw."));
- }
- catch (IOException) // Expected failure
- {
- await context.Response.Body.WriteAsync(new byte[10], 0, 10);
-
- lock (_abortedStreamIdsLock)
- {
- _abortedStreamIds.Add(streamIdFeature.StreamId);
- }
-
- _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
- }
- catch (Exception ex)
- {
- _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
- }
- });
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
- await sem.WaitAsync().DefaultTimeout();
- await SendRstStreamAsync(1);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
- [Fact]
- public async Task RST_STREAM_IncompleteRequest_RequestBodyThrows()
- {
- var sem = new SemaphoreSlim(0);
- await InitializeConnectionAsync(async context =>
- {
- var streamIdFeature = context.Features.Get();
-
- try
- {
- var read = await context.Request.Body.ReadAsync(new byte[100], 0, 100).DefaultTimeout();
- var readTask = context.Request.Body.ReadAsync(new byte[100], 0, 100).DefaultTimeout();
- sem.Release();
- await readTask;
-
- _runningStreams[streamIdFeature.StreamId].TrySetException(new Exception("ReadAsync was expected to throw."));
- }
- catch (IOException) // Expected failure
- {
- await context.Response.Body.WriteAsync(new byte[10], 0, 10);
-
- lock (_abortedStreamIdsLock)
- {
- _abortedStreamIds.Add(streamIdFeature.StreamId);
- }
-
- _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
- }
- catch (Exception ex)
- {
- _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
- }
- });
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
- await SendDataAsync(1, new byte[10], endStream: false);
- await sem.WaitAsync().DefaultTimeout();
- await SendRstStreamAsync(1);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
[Fact]
public async Task RST_STREAM_Received_StreamIdZero_ConnectionError()
{
@@ -2278,98 +1840,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.RST_STREAM, streamId: 1, headersStreamId: 1));
}
- [Fact]
- public async Task RequestAbort_SendsRstStream()
- {
- await InitializeConnectionAsync(async context =>
- {
- var streamIdFeature = context.Features.Get();
-
- try
- {
- context.RequestAborted.Register(() =>
- {
- lock (_abortedStreamIdsLock)
- {
- _abortedStreamIds.Add(streamIdFeature.StreamId);
- }
-
- _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
- });
-
- context.Abort();
-
- // Not sent
- await context.Response.Body.WriteAsync(new byte[10], 0, 10);
-
- await _runningStreams[streamIdFeature.StreamId].Task;
- }
- catch (Exception ex)
- {
- _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
- }
- });
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.CANCEL, expectedErrorMessage: null);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
- [Fact]
- public async Task RequestAbort_AfterDataSent_SendsRstStream()
- {
- await InitializeConnectionAsync(async context =>
- {
- var streamIdFeature = context.Features.Get();
-
- try
- {
- context.RequestAborted.Register(() =>
- {
- lock (_abortedStreamIdsLock)
- {
- _abortedStreamIds.Add(streamIdFeature.StreamId);
- }
-
- _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
- });
-
- await context.Response.Body.WriteAsync(new byte[10], 0, 10);
-
- context.Abort();
-
- // Not sent
- await context.Response.Body.WriteAsync(new byte[11], 0, 11);
-
- await _runningStreams[streamIdFeature.StreamId].Task;
- }
- catch (Exception ex)
- {
- _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
- }
- });
-
- await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
-
- var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 1);
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 10,
- withFlags: 0,
- withStreamId: 1);
-
- await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.CANCEL, expectedErrorMessage: null);
- await WaitForAllStreamsAsync();
- Assert.Contains(1, _abortedStreamIds);
-
- await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- }
-
[Fact]
public async Task SETTINGS_Received_Sends_ACK()
{
@@ -3540,28 +3010,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendAsync(frame.Raw);
}
- private Task SendStreamDataAsync(int streamId, Span data)
- {
- var tasks = new List();
- var frame = new Http2Frame();
-
- frame.PrepareData(streamId);
-
- while (data.Length > frame.Length)
- {
- data.Slice(0, frame.Length).CopyTo(frame.Payload);
- data = data.Slice(frame.Length);
- tasks.Add(SendAsync(frame.Raw));
- }
-
- frame.Length = data.Length;
- frame.DataFlags = Http2DataFrameFlags.END_STREAM;
- data.CopyTo(frame.Payload);
- tasks.Add(SendAsync(frame.Raw));
-
- return Task.WhenAll(tasks);
- }
-
private Task WaitForAllStreamsAsync()
{
return Task.WhenAll(_runningStreams.Values.Select(tcs => tcs.Task)).DefaultTimeout();
@@ -3912,14 +3360,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
}
- private async Task ReceiveSettingsAck()
- {
- var frame = await ReceiveFrameAsync();
-
- Assert.Equal(Http2FrameType.SETTINGS, frame.Type);
- Assert.Equal(Http2SettingsFrameFlags.ACK, frame.SettingsFlags);
- }
-
private async Task ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId)
{
var frame = await ReceiveFrameAsync();
@@ -4087,19 +3527,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
get
{
var data = new TheoryData>>();
- var methodHeader = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT") };
- var requestHeaders = new[]
- {
- new KeyValuePair(HeaderNames.Path, "/"),
- new KeyValuePair(HeaderNames.Scheme, "http"),
- new KeyValuePair(HeaderNames.Authority, "127.0.0.1"),
- };
-
- foreach (var headerField in requestHeaders)
- {
- var headers = methodHeader.Concat(requestHeaders.Except(new[] { headerField }));
- data.Add(headers);
- }
+ var methodHeader = new KeyValuePair(HeaderNames.Method, "CONNECT");
+ var headers = new[] { methodHeader };
+ data.Add(headers);
return data;
}
diff --git a/test/Kestrel.Core.Tests/Http2StreamTests.cs b/test/Kestrel.Core.Tests/Http2StreamTests.cs
new file mode 100644
index 0000000000..da8e98b987
--- /dev/null
+++ b/test/Kestrel.Core.Tests/Http2StreamTests.cs
@@ -0,0 +1,1190 @@
+// 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.Buffers;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Pipelines;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+{
+ public class Http2StreamTests : IDisposable, IHttpHeadersHandler
+ {
+ private static readonly string _largeHeaderValue = new string('a', HPackDecoder.MaxStringOctets);
+
+ private static readonly IEnumerable> _browserRequestHeaders = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
+ new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
+ new KeyValuePair("accept-language", "en-US,en;q=0.5"),
+ new KeyValuePair("accept-encoding", "gzip, deflate, br"),
+ new KeyValuePair("upgrade-insecure-requests", "1"),
+ };
+
+ private readonly MemoryPool _memoryPool = KestrelMemoryPool.Create();
+ private readonly DuplexPipe.DuplexPipePair _pair;
+ private readonly TestApplicationErrorLogger _logger;
+ private readonly Http2ConnectionContext _connectionContext;
+ private readonly Http2Connection _connection;
+ private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
+ private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
+ private readonly HPackDecoder _hpackDecoder;
+
+ private readonly ConcurrentDictionary> _runningStreams = new ConcurrentDictionary>();
+ private readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet _abortedStreamIds = new HashSet();
+ private readonly object _abortedStreamIdsLock = new object();
+
+ private readonly RequestDelegate _noopApplication;
+ private readonly RequestDelegate _echoMethod;
+ private readonly RequestDelegate _echoHost;
+ private readonly RequestDelegate _echoPath;
+ private readonly RequestDelegate _waitForAbortApplication;
+ private readonly RequestDelegate _waitForAbortFlushingApplication;
+ private readonly RequestDelegate _waitForAbortWithDataApplication;
+
+ private Task _connectionTask;
+
+ public Http2StreamTests()
+ {
+ // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
+ // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
+ // it easier to verify request frames are processed correctly immediately after sending the them.
+ var inputPipeOptions = new PipeOptions(
+ pool: _memoryPool,
+ readerScheduler: PipeScheduler.Inline,
+ writerScheduler: PipeScheduler.ThreadPool,
+ useSynchronizationContext: false
+ );
+ var outputPipeOptions = new PipeOptions(
+ pool: _memoryPool,
+ readerScheduler: PipeScheduler.ThreadPool,
+ writerScheduler: PipeScheduler.Inline,
+ useSynchronizationContext: false
+ );
+
+ _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
+
+ _noopApplication = context => Task.CompletedTask;
+
+ _echoMethod = context =>
+ {
+ context.Response.Headers["Method"] = context.Request.Method;
+
+ return Task.CompletedTask;
+ };
+
+ _echoHost = context =>
+ {
+ context.Response.Headers[HeaderNames.Host] = context.Request.Headers[HeaderNames.Host];
+
+ return Task.CompletedTask;
+ };
+
+ _echoPath = context =>
+ {
+ context.Response.Headers["path"] = context.Request.Path.ToString();
+ context.Response.Headers["rawtarget"] = context.Features.Get().RawTarget;
+
+ return Task.CompletedTask;
+ };
+
+ _waitForAbortApplication = async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+ var sem = new SemaphoreSlim(0);
+
+ context.RequestAborted.Register(() =>
+ {
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ sem.Release();
+ });
+
+ await sem.WaitAsync().DefaultTimeout();
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ };
+
+ _waitForAbortFlushingApplication = async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+ var sem = new SemaphoreSlim(0);
+
+ context.RequestAborted.Register(() =>
+ {
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ sem.Release();
+ });
+
+ await sem.WaitAsync().DefaultTimeout();
+
+ await context.Response.Body.FlushAsync();
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ };
+
+ _waitForAbortWithDataApplication = async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+ var sem = new SemaphoreSlim(0);
+
+ context.RequestAborted.Register(() =>
+ {
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ sem.Release();
+ });
+
+ await sem.WaitAsync().DefaultTimeout();
+
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ };
+
+ _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize);
+
+ _logger = new TestApplicationErrorLogger();
+
+ _connectionContext = new Http2ConnectionContext
+ {
+ ConnectionFeatures = new FeatureCollection(),
+ ServiceContext = new TestServiceContext()
+ {
+ Log = new TestKestrelTrace(_logger)
+ },
+ MemoryPool = _memoryPool,
+ Application = _pair.Application,
+ Transport = _pair.Transport
+ };
+
+ _connection = new Http2Connection(_connectionContext);
+ }
+
+ public void Dispose()
+ {
+ _pair.Application.Input.Complete();
+ _pair.Application.Output.Complete();
+ _pair.Transport.Input.Complete();
+ _pair.Transport.Output.Complete();
+ _memoryPool.Dispose();
+ }
+
+ void IHttpHeadersHandler.OnHeader(Span name, Span value)
+ {
+ _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_EmptyMethod_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, ""),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.FormatHttp2ErrorMethodInvalid(""));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_InvlaidCustomMethod_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "Hello,World"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.FormatHttp2ErrorMethodInvalid("Hello,World"));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_CustomMethod_Accepted()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "Custom"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+ await InitializeConnectionAsync(_echoMethod);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 70,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(4, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("Custom", _decodedHeaders["Method"]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_CONNECTMethod_Accepted()
+ {
+ await InitializeConnectionAsync(_echoMethod);
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT") };
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 71,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(4, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("CONNECT", _decodedHeaders["Method"]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_OPTIONSStar_LeftOutOfPath()
+ {
+ await InitializeConnectionAsync(_echoPath);
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "OPTIONS"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Path, "*")};
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 75,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(5, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("", _decodedHeaders["path"]);
+ Assert.Equal("*", _decodedHeaders["rawtarget"]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_OPTIONSSlash_Accepted()
+ {
+ await InitializeConnectionAsync(_echoPath);
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "OPTIONS"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Path, "/")};
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 76,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(5, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("/", _decodedHeaders["path"]);
+ Assert.Equal("/", _decodedHeaders["rawtarget"]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_PathAndQuery_Seperated()
+ {
+ await InitializeConnectionAsync(context =>
+ {
+ context.Response.Headers["path"] = context.Request.Path.Value;
+ context.Response.Headers["query"] = context.Request.QueryString.Value;
+ context.Response.Headers["rawtarget"] = context.Features.Get().RawTarget;
+ return Task.CompletedTask;
+ });
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Path, "/a/path?a&que%35ry")};
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 118,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(6, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("/a/path", _decodedHeaders["path"]);
+ Assert.Equal("?a&que%35ry", _decodedHeaders["query"]);
+ Assert.Equal("/a/path?a&que%35ry", _decodedHeaders["rawtarget"]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Theory]
+ [InlineData("/","/")]
+ [InlineData("/a%5E", "/a^")]
+ [InlineData("/a%E2%82%AC", "/a€")]
+ [InlineData("/a%2Fb", "/a%2Fb")] // Forward slash, not decoded
+ [InlineData("/a%b", "/a%b")] // Incomplete encoding, not decoded
+ [InlineData("/a/b/c/../d", "/a/b/d")] // Navigation processed
+ [InlineData("/a/b/c/../../../../d", "/d")] // Navigation escape prevented
+ [InlineData("/a/b/c/.%2E/d", "/a/b/d")] // Decode before navigation processing
+ public async Task HEADERS_Received_Path_DecodedAndNormalized(string input, string expected)
+ {
+ await InitializeConnectionAsync(context =>
+ {
+ Assert.Equal(expected, context.Request.Path.Value);
+ Assert.Equal(input, context.Features.Get().RawTarget);
+ return Task.CompletedTask;
+ });
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Path, input)};
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 55,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(3, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Theory]
+ [InlineData(HeaderNames.Path, "/")]
+ [InlineData(HeaderNames.Scheme, "http")]
+ public async Task HEADERS_Received_CONNECTMethod_WithSchemeOrPath_Reset(string headerName, string value)
+ {
+ await InitializeConnectionAsync(_noopApplication);
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT"),
+ new KeyValuePair(headerName, value) };
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_SchemeMismatch_Reset()
+ {
+ await InitializeConnectionAsync(_noopApplication);
+
+ // :path and :scheme are not allowed, :authority is optional
+ var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "https") }; // Not the expected "http"
+ await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
+ CoreStrings.FormatHttp2StreamErrorSchemeMismatch("https", "http"));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_MissingAuthority_200Status()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 55,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(3, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_EmptyAuthority_200Status()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, ""),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 55,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(3, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders["content-length"]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_MissingAuthorityFallsBackToHost_200Status()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("Host", "abc"),
+ };
+ await InitializeConnectionAsync(_echoHost);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 65,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(4, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ Assert.Equal("abc", _decodedHeaders[HeaderNames.Host]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_EmptyAuthorityIgnoredOverHost_200Status()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, ""),
+ new KeyValuePair("Host", "abc"),
+ };
+ await InitializeConnectionAsync(_echoHost);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 65,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(4, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ Assert.Equal("abc", _decodedHeaders[HeaderNames.Host]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_AuthorityOverridesHost_200Status()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "def"),
+ new KeyValuePair("Host", "abc"),
+ };
+ await InitializeConnectionAsync(_echoHost);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 65,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(4, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ Assert.Equal("def", _decodedHeaders[HeaderNames.Host]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_AuthorityOverridesInvalidHost_200Status()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "def"),
+ new KeyValuePair("Host", "a=bc"),
+ };
+ await InitializeConnectionAsync(_echoHost);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 65,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(4, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ Assert.Equal("def", _decodedHeaders[HeaderNames.Host]);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_InvalidAuthority_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "local=host:80"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
+ CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("local=host:80"));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_InvalidAuthorityWithValidHost_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "d=ef"),
+ new KeyValuePair("Host", "abc"),
+ };
+ await InitializeConnectionAsync(_echoHost);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
+ CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("d=ef"));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_TwoHosts_StreamReset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("Host", "host1"),
+ new KeyValuePair("Host", "host2"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
+ CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("host1,host2"));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RST_STREAM_Received_AbortsStream()
+ {
+ await InitializeConnectionAsync(_waitForAbortApplication);
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+ await SendRstStreamAsync(1);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RST_STREAM_Received_AbortsStream_FlushedHeadersNotSent()
+ {
+ await InitializeConnectionAsync(_waitForAbortFlushingApplication);
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+ await SendRstStreamAsync(1);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RST_STREAM_Received_AbortsStream_FlushedDataNotSent()
+ {
+ await InitializeConnectionAsync(_waitForAbortWithDataApplication);
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+ await SendRstStreamAsync(1);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RST_STREAM_WaitingForRequestBody_RequestBodyThrows()
+ {
+ var sem = new SemaphoreSlim(0);
+ await InitializeConnectionAsync(async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+
+ try
+ {
+ var readTask = context.Request.Body.ReadAsync(new byte[100], 0, 100).DefaultTimeout();
+ sem.Release();
+ await readTask;
+
+ _runningStreams[streamIdFeature.StreamId].TrySetException(new Exception("ReadAsync was expected to throw."));
+ }
+ catch (IOException) // Expected failure
+ {
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ }
+ catch (Exception ex)
+ {
+ _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
+ }
+ });
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
+ await sem.WaitAsync().DefaultTimeout();
+ await SendRstStreamAsync(1);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RST_STREAM_IncompleteRequest_RequestBodyThrows()
+ {
+ var sem = new SemaphoreSlim(0);
+ await InitializeConnectionAsync(async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+
+ try
+ {
+ var read = await context.Request.Body.ReadAsync(new byte[100], 0, 100).DefaultTimeout();
+ var readTask = context.Request.Body.ReadAsync(new byte[100], 0, 100).DefaultTimeout();
+ sem.Release();
+ await readTask;
+
+ _runningStreams[streamIdFeature.StreamId].TrySetException(new Exception("ReadAsync was expected to throw."));
+ }
+ catch (IOException) // Expected failure
+ {
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ }
+ catch (Exception ex)
+ {
+ _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
+ }
+ });
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
+ await SendDataAsync(1, new byte[10], endStream: false);
+ await sem.WaitAsync().DefaultTimeout();
+ await SendRstStreamAsync(1);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RequestAbort_SendsRstStream()
+ {
+ await InitializeConnectionAsync(async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+
+ try
+ {
+ context.RequestAborted.Register(() =>
+ {
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ });
+
+ context.Abort();
+
+ // Not sent
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ await _runningStreams[streamIdFeature.StreamId].Task;
+ }
+ catch (Exception ex)
+ {
+ _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
+ }
+ });
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.CANCEL, CoreStrings.ConnectionAbortedByApplication);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task RequestAbort_AfterDataSent_SendsRstStream()
+ {
+ await InitializeConnectionAsync(async context =>
+ {
+ var streamIdFeature = context.Features.Get();
+
+ try
+ {
+ context.RequestAborted.Register(() =>
+ {
+ lock (_abortedStreamIdsLock)
+ {
+ _abortedStreamIds.Add(streamIdFeature.StreamId);
+ }
+
+ _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
+ });
+
+ await context.Response.Body.WriteAsync(new byte[10], 0, 10);
+
+ context.Abort();
+
+ // Not sent
+ await context.Response.Body.WriteAsync(new byte[11], 0, 11);
+
+ await _runningStreams[streamIdFeature.StreamId].Task;
+ }
+ catch (Exception ex)
+ {
+ _runningStreams[streamIdFeature.StreamId].TrySetException(ex);
+ }
+ });
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 37,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 10,
+ withFlags: 0,
+ withStreamId: 1);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.CANCEL, CoreStrings.ConnectionAbortedByApplication);
+ await WaitForAllStreamsAsync();
+ Assert.Contains(1, _abortedStreamIds);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ private async Task InitializeConnectionAsync(RequestDelegate application)
+ {
+ _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application));
+
+ await SendPreambleAsync().ConfigureAwait(false);
+ await SendSettingsAsync();
+
+ await ExpectAsync(Http2FrameType.SETTINGS,
+ withLength: 0,
+ withFlags: 0,
+ withStreamId: 0);
+
+ await ExpectAsync(Http2FrameType.SETTINGS,
+ withLength: 0,
+ withFlags: (byte)Http2SettingsFrameFlags.ACK,
+ withStreamId: 0);
+ }
+
+ private async Task StartStreamAsync(int streamId, IEnumerable> headers, bool endStream)
+ {
+ var tcs = new TaskCompletionSource