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
+
+
+
+
+
+