parent
603cd03bfa
commit
612fcca729
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue