Host header format validation

This commit is contained in:
Chris Ross (ASP.NET) 2018-01-19 09:28:04 -08:00
parent bcea8330c4
commit bfdb48717f
12 changed files with 825 additions and 31 deletions

View File

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

View File

@ -3,13 +3,11 @@
using System;
using System.Buffers;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
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
// 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
@ -368,8 +361,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Host header field with an invalid field-value.
var host = HttpRequestHeaders.HeaderHost;
var hostText = host.ToString();
if (host.Count <= 0)
{
if (_httpVersion == Http.HttpVersion.Http10)
{
return;
}
BadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader);
}
else if (host.Count > 1)
@ -380,7 +378,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
if (!host.Equals(RawTarget))
{
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, in host);
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
}
}
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.
// When IsDefaultPort = true, we will allow Host: with or without the default port
var authorityAndPort = _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port;
if ((host != _absoluteRequestTarget.Authority || !_absoluteRequestTarget.IsDefaultPort)
&& host != authorityAndPort)
if (host != _absoluteRequestTarget.Authority)
{
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()

View File

@ -261,6 +261,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
{
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
if (_integerDecoder.Value == 0)
{
ProcessHeaderValue(handler);
}
}
else
{
@ -272,6 +276,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_integerDecoder.Decode(b))
{
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
if (_integerDecoder.Value == 0)
{
ProcessHeaderValue(handler);
}
}
break;
@ -280,17 +288,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_stringIndex == _stringLength)
{
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);
}
ProcessHeaderValue(handler);
}
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)
{
var header = GetHeader(index);

View File

@ -9,6 +9,8 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
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
// do the reading from a pipeline, nor do we use endConnection to report connection-level errors.
_httpVersion = Http.HttpVersion.Http2;
var methodText = RequestHeaders[":method"];
Method = HttpUtilities.GetKnownMethod(methodText);
_methodText = methodText;
Scheme = RequestHeaders[":scheme"];
_httpVersion = Http.HttpVersion.Http2;
if (!string.Equals(RequestHeaders[":scheme"], Scheme, StringComparison.OrdinalIgnoreCase))
{
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
}
var path = RequestHeaders[":path"].ToString();
var queryIndex = path.IndexOf('?');
@ -68,7 +72,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex);
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;
return true;

View File

@ -51,6 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
SetKnownMethod(_mask8Chars, _httpConnectMethodLong, HttpMethod.Connect, 7);
SetKnownMethod(_mask8Chars, _httpOptionsMethodLong, HttpMethod.Options, 7);
FillKnownMethodsGaps();
InitializeHostCharValidity();
_methodNames[(byte)HttpMethod.Connect] = HttpMethods.Connect;
_methodNames[(byte)HttpMethod.Delete] = HttpMethods.Delete;
_methodNames[(byte)HttpMethod.Get] = HttpMethods.Get;

View File

@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public static partial class HttpUtilities
{
private static readonly bool[] HostCharValidity = new bool[127];
public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1";
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 _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)]
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;
}
}
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');
}
}
}

View File

@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@ -813,6 +814,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
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)
{
const int MaxWaitLoop = 150;

View File

@ -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.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Net.Http.Headers;
using Xunit;
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>(":path", "/"),
new KeyValuePair<string, string>(":scheme", "http"),
new KeyValuePair<string, string>(":authority", "localhost:80"),
};
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>(":path", "/"),
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"),
};
@ -49,6 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"),
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>("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"),
@ -67,6 +70,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"),
new KeyValuePair<string, string>(":scheme", "http"),
new KeyValuePair<string, string>(":authority", "localhost:80"),
new KeyValuePair<string, string>("a", _largeHeaderValue),
new KeyValuePair<string, string>("b", _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>(":path", "/"),
new KeyValuePair<string, string>(":scheme", "http"),
new KeyValuePair<string, string>(":authority", "localhost:80"),
new KeyValuePair<string, string>("a", _largeHeaderValue),
new KeyValuePair<string, string>("b", _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 RequestDelegate _noopApplication;
private readonly RequestDelegate _echoHost;
private readonly RequestDelegate _readHeadersApplication;
private readonly RequestDelegate _readTrailersApplication;
private readonly RequestDelegate _bufferingApplication;
@ -134,6 +140,13 @@ 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)
@ -1178,6 +1191,311 @@ 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>(":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]
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
{

View File

@ -131,5 +131,100 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(knownString1, expected);
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));
}
}
}

View File

@ -132,6 +132,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
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]
public async Task BadRequestLogsAreNotHigherThanInformation()
{

View File

@ -603,7 +603,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
var requestTarget = new Uri(requestUrl, UriKind.Absolute);
var host = requestTarget.Authority;
if (!requestTarget.IsDefaultPort)
if (requestTarget.IsDefaultPort)
{
host += ":" + requestTarget.Port;
}

View File

@ -84,6 +84,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{{
{4}
FillKnownMethodsGaps();
InitializeHostCharValidity();
{5}
}}