diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln index c123191126..a4452cd103 100644 --- a/BasicMiddleware.sln +++ b/BasicMiddleware.sln @@ -61,7 +61,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsP EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpsPolicySample", "samples\HttpsPolicySample\HttpsPolicySample.csproj", "{AC424AEE-4883-49C6-945F-2FC916B8CA1C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "test\Microsoft.AspNetCore.HttpsEnforcement.Tests\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy.Tests", "test\Microsoft.AspNetCore.HttpsPolicy.Tests\Microsoft.AspNetCore.HttpsPolicy.Tests.csproj", "{1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs index f67543c880..252ae44c1e 100644 --- a/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs @@ -2,6 +2,7 @@ // 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.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy private readonly RequestDelegate _next; private readonly StringValues _strictTransportSecurityValue; + private readonly IList _excludedHosts; /// /// Initialize the HSTS middleware. @@ -30,24 +32,20 @@ namespace Microsoft.AspNetCore.HttpsPolicy /// public HstsMiddleware(RequestDelegate next, IOptions options) { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - if (options == null) { throw new ArgumentNullException(nameof(options)); } - _next = next; + _next = next ?? throw new ArgumentNullException(nameof(next)); var hstsOptions = options.Value; var maxAge = Convert.ToInt64(Math.Floor(hstsOptions.MaxAge.TotalSeconds)) - .ToString(CultureInfo.InvariantCulture); + .ToString(CultureInfo.InvariantCulture); var includeSubdomains = hstsOptions.IncludeSubDomains ? IncludeSubDomains : StringSegment.Empty; var preload = hstsOptions.Preload ? Preload : StringSegment.Empty; _strictTransportSecurityValue = new StringValues($"max-age={maxAge}{includeSubdomains}{preload}"); + _excludedHosts = hstsOptions.ExcludedHosts; } /// @@ -57,12 +55,29 @@ namespace Microsoft.AspNetCore.HttpsPolicy /// public Task Invoke(HttpContext context) { - if (context.Request.IsHttps) + if (context.Request.IsHttps && !IsHostExcluded(context.Request.Host.Host)) { context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue; } - return _next(context); + return _next(context); + } + + private bool IsHostExcluded(string host) + { + if (_excludedHosts == null) + { + return false; + } + + for (var i = 0; i < _excludedHosts.Count; i++) + { + if (string.Equals(host, _excludedHosts[i], StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } } } diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs index 77d1d76254..92803963eb 100644 --- a/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.HttpsPolicy { @@ -35,5 +36,15 @@ namespace Microsoft.AspNetCore.HttpsPolicy /// to preload HSTS sites on fresh install. See https://hstspreload.org/. /// public bool Preload { get; set; } + + /// + /// A list of host names that will not add the HSTS header. + /// + public IList ExcludedHosts { get; } = new List + { + "localhost", + "127.0.0.1", // ipv4 + "[::1]" // ipv6 + }; } } diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HstsMiddlewareTests.cs b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HstsMiddlewareTests.cs similarity index 57% rename from test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HstsMiddlewareTests.cs rename to test/Microsoft.AspNetCore.HttpsPolicy.Tests/HstsMiddlewareTests.cs index 64917f3e74..08df78f7c2 100644 --- a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HstsMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HstsMiddlewareTests.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Net; using System.Net.Http; @@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests var server = new TestServer(builder); var client = server.CreateClient(); - client.BaseAddress = new Uri("https://localhost:5050"); + client.BaseAddress = new Uri("https://example.com:5050"); var request = new HttpRequestMessage(HttpMethod.Get, ""); @@ -75,7 +76,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests var server = new TestServer(builder); var client = server.CreateClient(); - client.BaseAddress = new Uri("https://localhost:5050"); + client.BaseAddress = new Uri("https://example.com:5050"); var request = new HttpRequestMessage(HttpMethod.Get, ""); var response = await client.SendAsync(request); @@ -113,7 +114,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests var server = new TestServer(builder); var client = server.CreateClient(); - client.BaseAddress = new Uri("https://localhost:5050"); + client.BaseAddress = new Uri("https://example.com:5050"); var request = new HttpRequestMessage(HttpMethod.Get, ""); var response = await client.SendAsync(request); @@ -121,5 +122,98 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault()); } + + [Theory] + [InlineData("localhost")] + [InlineData("Localhost")] + [InlineData("LOCALHOST")] + [InlineData("127.0.0.1")] + [InlineData("[::1]")] + public async Task DefaultExcludesCommonLocalhostDomains_DoesNotSetHstsHeader(string host) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHsts(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + client.BaseAddress = new Uri($"https://{host}:5050"); + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + } + + [Theory] + [InlineData("localhost")] + [InlineData("127.0.0.1")] + [InlineData("[::1]")] + public async Task AllowLocalhostDomainsIfListIsReset_SetHstsHeader(string host) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHsts(options => + { + options.ExcludedHosts.Clear(); + }); + }) + .Configure(app => + { + app.UseHsts(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + client.BaseAddress = new Uri($"https://{host}:5050"); + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Single(response.Headers); + } + + [Theory] + [InlineData("example.com")] + [InlineData("Example.com")] + [InlineData("EXAMPLE.COM")] + public async Task AddExcludedDomains_DoesNotAddHstsHeader(string host) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHsts(options => { + options.ExcludedHosts.Add(host); + }); + }) + .Configure(app => + { + app.UseHsts(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + var server = new TestServer(builder); + var client = server.CreateClient(); + client.BaseAddress = new Uri($"https://{host}:5050"); + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + } } } diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsPolicyTests.cs b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs similarity index 97% rename from test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsPolicyTests.cs rename to test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs index 53fd91dde5..49a3d0a29a 100644 --- a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsPolicyTests.cs +++ b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs @@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests options.IncludeSubDomains = includeSubDomains; options.MaxAge = TimeSpan.FromSeconds(maxAge); options.Preload = preload; + options.ExcludedHosts.Clear(); // allowing localhost for testing }); }) .Configure(app => diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsRedirectionMiddlewareTests.cs b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsRedirectionMiddlewareTests.cs rename to test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj similarity index 100% rename from test/Microsoft.AspNetCore.HttpsEnforcement.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj rename to test/Microsoft.AspNetCore.HttpsPolicy.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj