diff --git a/build/dependencies.props b/build/dependencies.props index 229023f5d0..853f6f2f0e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,23 +3,25 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15626 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 + 2.1.0-preview1-15638 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 + 2.1.0-preview1-27855 2.0.0 2.1.0-preview1-26016-05 - 2.1.0-preview1-27849 + 2.1.0-preview1-27855 15.3.0 4.7.49 0.8.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 8d52a6128c..2e540bdffd 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15626 -commithash:fd6410e9c90c428bc01238372303ad09cb9ec889 +version:2.1.0-preview1-15638 +commithash:1d3a0c725dc6b8ae6b0e47800fd6b4d8f8b8d545 diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs index d6b827caab..600e29fead 100644 --- a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionBuilderExtensions.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -32,27 +32,7 @@ namespace Microsoft.AspNetCore.Builder var options = app.ApplicationServices.GetRequiredService>().Value; - // The tls port set in options will have priority over the one in configuration. - var httpsPort = options.HttpsPort; - if (httpsPort == null) - { - // Only read configuration if there is no httpsPort - var config = app.ApplicationServices.GetRequiredService(); - var configHttpsPort = config["HTTPS_PORT"]; - // If the string isn't empty, try to parse it. - if (!string.IsNullOrEmpty(configHttpsPort) - && int.TryParse(configHttpsPort, out var intHttpsPort)) - { - httpsPort = intHttpsPort; - } - } - - var rewriteOptions = new RewriteOptions(); - rewriteOptions.AddRedirectToHttps( - options.RedirectStatusCode, - httpsPort); - - app.UseRewriter(rewriteOptions); + app.UseMiddleware(app.ServerFeatures.Get()); return app; } diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionMiddleware.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionMiddleware.cs new file mode 100644 index 0000000000..f500bbb2bc --- /dev/null +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionMiddleware.cs @@ -0,0 +1,124 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.HttpsPolicy +{ + public class HttpsRedirectionMiddleware + { + private readonly RequestDelegate _next; + private int? _httpsPort; + private readonly int _statusCode; + + private readonly IServerAddressesFeature _serverAddressesFeature; + private readonly IConfiguration _config; + + /// + /// Initializes the HttpsRedirectionMiddleware + /// + /// + /// The + /// + /// + public HttpsRedirectionMiddleware(RequestDelegate next, IServerAddressesFeature serverAddressesFeature, IOptions options, IConfiguration config) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + _config = config ?? throw new ArgumentException(nameof(config)); + _next = next ?? throw new ArgumentNullException(nameof(next)); + _serverAddressesFeature = serverAddressesFeature ?? throw new ArgumentNullException(nameof(serverAddressesFeature)); + + var httpsRedirectionOptions = options.Value; + _httpsPort = httpsRedirectionOptions.HttpsPort; + _statusCode = httpsRedirectionOptions.RedirectStatusCode; + } + + /// + /// Invokes the HttpsRedirectionMiddleware + /// + /// + /// + public Task Invoke(HttpContext context) + { + if (context.Request.IsHttps) + { + return _next(context); + } + + if (!_httpsPort.HasValue) + { + CheckForHttpsPorts(); + } + + var host = context.Request.Host; + if (_httpsPort != 443) + { + host = new HostString(host.Host, _httpsPort.Value); + } + else + { + host = new HostString(host.Host); + } + + var request = context.Request; + var redirectUrl = UriHelper.BuildAbsolute( + "https", + host, + request.PathBase, + request.Path, + request.QueryString); + + context.Response.StatusCode = _statusCode; + context.Response.Headers[HeaderNames.Location] = redirectUrl; + + return Task.CompletedTask; + } + + private void CheckForHttpsPorts() + { + // The IServerAddressesFeature will not be ready until the middleware is Invoked, + // Order for finding the HTTPS port: + // 1. Set in the HttpsRedirectionOptions + // 2. HTTPS_PORT environment variable + // 3. IServerAddressesFeature + // 4. 443 (or not set) + + _httpsPort = _config.GetValue("HTTPS_PORT"); + if (_httpsPort.HasValue) + { + return; + } + + int? httpsPort = null; + foreach (var address in _serverAddressesFeature.Addresses) + { + var bindingAddress = BindingAddress.Parse(address); + if (bindingAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + // If we find multiple different https ports specified, throw + if (httpsPort.HasValue && httpsPort != bindingAddress.Port) + { + throw new ArgumentException("Cannot determine the https port from IServerAddressesFeature, multiple values were found. " + + "Please set the desired port explicitly on HttpsRedirectionOptions.HttpsPort."); + } + else + { + httpsPort = bindingAddress.Port; + } + } + } + _httpsPort = httpsPort ?? 443; + } + } +} diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs index 238f2eb484..52f2b09def 100644 --- a/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs +++ b/src/Microsoft.AspNetCore.HttpsPolicy/HttpsRedirectionOptions.cs @@ -16,10 +16,13 @@ namespace Microsoft.AspNetCore.HttpsPolicy public int RedirectStatusCode { get; set; } = StatusCodes.Status302Found; /// - /// The TLS port to be added to the redirected URL. + /// The HTTPS port to be added to the redirected URL. /// /// - /// Defaults to 443 if not provided. + /// If the HttpsPort is not set, we will try to get the HttpsPort from the following: + /// 1. HTTPS_PORT environment variable + /// 2. IServerAddressesFeature + /// 3. 443 (or not set) /// public int? HttpsPort { get; set; } } diff --git a/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj b/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj index b68d33b7dc..ee7e611a47 100644 --- a/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj +++ b/src/Microsoft.AspNetCore.HttpsPolicy/Microsoft.AspNetCore.HttpsPolicy.csproj @@ -11,6 +11,10 @@ - + + + + + diff --git a/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs index 49a3d0a29a..aa874fb8cd 100644 --- a/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs +++ b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsPolicyTests.cs @@ -8,7 +8,9 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; @@ -55,7 +57,9 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests }); }); - var server = new TestServer(builder); + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); var client = server.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, ""); diff --git a/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs index aabba1324e..14f7c5444f 100644 --- a/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.HttpsPolicy.Tests/HttpsRedirectionMiddlewareTests.cs @@ -8,7 +8,9 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; @@ -34,7 +36,9 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests }); }); - var server = new TestServer(builder); + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); var client = server.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, ""); @@ -73,7 +77,9 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests }); }); - var server = new TestServer(builder); + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); var client = server.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, ""); @@ -111,7 +117,9 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests }); }); - var server = new TestServer(builder); + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); var client = server.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, ""); @@ -123,13 +131,17 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests } [Theory] - [InlineData(null, null, "https://localhost/")] - [InlineData(null, "5000", "https://localhost:5000/")] - [InlineData(null, "443", "https://localhost/")] - [InlineData(443, "5000", "https://localhost/")] - [InlineData(4000, "5000", "https://localhost:4000/")] - [InlineData(5000, null, "https://localhost:5000/")] - public async Task SetHttpsPortEnvironmentVariable_ReturnsCorrectStatusCodeOnResponse(int? optionsHttpsPort, string configHttpsPort, string expectedUrl) + [InlineData(null, null, null, "https://localhost/")] + [InlineData(null, null, "https://localhost:4444/", "https://localhost:4444/")] + [InlineData(null, null, "https://localhost:443/", "https://localhost/")] + [InlineData(null, null, "http://localhost:5044/", "https://localhost/")] + [InlineData(null, null, "https://localhost/", "https://localhost/")] + [InlineData(null, "5000", "https://localhost:4444/", "https://localhost:5000/")] + [InlineData(null, "443", "https://localhost:4444/", "https://localhost/")] + [InlineData(443, "5000", "https://localhost:4444/", "https://localhost/")] + [InlineData(4000, "5000", "https://localhost:4444/", "https://localhost:4000/")] + [InlineData(5000, null, "https://localhost:4444/", "https://localhost:5000/")] + public async Task SetHttpsPortEnvironmentVariableAndServerFeature_ReturnsCorrectStatusCodeOnResponse(int? optionsHttpsPort, string configHttpsPort, string serverAddressFeatureUrl, string expectedUrl) { var builder = new WebHostBuilder() .ConfigureServices(services => @@ -147,8 +159,18 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests return context.Response.WriteAsync("Hello world"); }); }); + builder.UseSetting("HTTPS_PORT", configHttpsPort); - var server = new TestServer(builder); + + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + + var server = new TestServer(builder, featureCollection); + if (serverAddressFeatureUrl != null) + { + server.Features.Get().Addresses.Add(serverAddressFeatureUrl); + } + var client = server.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, ""); @@ -157,5 +179,106 @@ namespace Microsoft.AspNetCore.HttpsPolicy.Tests Assert.Equal(expectedUrl, response.Headers.Location.ToString()); } + + [Fact] + public async Task SetServerAddressesFeature_SingleHttpsAddress_Success() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHttpsRedirection(options => + { + }); + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); + + server.Features.Get().Addresses.Add("https://localhost:5050"); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal("https://localhost:5050/", response.Headers.Location.ToString()); + } + + [Fact] + public async Task SetServerAddressesFeature_MultipleHttpsAddresses_ThrowInMiddleware() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHttpsRedirection(options => + { + }); + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); + + server.Features.Get().Addresses.Add("https://localhost:5050"); + server.Features.Get().Addresses.Add("https://localhost:5051"); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + await Assert.ThrowsAsync(async () => await client.SendAsync(request)); + } + + [Fact] + public async Task SetServerAddressesFeature_MultipleHttpsAddressesWithSamePort_Success() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHttpsRedirection(options => + { + }); + }) + .Configure(app => + { + app.UseHttpsRedirection(); + app.Run(context => + { + return context.Response.WriteAsync("Hello world"); + }); + }); + + var featureCollection = new FeatureCollection(); + featureCollection.Set(new ServerAddressesFeature()); + var server = new TestServer(builder, featureCollection); + + server.Features.Get().Addresses.Add("https://localhost:5050"); + server.Features.Get().Addresses.Add("https://localhost:5050"); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, ""); + + var response = await client.SendAsync(request); + + Assert.Equal("https://localhost:5050/", response.Headers.Location.ToString()); + } } }