Merge pull request #2738 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Chris Ross 2018-07-20 13:07:22 -07:00 committed by GitHub
commit 09640bef56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1544 additions and 701 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; 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 Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { }
public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { } public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { }
public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException 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"> <data name="Http2StreamAborted" xml:space="preserve">
<value>The request stream was aborted.</value> <value>The request stream was aborted.</value>
</data> </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> </root>

View File

@ -204,6 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Debug.Assert(HttpVersion != null, "HttpVersion was not set"); 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) 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 /"); 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 // 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; // 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" // then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
string requestUrlPath = null;
string rawTarget = null;
try try
{ {
// Read raw target before mutating memory. // Read raw target before mutating memory.
rawTarget = target.GetAsciiStringNonNullCharacters(); RawTarget = target.GetAsciiStringNonNullCharacters();
QueryString = query.GetAsciiStringNonNullCharacters();
if (pathEncoded) Path = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
{
// 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();
}
}
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
ThrowRequestTargetRejected(target); ThrowRequestTargetRejected(target);
} }
QueryString = query.GetAsciiStringNonNullCharacters();
RawTarget = rawTarget;
Path = requestUrlPath;
} }
private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target) private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target)
@ -346,16 +306,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
QueryString = query.GetAsciiStringNonNullCharacters(); 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() internal void EnsureHostHeaderExists()
{ {
// https://tools.ietf.org/html/rfc7230#section-5.4 // https://tools.ietf.org/html/rfc7230#section-5.4
@ -383,10 +333,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Tail call // Tail call
ValidateNonOrginHostHeader(hostText); ValidateNonOrginHostHeader(hostText);
} }
else else if (!HttpUtilities.IsHostHeaderValid(hostText))
{ {
// Tail call BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
HttpUtilities.ValidateHostHeader(hostText);
} }
} }
@ -418,8 +367,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
} }
// Tail call if (!HttpUtilities.IsHostHeaderValid(hostText))
HttpUtilities.ValidateHostHeader(hostText); {
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
}
} }
protected override void OnReset() protected override void OnReset()

View File

@ -91,6 +91,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Read() will have already have greedily consumed the entire request body if able. // Read() will have already have greedily consumed the entire request body if able.
if (result.IsCompleted) if (result.IsCompleted)
{ {
// OnInputOrOutputCompleted() is an idempotent method that closes the connection. Sometimes
// input completion is observed here before the Input.OnWriterCompleted() callback is fired,
// so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400
// response is written after observing the unexpected end of request content instead of just
// closing the connection without a response as expected.
_context.OnInputOrOutputCompleted();
// Treat any FIN from an upgraded request as expected. // Treat any FIN from an upgraded request as expected.
// It's up to higher-level consumer (i.e. WebSocket middleware) to determine // It's up to higher-level consumer (i.e. WebSocket middleware) to determine
// if the end is actually expected based on higher-level framing. // if the end is actually expected based on higher-level framing.

View File

@ -1302,11 +1302,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
? target.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize) ? target.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize)
: string.Empty); : string.Empty);
public void SetBadRequestState(RequestRejectionReason reason)
{
SetBadRequestState(BadHttpRequestException.GetException(reason));
}
public void SetBadRequestState(BadHttpRequestException ex) public void SetBadRequestState(BadHttpRequestException ex)
{ {
Log.ConnectionBadRequest(ConnectionId, ex); Log.ConnectionBadRequest(ConnectionId, ex);

View File

@ -4,6 +4,9 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; 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 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 ByteSlash = (byte)'/';
private const byte ByteDot = (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 // 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) public static unsafe int RemoveDotSegments(Span<byte> input)
{ {

View File

@ -3,13 +3,18 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Abstractions;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
@ -55,71 +60,192 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
protected override MessageBody CreateMessageBody() protected override MessageBody CreateMessageBody()
=> Http2MessageBody.For(HttpRequestHeaders, this); => Http2MessageBody.For(HttpRequestHeaders, this);
// Compare to Http1Connection.OnStartLine
protected override bool TryParseRequest(ReadResult result, out bool endConnection) 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 // 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. // 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; _httpVersion = Http.HttpVersion.Http2;
var methodText = RequestHeaders[HeaderNames.Method];
Method = HttpUtilities.GetKnownMethod(methodText); if (!TryValidateMethod())
_methodText = methodText;
if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase))
{ {
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine); return false;
} }
var path = RequestHeaders[HeaderNames.Path].ToString(); if (!TryValidateAuthorityAndHost(out var hostText))
var queryIndex = path.IndexOf('?'); {
return false;
}
Path = queryIndex == -1 ? path : path.Substring(0, queryIndex); // CONNECT - :scheme and :path must be excluded
QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex); 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; 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 // https://tools.ietf.org/html/rfc7230#section-5.4
// A server MUST respond with a 400 (Bad Request) status code to any // 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 // 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 // request message that contains more than one Host header field or a
// Host header field with an invalid field-value. // Host header field with an invalid field-value.
hostText = host.ToString();
var authority = RequestHeaders[HeaderNames.Authority]; if (host.Count > 1 || !HttpUtilities.IsHostHeaderValid(hostText))
var host = HttpRequestHeaders.HeaderHost;
if (authority.Count > 0)
{ {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3 // RST replaces 400
// An intermediary that converts an HTTP/2 request to HTTP/1.1 MUST ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http2ErrorCode.PROTOCOL_ERROR);
// create a Host header field if one is not present in a request by return false;
// 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;
} }
// 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; 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) public async Task OnDataAsync(ArraySegment<byte> data, bool endStream)
{ {
// TODO: content-length accounting // TODO: content-length accounting
@ -164,7 +290,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
protected override void ApplicationAbort() protected override void ApplicationAbort()
{ {
Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier);
var abortReason = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication); var abortReason = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication);
ResetAndAbort(abortReason, Http2ErrorCode.CANCEL); ResetAndAbort(abortReason, Http2ErrorCode.CANCEL);
} }
@ -176,6 +301,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return; return;
} }
Log.Http2StreamResetAbort(TraceIdentifier, error, abortReason);
// Don't block on IO. This never faults. // Don't block on IO. This never faults.
_ = _http2Output.WriteRstStreamAsync(error); _ = _http2Output.WriteRstStreamAsync(error);

