Host header format validation
This commit is contained in:
parent
bcea8330c4
commit
bfdb48717f
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||||
|
{
|
||||||
|
public class Http1ConnectionBenchmark
|
||||||
|
{
|
||||||
|
private const int InnerLoopCount = 512;
|
||||||
|
|
||||||
|
private readonly HttpParser<Adapter> _parser = new HttpParser<Adapter>();
|
||||||
|
|
||||||
|
private ReadOnlySequence<byte> _buffer;
|
||||||
|
|
||||||
|
public Http1Connection Connection { get; set; }
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
var memoryPool = KestrelMemoryPool.Create();
|
||||||
|
var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
|
||||||
|
var pair = DuplexPipe.CreateConnectionPair(options, options);
|
||||||
|
|
||||||
|
var serviceContext = new ServiceContext
|
||||||
|
{
|
||||||
|
ServerOptions = new KestrelServerOptions(),
|
||||||
|
HttpParser = NullParser<Http1ParsingHandler>.Instance
|
||||||
|
};
|
||||||
|
|
||||||
|
var http1Connection = new Http1Connection(context: new Http1ConnectionContext
|
||||||
|
{
|
||||||
|
ServiceContext = serviceContext,
|
||||||
|
ConnectionFeatures = new FeatureCollection(),
|
||||||
|
MemoryPool = memoryPool,
|
||||||
|
TimeoutControl = new MockTimeoutControl(),
|
||||||
|
Application = pair.Application,
|
||||||
|
Transport = pair.Transport
|
||||||
|
});
|
||||||
|
|
||||||
|
http1Connection.Reset();
|
||||||
|
|
||||||
|
Connection = http1Connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
|
||||||
|
public void PlaintextTechEmpower()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
|
||||||
|
{
|
||||||
|
InsertData(RequestParsingData.PlaintextTechEmpowerRequest);
|
||||||
|
ParseData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
|
||||||
|
public void LiveAspNet()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
|
||||||
|
{
|
||||||
|
InsertData(RequestParsingData.LiveaspnetRequest);
|
||||||
|
ParseData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InsertData(byte[] data)
|
||||||
|
{
|
||||||
|
_buffer = new ReadOnlySequence<byte>(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ParseData()
|
||||||
|
{
|
||||||
|
if (!_parser.ParseRequestLine(new Adapter(this), _buffer, out var consumed, out var examined))
|
||||||
|
{
|
||||||
|
ErrorUtilities.ThrowInvalidRequestHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffer = _buffer.Slice(consumed, _buffer.End);
|
||||||
|
|
||||||
|
if (!_parser.ParseHeaders(new Adapter(this), _buffer, out consumed, out examined, out var consumedBytes))
|
||||||
|
{
|
||||||
|
ErrorUtilities.ThrowInvalidRequestHeaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.EnsureHostHeaderExists();
|
||||||
|
|
||||||
|
Connection.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Adapter : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||||
|
{
|
||||||
|
public Http1ConnectionBenchmark RequestHandler;
|
||||||
|
|
||||||
|
public Adapter(Http1ConnectionBenchmark requestHandler)
|
||||||
|
{
|
||||||
|
RequestHandler = requestHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||||
|
=> RequestHandler.Connection.OnHeader(name, value);
|
||||||
|
|
||||||
|
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||||
|
=> RequestHandler.Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Protocols.Abstractions;
|
using Microsoft.AspNetCore.Protocols.Abstractions;
|
||||||
|
|
@ -354,13 +352,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureHostHeaderExists()
|
internal void EnsureHostHeaderExists()
|
||||||
{
|
{
|
||||||
if (_httpVersion == Http.HttpVersion.Http10)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -368,8 +361,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
// Host header field with an invalid field-value.
|
// Host header field with an invalid field-value.
|
||||||
|
|
||||||
var host = HttpRequestHeaders.HeaderHost;
|
var host = HttpRequestHeaders.HeaderHost;
|
||||||
|
var hostText = host.ToString();
|
||||||
if (host.Count <= 0)
|
if (host.Count <= 0)
|
||||||
{
|
{
|
||||||
|
if (_httpVersion == Http.HttpVersion.Http10)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
BadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader);
|
BadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader);
|
||||||
}
|
}
|
||||||
else if (host.Count > 1)
|
else if (host.Count > 1)
|
||||||
|
|
@ -380,7 +378,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
if (!host.Equals(RawTarget))
|
if (!host.Equals(RawTarget))
|
||||||
{
|
{
|
||||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, in host);
|
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_requestTargetForm == HttpRequestTarget.AbsoluteForm)
|
else if (_requestTargetForm == HttpRequestTarget.AbsoluteForm)
|
||||||
|
|
@ -392,13 +390,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
// System.Uri doesn't not tell us if the port was in the original string or not.
|
// System.Uri doesn't not tell us if the port was in the original string or not.
|
||||||
// When IsDefaultPort = true, we will allow Host: with or without the default port
|
// When IsDefaultPort = true, we will allow Host: with or without the default port
|
||||||
var authorityAndPort = _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port;
|
if (host != _absoluteRequestTarget.Authority)
|
||||||
if ((host != _absoluteRequestTarget.Authority || !_absoluteRequestTarget.IsDefaultPort)
|
|
||||||
&& host != authorityAndPort)
|
|
||||||
{
|
{
|
||||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, in host);
|
if (!_absoluteRequestTarget.IsDefaultPort
|
||||||
|
|| host != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture))
|
||||||
|
{
|
||||||
|
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HttpUtilities.IsValidHostHeader(hostText))
|
||||||
|
{
|
||||||
|
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnReset()
|
protected override void OnReset()
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||||
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
|
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
|
||||||
{
|
{
|
||||||
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
|
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
|
||||||
|
if (_integerDecoder.Value == 0)
|
||||||
|
{
|
||||||
|
ProcessHeaderValue(handler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -272,6 +276,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||||
if (_integerDecoder.Decode(b))
|
if (_integerDecoder.Decode(b))
|
||||||
{
|
{
|
||||||
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
|
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
|
||||||
|
if (_integerDecoder.Value == 0)
|
||||||
|
{
|
||||||
|
ProcessHeaderValue(handler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -280,17 +288,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||||
|
|
||||||
if (_stringIndex == _stringLength)
|
if (_stringIndex == _stringLength)
|
||||||
{
|
{
|
||||||
OnString(nextState: State.Ready);
|
ProcessHeaderValue(handler);
|
||||||
|
|
||||||
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
|
|
||||||
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
|
|
||||||
|
|
||||||
handler.OnHeader(headerNameSpan, headerValueSpan);
|
|
||||||
|
|
||||||
if (_index)
|
|
||||||
{
|
|
||||||
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -314,6 +312,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProcessHeaderValue(IHttpHeadersHandler handler)
|
||||||
|
{
|
||||||
|
OnString(nextState: State.Ready);
|
||||||
|
|
||||||
|
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
|
||||||
|
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
|
||||||
|
|
||||||
|
handler.OnHeader(headerNameSpan, headerValueSpan);
|
||||||
|
|
||||||
|
if (_index)
|
||||||
|
{
|
||||||
|
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
||||||
{
|
{
|
||||||
var header = GetHeader(index);
|
var header = GetHeader(index);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||||
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;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
|
|
@ -54,12 +56,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
// 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.
|
||||||
|
|
||||||
|
_httpVersion = Http.HttpVersion.Http2;
|
||||||
var methodText = RequestHeaders[":method"];
|
var methodText = RequestHeaders[":method"];
|
||||||
Method = HttpUtilities.GetKnownMethod(methodText);
|
Method = HttpUtilities.GetKnownMethod(methodText);
|
||||||
_methodText = methodText;
|
_methodText = methodText;
|
||||||
|
if (!string.Equals(RequestHeaders[":scheme"], Scheme, StringComparison.OrdinalIgnoreCase))
|
||||||
Scheme = RequestHeaders[":scheme"];
|
{
|
||||||
_httpVersion = Http.HttpVersion.Http2;
|
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
|
||||||
|
}
|
||||||
|
|
||||||
var path = RequestHeaders[":path"].ToString();
|
var path = RequestHeaders[":path"].ToString();
|
||||||
var queryIndex = path.IndexOf('?');
|
var queryIndex = path.IndexOf('?');
|
||||||
|
|
@ -68,7 +72,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex);
|
QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex);
|
||||||
RawTarget = path;
|
RawTarget = path;
|
||||||
|
|
||||||
RequestHeaders["Host"] = RequestHeaders[":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[":authority"];
|
||||||
|
var host = HttpRequestHeaders.HeaderHost;
|
||||||
|
if (authority.Count > 0)
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
if (!HttpUtilities.IsValidHostHeader(hostText))
|
||||||
|
{
|
||||||
|
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
|
||||||
|
}
|
||||||
|
|
||||||
endConnection = false;
|
endConnection = false;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
SetKnownMethod(_mask8Chars, _httpConnectMethodLong, HttpMethod.Connect, 7);
|
SetKnownMethod(_mask8Chars, _httpConnectMethodLong, HttpMethod.Connect, 7);
|
||||||
SetKnownMethod(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7);
|
SetKnownMethod(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7);
|
||||||
FillKnownMethodsGaps();
|
FillKnownMethodsGaps();
|
||||||
|
InitializeHostCharValidity();
|
||||||
_methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect;
|
_methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect;
|
||||||
_methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete;
|
_methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete;
|
||||||
_methodNames[(byte)HttpMethod.Get] = HttpMethods.Get;
|
_methodNames[(byte)HttpMethod.Get] = HttpMethods.Get;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
{
|
{
|
||||||
public static partial class HttpUtilities
|
public static partial class HttpUtilities
|
||||||
{
|
{
|
||||||
|
private static readonly bool[] HostCharValidity = new bool[127];
|
||||||
|
|
||||||
public const string Http10Version = "HTTP/1.0";
|
public const string Http10Version = "HTTP/1.0";
|
||||||
public const string Http11Version = "HTTP/1.1";
|
public const string Http11Version = "HTTP/1.1";
|
||||||
public const string Http2Version = "HTTP/2";
|
public const string Http2Version = "HTTP/2";
|
||||||
|
|
@ -29,6 +31,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
|
private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
|
||||||
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
|
private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
|
||||||
|
|
||||||
|
// Only called from the static constructor
|
||||||
|
private static void InitializeHostCharValidity()
|
||||||
|
{
|
||||||
|
// Matches Http.Sys
|
||||||
|
// Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
|
||||||
|
HostCharValidity['!'] = true;
|
||||||
|
HostCharValidity['$'] = true;
|
||||||
|
HostCharValidity['&'] = true;
|
||||||
|
HostCharValidity['\''] = true;
|
||||||
|
HostCharValidity['('] = true;
|
||||||
|
HostCharValidity[')'] = true;
|
||||||
|
HostCharValidity['-'] = true;
|
||||||
|
HostCharValidity['.'] = true;
|
||||||
|
HostCharValidity['_'] = true;
|
||||||
|
HostCharValidity['~'] = true;
|
||||||
|
for (var ch = '0'; ch <= '9'; ch++)
|
||||||
|
{
|
||||||
|
HostCharValidity[ch] = true;
|
||||||
|
}
|
||||||
|
for (var ch = 'A'; ch <= 'Z'; ch++)
|
||||||
|
{
|
||||||
|
HostCharValidity[ch] = true;
|
||||||
|
}
|
||||||
|
for (var ch = 'a'; ch <= 'z'; ch++)
|
||||||
|
{
|
||||||
|
HostCharValidity[ch] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
|
private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
|
||||||
{
|
{
|
||||||
|
|
@ -394,5 +425,107 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValidHostHeader(string hostText)
|
||||||
|
{
|
||||||
|
// The spec allows empty values
|
||||||
|
if (string.IsNullOrEmpty(hostText))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostText[0] == '[')
|
||||||
|
{
|
||||||
|
return IsValidIPv6Host(hostText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostText[0] == ':')
|
||||||
|
{
|
||||||
|
// Only a port
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
for (; i < hostText.Length; i++)
|
||||||
|
{
|
||||||
|
if (!IsValidHostChar(hostText[i]))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return IsValidHostPort(hostText, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsValidHostChar(char ch)
|
||||||
|
{
|
||||||
|
return ch < HostCharValidity.Length && HostCharValidity[ch];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lead '[' was already checked
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsValidIPv6Host(string hostText)
|
||||||
|
{
|
||||||
|
for (var i = 1; i < hostText.Length; i++)
|
||||||
|
{
|
||||||
|
var ch = hostText[i];
|
||||||
|
if (ch == ']')
|
||||||
|
{
|
||||||
|
// [::1] is the shortest valid IPv6 host
|
||||||
|
if (i < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return IsValidHostPort(hostText, i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsHex(ch) && ch != ':' && ch != '.')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must contain a ']'
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsValidHostPort(string hostText, int offset)
|
||||||
|
{
|
||||||
|
if (offset == hostText.Length)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostText[offset] != ':' || hostText.Length == offset + 1)
|
||||||
|
{
|
||||||
|
// Must have at least one number after the colon if present.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = offset + 1; i < hostText.Length; i++)
|
||||||
|
{
|
||||||
|
if (!IsNumeric(hostText[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsNumeric(char ch)
|
||||||
|
{
|
||||||
|
return '0' <= ch && ch <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsHex(char ch)
|
||||||
|
{
|
||||||
|
return IsNumeric(ch)
|
||||||
|
|| ('a' <= ch && ch <= 'f')
|
||||||
|
|| ('A' <= ch && ch <= 'F');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -813,6 +814,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
mockMessageBody.Verify(body => body.ConsumeAsync(), Times.Once);
|
mockMessageBody.Verify(body => body.ConsumeAsync(), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Http10HostHeaderNotRequired()
|
||||||
|
{
|
||||||
|
_http1Connection.HttpVersion = "HTTP/1.0";
|
||||||
|
_http1Connection.EnsureHostHeaderExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Http10HostHeaderAllowed()
|
||||||
|
{
|
||||||
|
_http1Connection.HttpVersion = "HTTP/1.0";
|
||||||
|
_http1Connection.RequestHeaders[HeaderNames.Host] = "localhost:5000";
|
||||||
|
_http1Connection.EnsureHostHeaderExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Http11EmptyHostHeaderAccepted()
|
||||||
|
{
|
||||||
|
_http1Connection.HttpVersion = "HTTP/1.1";
|
||||||
|
_http1Connection.RequestHeaders[HeaderNames.Host] = "";
|
||||||
|
_http1Connection.EnsureHostHeaderExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Http11ValidHostHeadersAccepted()
|
||||||
|
{
|
||||||
|
_http1Connection.HttpVersion = "HTTP/1.1";
|
||||||
|
_http1Connection.RequestHeaders[HeaderNames.Host] = "localhost:5000";
|
||||||
|
_http1Connection.EnsureHostHeaderExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BadRequestFor10BadHostHeaderFormat()
|
||||||
|
{
|
||||||
|
_http1Connection.HttpVersion = "HTTP/1.0";
|
||||||
|
_http1Connection.RequestHeaders[HeaderNames.Host] = "a=b";
|
||||||
|
var ex = Assert.Throws<BadHttpRequestException>(() => _http1Connection.EnsureHostHeaderExists());
|
||||||
|
Assert.Equal(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BadRequestFor11BadHostHeaderFormat()
|
||||||
|
{
|
||||||
|
_http1Connection.HttpVersion = "HTTP/1.1";
|
||||||
|
_http1Connection.RequestHeaders[HeaderNames.Host] = "a=b";
|
||||||
|
var ex = Assert.Throws<BadHttpRequestException>(() => _http1Connection.EnsureHostHeaderExists());
|
||||||
|
Assert.Equal(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task WaitForCondition(TimeSpan timeout, Func<bool> condition)
|
private static async Task WaitForCondition(TimeSpan timeout, Func<bool> condition)
|
||||||
{
|
{
|
||||||
const int MaxWaitLoop = 150;
|
const int MaxWaitLoop = 150;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ 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;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
|
|
@ -33,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
new KeyValuePair<string, string>(":method", "POST"),
|
new KeyValuePair<string, string>(":method", "POST"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":scheme", "http"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":authority", "localhost:80"),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly IEnumerable<KeyValuePair<string, string>> _expectContinueRequestHeaders = new[]
|
private static readonly IEnumerable<KeyValuePair<string, string>> _expectContinueRequestHeaders = new[]
|
||||||
|
|
@ -40,7 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
new KeyValuePair<string, string>(":method", "POST"),
|
new KeyValuePair<string, string>(":method", "POST"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||||
new KeyValuePair<string, string>(":scheme", "https"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
new KeyValuePair<string, string>("expect", "100-continue"),
|
new KeyValuePair<string, string>("expect", "100-continue"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -49,6 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
new KeyValuePair<string, string>(":method", "GET"),
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":scheme", "http"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":authority", "localhost:80"),
|
||||||
new KeyValuePair<string, string>("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
|
new KeyValuePair<string, string>("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
|
||||||
new KeyValuePair<string, string>("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
new KeyValuePair<string, string>("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
||||||
new KeyValuePair<string, string>("accept-language", "en-US,en;q=0.5"),
|
new KeyValuePair<string, string>("accept-language", "en-US,en;q=0.5"),
|
||||||
|
|
@ -67,6 +70,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
new KeyValuePair<string, string>(":method", "GET"),
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":scheme", "http"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":authority", "localhost:80"),
|
||||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||||
|
|
@ -78,6 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
new KeyValuePair<string, string>(":method", "GET"),
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
new KeyValuePair<string, string>(":path", "/"),
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
new KeyValuePair<string, string>(":scheme", "http"),
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":authority", "localhost:80"),
|
||||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||||
|
|
@ -110,6 +115,7 @@ 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;
|
||||||
|
|
@ -134,6 +140,13 @@ 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)
|
||||||
|
|
@ -1178,6 +1191,311 @@ 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>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":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[":status"]);
|
||||||
|
Assert.Equal("0", _decodedHeaders["content-length"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HEADERS_Received_MissingAuthority_400Status()
|
||||||
|
{
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":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[":status"]);
|
||||||
|
Assert.Equal("0", _decodedHeaders["content-length"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HEADERS_Received_TwoHosts_400Status()
|
||||||
|
{
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":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[":status"]);
|
||||||
|
Assert.Equal("0", _decodedHeaders["content-length"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HEADERS_Received_EmptyAuthority_200Status()
|
||||||
|
{
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":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[":status"]);
|
||||||
|
Assert.Equal("0", _decodedHeaders["content-length"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HEADERS_Received_EmptyAuthorityOverridesHost_200Status()
|
||||||
|
{
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":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[":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>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":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[":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>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":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[":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>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":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[":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>(":method", "GET"),
|
||||||
|
new KeyValuePair<string, string>(":path", "/"),
|
||||||
|
new KeyValuePair<string, string>(":scheme", "http"),
|
||||||
|
new KeyValuePair<string, string>(":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[":status"]);
|
||||||
|
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
|
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -131,5 +131,100 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
Assert.Equal(knownString1, expected);
|
Assert.Equal(knownString1, expected);
|
||||||
Assert.Same(knownString1, knownString2);
|
Assert.Same(knownString1, knownString2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string> HostHeaderData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string>() {
|
||||||
|
"z",
|
||||||
|
"1",
|
||||||
|
"y:1",
|
||||||
|
"1:1",
|
||||||
|
"[ABCdef]",
|
||||||
|
"[abcDEF]:0",
|
||||||
|
"[abcdef:127.2355.1246.114]:0",
|
||||||
|
"[::1]:80",
|
||||||
|
"127.0.0.1:80",
|
||||||
|
"900.900.900.900:9523547852",
|
||||||
|
"foo",
|
||||||
|
"foo:234",
|
||||||
|
"foo.bar.baz",
|
||||||
|
"foo.BAR.baz:46245",
|
||||||
|
"foo.ba-ar.baz:46245",
|
||||||
|
"-foo:1234",
|
||||||
|
"xn--asdfaf:134",
|
||||||
|
"-",
|
||||||
|
"_",
|
||||||
|
"~",
|
||||||
|
"!",
|
||||||
|
"$",
|
||||||
|
"'",
|
||||||
|
"(",
|
||||||
|
")",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(HostHeaderData))]
|
||||||
|
public void ValidHostHeadersParsed(string host)
|
||||||
|
{
|
||||||
|
Assert.True(HttpUtilities.IsValidHostHeader(host));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string> HostHeaderInvalidData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// see https://tools.ietf.org/html/rfc7230#section-5.4
|
||||||
|
var data = new TheoryData<string>() {
|
||||||
|
"[]", // Too short
|
||||||
|
"[::]", // Too short
|
||||||
|
"[ghijkl]", // Non-hex
|
||||||
|
"[afd:adf:123", // Incomplete
|
||||||
|
"[afd:adf]123", // Missing :
|
||||||
|
"[afd:adf]:", // Missing port digits
|
||||||
|
"[afd adf]", // Space
|
||||||
|
"[ad-314]", // dash
|
||||||
|
":1234", // Missing host
|
||||||
|
"a:b:c", // Missing []
|
||||||
|
"::1", // Missing []
|
||||||
|
"::", // Missing everything
|
||||||
|
"abcd:1abcd", // Letters in port
|
||||||
|
"abcd:1.2", // Dot in port
|
||||||
|
"1.2.3.4:", // Missing port digits
|
||||||
|
"1.2 .4", // Space
|
||||||
|
};
|
||||||
|
|
||||||
|
// These aren't allowed anywhere in the host header
|
||||||
|
var invalid = "\"#%*+,/;<=>?@[]\\^`{}|";
|
||||||
|
foreach (var ch in invalid)
|
||||||
|
{
|
||||||
|
data.Add(ch.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid = "!\"#$%&'()*+,/;<=>?@[]\\^_`{}|~-";
|
||||||
|
foreach (var ch in invalid)
|
||||||
|
{
|
||||||
|
data.Add("[abd" + ch + "]:1234");
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid = "!\"#$%&'()*+,/;<=>?@[]\\^_`{}|~:abcABC-.";
|
||||||
|
foreach (var ch in invalid)
|
||||||
|
{
|
||||||
|
data.Add("a.b.c:" + ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(HostHeaderInvalidData))]
|
||||||
|
public void InvalidHostHeadersRejected(string host)
|
||||||
|
{
|
||||||
|
Assert.False(HttpUtilities.IsValidHostHeader(host));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +132,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim()));
|
CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task BadRequestFor10BadHostHeaderFormat()
|
||||||
|
{
|
||||||
|
return TestBadRequest(
|
||||||
|
$"GET / HTTP/1.0\r\nHost: a=b\r\n\r\n",
|
||||||
|
"400 Bad Request",
|
||||||
|
CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task BadRequestFor11BadHostHeaderFormat()
|
||||||
|
{
|
||||||
|
return TestBadRequest(
|
||||||
|
$"GET / HTTP/1.1\r\nHost: a=b\r\n\r\n",
|
||||||
|
"400 Bad Request",
|
||||||
|
CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BadRequestLogsAreNotHigherThanInformation()
|
public async Task BadRequestLogsAreNotHigherThanInformation()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -603,7 +603,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
{
|
{
|
||||||
var requestTarget = new Uri(requestUrl, UriKind.Absolute);
|
var requestTarget = new Uri(requestUrl, UriKind.Absolute);
|
||||||
var host = requestTarget.Authority;
|
var host = requestTarget.Authority;
|
||||||
if (!requestTarget.IsDefaultPort)
|
if (requestTarget.IsDefaultPort)
|
||||||
{
|
{
|
||||||
host += ":" + requestTarget.Port;
|
host += ":" + requestTarget.Port;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
{{
|
{{
|
||||||
{4}
|
{4}
|
||||||
FillKnownMethodsGaps();
|
FillKnownMethodsGaps();
|
||||||
|
InitializeHostCharValidity();
|
||||||
{5}
|
{5}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue