diff --git a/CORS.sln b/CORS.sln index 2887117253..2cf414de64 100644 --- a/CORS.sln +++ b/CORS.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22604.0 +VisualStudioVersion = 14.0.22529.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84FE6872-A610-4CEC-855F-A84CBF1F40FC}" EndProject @@ -12,6 +12,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Cors.Core", "src\Microsoft.AspNet.Cors.Core\Microsoft.AspNet.Cors.Core.kproj", "{C573AEE1-8D54-4A83-8D6B-61C85E8F713E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F32074C7-087C-46CC-A913-422BFD2D6E0A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Cors.Core.Test", "test\Microsoft.AspNet.Cors.Core.Test\Microsoft.AspNet.Cors.Core.Test.kproj", "{B4F83A06-EB8E-4186-84C4-C6DAF7EB03D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,11 +26,16 @@ Global {C573AEE1-8D54-4A83-8D6B-61C85E8F713E}.Debug|Any CPU.Build.0 = Debug|Any CPU {C573AEE1-8D54-4A83-8D6B-61C85E8F713E}.Release|Any CPU.ActiveCfg = Release|Any CPU {C573AEE1-8D54-4A83-8D6B-61C85E8F713E}.Release|Any CPU.Build.0 = Release|Any CPU + {B4F83A06-EB8E-4186-84C4-C6DAF7EB03D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B4F83A06-EB8E-4186-84C4-C6DAF7EB03D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B4F83A06-EB8E-4186-84C4-C6DAF7EB03D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B4F83A06-EB8E-4186-84C4-C6DAF7EB03D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {C573AEE1-8D54-4A83-8D6B-61C85E8F713E} = {84FE6872-A610-4CEC-855F-A84CBF1F40FC} + {B4F83A06-EB8E-4186-84C4-C6DAF7EB03D4} = {F32074C7-087C-46CC-A913-422BFD2D6E0A} EndGlobalSection EndGlobal diff --git a/global.json b/global.json index 840c36f6ad..be397c3721 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "sources": ["src"] + "sources": ["src", "test"] } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsConstants.cs b/src/Microsoft.AspNet.Cors.Core/CorsConstants.cs new file mode 100644 index 0000000000..fad7a5b533 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsConstants.cs @@ -0,0 +1,91 @@ +// 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.Cors.Core +{ + /// + /// CORS-related constants. + /// + public static class CorsConstants + { + /// + /// The HTTP method for the CORS preflight request. + /// + public static readonly string PreflightHttpMethod = "OPTIONS"; + + /// + /// The Origin request header. + /// + public static readonly string Origin = "Origin"; + + /// + /// The value for the Access-Control-Allow-Origin response header to allow all origins. + /// + public static readonly string AnyOrigin = "*"; + + /// + /// The Access-Control-Request-Method request header. + /// + public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method"; + + /// + /// The Access-Control-Request-Headers request header. + /// + public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers"; + + /// + /// The Access-Control-Allow-Origin response header. + /// + public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; + + /// + /// The Access-Control-Allow-Headers response header. + /// + public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; + + /// + /// The Access-Control-Expose-Headers response header. + /// + public static readonly string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; + + /// + /// The Access-Control-Allow-Methods response header. + /// + public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods"; + + /// + /// The Access-Control-Allow-Credentials response header. + /// + public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; + + /// + /// The Access-Control-Max-Age response header. + /// + public static readonly string AccessControlMaxAge = "Access-Control-Max-Age"; + + internal static readonly string[] SimpleRequestHeaders = + { + "Origin", + "Accept", + "Accept-Language", + "Content-Language", + }; + + internal static readonly string[] SimpleResponseHeaders = + { + "Cache-Control", + "Content-Language", + "Content-Type", + "Expires", + "Last-Modified", + "Pragma" + }; + + internal static readonly string[] SimpleMethods = + { + "GET", + "HEAD", + "POST" + }; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsOptions.cs b/src/Microsoft.AspNet.Cors.Core/CorsOptions.cs new file mode 100644 index 0000000000..7d76181e62 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsOptions.cs @@ -0,0 +1,50 @@ +// 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 Microsoft.AspNet.Cors.Core; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + /// Provides programmatic configuration for Cors. + /// + public class CorsOptions + { + private IDictionary PolicyMap { get; } = new Dictionary(); + + /// + /// Adds a new policy. + /// + /// The name of the policy. + /// The policy to be added. + public void AddPolicy([NotNull] string name, [NotNull] CorsPolicy policy) + { + PolicyMap[name] = policy; + } + + /// + /// Adds a new policy. + /// + /// The name of the policy. + /// A delegate which can use a policy builder to build a policy. + public void AddPolicy([NotNull] string name, [NotNull] Action configurePolicy) + { + var policyBuilder = new CorsPolicyBuilder(); + configurePolicy(policyBuilder); + PolicyMap[name] = policyBuilder.Build(); + } + + /// + /// Gets the policy based on the + /// + /// The name of the policy to lookup. + /// The if the policy was added.null otherwise. + public CorsPolicy GetPolicy([NotNull] string name) + { + return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsPolicy.cs b/src/Microsoft.AspNet.Cors.Core/CorsPolicy.cs new file mode 100644 index 0000000000..9fc7ba9d2b --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsPolicy.cs @@ -0,0 +1,146 @@ +// 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.Globalization; +using System.Text; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + /// Defines the policy for Cross-Origin requests based on the CORS specifications. + /// + public class CorsPolicy + { + private TimeSpan? _preflightMaxAge; + + /// + /// Gets a value indicating if all headers are allowed. + /// + public bool AllowAnyHeader + { + get + { + if (Headers == null || Headers.Count != 1 || Headers.Count == 1 && Headers[0] != "*") + { + return false; + } + + return true; + } + } + + /// + /// Gets a value indicating if all methods are allowed. + /// + public bool AllowAnyMethod + { + get + { + if (Methods == null || Methods.Count != 1 || Methods.Count == 1 && Methods[0] != "*") + { + return false; + } + + return true; + } + } + + /// + /// Gets a value indicating if all origins are allowed. + /// + public bool AllowAnyOrigin + { + get + { + if (Origins == null || Origins.Count != 1 || Origins.Count == 1 && Origins[0] != "*") + { + return false; + } + + return true; + } + } + + /// + /// Gets the headers that the resource might use and can be exposed. + /// + public IList ExposedHeaders { get; } = new List(); + + /// + /// Gets the headers that are supported by the resource. + /// + public IList Headers { get; } = new List(); + + /// + /// Gets the methods that are supported by the resource. + /// + public IList Methods { get; } = new List(); + + /// + /// Gets the origins that are allowed to access the resource. + /// + public IList Origins { get; } = new List(); + + /// + /// Gets or sets the for which the results of a preflight request can be cached. + /// + public TimeSpan? PreflightMaxAge + { + get + { + return _preflightMaxAge; + } + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException("value", Resources.PreflightMaxAgeOutOfRange); + } + + _preflightMaxAge = value; + } + } + + /// + /// Gets or sets a value indicating whether the resource supports user credentials in the request. + /// + public bool SupportsCredentials { get; set; } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append("AllowAnyHeader: "); + builder.Append(AllowAnyHeader); + builder.Append(", AllowAnyMethod: "); + builder.Append(AllowAnyMethod); + builder.Append(", AllowAnyOrigin: "); + builder.Append(AllowAnyOrigin); + builder.Append(", PreflightMaxAge: "); + builder.Append(PreflightMaxAge.HasValue ? + PreflightMaxAge.Value.TotalSeconds.ToString() : "null"); + builder.Append(", SupportsCredentials: "); + builder.Append(SupportsCredentials); + builder.Append(", Origins: {"); + builder.Append(string.Join(",", Origins)); + builder.Append("}"); + builder.Append(", Methods: {"); + builder.Append(string.Join(",", Methods)); + builder.Append("}"); + builder.Append(", Headers: {"); + builder.Append(string.Join(",", Headers)); + builder.Append("}"); + builder.Append(", ExposedHeaders: {"); + builder.Append(string.Join(",", ExposedHeaders)); + builder.Append("}"); + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsPolicyBuilder.cs b/src/Microsoft.AspNet.Cors.Core/CorsPolicyBuilder.cs new file mode 100644 index 0000000000..83303009bb --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsPolicyBuilder.cs @@ -0,0 +1,194 @@ +// 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 Microsoft.AspNet.Cors.Core; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Cors +{ + /// + /// Exposes methods to build a policy. + /// + public class CorsPolicyBuilder + { + private readonly CorsPolicy _policy = new CorsPolicy(); + + /// + /// Creates a new instance of the . + /// + /// list of origins which can be added. + public CorsPolicyBuilder(params string[] origins) + { + AddOrigins(origins); + } + + /// + /// Creates a new instance of the . + /// + /// The policy which will be used to intialize the builder. + public CorsPolicyBuilder(CorsPolicy policy) + { + Combine(policy); + } + + /// + /// Adds the specified to the policy. + /// + /// The origins that are allowed. + /// The current policy builder + public CorsPolicyBuilder AddOrigins(params string[] origins) + { + foreach (var req in origins) + { + _policy.Origins.Add(req); + } + + return this; + } + + /// + /// Adds the specified to the policy. + /// + /// The headers which need to be allowed in the request. + /// The current policy builder + public CorsPolicyBuilder AddHeaders(params string[] headers) + { + foreach (var req in headers) + { + _policy.Headers.Add(req); + } + return this; + } + + /// + /// Adds the specified to the policy. + /// + /// The headers which need to be exposed to the client. + /// The current policy builder + public CorsPolicyBuilder AddExposedHeaders(params string[] exposedHeaders) + { + foreach (var req in exposedHeaders) + { + _policy.ExposedHeaders.Add(req); + } + + return this; + } + + /// + /// Adds the specified to the policy. + /// + /// The methods which need to be added to the policy. + /// The current policy builder + public CorsPolicyBuilder AddMethods(params string[] methods) + { + foreach (var req in methods) + { + _policy.Methods.Add(req); + } + + return this; + } + + /// + /// Sets the policy to allow credentials. + /// + /// The current policy builder + public CorsPolicyBuilder AllowCredentials() + { + _policy.SupportsCredentials = true; + return this; + } + + /// + /// Sets the policy to not allow credentials. + /// + /// The current policy builder + public CorsPolicyBuilder DisallowCredentials() + { + _policy.SupportsCredentials = false; + return this; + } + + /// + /// Ensures that the policy allows any origin. + /// + /// The current policy builder + public CorsPolicyBuilder AllowAnyOrigin() + { + _policy.Origins.Clear(); + _policy.Origins.Add(CorsConstants.AnyOrigin); + return this; + } + + /// + /// Ensures that the policy allows any method. + /// + /// The current policy builder + public CorsPolicyBuilder AllowAnyMethod() + { + _policy.Methods.Clear(); + _policy.Methods.Add("*"); + return this; + } + + /// + /// Ensures that the policy allows any header. + /// + /// The current policy builder + public CorsPolicyBuilder AllowAnyHeader() + { + _policy.Headers.Clear(); + _policy.Headers.Add("*"); + return this; + } + + /// + /// Sets the preflightMaxAge for the underlying policy. + /// + /// A positive indicating the time a preflight + /// request can be cached. + /// + public CorsPolicyBuilder SetPreflightMaxAge(TimeSpan preflightMaxAge) + { + _policy.PreflightMaxAge = preflightMaxAge; + return this; + } + + /// + /// Builds a new using the entries added. + /// + /// The constructed . + public CorsPolicy Build() + { + return _policy; + } + + /// + /// Combines the given to the existing properties in the builder. + /// + /// The policy which needs to be combined. + /// The current policy builder + private CorsPolicyBuilder Combine([NotNull] CorsPolicy policy) + { + AddOrigins(policy.Origins.ToArray()); + AddHeaders(policy.Headers.ToArray()); + AddExposedHeaders(policy.ExposedHeaders.ToArray()); + AddMethods(policy.Methods.ToArray()); + SetPreflightMaxAge(policy.PreflightMaxAge.Value); + + if (policy.SupportsCredentials) + { + AllowCredentials(); + } + else + { + DisallowCredentials(); + } + + return this; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsResult.cs b/src/Microsoft.AspNet.Cors.Core/CorsResult.cs new file mode 100644 index 0000000000..024f2b6615 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsResult.cs @@ -0,0 +1,96 @@ +// 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.Globalization; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + /// Results returned by . + /// + public class CorsResult + { + private TimeSpan? _preflightMaxAge; + + /// + /// Gets or sets the allowed origin. + /// + public string AllowedOrigin { get; set; } + + /// + /// Gets or sets a value indicating whether the resource supports user credentials. + /// + public bool SupportsCredentials { get; set; } + + /// + /// Gets the allowed methods. + /// + public IList AllowedMethods { get; } = new List(); + + /// + /// Gets the allowed headers. + /// + public IList AllowedHeaders { get; } = new List(); + + /// + /// Gets the allowed headers that can be exposed on the response. + /// + public IList AllowedExposedHeaders { get; } = new List(); + + /// + /// Gets or sets a value indicating if a 'Vary' header with the value 'Origin' is required. + /// + public bool VaryByOrigin { get; set; } + + /// + /// Gets or sets the for which the results of a preflight request can be cached. + /// + public TimeSpan? PreflightMaxAge + { + get + { + return _preflightMaxAge; + } + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException("value", Resources.PreflightMaxAgeOutOfRange); + } + _preflightMaxAge = value; + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append("AllowCredentials: "); + builder.Append(SupportsCredentials); + builder.Append(", PreflightMaxAge: "); + builder.Append(PreflightMaxAge.HasValue ? + PreflightMaxAge.Value.TotalSeconds.ToString() : "null"); + builder.Append(", AllowOrigin: "); + builder.Append(AllowedOrigin); + builder.Append(", AllowExposedHeaders: {"); + builder.Append(string.Join(",", AllowedExposedHeaders)); + builder.Append("}"); + builder.Append(", AllowHeaders: {"); + builder.Append(string.Join(",", AllowedHeaders)); + builder.Append("}"); + builder.Append(", AllowMethods: {"); + builder.Append(string.Join(",", AllowedMethods)); + builder.Append("}"); + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsService.cs b/src/Microsoft.AspNet.Cors.Core/CorsService.cs new file mode 100644 index 0000000000..9cab11612c --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsService.cs @@ -0,0 +1,212 @@ +// 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 Microsoft.AspNet.Http; +using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + /// Default implementation of . + /// + public class CorsService : ICorsService + { + private readonly CorsOptions _options; + + /// + /// Creates a new instance of the . + /// + /// The option model representing . + public CorsService([NotNull] IOptions options) + { + _options = options.Options; + } + + /// + /// Looks up a policy using the and then evaluates the policy using the passed in + /// . + /// + /// + /// + /// A which contains the result of policy evaluation and can be + /// used by the caller to set apporpriate response headers. + public CorsResult EvaluatePolicy([NotNull] HttpContext context, string policyName) + { + var policy = _options.GetPolicy(policyName); + return EvaluatePolicy(context, policy); + } + + /// + public CorsResult EvaluatePolicy([NotNull] HttpContext context, [NotNull] CorsPolicy policy) + { + var corsResult = new CorsResult(); + var accessControlRequestMethod = context.Request.Headers.Get(CorsConstants.AccessControlRequestMethod); + if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.Ordinal) && + accessControlRequestMethod != null) + { + EvaluatePreflightRequest(context, policy, corsResult); + } + else + { + EvaluateRequest(context, policy, corsResult); + } + + return corsResult; + } + + public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result) + { + var origin = context.Request.Headers.Get(CorsConstants.Origin); + if (origin == null || !policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + { + return; + } + + AddOriginToResult(origin, policy, result); + result.SupportsCredentials = policy.SupportsCredentials; + AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders); + } + + public virtual void EvaluatePreflightRequest(HttpContext context, CorsPolicy policy, CorsResult result) + { + var origin = context.Request.Headers.Get(CorsConstants.Origin); + if (origin == null || !policy.AllowAnyOrigin && !policy.Origins.Contains(origin)) + { + return; + } + + var accessControlRequestMethod = context.Request.Headers.Get(CorsConstants.AccessControlRequestMethod); + if (accessControlRequestMethod == null) + { + return; + } + + var requestHeaders = + context.Request.Headers.GetCommaSeparatedValues(CorsConstants.AccessControlRequestHeaders); + + if (!policy.AllowAnyMethod && !policy.Methods.Contains(accessControlRequestMethod)) + { + return; + } + + if (!policy.AllowAnyHeader && + requestHeaders != null && + !requestHeaders.All(header => policy.Headers.Contains(header, StringComparer.Ordinal))) + { + return; + } + + AddOriginToResult(origin, policy, result); + result.SupportsCredentials = policy.SupportsCredentials; + result.PreflightMaxAge = policy.PreflightMaxAge; + result.AllowedMethods.Add(accessControlRequestMethod); + AddHeaderValues(result.AllowedHeaders, requestHeaders); + } + + /// + public virtual void ApplyResult(CorsResult result, HttpResponse response) + { + var headers = response.Headers; + + if (result.AllowedOrigin != null) + { + headers.Add(CorsConstants.AccessControlAllowOrigin, new[] { result.AllowedOrigin }); + } + + if (result.VaryByOrigin) + { + headers.Set("Vary", "Origin"); + } + + if (result.SupportsCredentials) + { + headers.Add(CorsConstants.AccessControlAllowCredentials, new[] { "true" }); + } + + if (result.AllowedMethods.Count > 0) + { + // Filter out simple methods + var nonSimpleAllowMethods = result.AllowedMethods + .Where(m => + !CorsConstants.SimpleMethods.Contains(m, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + if (nonSimpleAllowMethods.Length > 0) + { + headers.Add(CorsConstants.AccessControlAllowMethods, nonSimpleAllowMethods); + } + } + + if (result.AllowedHeaders.Count > 0) + { + // Filter out simple request headers + var nonSimpleAllowRequestHeaders = result.AllowedHeaders + .Where(header => + !CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + if (nonSimpleAllowRequestHeaders.Length > 0) + { + headers.Add(CorsConstants.AccessControlAllowHeaders, nonSimpleAllowRequestHeaders); + } + } + + if (result.AllowedExposedHeaders.Count > 0) + { + // Filter out simple response headers + var nonSimpleAllowResponseHeaders = result.AllowedExposedHeaders + .Where(header => + !CorsConstants.SimpleResponseHeaders.Contains(header, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + if (nonSimpleAllowResponseHeaders.Length > 0) + { + headers.Add(CorsConstants.AccessControlExposeHeaders, nonSimpleAllowResponseHeaders.ToArray()); + } + } + + if (result.PreflightMaxAge.HasValue) + { + headers.Set( + CorsConstants.AccessControlMaxAge, + result.PreflightMaxAge.Value.TotalSeconds.ToString()); + } + } + + private void AddOriginToResult(string origin, CorsPolicy policy, CorsResult result) + { + if (policy.AllowAnyOrigin) + { + if (policy.SupportsCredentials) + { + result.AllowedOrigin = origin; + result.VaryByOrigin = true; + } + else + { + result.AllowedOrigin = CorsConstants.AnyOrigin; + } + } + else if (policy.Origins.Contains(origin)) + { + result.AllowedOrigin = origin; + } + } + + private static void AddHeaderValues(IList target, IEnumerable headerValues) + { + if (headerValues == null) + { + return; + } + + foreach (var current in headerValues) + { + target.Add(current); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/CorsServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Cors.Core/CorsServiceCollectionExtensions.cs new file mode 100644 index 0000000000..adb38d189a --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/CorsServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +// 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 Microsoft.AspNet.Cors; +using Microsoft.AspNet.Cors.Core; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.Internal; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// The extensions for enabling CORS support. + /// + public static class CorsServiceCollectionExtensions + { + /// + /// Can be used to configure services in the . + /// + /// The service collection which needs to be configured. + /// A delegate which is run to configure the services. + /// + public static IServiceCollection ConfigureCors( + [NotNull] this IServiceCollection serviceCollection, + [NotNull] Action configure) + { + return serviceCollection.Configure(configure); + } + + /// + /// Add services needed to support CORS to the given . + /// + /// The service collection to which CORS services are added. + /// The updated . + public static IServiceCollection AddCors(this IServiceCollection serviceCollection) + { + serviceCollection.AddOptions(); + serviceCollection.AddTransient(); + return serviceCollection; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/DisableCorsAttribute.cs b/src/Microsoft.AspNet.Cors.Core/DisableCorsAttribute.cs new file mode 100644 index 0000000000..4ebcb449b1 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/DisableCorsAttribute.cs @@ -0,0 +1,13 @@ +// 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; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public class DisableCorsAttribute : Attribute, IDisableCorsMetadata + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/EnableCorsAttribute.cs b/src/Microsoft.AspNet.Cors.Core/EnableCorsAttribute.cs new file mode 100644 index 0000000000..d53016e09f --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/EnableCorsAttribute.cs @@ -0,0 +1,24 @@ +// 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; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class EnableCorsAttribute : Attribute, IEnableCorsMetadata + { + /// + /// Creates a new instance of the . + /// + /// The name of the policy to be applied. + public EnableCorsAttribute(string policyName) + { + PolicyName = policyName; + } + + /// + public string PolicyName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/ICorsService.cs b/src/Microsoft.AspNet.Cors.Core/ICorsService.cs new file mode 100644 index 0000000000..59216ee576 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/ICorsService.cs @@ -0,0 +1,32 @@ +// 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.Http; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Cors.Core +{ + /// + /// A type which can evaluate a policy for a particular . + /// + public interface ICorsService + { + /// + /// Evaluates the given using the passed in . + /// + /// The associated with the call. + /// The which needs to be evaluated. + /// A which contains the result of policy evaluation and can be + /// used by the caller to set apporpriate response headers. + CorsResult EvaluatePolicy([NotNull] HttpContext context, [NotNull] CorsPolicy policy); + + + /// + /// Adds CORS-specific response headers to the given . + /// + /// The used to read the allowed values. + /// The associated with the current call. + void ApplyResult([NotNull] CorsResult result, [NotNull] HttpResponse response); + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/IDisableCorsMetadata.cs b/src/Microsoft.AspNet.Cors.Core/IDisableCorsMetadata.cs new file mode 100644 index 0000000000..6afe8fc272 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/IDisableCorsMetadata.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.Cors.Core +{ + /// + /// An interface which can be used to identify a type which provides metdata to disable cors for a resource. + /// + public interface IDisableCorsMetadata + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/IEnableCorsMetadata.cs b/src/Microsoft.AspNet.Cors.Core/IEnableCorsMetadata.cs new file mode 100644 index 0000000000..2270ff7bf5 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/IEnableCorsMetadata.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. + +namespace Microsoft.AspNet.Cors.Core +{ + /// + /// An interface which can be used to identify a type which provides metadata needed for enabling CORS support. + /// + public interface IEnableCorsMetadata + { + /// + /// The name of the policy which needs to be applied. + /// + string PolicyName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Cors.Core/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..f50fb004a2 --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/Properties/Resources.Designer.cs @@ -0,0 +1,110 @@ +// +namespace Microsoft.AspNet.Cors.Core +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Cors.Core.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The collection of headers '{0}' is not allowed. + /// + internal static string HeadersNotAllowed + { + get { return GetString("HeadersNotAllowed"); } + } + + /// + /// The collection of headers '{0}' is not allowed. + /// + internal static string FormatHeadersNotAllowed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HeadersNotAllowed"), p0); + } + + /// + /// The method '{0}' is not allowed. + /// + internal static string MethodNotAllowed + { + get { return GetString("MethodNotAllowed"); } + } + + /// + /// The method '{0}' is not allowed. + /// + internal static string FormatMethodNotAllowed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MethodNotAllowed"), p0); + } + + /// + /// The request does not contain the Origin header. + /// + internal static string NoOriginHeader + { + get { return GetString("NoOriginHeader"); } + } + + /// + /// The request does not contain the Origin header. + /// + internal static string FormatNoOriginHeader() + { + return GetString("NoOriginHeader"); + } + + /// + /// The origin '{0}' is not allowed. + /// + internal static string OriginNotAllowed + { + get { return GetString("OriginNotAllowed"); } + } + + /// + /// The origin '{0}' is not allowed. + /// + internal static string FormatOriginNotAllowed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("OriginNotAllowed"), p0); + } + + /// + /// PreflightMaxAge must be greater than or equal to 0. + /// + internal static string PreflightMaxAgeOutOfRange + { + get { return GetString("PreflightMaxAgeOutOfRange"); } + } + + /// + /// PreflightMaxAge must be greater than or equal to 0. + /// + internal static string FormatPreflightMaxAgeOutOfRange() + { + return GetString("PreflightMaxAgeOutOfRange"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Cors.Core/Resources.resx b/src/Microsoft.AspNet.Cors.Core/Resources.resx new file mode 100644 index 0000000000..ef0e5b5f1f --- /dev/null +++ b/src/Microsoft.AspNet.Cors.Core/Resources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The collection of headers '{0}' is not allowed. + + + The method '{0}' is not allowed. + + + The request does not contain the Origin header. + + + The origin '{0}' is not allowed. + + + PreflightMaxAge must be greater than or equal to 0. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Cors.Core/project.json b/src/Microsoft.AspNet.Cors.Core/project.json index f868fb80f4..db6b0673eb 100644 --- a/src/Microsoft.AspNet.Cors.Core/project.json +++ b/src/Microsoft.AspNet.Cors.Core/project.json @@ -1,7 +1,12 @@ { - "version": "1.0.0-*", - "dependencies": { - }, + "version": "1.0.0-*", + "dependencies": { + "Microsoft.Framework.ConfigurationModel.Interfaces": "1.0.0-*", + "Microsoft.Framework.DependencyInjection.Interfaces": "1.0.0-*", + "Microsoft.Framework.OptionsModel": "1.0.0-*", + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" } + }, "frameworks" : { "dnx451" : { @@ -14,4 +19,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Cors.Core.Test/CorsPolicyBuilderTests.cs b/test/Microsoft.AspNet.Cors.Core.Test/CorsPolicyBuilderTests.cs new file mode 100644 index 0000000000..3a7a7227ec --- /dev/null +++ b/test/Microsoft.AspNet.Cors.Core.Test/CorsPolicyBuilderTests.cs @@ -0,0 +1,233 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.Cors.Core.Test +{ + public class CorsPolicyBuilderTests + { + [Fact] + public void Constructor_WithPolicy_AddsTheGivenPolicy() + { + // Arrange + var policy = new CorsPolicy(); + policy.Origins.Add("http://existing.com"); + policy.Headers.Add("Existing"); + policy.Methods.Add("GET"); + policy.ExposedHeaders.Add("ExistingExposed"); + policy.SupportsCredentials = true; + policy.PreflightMaxAge = TimeSpan.FromSeconds(12); + + // Act + var builder = new CorsPolicyBuilder(policy); + + // Assert + var corsPolicy = builder.Build(); + + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.True(corsPolicy.SupportsCredentials); + Assert.Equal(policy.Headers, corsPolicy.Headers); + Assert.Equal(policy.Methods, corsPolicy.Methods); + Assert.Equal(policy.Origins, corsPolicy.Origins); + Assert.Equal(policy.ExposedHeaders, corsPolicy.ExposedHeaders); + Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); + } + + [Fact] + public void Constructor_WithNoOrigin() + { + // Arrange & Act + var builder = new CorsPolicyBuilder(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Theory] + [InlineData("")] + [InlineData("http://example.com,http://example2.com")] + public void Constructor_WithParamsOrigin_InitializesOrigin(string origin) + { + // Arrange + var origins = origin.Split(','); + + // Act + var builder = new CorsPolicyBuilder(origins); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Equal(origins.ToList(), corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Fact] + public void AddOrigins_AddsOrigins() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AddOrigins("http://example.com", "http://example2.com"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "http://example.com", "http://example2.com" }, corsPolicy.Origins); + } + + [Fact] + public void AllowAnyOrigin_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyOrigin(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "*" }, corsPolicy.Origins); + } + + + [Fact] + public void AddMethods_AddsMethods() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AddMethods("PUT", "GET"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.Equal(new List() { "PUT", "GET" }, corsPolicy.Methods); + } + + [Fact] + public void AllowAnyMethod_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyMethod(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyMethod); + Assert.Equal(new List() { "*" }, corsPolicy.Methods); + } + + [Fact] + public void AddHeaders_AddsHeaders() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AddHeaders("example1", "example2"); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.AllowAnyHeader); + Assert.Equal(new List() { "example1", "example2" }, corsPolicy.Headers); + } + + [Fact] + public void AllowAnyHeaders_AllowsAny() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowAnyHeader(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.AllowAnyHeader); + Assert.Equal(new List() { "*" }, corsPolicy.Headers); + } + + [Fact] + public void AddExposedHeaders_AddsExposedHeaders() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AddExposedHeaders("exposed1", "exposed2"); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(new List() { "exposed1", "exposed2" }, corsPolicy.ExposedHeaders); + } + + [Fact] + public void SetPreFlightMaxAge_SetsThePreFlightAge() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.SetPreflightMaxAge(TimeSpan.FromSeconds(12)); + + // Assert + var corsPolicy = builder.Build(); + Assert.Equal(TimeSpan.FromSeconds(12), corsPolicy.PreflightMaxAge); + } + + [Fact] + public void AllowCredential_SetsSupportsCredentials_ToTrue() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.AllowCredentials(); + + // Assert + var corsPolicy = builder.Build(); + Assert.True(corsPolicy.SupportsCredentials); + } + + + [Fact] + public void DisallowCredential_SetsSupportsCredentials_ToFalse() + { + // Arrange + var builder = new CorsPolicyBuilder(); + + // Act + builder.DisallowCredentials(); + + // Assert + var corsPolicy = builder.Build(); + Assert.False(corsPolicy.SupportsCredentials); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Cors.Core.Test/CorsPolicyTests.cs b/test/Microsoft.AspNet.Cors.Core.Test/CorsPolicyTests.cs new file mode 100644 index 0000000000..af257ed411 --- /dev/null +++ b/test/Microsoft.AspNet.Cors.Core.Test/CorsPolicyTests.cs @@ -0,0 +1,73 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.Cors.Core.Test +{ + public class CorsPolicyTest + { + [Fact] + public void Default_Constructor() + { + // Arrange & Act + var corsPolicy = new CorsPolicy(); + + // Assert + Assert.False(corsPolicy.AllowAnyHeader); + Assert.False(corsPolicy.AllowAnyMethod); + Assert.False(corsPolicy.AllowAnyOrigin); + Assert.False(corsPolicy.SupportsCredentials); + Assert.Empty(corsPolicy.ExposedHeaders); + Assert.Empty(corsPolicy.Headers); + Assert.Empty(corsPolicy.Methods); + Assert.Empty(corsPolicy.Origins); + Assert.Null(corsPolicy.PreflightMaxAge); + } + + [Fact] + public void SettingNegativePreflightMaxAge_Throws() + { + // Arrange + var policy = new CorsPolicy(); + + // Act + var exception = Assert.Throws(() => + { + policy.PreflightMaxAge = TimeSpan.FromSeconds(-12); + }); + + // Assert + Assert.Equal( + "PreflightMaxAge must be greater than or equal to 0.\r\nParameter name: value", + exception.Message); + } + + [Fact] + public void ToString_ReturnsThePropertyValues() + { + // Arrange + var corsPolicy = new CorsPolicy + { + PreflightMaxAge = TimeSpan.FromSeconds(12), + SupportsCredentials = true + }; + corsPolicy.Headers.Add("foo"); + corsPolicy.Headers.Add("bar"); + corsPolicy.Origins.Add("http://example.com"); + corsPolicy.Origins.Add("http://example.org"); + corsPolicy.Methods.Add("GET"); + + // Act + var policyString = corsPolicy.ToString(); + + // Assert + Assert.Equal( + @"AllowAnyHeader: False, AllowAnyMethod: False, AllowAnyOrigin: False, PreflightMaxAge: 12,"+ + " SupportsCredentials: True, Origins: {http://example.com,http://example.org}, Methods: {GET},"+ + " Headers: {foo,bar}, ExposedHeaders: {}", + policyString); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Cors.Core.Test/CorsResultTests.cs b/test/Microsoft.AspNet.Cors.Core.Test/CorsResultTests.cs new file mode 100644 index 0000000000..ab094a86a3 --- /dev/null +++ b/test/Microsoft.AspNet.Cors.Core.Test/CorsResultTests.cs @@ -0,0 +1,69 @@ +// 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 Xunit; + +namespace Microsoft.AspNet.Cors.Core.Test +{ + public class CorsResultTest + { + [Fact] + public void Default_Constructor() + { + // Arrange & Act + var result = new CorsResult(); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.False(result.SupportsCredentials); + Assert.Null(result.AllowedOrigin); + Assert.Null(result.PreflightMaxAge); + } + + [Fact] + public void SettingNegativePreflightMaxAge_Throws() + { + // Arrange + var result = new CorsResult(); + + // Act + var exception = Assert.Throws(() => + { + result.PreflightMaxAge = TimeSpan.FromSeconds(-1); + }); + + // Assert + Assert.Equal( + "PreflightMaxAge must be greater than or equal to 0.\r\nParameter name: value", + exception.Message); + } + + [Fact] + public void ToString_ReturnsThePropertyValues() + { + // Arrange + var corsResult = new CorsResult + { + SupportsCredentials = true, + PreflightMaxAge = TimeSpan.FromSeconds(30), + AllowedOrigin = "*" + }; + corsResult.AllowedExposedHeaders.Add("foo"); + corsResult.AllowedHeaders.Add("bar"); + corsResult.AllowedHeaders.Add("baz"); + corsResult.AllowedMethods.Add("GET"); + + // Act + var result = corsResult.ToString(); + + // Assert + Assert.Equal( + @"AllowCredentials: True, PreflightMaxAge: 30, AllowOrigin: *," + + " AllowExposedHeaders: {foo}, AllowHeaders: {bar,baz}, AllowMethods: {GET}", + result); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Cors.Core.Test/CorsServiceTests.cs b/test/Microsoft.AspNet.Cors.Core.Test/CorsServiceTests.cs new file mode 100644 index 0000000000..24c20b1b2e --- /dev/null +++ b/test/Microsoft.AspNet.Cors.Core.Test/CorsServiceTests.cs @@ -0,0 +1,904 @@ +// 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 Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core; +using Microsoft.Framework.OptionsModel; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Cors.Core.Test +{ + public class CorsServiceTests + { + [Fact] + public void EvaluatePolicy_NoOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext("GET", origin: null); + + // Act + var result = corsService.EvaluatePolicy(requestContext, new CorsPolicy()); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_NoMatchingOrigin_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add("bar"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_EmptyOriginsPolicy_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.AllowedOrigin); + Assert.False(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_DoesNotSupportCredentials_EmitsWildcardForOrigin() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + + var policy = new CorsPolicy + { + SupportsCredentials = false + }; + + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("*", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_AllowAnyOrigin_SupportsCredentials_AddsSpecificOrigin() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + Assert.True(result.VaryByOrigin); + } + + [Fact] + public void EvaluatePolicy_DoesNotSupportCredentials_AllowCredentialsReturnsFalse() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = false + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.False(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_SupportsCredentials_AllowCredentialsReturnsTrue() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_NoExposedHeaders_NoAllowExposedHeaders() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_OneExposedHeaders_HeadersAllowed() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.ExposedHeaders.Add("foo"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedExposedHeaders.Count); + Assert.Contains("foo", result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_ManyExposedHeaders_HeadersAllowed() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(origin: "http://example.com"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.ExposedHeaders.Add("foo"); + policy.ExposedHeaders.Add("bar"); + policy.ExposedHeaders.Add("baz"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(3, result.AllowedExposedHeaders.Count); + Assert.Contains("foo", result.AllowedExposedHeaders); + Assert.Contains("bar", result.AllowedExposedHeaders); + Assert.Contains("baz", result.AllowedExposedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_MethodNotAllowed_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("GET"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_MethodAllowed_ReturnsAllowMethods() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("PUT"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.NotNull(result); + Assert.Contains("PUT", result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_OriginAllowed_ReturnsOrigin() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Origins.Add("http://example.com"); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal("http://example.com", result.AllowedOrigin); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_SupportsCredentials_AllowCredentialsReturnsTrue() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + SupportsCredentials = true + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.True(result.SupportsCredentials); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_NoPreflightMaxAge_NoPreflightMaxAgeSet() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + PreflightMaxAge = null + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Null(result.PreflightMaxAge); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_PreflightMaxAge_PreflightMaxAgeSet() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy + { + PreflightMaxAge = TimeSpan.FromSeconds(10) + }; + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(TimeSpan.FromSeconds(10), result.PreflightMaxAge); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_AnyMethod_ReturnsRequestMethod() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "GET"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedMethods.Count); + Assert.Contains("GET", result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_ListedMethod_ReturnsSubsetOfListedMethods() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("PUT"); + policy.Methods.Add("DELETE"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedMethods.Count); + Assert.Contains("PUT", result.AllowedMethods); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_NoHeadersRequested_AllowedAllHeaders_ReturnsEmptyHeaders() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext(method: "OPTIONS", origin: "http://example.com", accessControlRequestMethod: "PUT"); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_AllowAllHeaders_ReturnsRequestedHeaders() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "foo", "bar" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("*"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(2, result.AllowedHeaders.Count); + Assert.Contains("foo", result.AllowedHeaders); + Assert.Contains("bar", result.AllowedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_AllowSomeHeaders_ReturnsSubsetOfListedHeaders() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "Content-Type" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("foo"); + policy.Headers.Add("bar"); + policy.Headers.Add("Content-Type"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Equal(1, result.AllowedHeaders.Count); + Assert.Contains("Content-Type", result.AllowedHeaders); + } + + [Fact] + public void EvaluatePolicy_PreflightRequest_HeadersRequested_NotAllHeaderMatches_ReturnsInvalidResult() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + var requestContext = GetHttpContext( + method: "OPTIONS", + origin: "http://example.com", + accessControlRequestMethod: "PUT", + accessControlRequestHeaders: new[] { "match", "noMatch" }); + var policy = new CorsPolicy(); + policy.Origins.Add(CorsConstants.AnyOrigin); + policy.Methods.Add("*"); + policy.Headers.Add("match"); + policy.Headers.Add("foo"); + + // Act + var result = corsService.EvaluatePolicy(requestContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Null(result.AllowedOrigin); + } + + [Fact] + public void EaluatePolicy_DoesCaseSensitiveComparison() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + + var policy = new CorsPolicy(); + policy.Methods.Add("POST"); + var httpContext = GetHttpContext(origin: null, accessControlRequestMethod: "post"); + + // Act + var result = corsService.EvaluatePolicy(httpContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Null(result.AllowedOrigin); + } + + [Fact] + public void TryValidateOrigin_DoesCaseSensitiveComparison() + { + // Arrange + var corsService = new CorsService(Mock.Of>()); + + var policy = new CorsPolicy(); + policy.Origins.Add("http://Example.com"); + var httpContext = GetHttpContext(origin: "http://example.com"); + + // Act + var result = corsService.EvaluatePolicy(httpContext, policy); + + // Assert + Assert.Empty(result.AllowedHeaders); + Assert.Empty(result.AllowedMethods); + Assert.Empty(result.AllowedExposedHeaders); + Assert.Null(result.AllowedOrigin); + } + + + [Fact] + public void ApplyResult_ReturnsNoHeaders_ByDefault() + { + // Arrange + var result = new CorsResult(); + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Empty(httpContext.Response.Headers); + } + + [Fact] + public void ApplyResult_AllowOrigin_AllowOriginHeaderAdded() + { + // Arrange + var result = new CorsResult + { + AllowedOrigin = "http://example.com" + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("http://example.com", httpContext.Response.Headers["Access-Control-Allow-Origin"]); + } + + [Fact] + public void ApplyResult_NoAllowOrigin_AllowOriginHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + AllowedOrigin = null + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Origin", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_AllowCredentials_AllowCredentialsHeaderAdded() + { + // Arrange + var result = new CorsResult + { + SupportsCredentials = true + }; + + var service = new CorsService(Mock.Of>()); + + // Act + var httpContext = new DefaultHttpContext(); + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("true", httpContext.Response.Headers["Access-Control-Allow-Credentials"]); + } + + [Fact] + public void ApplyResult_AddVaryHeader_VaryHeaderAdded() + { + // Arrange + var result = new CorsResult + { + VaryByOrigin = true + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("Origin", httpContext.Response.Headers["Vary"]); + } + + [Fact] + public void ApplyResult_NoAllowCredentials_AllowCredentialsHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + SupportsCredentials = false + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Credentials", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowMethods_AllowMethodsHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowMethods is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowMethods_AllowMethodsHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedMethods.Add("PUT"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("PUT", httpContext.Response.Headers["Access-Control-Allow-Methods"]); + } + + [Fact] + public void ApplyResult_SomeSimpleAllowMethods_AllowMethodsHeaderAddedForNonSimpleMethods() + { + // Arrange + var result = new CorsResult(); + result.AllowedMethods.Add("PUT"); + result.AllowedMethods.Add("get"); + result.AllowedMethods.Add("DELETE"); + result.AllowedMethods.Add("POST"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + var methods = httpContext.Response.Headers["Access-Control-Allow-Methods"].Split(','); + Assert.Equal(2, methods.Length); + Assert.Contains("PUT", methods); + Assert.Contains("DELETE", methods); + } + + [Fact] + public void ApplyResult_SimpleAllowMethods_AllowMethodsHeaderNotAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedMethods.Add("GET"); + result.AllowedMethods.Add("HEAD"); + result.AllowedMethods.Add("POST"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Methods", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowHeaders_AllowHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowHeaders is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowHeaders_AllowHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("foo"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Allow-Headers"]); + } + + [Fact] + public void ApplyResult_ManyAllowHeaders_AllowHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("foo"); + result.AllowedHeaders.Add("bar"); + result.AllowedHeaders.Add("baz"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + string[] headerValues = httpContext.Response.Headers["Access-Control-Allow-Headers"].Split(','); + Assert.Equal(3, headerValues.Length); + Assert.Contains("foo", headerValues); + Assert.Contains("bar", headerValues); + Assert.Contains("baz", headerValues); + } + + [Fact] + public void ApplyResult_SomeSimpleAllowHeaders_AllowHeadersHeaderAddedForNonSimpleHeaders() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("Content-Language"); + result.AllowedHeaders.Add("foo"); + result.AllowedHeaders.Add("bar"); + result.AllowedHeaders.Add("Accept"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + string[] headerValues = httpContext.Response.Headers["Access-Control-Allow-Headers"].Split(','); + Assert.Equal(2, headerValues.Length); + Assert.Contains("foo", headerValues); + Assert.Contains("bar", headerValues); + } + + [Fact] + public void ApplyResult_SimpleAllowHeaders_AllowHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedHeaders.Add("Accept"); + result.AllowedHeaders.Add("Accept-Language"); + result.AllowedHeaders.Add("Content-Language"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Allow-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_NoAllowExposedHeaders_ExposedHeadersHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + // AllowExposedHeaders is empty by default + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_OneAllowExposedHeaders_ExposedHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedExposedHeaders.Add("foo"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("foo", httpContext.Response.Headers["Access-Control-Expose-Headers"]); + } + + [Fact] + public void ApplyResult_ManyAllowExposedHeaders_ExposedHeadersHeaderAdded() + { + // Arrange + var result = new CorsResult(); + result.AllowedExposedHeaders.Add("foo"); + result.AllowedExposedHeaders.Add("bar"); + result.AllowedExposedHeaders.Add("baz"); + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Contains("Access-Control-Expose-Headers", httpContext.Response.Headers.Keys); + string[] exposedHeaderValues = httpContext.Response.Headers["Access-Control-Expose-Headers"].Split(','); + Assert.Equal(3, exposedHeaderValues.Length); + Assert.Contains("foo", exposedHeaderValues); + Assert.Contains("bar", exposedHeaderValues); + Assert.Contains("baz", exposedHeaderValues); + } + + [Fact] + public void ApplyResult_NoPreflightMaxAge_MaxAgeHeaderNotAdded() + { + // Arrange + var result = new CorsResult + { + PreflightMaxAge = null + }; + + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.DoesNotContain("Access-Control-Max-Age", httpContext.Response.Headers.Keys); + } + + [Fact] + public void ApplyResult_PreflightMaxAge_MaxAgeHeaderAdded() + { + // Arrange + var result = new CorsResult + { + PreflightMaxAge = TimeSpan.FromSeconds(30) + }; + var httpContext = new DefaultHttpContext(); + var service = new CorsService(Mock.Of>()); + + // Act + service.ApplyResult(result, httpContext.Response); + + // Assert + Assert.Equal("30", httpContext.Response.Headers["Access-Control-Max-Age"]); + } + + + + private static HttpContext GetHttpContext( + string method = null, + string origin = null, + string accessControlRequestMethod = null, + string[] accessControlRequestHeaders = null) + { + var context = new DefaultHttpContext(); + + if (method != null) + { + context.Request.Method = method; + } + + if (origin != null) + { + context.Request.Headers.Add(CorsConstants.Origin, new[] { origin }); + } + + if (accessControlRequestMethod != null) + { + context.Request.Headers.Add(CorsConstants.AccessControlRequestMethod, new[] { accessControlRequestMethod }); + } + + if (accessControlRequestHeaders != null) + { + context.Request.Headers.Add(CorsConstants.AccessControlRequestHeaders, accessControlRequestHeaders); + } + + return context; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Cors.Core.Test/Microsoft.AspNet.Cors.Core.Test.kproj b/test/Microsoft.AspNet.Cors.Core.Test/Microsoft.AspNet.Cors.Core.Test.kproj new file mode 100644 index 0000000000..11bdbb5b21 --- /dev/null +++ b/test/Microsoft.AspNet.Cors.Core.Test/Microsoft.AspNet.Cors.Core.Test.kproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + b4f83a06-eb8e-4186-84c4-c6daf7eb03d4 + Microsoft.AspNet.Cors.Core.Test + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Cors.Core.Test/project.json b/test/Microsoft.AspNet.Cors.Core.Test/project.json new file mode 100644 index 0000000000..3391960147 --- /dev/null +++ b/test/Microsoft.AspNet.Cors.Core.Test/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + "Microsoft.AspNet.Cors.Core": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", + "Moq": "4.2.1312.1622", + "xunit.runner.kre": "1.0.0-*" + }, + + "commands": { + "test": "xunit.runner.kre" + }, + + "frameworks" : { + "dnx451" : { + "dependencies": { + } + } + } +}