From 768cad8a8a17766c790cec04b340185a90d3b0cc Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Wed, 11 Apr 2018 11:44:58 -0700 Subject: [PATCH] Reload HostFilter options on change #317 --- build/dependencies.props | 1 + samples/HostFilteringSample/Program.cs | 4 +- samples/HostFilteringSample/Startup.cs | 20 ++++++- .../appsettings.Development.json | 2 +- .../appsettings.Production.json | 2 +- .../HostFilteringMiddleware.cs | 33 +++++++---- .../HostFilteringMiddlewareTests.cs | 57 +++++++++++++++++++ ...soft.AspNetCore.HostFiltering.Tests.csproj | 1 + 8 files changed, 103 insertions(+), 17 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 8b795c771b..620c2bd49d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -14,6 +14,7 @@ 2.1.0-preview3-32110 2.1.0-preview3-32110 2.1.0-preview3-32110 + 2.1.0-preview3-32110 2.1.0-preview3-32110 2.1.0-preview3-32110 2.1.0-preview3-32110 diff --git a/samples/HostFilteringSample/Program.cs b/samples/HostFilteringSample/Program.cs index 0d4ffa9324..0ccc7cdb2e 100644 --- a/samples/HostFilteringSample/Program.cs +++ b/samples/HostFilteringSample/Program.cs @@ -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(); diff --git a/samples/HostFilteringSample/Startup.cs b/samples/HostFilteringSample/Startup.cs index 93c217b71c..283d8ee7f9 100644 --- a/samples/HostFilteringSample/Startup.cs +++ b/samples/HostFilteringSample/Startup.cs @@ -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>(); + }); + + // Fallback + services.PostConfigure(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>(new ConfigurationChangeTokenSource(Config)); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) diff --git a/samples/HostFilteringSample/appsettings.Development.json b/samples/HostFilteringSample/appsettings.Development.json index 7c2d8e26dc..b8d74ac7a2 100644 --- a/samples/HostFilteringSample/appsettings.Development.json +++ b/samples/HostFilteringSample/appsettings.Development.json @@ -1,3 +1,3 @@ { - "AllowedHosts": [ "localhost", "127.0.0.1", "[::1]" ] + "AllowedHosts": "localhost;127.0.0.1;[::1]" } diff --git a/samples/HostFilteringSample/appsettings.Production.json b/samples/HostFilteringSample/appsettings.Production.json index f2fc90c390..0c6fabda95 100644 --- a/samples/HostFilteringSample/appsettings.Production.json +++ b/samples/HostFilteringSample/appsettings.Production.json @@ -1,3 +1,3 @@ { - "AllowedHosts": [ "example.com", "localhost" ] + "AllowedHosts": "example.com;localhost" } diff --git a/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs index e1e38eca37..d355edd0fc 100644 --- a/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs +++ b/src/Microsoft.AspNetCore.HostFiltering/HostFilteringMiddleware.cs @@ -30,7 +30,8 @@ namespace Microsoft.AspNetCore.HostFiltering private readonly RequestDelegate _next; private readonly ILogger _logger; - private readonly HostFilteringOptions _options; + private readonly IOptionsMonitor _optionsMonitor; + private HostFilteringOptions _options; private IList _allowedHosts; private bool? _allowAnyNonEmptyHost; @@ -39,13 +40,21 @@ namespace Microsoft.AspNetCore.HostFiltering /// /// /// - /// + /// public HostFilteringMiddleware(RequestDelegate next, ILogger logger, - IOptions options) + IOptionsMonitor 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(); + _allowAnyNonEmptyHost = null; + }); } /// @@ -55,9 +64,9 @@ namespace Microsoft.AspNetCore.HostFiltering /// 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 EnsureConfigured() { if (_allowAnyNonEmptyHost == true || _allowedHosts?.Count > 0) { - return; + return _allowedHosts; } var allowedHosts = new List(); 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 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; diff --git a/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs b/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs index 2fc7b8c713..ab45bc9554 100644 --- a/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.HostFiltering.Tests/HostFilteringMiddlewareTests.cs @@ -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>(new ConfigurationChangeTokenSource(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(); + } + } } } diff --git a/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj b/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj index d71805c949..94262065fa 100644 --- a/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj +++ b/test/Microsoft.AspNetCore.HostFiltering.Tests/Microsoft.AspNetCore.HostFiltering.Tests.csproj @@ -6,6 +6,7 @@ +