Improve HostFiltering perf (#9359)

This commit is contained in:
Ben Adams 2019-06-10 17:32:00 +01:00 committed by Chris Ross
parent 7ee7f5ddee
commit 37e6ad7e12
7 changed files with 100 additions and 33 deletions

View File

@ -247,7 +247,8 @@ namespace Microsoft.AspNetCore.Http
}
}
for (int i = 0; i < patterns.Count; i++)
var count = patterns.Count;
for (int i = 0; i < count; i++)
{
var pattern = patterns[i];

View File

@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Http
}
ThrowIfReadOnly();
if (StringValues.IsNullOrEmpty(value))
if (value.Count == 0)
{
Store?.Remove(key);
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -68,19 +69,13 @@ namespace Microsoft.AspNetCore.HostFiltering
if (!CheckHost(context, allowedHosts))
{
context.Response.StatusCode = 400;
if (_options.IncludeFailureMessage)
{
context.Response.ContentLength = DefaultResponse.Length;
context.Response.ContentType = "text/html";
return context.Response.Body.WriteAsync(DefaultResponse, 0, DefaultResponse.Length);
}
return Task.CompletedTask;
return HostValidationFailed(context);
}
return _next(context);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private IList<StringSegment> EnsureConfigured()
{
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
@ -88,10 +83,28 @@ namespace Microsoft.AspNetCore.HostFiltering
return _allowedHosts;
}
return Configure();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private Task HostValidationFailed(HttpContext context)
{
context.Response.StatusCode = 400;
if (_options.IncludeFailureMessage)
{
context.Response.ContentLength = DefaultResponse.Length;
context.Response.ContentType = "text/html";
return context.Response.Body.WriteAsync(DefaultResponse, 0, DefaultResponse.Length);
}
return Task.CompletedTask;
}
private IList<StringSegment> Configure()
{
var allowedHosts = new List<StringSegment>();
if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts))
{
_logger.LogDebug("Wildcard detected, all requests with hosts will be allowed.");
_logger.WildcardDetected();
_allowedHosts = allowedHosts;
_allowAnyNonEmptyHost = true;
return _allowedHosts;
@ -104,7 +117,7 @@ namespace Microsoft.AspNetCore.HostFiltering
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Allowed hosts: {Hosts}", string.Join("; ", allowedHosts));
_logger.AllowedHosts(string.Join("; ", allowedHosts));
}
_allowedHosts = allowedHosts;
@ -142,37 +155,50 @@ namespace Microsoft.AspNetCore.HostFiltering
}
// This does not duplicate format validations that are expected to be performed by the host.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool CheckHost(HttpContext context, IList<StringSegment> allowedHosts)
{
var host = new StringSegment(context.Request.Headers[HeaderNames.Host].ToString()).Trim();
var host = context.Request.Headers[HeaderNames.Host].ToString();
if (StringSegment.IsNullOrEmpty(host))
if (host.Length == 0)
{
// Http/1.0 does not require the host header.
// Http/1.1 requires the header but the value may be empty.
if (!_options.AllowEmptyHosts)
{
_logger.LogInformation("{Protocol} request rejected due to missing or empty host header.", context.Request.Protocol);
return false;
}
_logger.LogDebug("{Protocol} request allowed with missing or empty host header.", context.Request.Protocol);
return true;
return IsEmptyHostAllowed(context);
}
if (_allowAnyNonEmptyHost == true)
{
_logger.LogTrace("All hosts are allowed.");
_logger.AllHostsAllowed();
return true;
}
if (HostString.MatchesAny(host, allowedHosts))
return CheckHostInAllowList(allowedHosts, host);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool CheckHostInAllowList(IList<StringSegment> allowedHosts, string host)
{
if (HostString.MatchesAny(new StringSegment(host), allowedHosts))
{
_logger.LogTrace("The host '{Host}' matches an allowed host.", host);
_logger.AllowedHostMatched(host);
return true;
}
_logger.LogInformation("The host '{Host}' does not match an allowed host.", host);
_logger.NoAllowedHostMatched(host);
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool IsEmptyHostAllowed(HttpContext context)
{
// Http/1.0 does not require the host header.
// Http/1.1 requires the header but the value may be empty.
if (!_options.AllowEmptyHosts)
{
_logger.RequestRejectedMissingHost(context.Request.Protocol);
return false;
}
_logger.RequestAllowedMissingHost(context.Request.Protocol);
return true;
}
}
}