View File

@ -191,13 +191,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
// Called by http/2 // Called by http/2
if (value == null) if (value == null)
{ {
throw new ArgumentNullException(nameof(value)); return HttpMethod.None;
} }
var length = value.Length; var length = value.Length;
if (length == 0) if (length == 0)
{ {
throw new ArgumentException(nameof(value)); return HttpMethod.None;
} }
// Start with custom and assign if known method is found // 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)) if (string.IsNullOrEmpty(hostText))
{ {
// The spec allows empty values // The spec allows empty values
return; return true;
} }
var firstChar = hostText[0]; var firstChar = hostText[0];
if (firstChar == '[') if (firstChar == '[')
{ {
// Tail call // Tail call
ValidateIPv6Host(hostText); return IsIPv6HostValid(hostText);
} }
else else
{ {
if (firstChar == ':') if (firstChar == ':')
{ {
// Only a port // Only a port
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); return false;
} }
var invalid = HttpCharacters.IndexOfInvalidHostChar(hostText); var invalid = HttpCharacters.IndexOfInvalidHostChar(hostText);
if (invalid >= 0) if (invalid >= 0)
{ {
// Tail call // Tail call
ValidateHostPort(hostText, invalid); return IsHostPortValid(hostText, invalid);
} }
return true;
} }
} }
// The lead '[' was already checked // 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++) 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 // [::1] is the shortest valid IPv6 host
if (i < 4) if (i < 4)
{ {
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); return false;
} }
else if (i + 1 < hostText.Length) else if (i + 1 < hostText.Length)
{ {
// Tail call // Tail call
ValidateHostPort(hostText, i + 1); return IsHostPortValid(hostText, i + 1);
} }
return; return true;
} }
if (!IsHex(ch) && ch != ':' && ch != '.') if (!IsHex(ch) && ch != ':' && ch != '.')
{ {
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); return false;
} }
} }
// Must contain a ']' // 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]; var firstChar = hostText[offset];
offset++; offset++;
if (firstChar != ':' || offset == hostText.Length) if (firstChar != ':' || offset == hostText.Length)
{ {
// Must have at least one number after the colon if present. // 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++) for (var i = offset; i < hostText.Length; i++)
{ {
if (!IsNumeric(hostText[i])) if (!IsNumeric(hostText[i]))
{ {
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); return false;
} }
} }
return true;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -58,6 +59,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void Http2StreamError(string connectionId, Http2StreamErrorException ex); void Http2StreamError(string connectionId, Http2StreamErrorException ex);
void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason);
void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; 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 = 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."); 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; protected readonly ILogger _logger;
public KestrelTrace(ILogger logger) public KestrelTrace(ILogger logger)
@ -213,6 +218,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_http2StreamError(_logger, connectionId, ex); _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) public virtual void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex)
{ {
_hpackDecodingError(_logger, connectionId, streamId, ex); _hpackDecodingError(_logger, connectionId, streamId, ex);

View File

@ -2002,6 +2002,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2StreamAborted() internal static string FormatHttp2StreamAborted()
=> GetString("Http2StreamAborted"); => 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); 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 object _abortedStreamIdsLock = new object();
private readonly RequestDelegate _noopApplication; private readonly RequestDelegate _noopApplication;
private readonly RequestDelegate _echoHost;
private readonly RequestDelegate _readHeadersApplication; private readonly RequestDelegate _readHeadersApplication;
private readonly RequestDelegate _readTrailersApplication; private readonly RequestDelegate _readTrailersApplication;
private readonly RequestDelegate _bufferingApplication; private readonly RequestDelegate _bufferingApplication;
@ -151,13 +150,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_noopApplication = context => Task.CompletedTask; _noopApplication = context => Task.CompletedTask;
_echoHost = context =>
{
context.Response.Headers[HeaderNames.Host] = context.Request.Headers[HeaderNames.Host];
return Task.CompletedTask;
};
_readHeadersApplication = context => _readHeadersApplication = context =>
{ {
foreach (var header in context.Request.Headers) foreach (var header in context.Request.Headers)
@ -1495,311 +1487,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); 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] [Fact]
public async Task PRIORITY_Received_StreamIdZero_ConnectionError() public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
{ {
@ -1873,45 +1560,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamSelfDependency(Http2FrameType.PRIORITY, 1)); 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] [Fact]
public async Task RST_STREAM_Received_RelievesConnectionBackpressure() public async Task RST_STREAM_Received_RelievesConnectionBackpressure()
{ {
@ -2116,92 +1764,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Contains(5, _abortedStreamIds); 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] [Fact]
public async Task RST_STREAM_Received_StreamIdZero_ConnectionError() 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)); 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] [Fact]
public async Task SETTINGS_Received_Sends_ACK() public async Task SETTINGS_Received_Sends_ACK()
{ {
@ -3540,28 +3010,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendAsync(frame.Raw); 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() private Task WaitForAllStreamsAsync()
{ {
return Task.WhenAll(_runningStreams.Values.Select(tcs => tcs.Task)).DefaultTimeout(); 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) private async Task<Http2Frame> ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId)
{ {
var frame = await ReceiveFrameAsync(); var frame = await ReceiveFrameAsync();
@ -4087,19 +3527,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
get get
{ {
var data = new TheoryData<IEnumerable<KeyValuePair<string, string>>>(); var data = new TheoryData<IEnumerable<KeyValuePair<string, string>>>();
var methodHeader = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT") }; var methodHeader = new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT");
var requestHeaders = new[] var headers = new[] { methodHeader };
{ data.Add(headers);
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);
}
return data; 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))] [MemberData(nameof(HostHeaderData))]
public void ValidHostHeadersParsed(string host) public void ValidHostHeadersParsed(string host)
{ {
HttpUtilities.ValidateHostHeader(host); Assert.True(HttpUtilities.IsHostHeaderValid(host));
// Shouldn't throw
} }
public static TheoryData<string> HostHeaderInvalidData public static TheoryData<string> HostHeaderInvalidData
@ -225,7 +224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[MemberData(nameof(HostHeaderInvalidData))] [MemberData(nameof(HostHeaderInvalidData))]
public void InvalidHostHeadersRejected(string host) 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 get
{ {
var dataset = new TheoryData<H2SpecTestCase>(); 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()) foreach (var testcase in H2SpecCommands.EnumerateTestCases())
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO.Pipelines; using System.IO.Pipelines;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@ -189,5 +190,11 @@ namespace Microsoft.AspNetCore.Testing
_trace1.HPackDecodingError(connectionId, streamId, ex); _trace1.HPackDecodingError(connectionId, streamId, ex);
_trace2.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);
}
} }
} }