[Fixes #911] RequireHttpsAttribute does not exist in MVC 6

1. GET requests will be redirected to the equivalent HTTPS url.
2. Requests with any other http method will fail with a 400.
This commit is contained in:
jacalvar 2014-09-11 18:21:39 -07:00
Родитель 18400481b5
Коммит 25838cee55
4 изменённых файлов: 272 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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;
namespace Microsoft.AspNet.Mvc
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class RequireHttpsAttribute :
Attribute, IAuthorizationFilter, IOrderedFilter
{
public int Order { get; set; }
public virtual void OnAuthorization([NotNull]AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.Request.IsSecure)
{
HandleNonHttpsRequest(filterContext);
}
}
protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
// only redirect for GET requests, otherwise the browser might not propagate the verb and request
// body correctly.
if (!string.Equals(filterContext.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
filterContext.Result = new HttpStatusCodeResult(403);
}
else
{
var request = filterContext.HttpContext.Request;
var newUrl = string.Concat(
"https://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent(),
request.Path.ToUriComponent(),
request.QueryString.ToUriComponent());
// redirect to HTTPS version of page
filterContext.Result = new RedirectResult(newUrl, permanent: true);
}
}
}
}

Просмотреть файл

@ -0,0 +1,160 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Moq;
using Xunit;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc
{
public class RequireHttpsAttributeTests
{
[Fact]
public void OnAuthorization_AllowsTheRequestIfItIsSecure()
{
// Arrange
var requestContext = new DefaultHttpContext();
requestContext.Request.Scheme = "https";
var authContext = CreateAuthorizationContext(requestContext);
var attr = new RequireHttpsAttribute();
// Act
attr.OnAuthorization(authContext);
// Assert
Assert.Null(authContext.Result);
}
public static IEnumerable<object[]> RedirectToHttpEndpointTestData
{
get
{
// host, pathbase, path, query, expectedRedirectUrl
var data = new TheoryData<string, string, string, string, string>();
data.Add("localhost", null, null, null, "https://localhost");
data.Add("localhost:5000", null, null, null, "https://localhost:5000");
data.Add("localhost", "/pathbase", null, null, "https://localhost/pathbase");
data.Add("localhost", "/pathbase", "/path", null, "https://localhost/pathbase/path");
data.Add("localhost", "/pathbase", "/path", "?foo=bar", "https://localhost/pathbase/path?foo=bar");
// Encode some special characters on the url.
data.Add("localhost", "/path?base", null, null, "https://localhost/path%3Fbase");
data.Add("localhost", null, "/pa?th", null, "https://localhost/pa%3Fth");
data.Add("localhost", "/", null, "?foo=bar%2Fbaz", "https://localhost/?foo=bar%2Fbaz");
// Urls with punycode
// 本地主機 is "localhost" in chinese traditional, "xn--tiq21tzznx7c" is the
// punycode representation.
data.Add("本地主機", "/", null, null, "https://xn--tiq21tzznx7c/");
return data;
}
}
[Theory]
[MemberData(nameof(RedirectToHttpEndpointTestData))]
public void OnAuthorization_RedirectsToHttpsEndpoint_ForNonHttpsGetRequests(
string host,
string pathBase,
string path,
string queryString,
string expectedUrl)
{
// Arrange
var requestContext = new DefaultHttpContext();
requestContext.Request.Scheme = "http";
requestContext.Request.Method = "GET";
requestContext.Request.Host = HostString.FromUriComponent(host);
if (pathBase != null)
{
requestContext.Request.PathBase = new PathString(pathBase);
}
if (path != null)
{
requestContext.Request.Path = new PathString(path);
}
if (queryString != null)
{
requestContext.Request.QueryString = new QueryString(queryString);
}
var authContext = CreateAuthorizationContext(requestContext);
var attr = new RequireHttpsAttribute();
// Act
attr.OnAuthorization(authContext);
// Assert
Assert.NotNull(authContext.Result);
var result = Assert.IsType<RedirectResult>(authContext.Result);
Assert.True(result.Permanent);
Assert.Equal(expectedUrl, result.Url);
}
[Theory]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
public void OnAuthorization_SignalsBadRequestStatusCode_ForNonHttpsAndNonGetRequests(string method)
{
// Arrange
var requestContext = new DefaultHttpContext();
requestContext.Request.Scheme = "http";
requestContext.Request.Method = method;
var authContext = CreateAuthorizationContext(requestContext);
var attr = new RequireHttpsAttribute();
// Act
attr.OnAuthorization(authContext);
// Assert
Assert.NotNull(authContext.Result);
var result = Assert.IsType<HttpStatusCodeResult>(authContext.Result);
Assert.Equal(403, result.StatusCode);
}
[Fact]
public void HandleNonHttpsRequestExtensibility()
{
// Arrange
var requestContext = new DefaultHttpContext();
requestContext.Request.Scheme = "http";
var authContext = CreateAuthorizationContext(requestContext);
var attr = new CustomRequireHttpsAttribute();
// Act
attr.OnAuthorization(authContext);
// Assert
var result = Assert.IsType<HttpStatusCodeResult>(authContext.Result);
Assert.Equal(404, result.StatusCode);
}
private class CustomRequireHttpsAttribute : RequireHttpsAttribute
{
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
filterContext.Result = new HttpStatusCodeResult(404);
}
}
private static AuthorizationContext CreateAuthorizationContext(HttpContext ctx)
{
var actionContext = new ActionContext(ctx, new RouteData(), actionDescriptor: null);
return new AuthorizationContext(actionContext, Enumerable.Empty<IFilter>().ToList());
}
}
}

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
@ -125,5 +126,63 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedContent, responseContent);
}
}
[Fact]
public async Task ActionWithRequireHttps_RedirectsToSecureUrl_ForNonHttpsGetRequests()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/Home/HttpsOnlyAction");
// Assert
Assert.Equal(HttpStatusCode.MovedPermanently, response.StatusCode);
Assert.NotNull(response.Headers.Location);
Assert.Equal("https://localhost/Home/HttpsOnlyAction", response.Headers.Location.ToString());
Assert.Equal(0, response.Content.Headers.ContentLength);
var responseBytes = await response.Content.ReadAsByteArrayAsync();
Assert.Equal(0, responseBytes.Length);
}
[Fact]
public async Task ActionWithRequireHttps_ReturnsBadRequestResponse_ForNonHttpsNonGetRequests()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.SendAsync(new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/Home/HttpsOnlyAction"));
// Assert
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
Assert.Equal(0, response.Content.Headers.ContentLength);
var responseBytes = await response.Content.ReadAsByteArrayAsync();
Assert.Equal(0, responseBytes.Length);
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
public async Task ActionWithRequireHttps_AllowsHttpsRequests(string method)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = new HttpClient(server.CreateHandler(), false);
// Act
var response = await client.SendAsync(new HttpRequestMessage(
new HttpMethod(method),
"https://localhost/Home/HttpsOnlyAction"));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
}

Просмотреть файл

@ -24,6 +24,13 @@ namespace BasicWebSite.Controllers
return new HttpStatusCodeResult(204);
}
[AcceptVerbs("GET", "POST")]
[RequireHttps]
public IActionResult HttpsOnlyAction()
{
return new HttpStatusCodeResult(200);
}
public async Task ActionReturningTask()
{
// TODO: #1077. With HttpResponseMessage, there seems to be a race between the write operation setting the