Reload HostFilter options on change #317

This commit is contained in:
Chris Ross (ASP.NET) 2018-04-11 11:44:58 -07:00
parent fc3ab083d2
commit 768cad8a8a
8 changed files with 103 additions and 17 deletions

View File

@ -14,6 +14,7 @@
<MicrosoftAspNetCoreTestHostPackageVersion>2.1.0-preview3-32110</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftExtensionsConfigurationAbstractionsPackageVersion>2.1.0-preview3-32110</MicrosoftExtensionsConfigurationAbstractionsPackageVersion>
<MicrosoftExtensionsConfigurationBinderPackageVersion>2.1.0-preview3-32110</MicrosoftExtensionsConfigurationBinderPackageVersion>
<MicrosoftExtensionsConfigurationExtensionsPackageVersion>2.1.0-preview3-32110</MicrosoftExtensionsConfigurationExtensionsPackageVersion>
<MicrosoftExtensionsConfigurationJsonPackageVersion>2.1.0-preview3-32110</MicrosoftExtensionsConfigurationJsonPackageVersion>
<MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>2.1.0-preview3-32110</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.0-preview3-32110</MicrosoftExtensionsLoggingAbstractionsPackageVersion>

View File

@ -25,8 +25,8 @@ namespace HostFilteringSample
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
})
.UseKestrel()
.UseStartup<Startup>();

View File

@ -1,12 +1,15 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace HostFilteringSample
{
@ -23,9 +26,22 @@ namespace HostFilteringSample
{
services.AddHostFiltering(options =>
{
// If this is excluded then it will fall back to the server's addresses
options.AllowedHosts = Config.GetSection("AllowedHosts").Get<List<string>>();
});
// Fallback
services.PostConfigure<HostFilteringOptions>(options =>
{
if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
{
// "AllowedHosts": "localhost;127.0.0.1;[::1]"
var hosts = Config["AllowedHosts"]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
// Fall back to "*" to disable.
options.AllowedHosts = (hosts?.Length > 0 ? hosts : new[] { "*" });
}
});
// Change notification
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(new ConfigurationChangeTokenSource<HostFilteringOptions>(Config));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)

View File

@ -1,3 +1,3 @@
{
"AllowedHosts": [ "localhost", "127.0.0.1", "[::1]" ]
"AllowedHosts": "localhost;127.0.0.1;[::1]"
}

View File

@ -1,3 +1,3 @@
{
"AllowedHosts": [ "example.com", "localhost" ]
"AllowedHosts": "example.com;localhost"
}

View File

@ -30,7 +30,8 @@ namespace Microsoft.AspNetCore.HostFiltering
private readonly RequestDelegate _next;
private readonly ILogger<HostFilteringMiddleware> _logger;
private readonly HostFilteringOptions _options;
private readonly IOptionsMonitor<HostFilteringOptions> _optionsMonitor;
private HostFilteringOptions _options;
private IList<StringSegment> _allowedHosts;
private bool? _allowAnyNonEmptyHost;
@ -39,13 +40,21 @@ namespace Microsoft.AspNetCore.HostFiltering
/// </summary>
/// <param name="next"></param>
/// <param name="logger"></param>
/// <param name="options"></param>
/// <param name="optionsMonitor"></param>
public HostFilteringMiddleware(RequestDelegate next, ILogger<HostFilteringMiddleware> logger,
IOptions<HostFilteringOptions> options)
IOptionsMonitor<HostFilteringOptions> optionsMonitor)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_options = _optionsMonitor.CurrentValue;
_optionsMonitor.OnChange(options =>
{
// Clear the cached settings so the next EnsureConfigured will re-evaluate.
_options = options;
_allowedHosts = new List<StringSegment>();
_allowAnyNonEmptyHost = null;
});
}
/// <summary>
@ -55,9 +64,9 @@ namespace Microsoft.AspNetCore.HostFiltering
/// <returns></returns>
public Task Invoke(HttpContext context)
{
EnsureConfigured();
var allowedHosts = EnsureConfigured();
if (!CheckHost(context))
if (!CheckHost(context, allowedHosts))
{
context.Response.StatusCode = 400;
if (_options.IncludeFailureMessage)
@ -72,19 +81,20 @@ namespace Microsoft.AspNetCore.HostFiltering
return _next(context);
}
private void EnsureConfigured()
private IList<StringSegment> EnsureConfigured()
{
if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0)
{
return;
return _allowedHosts;
}
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.");
_allowedHosts = allowedHosts;
_allowAnyNonEmptyHost = true;
return;
return _allowedHosts;
}
if (allowedHosts.Count == 0)
@ -94,6 +104,7 @@ namespace Microsoft.AspNetCore.HostFiltering
_logger.LogDebug("Allowed hosts: " + string.Join("; ", allowedHosts));
_allowedHosts = allowedHosts;
return _allowedHosts;
}
// returns false if any wildcards were found
@ -127,7 +138,7 @@ namespace Microsoft.AspNetCore.HostFiltering
}
// This does not duplicate format validations that are expected to be performed by the host.
private bool CheckHost(HttpContext context)
private bool CheckHost(HttpContext context, IList<StringSegment> allowedHosts)
{
var host = new StringSegment(context.Request.Headers[HeaderNames.Host].ToString()).Trim();
@ -153,7 +164,7 @@ namespace Microsoft.AspNetCore.HostFiltering
return true;
}
if (HostString.MatchesAny(host, _allowedHosts))
if (HostString.MatchesAny(host, allowedHosts))
{
_logger.LogTrace($"The host '{host}' matches an allowed host.");
return true;

View File

@ -2,10 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -179,5 +183,58 @@ namespace Microsoft.AspNetCore.HostFiltering
var response = await server.CreateRequest("/").GetAsync();
Assert.Equal(400, (int)response.StatusCode);
}
[Fact]
public async Task SupportsDynamicOptionsReload()
{
var config = new ConfigurationBuilder().Add(new ReloadableMemorySource()).Build();
config["AllowedHosts"] = "localhost";
var currentHost = "otherHost";
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddHostFiltering(options =>
{
options.AllowedHosts = new[] { config["AllowedHosts"] };
});
services.AddSingleton<IOptionsChangeTokenSource<HostFilteringOptions>>(new ConfigurationChangeTokenSource<HostFilteringOptions>(config));
})
.Configure(app =>
{
app.Use((ctx, next) =>
{
ctx.Request.Headers[HeaderNames.Host] = currentHost;
return next();
});
app.UseHostFiltering();
app.Run(c => Task.CompletedTask);
});
var server = new TestServer(builder);
var response = await server.CreateRequest("/").GetAsync();
Assert.Equal(400, (int)response.StatusCode);
config["AllowedHosts"] = "otherHost";
response = await server.CreateRequest("/").GetAsync();
Assert.Equal(200, (int)response.StatusCode);
}
private class ReloadableMemorySource : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new ReloadableMemoryProvider();
}
}
internal class ReloadableMemoryProvider : ConfigurationProvider
{
public override void Set(string key, string value)
{
base.Set(key, value);
OnReload();
}
}
}
}

View File

@ -6,6 +6,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.HostFiltering\Microsoft.AspNetCore.HostFiltering.csproj" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(MicrosoftExtensionsConfigurationExtensionsPackageVersion)" />
</ItemGroup>
</Project>