View File

@ -0,0 +1,40 @@
// 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 Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.HostFiltering
{
internal static class LoggerExtensions
{
private static readonly Action<ILogger, Exception> _wildcardDetected =
LoggerMessage.Define(LogLevel.Debug, new EventId(0, nameof(WildcardDetected)), "Wildcard detected, all requests with hosts will be allowed.");
private static readonly Action<ILogger, string, Exception> _allowedHosts =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, nameof(AllowedHosts)), "Allowed hosts: {Hosts}");
private static readonly Action<ILogger, Exception> _allHostsAllowed =
LoggerMessage.Define(LogLevel.Trace, new EventId(2, nameof(AllHostsAllowed)), "All hosts are allowed.");
private static readonly Action<ILogger, string, Exception> _requestRejectedMissingHost =
LoggerMessage.Define<string>(LogLevel.Information, new EventId(3, nameof(RequestRejectedMissingHost)), "{Protocol} request rejected due to missing or empty host header.");
private static readonly Action<ILogger, string, Exception> _requestAllowedMissingHost =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(4, nameof(RequestAllowedMissingHost)), "{Protocol} request allowed with missing or empty host header.");
private static readonly Action<ILogger, string, Exception> _allowedHostMatched =
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(5, nameof(AllowedHostMatched)), "The host '{Host}' matches an allowed host.");
private static readonly Action<ILogger, string, Exception> _noAllowedHostMatched =
LoggerMessage.Define<string>(LogLevel.Information, new EventId(6, nameof(NoAllowedHostMatched)), "The host '{Host}' does not match an allowed host.");
public static void WildcardDetected(this ILogger logger) => _wildcardDetected(logger, null);
public static void AllowedHosts(this ILogger logger, string allowedHosts) => _allowedHosts(logger, allowedHosts, null);
public static void AllHostsAllowed(this ILogger logger) => _allHostsAllowed(logger, null);
public static void RequestRejectedMissingHost(this ILogger logger, string protocol) => _requestRejectedMissingHost(logger, protocol, null);
public static void RequestAllowedMissingHost(this ILogger logger, string protocol) => _requestAllowedMissingHost(logger, protocol, null);
public static void AllowedHostMatched(this ILogger logger, string host) => _allowedHostMatched(logger, host, null);
public static void NoAllowedHostMatched(this ILogger logger, string host) => _noAllowedHostMatched(logger, host, null);
}
}

View File

@ -80,14 +80,14 @@ namespace Microsoft.AspNetCore.HostFiltering
{
app.Use((ctx, next) =>
{
ctx.Request.Headers[HeaderNames.Host] = " ";
ctx.Request.Headers[HeaderNames.Host] = "";
return next();
});
app.UseHostFiltering();
app.Run(c =>
{
Assert.True(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
Assert.True(StringValues.Equals(" ", host));
Assert.True(StringValues.Equals("", host));
return Task.CompletedTask;
});
app.Run(c => Task.CompletedTask);

View File

@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc
//
// Requests without a content type do not return a 415. It is a common pattern to place [Consumes] on
// a controller and have GET actions
if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType))
if (!string.IsNullOrEmpty(requestContentType) && !IsSubsetOfAnyContentType(requestContentType))
{
context.Result = new UnsupportedMediaTypeResult();
}
@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Mvc
// In case there is a single candidate with a constraint it should be selected.
// If there are multiple actions with consumes action constraints this should result in ambiguous exception
// unless there is another action without a consumes constraint.
if (requestContentType == null)
if (string.IsNullOrEmpty(requestContentType))
{
var isActionWithoutConsumeConstraintPresent = context.Candidates.Any(
candidate => candidate.Constraints == null ||

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
var requestContentType = context.HttpContext.Request.ContentType;
var requestMediaType = requestContentType == null ? default(MediaType) : new MediaType(requestContentType);
var requestMediaType = string.IsNullOrEmpty(requestContentType) ? default : new MediaType(requestContentType);
if (requestMediaType.Charset.HasValue)
{
// Create Encoding based on requestMediaType.Charset to support charset aliases and custom Encoding