diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index 6e00e85149..d1703527f2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -127,5 +127,11 @@ public int MaxModelValidationErrors /// Gets a list of used by this application. /// public IList ValueProviderFactories { get; } + + /// + /// Gets or sets the SSL port that is used by this application when + /// is used. If not set the port won't be specified in the secured URL e.g. https://localhost/path. + /// + public int? SslPort { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/RequireHttpsAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/RequireHttpsAttribute.cs index 003ecfaa54..0cd41aae71 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/RequireHttpsAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/RequireHttpsAttribute.cs @@ -4,6 +4,8 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc { @@ -35,10 +37,25 @@ protected virtual void HandleNonHttpsRequest(AuthorizationFilterContext filterCo } else { + var optionsAccessor = filterContext.HttpContext.RequestServices.GetRequiredService>(); + var request = filterContext.HttpContext.Request; + + var host = request.Host; + if (optionsAccessor.Value.SslPort.HasValue && optionsAccessor.Value.SslPort > 0) + { + // a specific SSL port is specified + host = new HostString(host.Host, optionsAccessor.Value.SslPort.Value); + } + else + { + // clear the port + host = new HostString(host.Host); + } + var newUrl = string.Concat( "https://", - request.Host.ToUriComponent(), + host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent()); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RequireHttpsAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RequireHttpsAttributeTests.cs index 41c16d7fc2..d571565e48 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RequireHttpsAttributeTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RequireHttpsAttributeTests.cs @@ -1,11 +1,14 @@ // 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.Http; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.Mvc @@ -37,7 +40,7 @@ public static TheoryData RedirectToHttpE return new TheoryData { { "localhost", null, null, null, "https://localhost" }, - { "localhost:5000", null, null, null, "https://localhost:5000" }, + { "localhost:5000", null, null, null, "https://localhost" }, { "localhost", "/pathbase", null, null, "https://localhost/pathbase" }, { "localhost", "/pathbase", "/path", null, "https://localhost/pathbase/path" }, { "localhost", "/pathbase", "/path", "?foo=bar", "https://localhost/pathbase/path?foo=bar" }, @@ -67,6 +70,7 @@ public void OnAuthorization_RedirectsToHttpsEndpoint_ForNonHttpsGetRequests( { // Arrange var requestContext = new DefaultHttpContext(); + requestContext.RequestServices = CreateServices(); requestContext.Request.Scheme = "http"; requestContext.Request.Method = "GET"; requestContext.Request.Host = HostString.FromUriComponent(host); @@ -109,6 +113,7 @@ public void OnAuthorization_SignalsBadRequestStatusCode_ForNonHttpsAndNonGetRequ { // Arrange var requestContext = new DefaultHttpContext(); + requestContext.RequestServices = CreateServices(); requestContext.Request.Scheme = "http"; requestContext.Request.Method = method; var authContext = CreateAuthorizationContext(requestContext); @@ -128,6 +133,7 @@ public void HandleNonHttpsRequestExtensibility() { // Arrange var requestContext = new DefaultHttpContext(); + requestContext.RequestServices = CreateServices(); requestContext.Request.Scheme = "http"; var authContext = CreateAuthorizationContext(requestContext); @@ -141,6 +147,51 @@ public void HandleNonHttpsRequestExtensibility() Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); } + [Theory] + [InlineData("http://localhost", null, "https://localhost/")] + [InlineData("http://localhost:5000", null, "https://localhost/")] + [InlineData("http://[2001:db8:a0b:12f0::1]", null, "https://[2001:db8:a0b:12f0::1]/")] + [InlineData("http://[2001:db8:a0b:12f0::1]:5000", null, "https://[2001:db8:a0b:12f0::1]/")] + [InlineData("http://localhost:5000/path", null, "https://localhost/path")] + [InlineData("http://localhost:5000/path?foo=bar", null, "https://localhost/path?foo=bar")] + [InlineData("http://本地主機:5000", null, "https://xn--tiq21tzznx7c/")] + [InlineData("http://localhost", 44380, "https://localhost:44380/")] + [InlineData("http://localhost:5000", 44380, "https://localhost:44380/")] + [InlineData("http://[2001:db8:a0b:12f0::1]", 44380, "https://[2001:db8:a0b:12f0::1]:44380/")] + [InlineData("http://[2001:db8:a0b:12f0::1]:5000", 44380, "https://[2001:db8:a0b:12f0::1]:44380/")] + [InlineData("http://localhost:5000/path", 44380, "https://localhost:44380/path")] + [InlineData("http://localhost:5000/path?foo=bar", 44380, "https://localhost:44380/path?foo=bar")] + [InlineData("http://本地主機:5000", 44380, "https://xn--tiq21tzznx7c:44380/")] + public void OnAuthorization_RedirectsToHttpsEndpoint_ForCustomSslPort( + string url, + int? sslPort, + string expectedUrl) + { + // Arrange + var options = new TestOptionsManager(); + var uri = new Uri(url); + + var requestContext = new DefaultHttpContext(); + requestContext.RequestServices = CreateServices(sslPort); + requestContext.Request.Scheme = "http"; + requestContext.Request.Method = "GET"; + requestContext.Request.Host = HostString.FromUriComponent(uri); + requestContext.Request.Path = PathString.FromUriComponent(uri); + requestContext.Request.QueryString = QueryString.FromUriComponent(uri); + + var authContext = CreateAuthorizationContext(requestContext); + var attr = new RequireHttpsAttribute(); + + // Act + attr.OnAuthorization(authContext); + + // Assert + Assert.NotNull(authContext.Result); + var result = Assert.IsType(authContext.Result); + + Assert.Equal(expectedUrl, result.Url); + } + private class CustomRequireHttpsAttribute : RequireHttpsAttribute { protected override void HandleNonHttpsRequest(AuthorizationFilterContext filterContext) @@ -154,5 +205,16 @@ private static AuthorizationFilterContext CreateAuthorizationContext(HttpContext var actionContext = new ActionContext(ctx, new RouteData(), new ActionDescriptor()); return new AuthorizationFilterContext(actionContext, new IFilterMetadata[0]); } + + private static IServiceProvider CreateServices(int? sslPort = null) + { + var options = new TestOptionsManager(); + options.Value.SslPort = sslPort; + + var services = new ServiceCollection(); + services.AddSingleton>(options); + + return services.BuildServiceProvider(); + } } } \ No newline at end of file