diff --git a/Mvc.sln b/Mvc.sln index 89c0a722ec..3688935b62 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -152,6 +152,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TempDataWebSite", "test\Web EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TestCommon", "test\Microsoft.AspNet.Mvc.TestCommon\Microsoft.AspNet.Mvc.TestCommon.xproj", "{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsWebSite", "test\WebSites\CorsWebSite\CorsWebSite.xproj", "{94BA134D-04B3-48AA-BA55-5A4DB8640F2D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -894,6 +896,18 @@ Global {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.ActiveCfg = Release|Any CPU {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.Build.0 = Release|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|x86.Build.0 = Debug|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Release|Any CPU.Build.0 = Release|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Release|x86.ActiveCfg = Release|Any CPU + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -968,5 +982,6 @@ Global {BCDB13A6-7D6E-485E-8424-A156432B71AC} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {8AEB631E-AB74-4D2E-83FB-8931EE10D9D3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {94BA134D-04B3-48AA-BA55-5A4DB8640F2D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs index 5ca4382004..0b2dc460eb 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultActionModelBuilder.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Cors; +using Microsoft.AspNet.Cors.Core; using Microsoft.AspNet.Mvc.Description; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Routing; @@ -21,9 +23,9 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels { private readonly AuthorizationOptions _authorizationOptions; - public DefaultActionModelBuilder(IOptions options) + public DefaultActionModelBuilder(IOptions authorizationOptions) { - _authorizationOptions = options?.Options ?? new AuthorizationOptions(); + _authorizationOptions = authorizationOptions?.Options ?? new AuthorizationOptions(); } /// @@ -266,6 +268,18 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels AddRange(actionModel.ActionConstraints, attributes.OfType()); AddRange(actionModel.Filters, attributes.OfType()); + var enableCors = attributes.OfType().SingleOrDefault(); + if (enableCors != null) + { + actionModel.Filters.Add(new CorsAuthorizationFilterFactory(enableCors.PolicyName)); + } + + var disableCors = attributes.OfType().SingleOrDefault(); + if (disableCors != null) + { + actionModel.Filters.Add(new DisableCorsAuthorizationFilter()); + } + var policy = AuthorizationPolicy.Combine(_authorizationOptions, attributes.OfType()); if (policy != null) { diff --git a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs index 7c4af1010c..eb9ed9883c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ApplicationModels/DefaultControllerModelBuilder.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Cors; +using Microsoft.AspNet.Cors.Core; using Microsoft.AspNet.Mvc.Description; using Microsoft.AspNet.Mvc.Filters; using Microsoft.AspNet.Mvc.Routing; @@ -31,11 +33,11 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public DefaultControllerModelBuilder( IActionModelBuilder actionModelBuilder, ILoggerFactory loggerFactory, - IOptions options) + IOptions authorizationOptions) { _actionModelBuilder = actionModelBuilder; _logger = loggerFactory.CreateLogger(); - _authorizationOptions = options?.Options ?? new AuthorizationOptions(); + _authorizationOptions = authorizationOptions?.Options ?? new AuthorizationOptions(); } /// @@ -80,6 +82,18 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels AddRange(controllerModel.Filters, attributes.OfType()); AddRange(controllerModel.RouteConstraints, attributes.OfType()); + var enableCors = attributes.OfType().SingleOrDefault(); + if (enableCors != null) + { + controllerModel.Filters.Add(new CorsAuthorizationFilterFactory(enableCors.PolicyName)); + } + + var disableCors = attributes.OfType().SingleOrDefault(); + if (disableCors != null) + { + controllerModel.Filters.Add(new DisableCorsAuthorizationFilter()); + } + var policy = AuthorizationPolicy.Combine(_authorizationOptions, attributes.OfType()); if (policy != null) { diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultOrder.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultOrder.cs index 83862e8802..d3e88275aa 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultOrder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultOrder.cs @@ -11,5 +11,11 @@ namespace Microsoft.AspNet.Mvc /// User code should order at bigger than 0 or smaller than -2000. /// public static readonly int DefaultFrameworkSortOrder = -1000; + + /// + /// The default order for , + /// and . + /// + public static readonly int DefaultCorsSortOrder = int.MaxValue - 100; } } diff --git a/src/Microsoft.AspNet.Mvc.Core/DisableCorsAuthorizationFilter.cs b/src/Microsoft.AspNet.Mvc.Core/DisableCorsAuthorizationFilter.cs new file mode 100644 index 0000000000..c6137f569f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/DisableCorsAuthorizationFilter.cs @@ -0,0 +1,45 @@ +// 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; +using Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// An which ensures that an action does not run for a pre-flight request. + /// + public class DisableCorsAuthorizationFilter : ICorsAuthorizationFilter + { + /// + public int Order + { + get + { + return DefaultOrder.DefaultCorsSortOrder; + } + } + + /// + public Task OnAuthorizationAsync([NotNull] AuthorizationContext context) + { + var accessControlRequestMethod = + context.HttpContext.Request.Headers.Get(CorsConstants.AccessControlRequestMethod); + if (string.Equals( + context.HttpContext.Request.Method, + CorsConstants.PreflightHttpMethod, + StringComparison.Ordinal) && + accessControlRequestMethod != null) + { + // Short circuit if the request is preflight as that should not result in action execution. + context.Result = new HttpStatusCodeResult(StatusCodes.Status200OK); + } + + // Let the action be executed. + return Task.FromResult(true); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/CorsAuthorizationFilter.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/CorsAuthorizationFilter.cs new file mode 100644 index 0000000000..152f8a9dc3 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/CorsAuthorizationFilter.cs @@ -0,0 +1,94 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A filter which applies the given and adds appropriate response headers. + /// + public class CorsAuthorizationFilter : ICorsAuthorizationFilter + { + private ICorsService _corsService; + private ICorsPolicyProvider _corsPolicyProvider; + + /// + /// Creates a new instace of . + /// + /// The . + /// The . + public CorsAuthorizationFilter(ICorsService corsService, ICorsPolicyProvider policyProvider) + { + _corsService = corsService; + _corsPolicyProvider = policyProvider; + } + + /// + /// The policy name used to fetch a . + /// + public string PolicyName { get; set; } + + /// + public int Order + { + get + { + return DefaultOrder.DefaultCorsSortOrder; + } + } + + + /// + public async Task OnAuthorizationAsync([NotNull] AuthorizationContext context) + { + // If this filter is not closest to the action, it is not applicable. + if (!IsClosestToAction(context.Filters)) + { + return; + } + + var httpContext = context.HttpContext; + var request = httpContext.Request; + if (request.Headers.ContainsKey(CorsConstants.Origin)) + { + var policy = await _corsPolicyProvider.GetPolicyAsync(httpContext, PolicyName); + var result = _corsService.EvaluatePolicy(context.HttpContext, policy); + _corsService.ApplyResult(result, context.HttpContext.Response); + + var accessControlRequestMethod = + httpContext.Request.Headers.Get(CorsConstants.AccessControlRequestMethod); + if (string.Equals( + request.Method, + CorsConstants.PreflightHttpMethod, + StringComparison.Ordinal) && + accessControlRequestMethod != null) + { + // If this was a preflight, there is no need to run anything else. + // Also the response is always 200 so that anyone after mvc can handle the pre flight request. + context.Result = new HttpStatusCodeResult(StatusCodes.Status200OK); + await Task.FromResult(true); + } + + // Continue with other filters and action. + } + } + + private bool IsClosestToAction(IEnumerable filters) + { + // If there are multiple ICorsAuthorizationFilter which are defined at the class and + // at the action level, the one closest to the action overrides the others. + // Since filterdescriptor collection is ordered (the last filter is the one closest to the action), + // we apply this constraint only if there is no ICorsAuthorizationFilter after this. + return filters.Last(filter => filter is ICorsAuthorizationFilter) == this; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Filters/CorsAuthorizationFilterFactory.cs b/src/Microsoft.AspNet.Mvc.Core/Filters/CorsAuthorizationFilterFactory.cs new file mode 100644 index 0000000000..14c1b71daf --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Filters/CorsAuthorizationFilterFactory.cs @@ -0,0 +1,48 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A filter factory which creates a new instance of . + /// + public class CorsAuthorizationFilterFactory : IFilterFactory, IOrderedFilter + { + private readonly string _policyName; + + /// + /// Creates a new insntace of . + /// + /// + public CorsAuthorizationFilterFactory(string policyName) + { + _policyName = policyName; + } + + /// + public int Order + { + get + { + return DefaultOrder.DefaultCorsSortOrder; + } + } + + public IFilter CreateInstance([NotNull] IServiceProvider serviceProvider) + { + var filter = serviceProvider.GetRequiredService(); + filter.PolicyName = _policyName; + return filter; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpMethodConstraint.cs b/src/Microsoft.AspNet.Mvc.Core/HttpMethodConstraint.cs index 5085d5e041..c0ee190072 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpMethodConstraint.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpMethodConstraint.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Microsoft.AspNet.Cors.Core; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc @@ -54,8 +55,22 @@ namespace Microsoft.AspNet.Mvc } var request = context.RouteContext.HttpContext.Request; + var method = request.Method; + if (request.Headers.ContainsKey(CorsConstants.Origin)) + { + // Update the http method if it is preflight request. + var accessControlRequestMethod = request.Headers.Get(CorsConstants.AccessControlRequestMethod); + if (string.Equals( + request.Method, + CorsConstants.PreflightHttpMethod, + StringComparison.Ordinal) && + accessControlRequestMethod != null) + { + method = accessControlRequestMethod; + } + } - return (HttpMethods.Any(m => m.Equals(request.Method, StringComparison.Ordinal))); + return (HttpMethods.Any(m => m.Equals(method, StringComparison.Ordinal))); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ICorsAuthorizationFilter.cs b/src/Microsoft.AspNet.Mvc.Core/ICorsAuthorizationFilter.cs new file mode 100644 index 0000000000..2126382c07 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ICorsAuthorizationFilter.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.AspNet.Mvc +{ + /// + /// A filter which can be used to enable/disable cors support for a resource. + /// + public interface ICorsAuthorizationFilter : IAsyncAuthorizationFilter, IOrderedFilter + { + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index da5b0c80af..a0dc0cc379 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -7,6 +7,7 @@ "dependencies": { "Microsoft.AspNet.Authentication": "1.0.0-*", "Microsoft.AspNet.Authorization": "1.0.0-*", + "Microsoft.AspNet.Cors.Core": "1.0.0-*", "Microsoft.AspNet.DataProtection": "1.0.0-*", "Microsoft.AspNet.Diagnostics.Interfaces": "1.0.0-*", "Microsoft.AspNet.FileProviders": "1.0.0-*", diff --git a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs index 289b22af0c..4efb58ca18 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.AspNet.Builder; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.Internal; @@ -87,6 +88,7 @@ namespace Microsoft.Framework.DependencyInjection services.AddOptions(); services.AddDataProtection(); services.AddRouting(); + services.AddCors(); services.AddAuthorization(); services.AddWebEncoders(); services.Configure( diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 38587e8128..02277883da 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -75,6 +75,7 @@ namespace Microsoft.AspNet.Mvc services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Dataflow - ModelBinding, Validation and Formatting // diff --git a/src/Microsoft.AspNet.Mvc/project.json b/src/Microsoft.AspNet.Mvc/project.json index b25e4ba7a7..5147615251 100644 --- a/src/Microsoft.AspNet.Mvc/project.json +++ b/src/Microsoft.AspNet.Mvc/project.json @@ -6,6 +6,7 @@ }, "dependencies": { "Microsoft.AspNet.Authorization": "1.0.0-*", + "Microsoft.AspNet.Cors": "1.0.0-*", "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", "Microsoft.Framework.Caching.Memory": "1.0.0-*", diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs index 9c7d1f176e..56ea0d43fc 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultActionModelBuilderTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Cors.Core; using Microsoft.Framework.Internal; using Microsoft.Framework.OptionsModel; using Moq; @@ -284,6 +285,38 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Assert.False(isValid); } + [Fact] + public void BuildActionModel_EnableCorsAttributeAddsCorsAuthorizationFilterFactory() + { + // Arrange + var builder = new DefaultActionModelBuilder(authorizationOptions: null); + var typeInfo = typeof(EnableCorsController).GetTypeInfo(); + var method = typeInfo.GetMethod("Action"); + + // Act + var actions = builder.BuildActionModels(typeInfo, method); + + // Assert + var action = Assert.Single(actions); + Assert.Single(action.Filters, f => f is CorsAuthorizationFilterFactory); + } + + [Fact] + public void BuildActionModel_DisableCorsAttributeAddsDisableCorsAuthorizationFilter() + { + // Arrange + var builder = new DefaultActionModelBuilder(authorizationOptions: null); + var typeInfo = typeof(DisableCorsController).GetTypeInfo(); + var method = typeInfo.GetMethod("Action"); + + // Act + var actions = builder.BuildActionModels(typeInfo, method); + + // Assert + var action = Assert.Single(actions); + Assert.True(action.Filters.Any(f => f is DisableCorsAuthorizationFilter)); + } + [Fact] public void GetActions_ConventionallyRoutedAction_WithoutHttpConstraints() { @@ -927,6 +960,22 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels public void Invalid() { } } + private class EnableCorsController + { + [EnableCors("policy")] + public void Action() + { + } + } + + private class DisableCorsController + { + [DisableCors] + public void Action() + { + } + } + // Here the constraints on the methods are acting as an IActionHttpMethodProvider and // not as an IRouteTemplateProvider given that there is no RouteAttribute // on the controller and the template for all the constraints on a method is null. diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs index 8e37685c7d..b853dfc628 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ApplicationModel/DefaultControllerModelBuilderTest.cs @@ -6,8 +6,11 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Cors.Core; using Microsoft.AspNet.Mvc.Filters; using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ApplicationModels @@ -47,6 +50,48 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels Assert.True(model.Filters.Any(f => f is AuthorizeFilter)); } + [Fact] + public void BuildControllerModel_EnableCorsAttributeAddsCorsAuthorizationFilterFactory() + { + // Arrange + var corsOptions = new CorsOptions(); + corsOptions.AddPolicy("policy", new CorsPolicy()); + var mockOptions = new Mock>(); + mockOptions.SetupGet(o => o.Options) + .Returns(corsOptions); + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(null), + NullLoggerFactory.Instance, + authorizationOptions: null); + var typeInfo = typeof(CorsController).GetTypeInfo(); + + // Act + var model = builder.BuildControllerModel(typeInfo); + + // Assert + Assert.Single(model.Filters, f => f is CorsAuthorizationFilterFactory); + } + + [Fact] + public void BuildControllerModel_DisableCorsAttributeAddsDisableCorsAuthorizationFilter() + { + // Arrange + var corsOptions = new CorsOptions(); + corsOptions.AddPolicy("policy", new CorsPolicy()); + var mockOptions = new Mock>(); + mockOptions.SetupGet(o => o.Options) + .Returns(corsOptions); + var builder = new DefaultControllerModelBuilder(new DefaultActionModelBuilder(null), + NullLoggerFactory.Instance, + authorizationOptions: null); + var typeInfo = typeof(DisableCorsController).GetTypeInfo(); + + // Act + var model = builder.BuildControllerModel(typeInfo); + + // Assert + Assert.True(model.Filters.Any(f => f is DisableCorsAuthorizationFilter)); + } + // This class has a filter attribute, but doesn't implement any filter interfaces, // so ControllerFilter is not present. [Fact] @@ -113,6 +158,16 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels { } + [EnableCors("policy")] + public class CorsController + { + } + + [DisableCors] + public class DisableCorsController + { + } + public class SomeFiltersController : IAsyncActionFilter, IResultFilter { public Task OnActionExecutionAsync( diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs index 26d9d9a698..6b404b9f11 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/DefaultActionSelectorTests.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Core; +using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.Mvc.ActionConstraints; using Microsoft.AspNet.Mvc.ApplicationModels; using Microsoft.AspNet.Mvc.Core; @@ -867,7 +868,7 @@ namespace Microsoft.AspNet.Mvc var request = new Mock(MockBehavior.Strict); request.SetupGet(r => r.Method).Returns(httpMethod); request.SetupGet(r => r.Path).Returns(new PathString()); - + request.SetupGet(r => r.Headers).Returns(new HeaderDictionary()); httpContext.SetupGet(c => c.Request).Returns(request.Object); httpContext.SetupGet(c => c.RequestServices).Returns(serviceContainer); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/CorsAuthorizationFilterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/CorsAuthorizationFilterTest.cs new file mode 100644 index 0000000000..9d440d15da --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/CorsAuthorizationFilterTest.cs @@ -0,0 +1,281 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Moq; +using Newtonsoft.Json.Utilities; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Test +{ + public class CorsAuthorizationFilterTest + { + [Fact] + public async Task PreFlightRequest_SuccessfulMatch_WritesHeaders() + { + // Arrange + var mockEngine = GetPassingEngine(supportsCredentials:true); + var filter = GetFilter(mockEngine); + + var authorizationContext = GetAuthorizationContext( + new[] { new FilterDescriptor(filter, FilterScope.Action) }, + GetRequestHeaders(true), + isPreflight: true); + + // Act + await filter.OnAuthorizationAsync(authorizationContext); + await authorizationContext.Result.ExecuteResultAsync(authorizationContext); + + // Assert + var response = authorizationContext.HttpContext.Response; + Assert.Equal(200, response.StatusCode); + Assert.Equal("http://example.com", response.Headers[CorsConstants.AccessControlAllowOrigin]); + Assert.Equal("header1,header2", response.Headers[CorsConstants.AccessControlAllowHeaders]); + + // Notice: GET header gets filtered because it is a simple header. + Assert.Equal("PUT", response.Headers[CorsConstants.AccessControlAllowMethods]); + Assert.Equal("exposed1,exposed2", response.Headers[CorsConstants.AccessControlExposeHeaders]); + Assert.Equal("123", response.Headers[CorsConstants.AccessControlMaxAge]); + Assert.Equal("true", response.Headers[CorsConstants.AccessControlAllowCredentials]); + } + + [Fact] + public async Task PreFlight_FailedMatch_Writes200() + { + // Arrange + var mockEngine = GetFailingEngine(); + var filter = GetFilter(mockEngine); + + var authorizationContext = GetAuthorizationContext( + new[] { new FilterDescriptor(filter, FilterScope.Action) }, + GetRequestHeaders(), + isPreflight: true); + + // Act + await filter.OnAuthorizationAsync(authorizationContext); + await authorizationContext.Result.ExecuteResultAsync(authorizationContext); + + // Assert + Assert.Equal(200, authorizationContext.HttpContext.Response.StatusCode); + Assert.Empty(authorizationContext.HttpContext.Response.Headers); + } + + [Fact] + public async Task CorsRequest_SuccessfulMatch_WritesHeaders() + { + // Arrange + var mockEngine = GetPassingEngine(supportsCredentials: true); + var filter = GetFilter(mockEngine); + + var authorizationContext = GetAuthorizationContext( + new[] { new FilterDescriptor(filter, FilterScope.Action) }, + GetRequestHeaders(true), + isPreflight: true); + + // Act + await filter.OnAuthorizationAsync(authorizationContext); + await authorizationContext.Result.ExecuteResultAsync(authorizationContext); + + // Assert + var response = authorizationContext.HttpContext.Response; + Assert.Equal(200, response.StatusCode); + Assert.Equal("http://example.com", response.Headers[CorsConstants.AccessControlAllowOrigin]); + Assert.Equal("exposed1,exposed2", response.Headers[CorsConstants.AccessControlExposeHeaders]); + } + + [Fact] + public async Task CorsRequest_FailedMatch_Writes200() + { + // Arrange + var mockEngine = GetFailingEngine(); + var filter = GetFilter(mockEngine); + + var authorizationContext = GetAuthorizationContext( + new[] { new FilterDescriptor(filter, FilterScope.Action) }, + GetRequestHeaders(), + isPreflight: false); + + // Act + await filter.OnAuthorizationAsync(authorizationContext); + + // Assert + Assert.Equal(200, authorizationContext.HttpContext.Response.StatusCode); + Assert.Empty(authorizationContext.HttpContext.Response.Headers); + } + + private CorsAuthorizationFilter GetFilter(ICorsService corsService) + { + var policyProvider = new Mock(); + policyProvider + .Setup(o => o.GetPolicyAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new CorsPolicy())); + + return new CorsAuthorizationFilter(corsService, policyProvider.Object) + { + PolicyName = string.Empty + }; + } + + private AuthorizationContext GetAuthorizationContext( + FilterDescriptor[] filterDescriptors, + RequestHeaders headers = null, + bool isPreflight = false) + { + + // HttpContext + var httpContext = new DefaultHttpContext(); + if (headers != null) + { + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestHeaders, headers.Headers.Split(',')); + httpContext.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { headers.Method }); + httpContext.Request.Headers.Add(CorsConstants.AccessControlExposeHeaders, headers.ExposedHeaders.Split(',')); + httpContext.Request.Headers.Add(CorsConstants.Origin, new[] { headers.Origin }); + } + + var method = isPreflight ? CorsConstants.PreflightHttpMethod : "GET"; + httpContext.Request.Method = method; + + // AuthorizationContext + var actionContext = new ActionContext( + httpContext: httpContext, + routeData: new RouteData(), + actionDescriptor: new ActionDescriptor() { FilterDescriptors = filterDescriptors }); + + var authorizationContext = new AuthorizationContext( + actionContext, + filterDescriptors.Select(filter => filter.Filter).ToList() + ); + + return authorizationContext; + } + + private ICorsService GetFailingEngine() + { + var mockEngine = new Mock(); + var result = GetCorsResult("http://example.com"); + + mockEngine + .Setup(o => o.EvaluatePolicy(It.IsAny(), It.IsAny())) + .Returns(result); + return mockEngine.Object; + } + + private ICorsService GetPassingEngine(bool supportsCredentials = false) + { + var mockEngine = new Mock(); + var result = GetCorsResult( + "http://example.com", + new List { "header1", "header2" }, + new List { "PUT" }, + new List { "exposed1", "exposed2" }, + 123, + supportsCredentials); + + mockEngine + .Setup(o => o.EvaluatePolicy(It.IsAny(), It.IsAny())) + .Returns(result); + + mockEngine + .Setup(o => o.ApplyResult(It.IsAny(), It.IsAny())) + .Callback((result1, response1) => + { + var headers = response1.Headers; + headers.Set( + CorsConstants.AccessControlMaxAge, + result1.PreflightMaxAge.Value.TotalSeconds.ToString()); + headers.Add(CorsConstants.AccessControlAllowOrigin, new[] { result1.AllowedOrigin }); + if (result1.SupportsCredentials) + { + headers.Add(CorsConstants.AccessControlAllowCredentials, new[] { "true" }); + } + + headers.Add(CorsConstants.AccessControlAllowHeaders, result1.AllowedHeaders.ToArray()); + headers.Add(CorsConstants.AccessControlAllowMethods, result1.AllowedMethods.ToArray()); + headers.Add(CorsConstants.AccessControlExposeHeaders, result1.AllowedExposedHeaders.ToArray()); + }); + + return mockEngine.Object; + } + + private RequestHeaders GetRequestHeaders(bool supportsCredentials = false) + { + return new RequestHeaders + { + Origin = "http://example.com", + Headers = "header1,header2", + Method = "GET", + ExposedHeaders = "exposed1,exposed2", + }; + } + + private CorsResult GetCorsResult( + string origin = null, + IList headers = null, + IList methods = null, + IList exposedHeaders = null, + long? preFlightMaxAge = null, + bool? supportsCredentials = null) + { + var result = new CorsResult(); + + if (origin != null) + { + result.AllowedOrigin = origin; + } + + if (headers != null) + { + AddRange(result.AllowedHeaders, headers); + } + + if (methods != null) + { + AddRange(result.AllowedMethods, methods); + } + + if (exposedHeaders != null) + { + AddRange(result.AllowedExposedHeaders, exposedHeaders); + } + + if (preFlightMaxAge != null) + { + result.PreflightMaxAge = TimeSpan.FromSeconds(preFlightMaxAge.Value); + } + + if (supportsCredentials != null) + { + result.SupportsCredentials = supportsCredentials.Value; + } + + return result; + } + + private void AddRange(IList target, IList source) + { + foreach (var item in source) + { + target.Add(item); + } + } + + private class RequestHeaders + { + public string Origin { get; set; } + + public string Headers { get; set; } + + public string ExposedHeaders { get; set; } + + public string Method { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/CorsMiddlewareTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/CorsMiddlewareTests.cs new file mode 100644 index 0000000000..a332b951f0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/CorsMiddlewareTests.cs @@ -0,0 +1,104 @@ +// 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.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.Http; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class CorsMiddlewareTests + { + private const string SiteName = nameof(CorsMiddlewareWebSite); + private readonly Action _app = new CorsMiddlewareWebSite.Startup().Configure; + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task ResourceWithSimpleRequestPolicy_Allows_SimpleRequests(string method) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + var origin = "http://example.com"; + + var requestBuilder = server + .CreateRequest("http://localhost/CorsMiddleware/GetExclusiveContent") + .AddHeader(CorsConstants.Origin, origin); + + // Act + var response = await requestBuilder.SendAsync(method); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("exclusive", content); + var responseHeaders = response.Headers; + var header = Assert.Single(response.Headers); + Assert.Equal(CorsConstants.AccessControlAllowOrigin, header.Key); + Assert.Equal(new[] { "http://example.com" }, header.Value.ToArray()); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + [InlineData("PUT")] + public async Task PolicyFailed_Disallows_PreFlightRequest(string method) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Adding a custom header makes it a non simple request. + var requestBuilder = server + .CreateRequest("http://localhost/CorsMiddleware/GetExclusiveContent") + .AddHeader(CorsConstants.Origin, "http://example.com") + .AddHeader(CorsConstants.AccessControlRequestMethod, method) + .AddHeader(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await requestBuilder.SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + // Middleware applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Empty(response.Headers); + + // It should short circuit and hence no result. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(string.Empty, content); + } + + [Fact] + public async Task PolicyFailed_Allows_ActualRequest_WithMissingResponseHeaders() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Adding a custom header makes it a non simple request. + var requestBuilder = server + .CreateRequest("http://localhost/CorsMiddleware/GetExclusiveContent") + .AddHeader(CorsConstants.Origin, "http://example2.com"); + + // Act + var response = await requestBuilder.SendAsync("PUT"); + + // Assert + // Middleware applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // It still has executed the action. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("exclusive", content); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/CorsTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/CorsTests.cs new file mode 100644 index 0000000000..760b67f7d5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/CorsTests.cs @@ -0,0 +1,231 @@ +// 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.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.Http; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class CorsTests + { + private const string SiteName = nameof(CorsWebSite); + private readonly Action _app = new CorsWebSite.Startup().Configure; + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task ResourceWithSimpleRequestPolicy_Allows_SimpleRequests(string method) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + var origin = "http://example.com"; + + var requestBuilder = server + .CreateRequest("http://localhost/Cors/GetBlogComments") + .AddHeader(CorsConstants.Origin, origin); + + // Act + var response = await requestBuilder.SendAsync(method); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("[\"comment1\",\"comment2\",\"comment3\"]", content); + var responseHeaders = response.Headers; + var header = Assert.Single(response.Headers); + Assert.Equal(CorsConstants.AccessControlAllowOrigin, header.Key); + Assert.Equal(new[] { "*" }, header.Value.ToArray()); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + [InlineData("PUT")] + public async Task PolicyFailed_Disallows_PreFlightRequest(string method) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Adding a custom header makes it a non simple request. + var requestBuilder = server + .CreateRequest("http://localhost/Cors/GetBlogComments") + .AddHeader(CorsConstants.Origin, "http://example.com") + .AddHeader(CorsConstants.AccessControlRequestMethod, method) + .AddHeader(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await requestBuilder.SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + // MVC applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // It should short circuit and hence no result. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(string.Empty, content); + } + + [Fact] + public async Task SuccessfulCorsRequest_AllowsCredentials_IfThePolicyAllowsCredentials() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Adding a custom header makes it a non simple request. + var requestBuilder = server + .CreateRequest("http://localhost/Cors/EditUserComment?userComment=abcd") + .AddHeader(CorsConstants.Origin, "http://example.com") + .AddHeader(CorsConstants.AccessControlExposeHeaders, "exposed1,exposed2"); + + // Act + var response = await requestBuilder.SendAsync("PUT"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseHeaders = response.Headers; + Assert.Equal( + new[] { "http://example.com" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); + Assert.Equal( + new[] { "true" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); + Assert.Equal( + new[] { "exposed1", "exposed2" }, + responseHeaders.GetValues(CorsConstants.AccessControlExposeHeaders).ToArray()); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("abcd", content); + } + + [Fact] + public async Task SuccessfulPreflightRequest_AllowsCredentials_IfThePolicyAllowsCredentials() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Adding a custom header makes it a non simple request. + var requestBuilder = server + .CreateRequest("http://localhost/Cors/EditUserComment?userComment=abcd") + .AddHeader(CorsConstants.Origin, "http://example.com") + .AddHeader(CorsConstants.AccessControlRequestMethod, "PUT") + .AddHeader(CorsConstants.AccessControlRequestHeaders, "header1,header2"); + + // Act + var response = await requestBuilder.SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseHeaders = response.Headers; + Assert.Equal( + new[] { "http://example.com" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowOrigin).ToArray()); + Assert.Equal( + new[] { "true" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowCredentials).ToArray()); + Assert.Equal( + new[] { "header1", "header2" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowHeaders).ToArray()); + Assert.Equal( + new[] { "PUT" }, + responseHeaders.GetValues(CorsConstants.AccessControlAllowMethods).ToArray()); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + + [Fact] + public async Task PolicyFailed_Allows_ActualRequest_WithMissingResponseHeaders() + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Adding a custom header makes it a non simple request. + var requestBuilder = server + .CreateRequest("http://localhost/Cors/GetUserComments") + .AddHeader(CorsConstants.Origin, "http://example2.com"); + + // Act + var response = await requestBuilder.SendAsync("PUT"); + + // Assert + // MVC applied the policy and since that did not pass, there were no access control headers. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // It still have executed the action. + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("[\"usercomment1\",\"usercomment2\",\"usercomment3\"]", content); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task DisableCors_ActionsCanOverride_ControllerLevel(string method) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Exclusive content is not available on other sites. + var requestBuilder = server + .CreateRequest("http://localhost/Cors/GetExclusiveContent") + .AddHeader(CorsConstants.Origin, "http://example.com"); + + // Act + var response = await requestBuilder.SendAsync(method); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Since there are no response headers, the client should step in to block the content. + Assert.Empty(response.Headers); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("exclusive", content); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("POST")] + public async Task DisableCors_PreFlight_ActionsCanOverride_ControllerLevel(string method) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName); + var client = server.CreateClient(); + + // Exclusive content is not available on other sites. + var requestBuilder = server + .CreateRequest("http://localhost/Cors/GetExclusiveContent") + .AddHeader(CorsConstants.Origin, "http://example.com") + .AddHeader(CorsConstants.AccessControlRequestMethod, method) + .AddHeader(CorsConstants.AccessControlRequestHeaders, "Custom"); + + // Act + var response = await requestBuilder.SendAsync(CorsConstants.PreflightHttpMethod); + + // Assert + // Since there are no response headers, the client should step in to block the content. + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Empty(response.Headers); + + // Nothing gets executed for a pre-flight request. + var content = await response.Content.ReadAsStringAsync(); + Assert.Empty(content); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 25d356f1ed..252e1c02f5 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -20,6 +20,8 @@ "ContentNegotiationWebSite": "1.0.0", "ControllerDiscoveryConventionsWebSite": "1.0.0", "ControllersFromServicesWebSite": "1.0.0", + "CorsWebSite": "1.0.0", + "CorsMiddlewareWebSite": "1.0.0", "CustomRouteWebSite": "1.0.0", "ErrorPageMiddlewareWebSite": "1.0.0", "FilesWebSite": "1.0.0", diff --git a/test/WebSites/CorsMiddlewareWebSite/Controllers/BlogController.cs b/test/WebSites/CorsMiddlewareWebSite/Controllers/BlogController.cs new file mode 100644 index 0000000000..f71786bcaf --- /dev/null +++ b/test/WebSites/CorsMiddlewareWebSite/Controllers/BlogController.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace CorsMiddlewareWebSite +{ + [Route("CorsMiddleWare/[action]")] + public class BlogController : Controller + { + public string GetExclusiveContent() + { + return "exclusive"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.xproj b/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.xproj new file mode 100644 index 0000000000..01e9f9db63 --- /dev/null +++ b/test/WebSites/CorsMiddlewareWebSite/CorsMiddlewareWebSite.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 94ba134d-04b3-48aa-ba55-5a4db8640f2d + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 41642 + + + \ No newline at end of file diff --git a/test/WebSites/CorsMiddlewareWebSite/Startup.cs b/test/WebSites/CorsMiddlewareWebSite/Startup.cs new file mode 100644 index 0000000000..22ce862ec1 --- /dev/null +++ b/test/WebSites/CorsMiddlewareWebSite/Startup.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.AspNet.Cors; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; + +namespace CorsMiddlewareWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(); + }); + + app.UseCors(policy => policy.WithOrigins("http://example.com")); + app.UseMvc(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/CorsMiddlewareWebSite/project.json b/test/WebSites/CorsMiddlewareWebSite/project.json new file mode 100644 index 0000000000..be26999ef7 --- /dev/null +++ b/test/WebSites/CorsMiddlewareWebSite/project.json @@ -0,0 +1,22 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Cors": "1.0.0-*", + "Microsoft.AspNet.Cors.Core": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.Xml": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnx50": { } + }, + "webroot": "wwwroot" +} \ No newline at end of file diff --git a/test/WebSites/CorsMiddlewareWebSite/readme.md b/test/WebSites/CorsMiddlewareWebSite/readme.md new file mode 100644 index 0000000000..d7f8b28106 --- /dev/null +++ b/test/WebSites/CorsMiddlewareWebSite/readme.md @@ -0,0 +1,4 @@ +CorsMiddlewareWebSite +=== + +This web site illustrates how to use CorsMiddleware to apply a policy for entire application. diff --git a/test/WebSites/CorsMiddlewareWebSite/wwwroot/HelloWorld.htm b/test/WebSites/CorsMiddlewareWebSite/wwwroot/HelloWorld.htm new file mode 100644 index 0000000000..3da1ec26e9 --- /dev/null +++ b/test/WebSites/CorsMiddlewareWebSite/wwwroot/HelloWorld.htm @@ -0,0 +1 @@ +HelloWorld diff --git a/test/WebSites/CorsWebSite/Controllers/BlogController.cs b/test/WebSites/CorsWebSite/Controllers/BlogController.cs new file mode 100644 index 0000000000..2a476a59e0 --- /dev/null +++ b/test/WebSites/CorsWebSite/Controllers/BlogController.cs @@ -0,0 +1,38 @@ +// 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 Microsoft.AspNet.Cors.Core; +using Microsoft.AspNet.Mvc; + +namespace CorsWebSite +{ + [Route("Cors/[action]")] + [EnableCors("AllowAnySimpleRequest")] + public class BlogController : Controller + { + public IEnumerable GetBlogComments(int id) + { + return new[] { "comment1", "comment2", "comment3" }; + } + + [EnableCors("AllowSpecificOrigin")] + public IEnumerable GetUserComments(int id) + { + return new[] { "usercomment1", "usercomment2", "usercomment3" }; + } + + [DisableCors] + [AcceptVerbs("HEAD", "GET", "POST")] + public string GetExclusiveContent() + { + return "exclusive"; + } + + [EnableCors("WithCredentialsAnyOrigin")] + public string EditUserComment(int id, string userComment) + { + return userComment; + } + } +} \ No newline at end of file diff --git a/test/WebSites/CorsWebSite/CorsWebSite.xproj b/test/WebSites/CorsWebSite/CorsWebSite.xproj new file mode 100644 index 0000000000..01e9f9db63 --- /dev/null +++ b/test/WebSites/CorsWebSite/CorsWebSite.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 94ba134d-04b3-48aa-ba55-5a4db8640f2d + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 41642 + + + \ No newline at end of file diff --git a/test/WebSites/CorsWebSite/Startup.cs b/test/WebSites/CorsWebSite/Startup.cs new file mode 100644 index 0000000000..f270455f6d --- /dev/null +++ b/test/WebSites/CorsWebSite/Startup.cs @@ -0,0 +1,60 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; + +namespace CorsWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(); + services.ConfigureCors(options => + { + options.AddPolicy( + "AllowAnySimpleRequest", + builder => + { + builder.AllowAnyOrigin() + .WithMethods("GET", "POST", "HEAD"); + }); + + options.AddPolicy( + "AllowSpecificOrigin", + builder => + { + builder.WithOrigins("http://example.com"); + }); + + options.AddPolicy( + "WithCredentials", + builder => + { + builder.AllowCredentials() + .WithOrigins("http://example.com"); + }); + + options.AddPolicy( + "WithCredentialsAnyOrigin", + builder => + { + builder.AllowCredentials() + .AllowAnyOrigin() + .AllowAnyHeader() + .WithMethods("PUT", "POST") + .WithExposedHeaders("exposed1", "exposed2"); + }); + }); + }); + + app.UseMvc(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/CorsWebSite/project.json b/test/WebSites/CorsWebSite/project.json new file mode 100644 index 0000000000..be26999ef7 --- /dev/null +++ b/test/WebSites/CorsWebSite/project.json @@ -0,0 +1,22 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Cors": "1.0.0-*", + "Microsoft.AspNet.Cors.Core": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.Xml": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnx50": { } + }, + "webroot": "wwwroot" +} \ No newline at end of file diff --git a/test/WebSites/CorsWebSite/readme.md b/test/WebSites/CorsWebSite/readme.md new file mode 100644 index 0000000000..02ac8dca3f --- /dev/null +++ b/test/WebSites/CorsWebSite/readme.md @@ -0,0 +1,4 @@ +CorsWebSite +=== + +This web site illustrates how to configure actions to allow/disallow cross origin requests. diff --git a/test/WebSites/CorsWebSite/wwwroot/HelloWorld.htm b/test/WebSites/CorsWebSite/wwwroot/HelloWorld.htm new file mode 100644 index 0000000000..3da1ec26e9 --- /dev/null +++ b/test/WebSites/CorsWebSite/wwwroot/HelloWorld.htm @@ -0,0 +1 @@ +HelloWorld