Validate Http/2 pseudo headers #2205 #2263 #2659

This commit is contained in:
Chris Ross (ASP.NET) 2018-07-17 16:25:58 -07:00
parent 603cd03bfa
commit 612fcca729
14 changed files with 1536 additions and 695 deletions

View File

@ -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) { }
}
}

View File

@ -545,4 +545,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2StreamAborted" xml:space="preserve">
<value>The request stream was aborted.</value>
</data>
<data name="Http2ErrorConnectMustNotSendSchemeOrPath" xml:space="preserve">
<value>CONNECT requests must not send :scheme or :path headers.</value>
</data>
<data name="Http2ErrorMethodInvalid" xml:space="preserve">
<value>The Method '{method}' is invalid.</value>
</data>
<data name="Http2StreamErrorPathInvalid" xml:space="preserve">
<value>The request :path is invalid: '{path}'</value>
</data>
<data name="Http2StreamErrorSchemeMismatch" xml:space="preserve">
<value>The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.</value>
</data>
</root>

View File

@ -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<byte> target, Span<byte> path, Span<byte> query, Span<byte> 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<byte> target)
@ -346,16 +306,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
QueryString = query.GetAsciiStringNonNullCharacters();
}
private static unsafe string GetUtf8String(Span<byte> 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()

View File

@ -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<byte> 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<byte> 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<byte> input)
{

View File

@ -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<char> 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<byte> 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);

View File

@ -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)]

View File

@ -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);
}
}

View File

@ -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<ILogger, string, string, Exception> _applicationAbortedConnection =
LoggerMessage.Define<string, string>(LogLevel.Information, new EventId(34, nameof(RequestBodyDrainTimedOut)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application aborted the connection.");
private static readonly Action<ILogger, string, Http2ErrorCode, Exception> _http2StreamResetError =
LoggerMessage.Define<string, Http2ErrorCode>(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);

View File

@ -2002,6 +2002,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2StreamAborted()
=> GetString("Http2StreamAborted");
/// <summary>
/// CONNECT requests must not send :scheme or :path headers.
/// </summary>
internal static string Http2ErrorConnectMustNotSendSchemeOrPath
{
get => GetString("Http2ErrorConnectMustNotSendSchemeOrPath");
}
/// <summary>
/// CONNECT requests must not send :scheme or :path headers.
/// </summary>
internal static string FormatHttp2ErrorConnectMustNotSendSchemeOrPath()
=> GetString("Http2ErrorConnectMustNotSendSchemeOrPath");
/// <summary>
/// The Method '{method}' is invalid.
/// </summary>
internal static string Http2ErrorMethodInvalid
{
get => GetString("Http2ErrorMethodInvalid");
}
/// <summary>
/// The Method '{method}' is invalid.
/// </summary>
internal static string FormatHttp2ErrorMethodInvalid(object method)
=> string.Format(CultureInfo.CurrentCulture, GetString("Http2ErrorMethodInvalid", "method"), method);
/// <summary>
/// The request :path is invalid: '{path}'
/// </summary>
internal static string Http2StreamErrorPathInvalid
{
get => GetString("Http2StreamErrorPathInvalid");
}
/// <summary>
/// The request :path is invalid: '{path}'
/// </summary>
internal static string FormatHttp2StreamErrorPathInvalid(object path)
=> string.Format(CultureInfo.CurrentCulture, GetString("Http2StreamErrorPathInvalid", "path"), path);
/// <summary>
/// The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.
/// </summary>
internal static string Http2StreamErrorSchemeMismatch
{
get => GetString("Http2StreamErrorSchemeMismatch");
}
/// <summary>
/// The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.
/// </summary>
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);

View File

@ -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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>("Host", "host1"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, ""),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "def"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "def"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "d=ef"),
new KeyValuePair<string, string>("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<IHttp2StreamIdFeature>();
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<IHttp2StreamIdFeature>();
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<IHttp2StreamIdFeature>();
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<IHttp2StreamIdFeature>();
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<byte> data)
{
var tasks = new List<Task>();
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<Http2Frame> 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<IEnumerable<KeyValuePair<string, string>>>();
var methodHeader = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT") };
var requestHeaders = new[]
{
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "CONNECT");
var headers = new[] { methodHeader };
data.Add(headers);
return data;
}

File diff suppressed because it is too large Load Diff

View File

@ -170,8 +170,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[MemberData(nameof(HostHeaderData))]
public void ValidHostHeadersParsed(string host)
{
HttpUtilities.ValidateHostHeader(host);
// Shouldn't throw
Assert.True(HttpUtilities.IsHostHeaderValid(host));
}
public static TheoryData<string> HostHeaderInvalidData
@ -225,7 +224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[MemberData(nameof(HostHeaderInvalidData))]
public void InvalidHostHeadersRejected(string host)
{
Assert.Throws<BadHttpRequestException>(() => HttpUtilities.ValidateHostHeader(host));
Assert.False(HttpUtilities.IsHostHeaderValid(host));
}
}
}

View File

@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
get
{
var dataset = new TheoryData<H2SpecTestCase>();
var toSkip = new[] { "hpack/4.2/1", "http2/5.1/8", "http2/8.1.2.3/1", "http2/8.1.2.6/1", "http2/8.1.2.6/2" };
var toSkip = new[] { "hpack/4.2/1", "http2/5.1/8", "http2/8.1.2.6/1", "http2/8.1.2.6/2" };
foreach (var testcase in H2SpecCommands.EnumerateTestCases())
{

View File

@ -3,6 +3,7 @@
using System;
using System.IO.Pipelines;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@ -189,5 +190,11 @@ namespace Microsoft.AspNetCore.Testing
_trace1.HPackDecodingError(connectionId, streamId, ex);
_trace2.HPackDecodingError(connectionId, streamId, ex);
}
public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason)
{
_trace1.Http2StreamResetAbort(traceIdentifier, error, abortReason);
_trace2.Http2StreamResetAbort(traceIdentifier, error, abortReason);
}
}
}