Skip to content

Commit 0843320

Browse files
author
Nate McMaster
committed
Merge source code from aspnet/BasicMiddleware
2 parents 63b26d4 + 2f2cec4 commit 0843320

File tree

237 files changed

+18973
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

237 files changed

+18973
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<Reference Include="Microsoft.AspNetCore.HostFiltering" />
9+
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
10+
<Reference Include="Microsoft.Extensions.Configuration.Json" />
11+
<Reference Include="Microsoft.Extensions.Logging.Console" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<Content Update="appsettings.Development.json">
16+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17+
</Content>
18+
<Content Update="appsettings.json">
19+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
20+
</Content>
21+
<Content Update="appsettings.Production.json">
22+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
23+
</Content>
24+
</ItemGroup>
25+
26+
</Project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 Microsoft.AspNetCore.Hosting;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace HostFilteringSample
9+
{
10+
public class Program
11+
{
12+
public static void Main(string[] args)
13+
{
14+
BuildWebHost(args).Run();
15+
}
16+
17+
public static IWebHost BuildWebHost(string[] args)
18+
{
19+
var hostBuilder = new WebHostBuilder()
20+
.ConfigureLogging((_, factory) =>
21+
{
22+
factory.SetMinimumLevel(LogLevel.Debug);
23+
factory.AddConsole();
24+
})
25+
.ConfigureAppConfiguration((hostingContext, config) =>
26+
{
27+
var env = hostingContext.HostingEnvironment;
28+
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
29+
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
30+
})
31+
.UseKestrel()
32+
.UseStartup<Startup>();
33+
34+
return hostBuilder.Build();
35+
}
36+
}
37+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:14124/",
7+
"sslPort": 0
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"HostFilteringSample": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development"
23+
},
24+
"applicationUrl": "http://localhost:14125/"
25+
}
26+
}
27+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 System.Collections.Generic;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.HostFiltering;
8+
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Options;
13+
14+
namespace HostFilteringSample
15+
{
16+
public class Startup
17+
{
18+
public IConfiguration Config { get; }
19+
20+
public Startup(IConfiguration config)
21+
{
22+
Config = config;
23+
}
24+
25+
public void ConfigureServices(IServiceCollection services)
26+
{
27+
services.AddHostFiltering(options =>
28+
{
29+
30+
});
31+
32+
// Fallback
33+
services.PostConfigure<HostFilteringOptions>(options =>
34+
{
35+
if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
36+
{
37+
// "AllowedHosts": "localhost;127.0.0.1;[::1]"
38+
var hosts = Config["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
39+
// Fall back to "*" to disable.
40+
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
41+
}
42+
});
43+
// Change notification
44+
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(new ConfigurationChangeTokenSource<HostFilteringOptions>(Config));
45+
}
46+
47+
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
48+
{
49+
app.UseHostFiltering();
50+
51+
app.Run(context =>
52+
{
53+
return context.Response.WriteAsync("Hello World! " + context.Request.Host);
54+
});
55+
}
56+
}
57+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"AllowedHosts": "example.com;localhost"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
3+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.AspNetCore.HostFiltering;
6+
7+
namespace Microsoft.AspNetCore.Builder
8+
{
9+
/// <summary>
10+
/// Extension methods for the HostFiltering middleware.
11+
/// </summary>
12+
public static class HostFilteringBuilderExtensions
13+
{
14+
/// <summary>
15+
/// Adds middleware for filtering requests by allowed host headers. Invalid requests will be rejected with a
16+
/// 400 status code.
17+
/// </summary>
18+
/// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
19+
/// <returns>The original <see cref="IApplicationBuilder"/>.</returns>
20+
public static IApplicationBuilder UseHostFiltering(this IApplicationBuilder app)
21+
{
22+
if (app == null)
23+
{
24+
throw new ArgumentNullException(nameof(app));
25+
}
26+
27+
app.UseMiddleware<HostFilteringMiddleware>();
28+
29+
return app;
30+
}
31+
}
32+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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 System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.Extensions.Logging;
11+
using Microsoft.Extensions.Options;
12+
using Microsoft.Extensions.Primitives;
13+
using Microsoft.Net.Http.Headers;
14+
15+
namespace Microsoft.AspNetCore.HostFiltering
16+
{
17+
/// <summary>
18+
/// A middleware used to filter requests by their Host header.
19+
/// </summary>
20+
public class HostFilteringMiddleware
21+
{
22+
// Matches Http.Sys.
23+
private static readonly byte[] DefaultResponse = Encoding.ASCII.GetBytes(
24+
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">\r\n"
25+
+ "<HTML><HEAD><TITLE>Bad Request</TITLE>\r\n"
26+
+ "<META HTTP-EQUIV=\"Content-Type\" Content=\"text/html; charset=us-ascii\"></ HEAD >\r\n"
27+
+ "<BODY><h2>Bad Request - Invalid Hostname</h2>\r\n"
28+
+ "<hr><p>HTTP Error 400. The request hostname is invalid.</p>\r\n"
29+
+ "</BODY></HTML>");
30+
31+
private readonly RequestDelegate _next;
32+
private readonly ILogger<HostFilteringMiddleware> _logger;
33+
private readonly IOptionsMonitor<HostFilteringOptions> _optionsMonitor;
34+
private HostFilteringOptions _options;
35+
private IList<StringSegment> _allowedHosts;
36+
private bool? _allowAnyNonEmptyHost;
37+
38+
/// <summary>
39+
/// A middleware used to filter requests by their Host header.
40+
/// </summary>
41+
/// <param name="next"></param>
42+
/// <param name="logger"></param>
43+
/// <param name="optionsMonitor"></param>
44+
public HostFilteringMiddleware(RequestDelegate next, ILogger<HostFilteringMiddleware> logger,
45+
IOptionsMonitor<HostFilteringOptions> optionsMonitor)
46+
{
47+
_next = next ?? throw new ArgumentNullException(nameof(next));
48+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
49+
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
50+
_options = _optionsMonitor.CurrentValue;
51+
_optionsMonitor.OnChange(options =>
52+
{
53+
// Clear the cached settings so the next EnsureConfigured will re-evaluate.
54+
_options = options;
55+
_allowedHosts = new List<StringSegment>();
56+
_allowAnyNonEmptyHost = null;
57+
});
58+
}
59+
60+
/// <summary>
61+
/// Processes requests
62+
/// </summary>
63+
/// <param name="context"></param>
64+
/// <returns></returns>
65+
public Task Invoke(HttpContext context)
66+
{
67+
var allowedHosts = EnsureConfigured();
68+
69+
if (!CheckHost(context, allowedHosts))
70+
{
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;
79+
}
80+
81+
return _next(context);
82+
}
83+
84+
private IList<StringSegment> EnsureConfigured()
85+
{
86+
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
87+
{
88+
return _allowedHosts;
89+
}
90+
91+
var allowedHosts = new List<StringSegment>();
92+
if (_options.AllowedHosts?.Count > 0 && !TryProcessHosts(_options.AllowedHosts, allowedHosts))
93+
{
94+
_logger.LogDebug("Wildcard detected, all requests with hosts will be allowed.");
95+
_allowedHosts = allowedHosts;
96+
_allowAnyNonEmptyHost = true;
97+
return _allowedHosts;
98+
}
99+
100+
if (allowedHosts.Count == 0)
101+
{
102+
throw new InvalidOperationException("No allowed hosts were configured.");
103+
}
104+
105+
_logger.LogDebug("Allowed hosts: " + string.Join("; ", allowedHosts));
106+
_allowedHosts = allowedHosts;
107+
return _allowedHosts;
108+
}
109+
110+
// returns false if any wildcards were found
111+
private bool TryProcessHosts(IEnumerable<string> incoming, IList<StringSegment> results)
112+
{
113+
foreach (var entry in incoming)
114+
{
115+
// Punycode. Http.Sys requires you to register Unicode hosts, but the headers contain punycode.
116+
var host = new HostString(entry).ToUriComponent();
117+
118+
if (IsTopLevelWildcard(host))
119+
{
120+
// Disable filtering
121+
return false;
122+
}
123+
124+
if (!results.Contains(host, StringSegmentComparer.OrdinalIgnoreCase))
125+
{
126+
results.Add(host);
127+
}
128+
}
129+
130+
return true;
131+
}
132+
133+
private bool IsTopLevelWildcard(string host)
134+
{
135+
return (string.Equals("*", host, StringComparison.Ordinal) // HttpSys wildcard
136+
|| string.Equals("[::]", host, StringComparison.Ordinal) // Kestrel wildcard, IPv6 Any
137+
|| string.Equals("0.0.0.0", host, StringComparison.Ordinal)); // IPv4 Any
138+
}
139+
140+
// This does not duplicate format validations that are expected to be performed by the host.
141+
private bool CheckHost(HttpContext context, IList<StringSegment> allowedHosts)
142+
{
143+
var host = new StringSegment(context.Request.Headers[HeaderNames.Host].ToString()).Trim();
144+
145+
if (StringSegment.IsNullOrEmpty(host))
146+
{
147+
// Http/1.0 does not require the host header.
148+
// Http/1.1 requires the header but the value may be empty.
149+
if (!_options.AllowEmptyHosts)
150+
{
151+
_logger.LogInformation($"{context.Request.Protocol} request rejected due to missing or empty host header.");
152+
return false;
153+
}
154+
if (_logger.IsEnabled(LogLevel.Debug))
155+
{
156+
_logger.LogDebug($"{context.Request.Protocol} request allowed with missing or empty host header.");
157+
}
158+
return true;
159+
}
160+
161+
if (_allowAnyNonEmptyHost == true)
162+
{
163+
_logger.LogTrace($"All hosts are allowed.");
164+
return true;
165+
}
166+
167+
if (HostString.MatchesAny(host, allowedHosts))
168+
{
169+
_logger.LogTrace($"The host '{host}' matches an allowed host.");
170+
return true;
171+
}
172+
173+
_logger.LogInformation($"The host '{host}' does not match an allowed host.");
174+
return false;
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)