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": {
+ }
+ }
+ }
+}