// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Cors;
using System.Web.Http.Cors.Properties;
namespace System.Web.Http.Cors
{
///
/// This class defines an attribute that can be applied to an action or a controller to enable CORS.
/// By default, it allows all origins, methods and headers.
///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
[SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "Attribute arguments are accessible as collections.")]
public sealed class EnableCorsAttribute : Attribute, ICorsPolicyProvider
{
private CorsPolicy _corsPolicy;
private bool _originsValidated;
///
/// Initializes a new instance of the class.
///
/// Comma-separated list of origins that are allowed to access the resource. Use "*" to allow all.
/// Comma-separated list of headers that are supported by the resource. Use "*" to allow all. Use null or empty string to allow none.
///
/// Comma-separated list of methods that are supported by the resource. Use "*" to allow all. Use null or empty string to allow none.
/// Note:
/// Http verbs are case-sensitive, if you don't use "*", you should use upper case when specifying GET, PUT, POST, DELETE etc.
/// For example:
/// var cors = new EnableCorsAttribute("http://localhost:1234", "*", "GET,PUT,POST,DELETE");
///
public EnableCorsAttribute(string origins, string headers, string methods)
: this(origins, headers, methods, null)
{
}
///
/// Initializes a new instance of the class.
///
/// Comma-separated list of origins that are allowed to access the resource. Use "*" to allow all.
/// Comma-separated list of headers that are supported by the resource. Use "*" to allow all. Use null or empty string to allow none.
///
/// Comma-separated list of methods that are supported by the resource. Use "*" to allow all. Use null or empty string to allow none.
/// Note:
/// Http verbs are case-sensitive, if you don't use "*", you should use upper case when specifying GET, PUT, POST, DELETE etc.
/// For example:
/// var cors = new EnableCorsAttribute("http://localhost:1234", "*", "GET,PUT,POST,DELETE");
///
/// Comma-separated list of headers that the resource might use and can be exposed. Use null or empty string to expose none.
public EnableCorsAttribute(string origins, string headers, string methods, string exposedHeaders)
{
if (String.IsNullOrEmpty(origins))
{
throw new ArgumentException(
SRResources.ArgumentCannotBeNullOrEmpty,
"origins");
}
_corsPolicy = new CorsPolicy();
if (origins == "*")
{
_corsPolicy.AllowAnyOrigin = true;
}
else
{
AddCommaSeparatedValuesToCollection(origins, _corsPolicy.Origins);
}
if (!String.IsNullOrEmpty(headers))
{
if (headers == "*")
{
_corsPolicy.AllowAnyHeader = true;
}
else
{
AddCommaSeparatedValuesToCollection(headers, _corsPolicy.Headers);
}
}
if (!String.IsNullOrEmpty(methods))
{
if (methods == "*")
{
_corsPolicy.AllowAnyMethod = true;
}
else
{
AddCommaSeparatedValuesToCollection(methods, _corsPolicy.Methods);
}
}
if (!String.IsNullOrEmpty(exposedHeaders))
{
AddCommaSeparatedValuesToCollection(exposedHeaders, _corsPolicy.ExposedHeaders);
}
}
///
/// Gets the headers that the resource might use and can be exposed.
///
public IList ExposedHeaders
{
get
{
return _corsPolicy.ExposedHeaders;
}
}
///
/// Gets the headers that are supported by the resource.
///
public IList Headers
{
get
{
return _corsPolicy.Headers;
}
}
///
/// Gets the methods that are supported by the resource.
///
public IList Methods
{
get
{
return _corsPolicy.Methods;
}
}
///
/// Gets the origins that are allowed to access the resource.
///
public IList Origins
{
get
{
return _corsPolicy.Origins;
}
}
///
/// Gets or sets the number of seconds the results of a preflight request can be cached.
///
public long PreflightMaxAge
{
get
{
return _corsPolicy.PreflightMaxAge ?? -1;
}
set
{
_corsPolicy.PreflightMaxAge = value;
}
}
///
/// Gets or sets a value indicating whether the resource supports user credentials in the request.
///
public bool SupportsCredentials
{
get
{
return _corsPolicy.SupportsCredentials;
}
set
{
_corsPolicy.SupportsCredentials = value;
}
}
///
public Task GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!_originsValidated)
{
ValidateOrigins(_corsPolicy.Origins);
_originsValidated = true;
}
return Task.FromResult(_corsPolicy);
}
private static void ValidateOrigins(IList origins)
{
foreach (string origin in origins)
{
if (String.IsNullOrEmpty(origin))
{
throw new InvalidOperationException(SRResources.OriginCannotBeNullOrEmpty);
}
if (origin.EndsWith("/", StringComparison.Ordinal))
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
SRResources.OriginCannotEndWithSlash,
origin));
}
if (!Uri.IsWellFormedUriString(origin, UriKind.Absolute))
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
SRResources.OriginNotWellFormed,
origin));
}
Uri originUri = new Uri(origin);
if ((!String.IsNullOrEmpty(originUri.AbsolutePath) && !String.Equals(originUri.AbsolutePath, "/", StringComparison.Ordinal)) ||
!String.IsNullOrEmpty(originUri.Query) ||
!String.IsNullOrEmpty(originUri.Fragment))
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
SRResources.OriginMustNotContainPathQueryOrFragment,
origin));
}
}
}
private static void AddCommaSeparatedValuesToCollection(string commaSeparatedValues, IList collection)
{
string[] values = commaSeparatedValues.Split(',');
for (int i = 0; i < values.Length; i++)
{
string value = values[i].Trim();
if (!String.IsNullOrEmpty(value))
{
collection.Add(value);
}
}
}
}
}