Skip to content

Commit 37e6ad7

Browse files
benaadamsTratcher
authored andcommitted
Improve HostFiltering perf (#9359)
1 parent 7ee7f5d commit 37e6ad7

File tree

7 files changed

+100
-33
lines changed

7 files changed

+100
-33
lines changed

src/Http/Http.Abstractions/src/HostString.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ public static bool MatchesAny(StringSegment value, IList<StringSegment> patterns
247247
}
248248
}
249249

250-
for (int i = 0; i < patterns.Count; i++)
250+
var count = patterns.Count;
251+
for (int i = 0; i < count; i++)
251252
{
252253
var pattern = patterns[i];
253254

src/Http/Http/src/HeaderDictionary.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public StringValues this[string key]
7474
}
7575
ThrowIfReadOnly();
7676

77-
if (StringValues.IsNullOrEmpty(value))
77+
if (value.Count == 0)
7878
{
7979
Store?.Remove(key);
8080
}

src/Middleware/HostFiltering/src/HostFilteringMiddleware.cs

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Runtime.CompilerServices;
78
using System.Text;
89
using System.Threading.Tasks;
910
using Microsoft.AspNetCore.Http;
@@ -68,30 +69,42 @@ public Task Invoke(HttpContext context)
6869

6970
if (!CheckHost(context, allowedHosts))
7071
{
71-
context.Response.StatusCode = 400;
72-
if (_options.IncludeFailureMessage)
73-
{
74-
context.Response.ContentLength = DefaultResponse.Length;
75-
context.Response.ContentType = "text/html";
76-
return context.Response.Body.WriteAsync(DefaultResponse, 0, DefaultResponse.Length);
77-
}
78-
return Task.CompletedTask;
72+
return HostValidationFailed(context);
7973
}
8074

8175
return _next(context);
8276
}
8377

78+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8479
private IList<StringSegment> EnsureConfigured()
8580
{
8681
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
8782
{
8883
return _allowedHosts;
8984
}
9085

86+
return Configure();
87+
}
88+
89+
[MethodImpl(MethodImplOptions.NoInlining)]
90+
private Task HostValidationFailed(HttpContext context)
91+
{
92+
context.Response.StatusCode = 400;
93+
if (_options.IncludeFailureMessage)
94+
{
95+
context.Response.ContentLength = DefaultResponse.Length;
96+
context.Response.ContentType = "text/html";
97+
return context.Response.Body.WriteAsync(DefaultResponse, 0, DefaultResponse.Length);
98+
}
99+
return Task.CompletedTask;
100+
}
101+
102+
private IList<StringSegment> Configure()
103+
{
91104
var allowedHosts = new List<StringSegment>();
92105
if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts))
93106
{
94-
_logger.LogDebug("Wildcard detected, all requests with hosts will be allowed.");
107+
_logger.WildcardDetected();
95108
_allowedHosts = allowedHosts;
96109
_allowAnyNonEmptyHost = true;
97110
return _allowedHosts;
@@ -104,7 +117,7 @@ private IList<StringSegment> EnsureConfigured()
104117

105118
if (_logger.IsEnabled(LogLevel.Debug))
106119
{
107-
_logger.LogDebug("Allowed hosts: {Hosts}", string.Join("; ", allowedHosts));
120+
_logger.AllowedHosts(string.Join("; ", allowedHosts));
108121
}
109122

110123
_allowedHosts = allowedHosts;
@@ -142,37 +155,50 @@ private bool IsTopLevelWildcard(string host)
142155
}
143156

