| File: Infrastructure\CorsPolicyBuilder.cs | Web Access |
| Project: src\aspnetcore\src\Middleware\CORS\src\Microsoft.AspNetCore.Cors.csproj (Microsoft.AspNetCore.Cors) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; namespace Microsoft.AspNetCore.Cors.Infrastructure; /// <summary> /// Exposes methods to build a policy. /// </summary> public class CorsPolicyBuilder { private readonly CorsPolicy _policy = new CorsPolicy(); /// <summary> /// Creates a new instance of the <see cref="CorsPolicyBuilder"/>. /// </summary> /// <param name="origins">list of origins which can be added.</param> /// <remarks> <see cref="WithOrigins(string[])"/> for details on normalizing the origin value.</remarks> public CorsPolicyBuilder(params string[] origins) { WithOrigins(origins); } /// <summary> /// Creates a new instance of the <see cref="CorsPolicyBuilder"/>. /// </summary> /// <param name="policy">The policy which will be used to initialize the builder.</param> public CorsPolicyBuilder(CorsPolicy policy) { Combine(policy); } /// <summary> /// Adds the specified <paramref name="origins"/> to the policy. /// </summary> /// <param name="origins">The origins that are allowed.</param> /// <returns>The current policy builder.</returns> /// <remarks> /// This method normalizes the origin value prior to adding it to <see cref="CorsPolicy.Origins"/> to match /// the normalization performed by the browser on the value sent in the <c>ORIGIN</c> header. /// <list type="bullet"> /// <item> /// <description>If the specified origin has an internationalized domain name (IDN), the punycoded value is used. If the origin /// specifies a default port (e.g. 443 for HTTPS or 80 for HTTP), this will be dropped as part of normalization. /// Finally, the scheme and punycoded host name are culture invariant lower cased before being added to the <see cref="CorsPolicy.Origins"/> /// collection.</description> /// </item> /// <item> /// <description>For all other origins, normalization involves performing a culture invariant lower casing of the host name.</description> /// </item> /// </list> /// </remarks> public CorsPolicyBuilder WithOrigins(params string[] origins) { ArgumentNullException.ThrowIfNull(origins); foreach (var origin in origins) { var normalizedOrigin = GetNormalizedOrigin(origin); _policy.Origins.Add(normalizedOrigin); } return this; } internal static string GetNormalizedOrigin(string origin) { ArgumentNullException.ThrowIfNull(origin); if (Uri.TryCreate(origin, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) && !string.Equals(uri.IdnHost, uri.Host, StringComparison.Ordinal)) { var builder = new UriBuilder(uri.Scheme.ToLowerInvariant(), uri.IdnHost.ToLowerInvariant()); if (!uri.IsDefaultPort) { // Uri does not have a way to differentiate between a port value inferred by default (e.g. Port = 80 for http://www.example.com) and // a default port value that is specified (e.g. Port = 80 for http://www.example.com:80). Although the HTTP or FETCH spec does not say // anything about including the default port as part of the Origin header, at the time of writing, browsers drop "default" port when navigating // and when sending the Origin header. All this goes to say, it appears OK to drop an explicitly specified port, // if it is the default port when working with an IDN host. builder.Port = uri.Port; } return builder.Uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped); } return origin.ToLowerInvariant(); } /// <summary> /// Adds the specified <paramref name="headers"/> to the policy. /// </summary> /// <param name="headers">The headers which need to be allowed in the request.</param> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder WithHeaders(params string[] headers) { foreach (var req in headers) { _policy.Headers.Add(req); } return this; } /// <summary> /// Adds the specified <paramref name="exposedHeaders"/> to the policy. /// </summary> /// <param name="exposedHeaders">The headers which need to be exposed to the client.</param> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder WithExposedHeaders(params string[] exposedHeaders) { foreach (var req in exposedHeaders) { _policy.ExposedHeaders.Add(req); } return this; } /// <summary> /// Adds the specified <paramref name="methods"/> to the policy. /// </summary> /// <param name="methods">The methods which need to be added to the policy.</param> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder WithMethods(params string[] methods) { foreach (var req in methods) { _policy.Methods.Add(req); } return this; } /// <summary> /// Sets the policy to allow credentials. /// </summary> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder AllowCredentials() { _policy.SupportsCredentials = true; return this; } /// <summary> /// Sets the policy to not allow credentials. /// </summary> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder DisallowCredentials() { _policy.SupportsCredentials = false; return this; } /// <summary> /// Ensures that the policy allows any origin. /// </summary> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder AllowAnyOrigin() { _policy.Origins.Clear(); _policy.Origins.Add(CorsConstants.AnyOrigin); return this; } /// <summary> /// Ensures that the policy allows any method. /// </summary> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder AllowAnyMethod() { _policy.Methods.Clear(); _policy.Methods.Add(CorsConstants.AnyMethod); return this; } /// <summary> /// Ensures that the policy allows any header. /// </summary> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder AllowAnyHeader() { _policy.Headers.Clear(); _policy.Headers.Add(CorsConstants.AnyHeader); return this; } /// <summary> /// Sets the preflightMaxAge for the underlying policy. /// </summary> /// <param name="preflightMaxAge">A positive <see cref="TimeSpan"/> indicating the time a preflight /// request can be cached.</param> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder SetPreflightMaxAge(TimeSpan preflightMaxAge) { _policy.PreflightMaxAge = preflightMaxAge; return this; } /// <summary> /// Sets the specified <paramref name="isOriginAllowed"/> for the underlying policy. /// </summary> /// <param name="isOriginAllowed">The function used by the policy to evaluate if an origin is allowed.</param> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder SetIsOriginAllowed(Func<string, bool> isOriginAllowed) { _policy.IsOriginAllowed = isOriginAllowed; return this; } /// <summary> /// Sets the <see cref="CorsPolicy.IsOriginAllowed"/> property of the policy to be a function /// that allows origins to match a configured wildcarded domain when evaluating if the /// origin is allowed. /// </summary> /// <returns>The current policy builder.</returns> public CorsPolicyBuilder SetIsOriginAllowedToAllowWildcardSubdomains() { _policy.IsOriginAllowed = _policy.IsOriginAnAllowedSubdomain; return this; } /// <summary> /// Builds a new <see cref="CorsPolicy"/> using the entries added. /// </summary> /// <returns>The constructed <see cref="CorsPolicy"/>.</returns> public CorsPolicy Build() { if (_policy.AllowAnyOrigin && _policy.SupportsCredentials) { throw new InvalidOperationException(Resources.InsecureConfiguration); } return _policy; } /// <summary> /// Combines the given <paramref name="policy"/> to the existing properties in the builder. /// </summary> /// <param name="policy">The policy which needs to be combined.</param> /// <returns>The current policy builder.</returns> private CorsPolicyBuilder Combine(CorsPolicy policy) { ArgumentNullException.ThrowIfNull(policy); WithOrigins(policy.Origins.ToArray()); WithHeaders(policy.Headers.ToArray()); WithExposedHeaders(policy.ExposedHeaders.ToArray()); WithMethods(policy.Methods.ToArray()); SetIsOriginAllowed(policy.IsOriginAllowed); if (policy.PreflightMaxAge.HasValue) { SetPreflightMaxAge(policy.PreflightMaxAge.Value); } if (policy.SupportsCredentials) { AllowCredentials(); } else { DisallowCredentials(); } return this; } }