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]; var pattern = patterns[i];

View File

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

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -68,19 +69,13 @@ namespace Microsoft.AspNetCore.HostFiltering
if (!CheckHost(context, allowedHosts)) if (!CheckHost(context, allowedHosts))
{ {
context.Response.StatusCode = 400; return HostValidationFailed(context);
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 _next(context); return _next(context);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private IList<StringSegment> EnsureConfigured() private IList<StringSegment> EnsureConfigured()
{ {
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0) if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
@ -88,10 +83,28 @@ namespace Microsoft.AspNetCore.HostFiltering
return _allowedHosts; 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>(); var allowedHosts = new List<StringSegment>();
if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts)) if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts))
{ {
_logger.LogDebug("Wildcard detected, all requests with hosts will be allowed."); _logger.WildcardDetected();
_allowedHosts = allowedHosts; _allowedHosts = allowedHosts;
_allowAnyNonEmptyHost = true; _allowAnyNonEmptyHost = true;
return _allowedHosts; return _allowedHosts;
@ -104,7 +117,7 @@ namespace Microsoft.AspNetCore.HostFiltering
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
_logger.LogDebug("Allowed hosts: {Hosts}", string.Join("; ", allowedHosts)); _logger.AllowedHosts(string.Join("; ", allowedHosts));
} }
_allowedHosts = 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. // 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) 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. return IsEmptyHostAllowed(context);
// 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;
} }
if (_allowAnyNonEmptyHost == true) if (_allowAnyNonEmptyHost == true)
{ {
_logger.LogTrace("All hosts are allowed."); _logger.AllHostsAllowed();
return true; 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; return true;
} }
_logger.LogInformation("The host '{Host}' does not match an allowed host.", host); _logger.NoAllowedHostMatched(host);
return false; 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) => app.Use((ctx, next) =>
{ {
ctx.Request.Headers[HeaderNames.Host] = " "; ctx.Request.Headers[HeaderNames.Host] = "";
return next(); return next();
}); });
app.UseHostFiltering(); app.UseHostFiltering();
app.Run(c => app.Run(c =>
{ {
Assert.True(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host)); Assert.True(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
Assert.True(StringValues.Equals(" ", host)); Assert.True(StringValues.Equals("", host));
return Task.CompletedTask; return Task.CompletedTask;
}); });
app.Run(c => 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 // 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 // a controller and have GET actions
if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType)) if (!string.IsNullOrEmpty(requestContentType) && !IsSubsetOfAnyContentType(requestContentType))
{ {
context.Result = new UnsupportedMediaTypeResult(); 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. // 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 // If there are multiple actions with consumes action constraints this should result in ambiguous exception
// unless there is another action without a consumes constraint. // unless there is another action without a consumes constraint.
if (requestContentType == null) if (string.IsNullOrEmpty(requestContentType))
{ {
var isActionWithoutConsumeConstraintPresent = context.Candidates.Any( var isActionWithoutConsumeConstraintPresent = context.Candidates.Any(
candidate => candidate.Constraints == null || candidate => candidate.Constraints == null ||

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
var requestContentType = context.HttpContext.Request.ContentType; 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) if (requestMediaType.Charset.HasValue)
{ {
// Create Encoding based on requestMediaType.Charset to support charset aliases and custom Encoding // Create Encoding based on requestMediaType.Charset to support charset aliases and custom Encoding