144157
// This does not duplicate format validations that are expected to be performed by the host.
158+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
145159
private bool CheckHost(HttpContext context, IList<StringSegment> allowedHosts)
146160
{
147-
var host = new StringSegment(context.Request.Headers[HeaderNames.Host].ToString()).Trim();
161+
var host = context.Request.Headers[HeaderNames.Host].ToString();
148162

149-
if (StringSegment.IsNullOrEmpty(host))
163+
if (host.Length == 0)
150164
{
151-
// Http/1.0 does not require the host header.
152-
// Http/1.1 requires the header but the value may be empty.
153-
if (!_options.AllowEmptyHosts)
154-
{
155-
_logger.LogInformation("{Protocol} request rejected due to missing or empty host header.", context.Request.Protocol);
156-
return false;
157-
}
158-
_logger.LogDebug("{Protocol} request allowed with missing or empty host header.", context.Request.Protocol);
159-
return true;
165+
return IsEmptyHostAllowed(context);
160166
}
161167

162168
if (_allowAnyNonEmptyHost == true)
163169
{
164-
_logger.LogTrace("All hosts are allowed.");
170+
_logger.AllHostsAllowed();
165171
return true;
166172
}
167173

168-
if (HostString.MatchesAny(host, allowedHosts))
174+
return CheckHostInAllowList(allowedHosts, host);
175+
}
176+
177+
[MethodImpl(MethodImplOptions.NoInlining)]
178+
private bool CheckHostInAllowList(IList<StringSegment> allowedHosts, string host)
179+
{
180+
if (HostString.MatchesAny(new StringSegment(host), allowedHosts))
169181
{
170-
_logger.LogTrace("The host '{Host}' matches an allowed host.", host);
182+
_logger.AllowedHostMatched(host);
171183
return true;
172184
}
173-
174-
_logger.LogInformation("The host '{Host}' does not match an allowed host.", host);
185+
186+
_logger.NoAllowedHostMatched(host);
175187
return false;
176188
}
189+
190+
[MethodImpl(MethodImplOptions.NoInlining)]
191+
private bool IsEmptyHostAllowed(HttpContext context)
192+
{
193+
// Http/1.0 does not require the host header.
194+
// Http/1.1 requires the header but the value may be empty.
195+
if (!_options.AllowEmptyHosts)
196+
{
197+
_logger.RequestRejectedMissingHost(context.Request.Protocol);
198+
return false;
199+
}
200+
_logger.RequestAllowedMissingHost(context.Request.Protocol);
201+
return true;
202+
}
177203
}
178204
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Microsoft.AspNetCore.HostFiltering
8+
{
9+
internal static class LoggerExtensions
10+
{
11+
private static readonly Action<ILogger, Exception> _wildcardDetected =
12+
LoggerMessage.Define(LogLevel.Debug, new EventId(0, nameof(WildcardDetected)), "Wildcard detected, all requests with hosts will be allowed.");
13+
14+
private static readonly Action<ILogger, string, Exception> _allowedHosts =
15+
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, nameof(AllowedHosts)), "Allowed hosts: {Hosts}");
16+
17+
private static readonly Action<ILogger, Exception> _allHostsAllowed =
18+
LoggerMessage.Define(LogLevel.Trace, new EventId(2, nameof(AllHostsAllowed)), "All hosts are allowed.");
19+
20+
private static readonly Action<ILogger, string, Exception> _requestRejectedMissingHost =
21+
LoggerMessage.Define<string>(LogLevel.Information, new EventId(3, nameof(RequestRejectedMissingHost)), "{Protocol} request rejected due to missing or empty host header.");
22+
23+
private static readonly Action<ILogger, string, Exception> _requestAllowedMissingHost =
24+
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(4, nameof(RequestAllowedMissingHost)), "{Protocol} request allowed with missing or empty host header.");
25+
26+
private static readonly Action<ILogger, string, Exception> _allowedHostMatched =
27+
LoggerMessage.Define<string>(LogLevel.Trace, new EventId(5, nameof(AllowedHostMatched)), "The host '{Host}' matches an allowed host.");
28+
29+
private static readonly Action<ILogger, string, Exception> _noAllowedHostMatched =
30+
LoggerMessage.Define<string>(LogLevel.Information, new EventId(6, nameof(NoAllowedHostMatched)), "The host '{Host}' does not match an allowed host.");
31+
32+
public static void WildcardDetected(this ILogger logger) => _wildcardDetected(logger, null);
33+
public static void AllowedHosts(this ILogger logger, string allowedHosts) => _allowedHosts(logger, allowedHosts, null);
34+
public static void AllHostsAllowed(this ILogger logger) => _allHostsAllowed(logger, null);
35+
public static void RequestRejectedMissingHost(this ILogger logger, string protocol) => _requestRejectedMissingHost(logger, protocol, null);
36+
public static void RequestAllowedMissingHost(this ILogger logger, string protocol) => _requestAllowedMissingHost(logger, protocol, null);
37+
public static void AllowedHostMatched(this ILogger logger, string host) => _allowedHostMatched(logger, host, null);
38+
public static void NoAllowedHostMatched(this ILogger logger, string host) => _noAllowedHostMatched(logger, host, null);
39+
}
40+
}

src/Middleware/HostFiltering/test/HostFilteringMiddlewareTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ public async Task AllowsEmptyHost(bool allowed, int status)
8080
{
8181
app.Use((ctx, next) =>
8282
{
83-
ctx.Request.Headers[HeaderNames.Host] = " ";
83+
ctx.Request.Headers[HeaderNames.Host] = "";
8484
return next();
8585
});
8686
app.UseHostFiltering();
8787
app.Run(c =>
8888
{
8989
Assert.True(c.Request.Headers.TryGetValue(HeaderNames.Host, out var host));
90-
Assert.True(StringValues.Equals(" ", host));
90+
Assert.True(StringValues.Equals("", host));
9191
return Task.CompletedTask;
9292
});
9393
app.Run(c => Task.CompletedTask);

src/Mvc/Mvc.Core/src/ConsumesAttribute.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void OnResourceExecuting(ResourceExecutingContext context)
7979
//
8080
// Requests without a content type do not return a 415. It is a common pattern to place [Consumes] on
8181
// a controller and have GET actions
82-
if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType))
82+
if (!string.IsNullOrEmpty(requestContentType) && !IsSubsetOfAnyContentType(requestContentType))
8383
{
8484
context.Result = new UnsupportedMediaTypeResult();
8585
}
@@ -127,7 +127,7 @@ public bool Accept(ActionConstraintContext context)
127127
// In case there is a single candidate with a constraint it should be selected.
128128
// If there are multiple actions with consumes action constraints this should result in ambiguous exception
129129
// unless there is another action without a consumes constraint.
130-
if (requestContentType == null)
130+
if (string.IsNullOrEmpty(requestContentType))
131131
{
132132
var isActionWithoutConsumeConstraintPresent = context.Candidates.Any(
133133
candidate => candidate.Constraints == null ||

src/Mvc/Mvc.Core/src/Formatters/TextInputFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ protected Encoding SelectCharacterEncoding(InputFormatterContext context)
9191
}
9292

9393
var requestContentType = context.HttpContext.Request.ContentType;
94-
var requestMediaType = requestContentType == null ? default(MediaType) : new MediaType(requestContentType);
94+
var requestMediaType = string.IsNullOrEmpty(requestContentType) ? default : new MediaType(requestContentType);
9595
if (requestMediaType.Charset.HasValue)
9696
{
9797
// Create Encoding based on requestMediaType.Charset to support charset aliases and custom Encoding

0 commit comments

Comments
 (0)