diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln index d915ed20..c1231911 100644 --- a/BasicMiddleware.sln +++ b/BasicMiddleware.sln @@ -57,6 +57,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution version.xml = version.xml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpsPolicy", "src\Microsoft.AspNetCore.HttpsPolicy\Microsoft.AspNetCore.HttpsPolicy.csproj", "{4D39C29B-4EC8-497C-B411-922DA494D71B}" +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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +117,18 @@ Global {B2A3CE38-51B2-4486-982C-98C380AF140E}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2A3CE38-51B2-4486-982C-98C380AF140E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2A3CE38-51B2-4486-982C-98C380AF140E}.Release|Any CPU.Build.0 = Release|Any CPU + {4D39C29B-4EC8-497C-B411-922DA494D71B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D39C29B-4EC8-497C-B411-922DA494D71B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D39C29B-4EC8-497C-B411-922DA494D71B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D39C29B-4EC8-497C-B411-922DA494D71B}.Release|Any CPU.Build.0 = Release|Any CPU + {AC424AEE-4883-49C6-945F-2FC916B8CA1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC424AEE-4883-49C6-945F-2FC916B8CA1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC424AEE-4883-49C6-945F-2FC916B8CA1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC424AEE-4883-49C6-945F-2FC916B8CA1C}.Release|Any CPU.Build.0 = Release|Any CPU + {1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C67B0F1-6E70-449E-A2F1-98B9D5C576CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -128,6 +146,9 @@ Global {45308A9D-F4C6-46A8-A24F-E73D995CC223} = {A5076D28-FA7E-4606-9410-FEDD0D603527} {3360A5D1-70C0-49EE-9051-04A6A6B836DC} = {8437B0F3-3894-4828-A945-A9187F37631D} {B2A3CE38-51B2-4486-982C-98C380AF140E} = {9587FE9F-5A17-42C4-8021-E87F59CECB98} + {4D39C29B-4EC8-497C-B411-922DA494D71B} = {A5076D28-FA7E-4606-9410-FEDD0D603527} + {AC424AEE-4883-49C6-945F-2FC916B8CA1C} = {9587FE9F-5A17-42C4-8021-E87F59CECB98} + {1C67B0F1-6E70-449E-A2F1-98B9D5C576CE} = {8437B0F3-3894-4828-A945-A9187F37631D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4518E9CE-3680-4E05-9259-B64EA7807158} diff --git a/samples/HttpsPolicySample/HttpsPolicySample.csproj b/samples/HttpsPolicySample/HttpsPolicySample.csproj new file mode 100644 index 00000000..a9d5f11b --- /dev/null +++ b/samples/HttpsPolicySample/HttpsPolicySample.csproj @@ -0,0 +1,17 @@ + + + + net461;netcoreapp2.0 + netcoreapp2.0 + + + + + + + + + + + + diff --git a/samples/HttpsPolicySample/Properties/launchSettings.json b/samples/HttpsPolicySample/Properties/launchSettings.json new file mode 100644 index 00000000..fbffc1f4 --- /dev/null +++ b/samples/HttpsPolicySample/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:31894/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "HttpsSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:31895/" + } + } +} diff --git a/samples/HttpsPolicySample/Startup.cs b/samples/HttpsPolicySample/Startup.cs new file mode 100644 index 00000000..4fb7e628 --- /dev/null +++ b/samples/HttpsPolicySample/Startup.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.Extensions.DependencyInjection; + +namespace HttpsSample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddHttpsRedirection(options => + { + options.RedirectStatusCode = StatusCodes.Status301MovedPermanently; + options.TlsPort = 5001; + }); + + services.AddHsts(options => + { + options.MaxAge = TimeSpan.FromDays(30); + options.Preload = true; + options.IncludeSubDomains = true; + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment environment) + { + if (!environment.IsDevelopment()) + { + app.UseHsts(); + } + app.UseHttpsRedirection(); + + app.Run(async context => + { + await context.Response.WriteAsync("Hello world!"); + }); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel( + options => + { + options.Listen(new IPEndPoint(IPAddress.Loopback, 5001), listenOptions => + { + listenOptions.UseHttps("testCert.pfx", "testPassword"); + }); + options.Listen(new IPEndPoint(IPAddress.Loopback, 5000), listenOptions => + { + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) // for the cert file + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/HttpsPolicySample/testCert.pfx b/samples/HttpsPolicySample/testCert.pfx new file mode 100644 index 00000000..7118908c Binary files /dev/null and b/samples/HttpsPolicySample/testCert.pfx differ diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HstsBuilderExtensions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HstsBuilderExtensions.cs new file mode 100644 index 00000000..840593c5 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HstsBuilderExtensions.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for the HSTS middleware. + /// + public static class HstsBuilderExtensions + { + /// + /// Adds middleware for using HSTS, which adds the Strict-Transport-Security header. + /// + /// The instance this method extends. + public static IApplicationBuilder UseHsts(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs new file mode 100644 index 00000000..f67543c8 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HstsMiddleware.cs @@ -0,0 +1,68 @@ +// 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.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HttpsPolicy +{ + /// + /// Enables HTTP Strict Transport Security (HSTS) + /// See https://tools.ietf.org/html/rfc6797. + /// + public class HstsMiddleware + { + private const string IncludeSubDomains = "; includeSubDomains"; + private const string Preload = "; preload"; + + private readonly RequestDelegate _next; + private readonly StringValues _strictTransportSecurityValue; + + /// + /// Initialize the HSTS middleware. + /// + /// + /// + public HstsMiddleware(RequestDelegate next, IOptions options) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + + var hstsOptions = options.Value; + var maxAge = Convert.ToInt64(Math.Floor(hstsOptions.MaxAge.TotalSeconds)) + .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}"); + } + + /// + /// Invoke the middleware. + /// + /// The . + /// + public Task Invoke(HttpContext context) + { + if (context.Request.IsHttps) + { + context.Response.Headers[HeaderNames.StrictTransportSecurity] = _strictTransportSecurityValue; + } + + return _next(context); + } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs new file mode 100644 index 00000000..77d1d762 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HstsOptions.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Microsoft.AspNetCore.HttpsPolicy +{ + /// + /// Options for the Hsts Middleware + /// + public class HstsOptions + { + /// + /// Sets the max-age parameter of the Strict-Transport-Security header. + /// + /// + /// Max-age is required; defaults to 30 days. + /// See: https://tools.ietf.org/html/rfc6797#section-6.1.1 + /// + public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(30); + + /// + /// Enables includeSubDomain parameter of the Strict-Transport-Security header. + /// + /// + /// See: https://tools.ietf.org/html/rfc6797#section-6.1.2 + /// + public bool IncludeSubDomains { get; set; } + + /// + /// Sets the preload parameter of the Strict-Transport-Security header. + /// + /// + /// Preload is not part of the RFC specification, but is supported by web browsers + /// to preload HSTS sites on fresh install. See https://hstspreload.org/. + /// + public bool Preload { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HstsServicesExtensions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HstsServicesExtensions.cs new file mode 100644 index 00000000..425ec904 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HstsServicesExtensions.cs @@ -0,0 +1,36 @@ +// 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 Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for the HSTS middleware. + /// + public static class HstsServicesExtensions + { + /// + /// Adds HSTS services. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection AddHsts(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs new file mode 100644 index 00000000..89823a0e --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs @@ -0,0 +1,44 @@ +// 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 Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for the HttpsRedirection middleware. + /// + public static class HttpsPolicyBuilderExtensions + { + /// + /// Adds middleware for redirecting HTTP Requests to HTTPS. + /// + /// The instance this method extends. + /// The for HttpsRedirection. + /// + /// HTTPS Enforcement interanlly uses the UrlRewrite middleware to redirect HTTP requests to HTTPS. + /// + public static IApplicationBuilder UseHttpsRedirection(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + var options = app.ApplicationServices.GetRequiredService>().Value; + + var rewriteOptions = new RewriteOptions(); + rewriteOptions.AddRedirectToHttps( + options.RedirectStatusCode, + options.TlsPort); + + app.UseRewriter(rewriteOptions); + + return app; + } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs new file mode 100644 index 00000000..d73df4d5 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.HttpsPolicy +{ + /// + /// Options for the HttpsRedirection middleware + /// + public class HttpsRedirectionOptions + { + /// + /// The status code to redirect the response to. + /// + public int RedirectStatusCode { get; set; } = StatusCodes.Status301MovedPermanently; + + /// + /// The TLS port to be added to the redirected URL. + /// + /// + /// Defaults to 443 if not provided. + /// + public int? TlsPort { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionServicesExtensions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionServicesExtensions.cs new file mode 100644 index 00000000..cdc6f005 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionServicesExtensions.cs @@ -0,0 +1,36 @@ +// 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 Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for the HttpsRedirection middleware. + /// + public static class HttpsRedirectionServicesExtensions + { + /// + /// Adds HTTPS redirection services. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection AddHttpsRedirection(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj b/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj new file mode 100644 index 00000000..b68d33b7 --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj @@ -0,0 +1,16 @@ + + + + + ASP.NET Core basic middleware for supporting HTTPS Redirection and HTTP Strict-Transport-Security. + + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;https;hsts + + + + + + diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HstsMiddlewareTests.cs b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HstsMiddlewareTests.cs new file mode 100644 index 00000000..e6d32576 --- /dev/null +++ b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HstsMiddlewareTests.cs @@ -0,0 +1,128 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.HttpsPolicy.Tests +{ + public class HstsMiddlewareTests + { + [Fact] + public async Task SetOptionsWithDefault_SetsMaxAgeToCorrectValue() + { + var builder = new WebHostBuilder() + .UseUrls("https://*:5050") + .ConfigureServices(services => + { + }) + .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://localhost:5050"); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("max-age=2592000", response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault()); + } + + [Theory] + [InlineData(0, false, false, "max-age=0")] + [InlineData(-1, false, false, "max-age=-1")] + [InlineData(0, true, false, "max-age=0; includeSubDomains")] + [InlineData(50000, false, true, "max-age=50000; preload")] + [InlineData(0, true, true, "max-age=0; includeSubDomains; preload")] + [InlineData(50000, true, true, "max-age=50000; includeSubDomains; preload")] + public async Task SetOptionsThroughConfigure_SetsHeaderCorrectly(int maxAge, bool includeSubDomains, bool preload, string expected) + { + var builder = new WebHostBuilder() + .UseUrls("https://*:5050") + .ConfigureServices(services => + { + services.Configure(options => { + options.Preload = preload; + options.IncludeSubDomains = includeSubDomains; + options.MaxAge = TimeSpan.FromSeconds(maxAge); + }); + }) + .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://localhost:5050"); + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault()); + } + + [Theory] + [InlineData(0, false, false, "max-age=0")] + [InlineData(-1, false, false, "max-age=-1")] + [InlineData(0, true, false, "max-age=0; includeSubDomains")] + [InlineData(50000, false, true, "max-age=50000; preload")] + [InlineData(0, true, true, "max-age=0; includeSubDomains; preload")] + [InlineData(50000, true, true, "max-age=50000; includeSubDomains; preload")] + public async Task SetOptionsThroughHelper_SetsHeaderCorrectly(int maxAge, bool includeSubDomains, bool preload, string expected) + { + var builder = new WebHostBuilder() + .UseUrls("https://*:5050") + .ConfigureServices(services => + { + services.AddHsts(options => { + options.Preload = preload; + options.IncludeSubDomains = includeSubDomains; + options.MaxAge = TimeSpan.FromSeconds(maxAge); + }); + }) + .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://localhost:5050"); + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault()); + } + } +} diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsPolicyTests.cs b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsPolicyTests.cs new file mode 100644 index 00000000..58a0be13 --- /dev/null +++ b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsPolicyTests.cs @@ -0,0 +1,76 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.HttpsPolicy.Tests +{ + public class HttpsPolicyTests + { + [Theory] + [InlineData(302, null, 2592000, false, false, "max-age=2592000", "https://localhost/")] + [InlineData(301, 5050, 2592000, false, false, "max-age=2592000", "https://localhost:5050/")] + [InlineData(301, 443, 2592000, false, false, "max-age=2592000", "https://localhost/")] + [InlineData(301, 443, 2592000, true, false, "max-age=2592000; includeSubDomains", "https://localhost/")] + [InlineData(301, 443, 2592000, false, true, "max-age=2592000; preload", "https://localhost/")] + [InlineData(301, null, 2592000, true, true, "max-age=2592000; includeSubDomains; preload", "https://localhost/")] + [InlineData(302, 5050, 2592000, true, true, "max-age=2592000; includeSubDomains; preload", "https://localhost:5050/")] + public async Task SetsBothHstsAndHttpsRedirection_RedirectOnFirstRequest_HstsOnSecondRequest(int statusCode, int? tlsPort, int maxAge, bool includeSubDomains, bool preload, string expectedHstsHeader, string expectedUrl) + { + + var builder = new WebHostBuilder() + .UseUrls("https://*:5050", "http://*:5050") + .ConfigureServices(services => + { + services.Configure(options => + { + options.RedirectStatusCode = statusCode; + options.TlsPort = tlsPort; + }); + services.Configure(options => + { + options.IncludeSubDomains = includeSubDomains; + options.MaxAge = TimeSpan.FromSeconds(maxAge); + options.Preload = preload; + }); + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.UseHsts(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(statusCode, (int)response.StatusCode); + Assert.Equal(expectedUrl, response.Headers.Location.ToString()); + + client = server.CreateClient(); + client.BaseAddress = new Uri(response.Headers.Location.ToString()); + request = new HttpRequestMessage(HttpMethod.Get, ""); + response = await client.SendAsync(request); + + Assert.Equal(expectedHstsHeader, response.Headers.GetValues(HeaderNames.StrictTransportSecurity).FirstOrDefault()); + } + } +} diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsRedirectionMiddlewareTests.cs b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsRedirectionMiddlewareTests.cs new file mode 100644 index 00000000..1e77683b --- /dev/null +++ b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/HttpsRedirectionMiddlewareTests.cs @@ -0,0 +1,125 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.HttpsPolicy.Tests +{ + public class HttpsRedirectionMiddlewareTests + { + [Fact] + public async Task SetOptions_DefaultsSetCorrectly() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode); + Assert.Equal("https://localhost/", response.Headers.Location.ToString()); + } + + [Theory] + [InlineData(301, null, "https://localhost/")] + [InlineData(302, null, "https://localhost/")] + [InlineData(307, null, "https://localhost/")] + [InlineData(308, null, "https://localhost/")] + [InlineData(301, 5050, "https://localhost:5050/")] + [InlineData(301, 443, "https://localhost/")] + public async Task SetOptions_SetStatusCodeTlsPort(int statusCode, int? tlsPort, string expected) + { + + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + options.RedirectStatusCode = statusCode; + options.TlsPort = tlsPort; + }); + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(statusCode, (int)response.StatusCode); + Assert.Equal(expected, response.Headers.Location.ToString()); + } + + [Theory] + [InlineData(301, null, "https://localhost/")] + [InlineData(302, null, "https://localhost/")] + [InlineData(307, null, "https://localhost/")] + [InlineData(308, null, "https://localhost/")] + [InlineData(301, 5050, "https://localhost:5050/")] + [InlineData(301, 443, "https://localhost/")] + public async Task SetOptionsThroughHelperMethod_SetStatusCodeTlsPort(int statusCode, int? tlsPort, string expectedUrl) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHttpsRedirection(options => + { + options.RedirectStatusCode = statusCode; + options.TlsPort = tlsPort; + }); + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var server = new TestServer(builder); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal(statusCode, (int)response.StatusCode); + Assert.Equal(expectedUrl, response.Headers.Location.ToString()); + } + } +} diff --git a/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj new file mode 100644 index 00000000..acdd245d --- /dev/null +++ b/test/Microsoft.AspNetCore.HttpsEnforcement.Tests/Microsoft.AspNetCore.HttpsPolicy.Tests.csproj @@ -0,0 +1,10 @@ + + + + netcoreapp2.0 + + + + + +