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