// 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Routing.Matching
{
///
/// An that implements filtering and selection by
/// the HTTP method of a request.
///
public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
{
// Used in tests
internal static readonly string OriginHeader = "Origin";
internal static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
internal static readonly string PreflightHttpMethod = "OPTIONS";
// Used in tests
internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported";
// Used in tests
internal const string AnyMethod = "*";
///
/// For framework use only.
///
public IComparer Comparer => new HttpMethodMetadataEndpointComparer();
// The order value is chosen to be less than 0, so that it comes before naively
// written policies.
///
/// For framework use only.
///
public override int Order => -1000;
///
/// For framework use only.
///
///
///
public bool AppliesToNode(IReadOnlyList endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
for (var i = 0; i < endpoints.Count; i++)
{
if (endpoints[i].Metadata.GetMetadata()?.HttpMethods.Any() == true)
{
return true;
}
}
return false;
}
///
/// For framework use only.
///
///
///
public IReadOnlyList GetEdges(IReadOnlyList endpoints)
{
// The algorithm here is designed to be preserve the order of the endpoints
// while also being relatively simple. Preserving order is important.
// First, build a dictionary of all possible HTTP method/CORS combinations
// that exist in this list of endpoints.
//
// For now we're just building up the set of keys. We don't add any endpoints
// to lists now because we don't want ordering problems.
var allHttpMethods = new HashSet(StringComparer.OrdinalIgnoreCase);
var edges = new Dictionary>();
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint);
// If the action doesn't list HTTP methods then it supports all methods.
// In this phase we use a sentinel value to represent the *other* HTTP method
// a state that represents any HTTP method that doesn't have a match.
if (httpMethods.Count == 0)
{
httpMethods = new[] { AnyMethod, };
}
for (var j = 0; j < httpMethods.Count; j++)
{
// An endpoint that allows CORS reqests will match both CORS and non-CORS
// so we model it as both.
var httpMethod = httpMethods[j];
var key = new EdgeKey(httpMethod, acceptCorsPreFlight);
if (!edges.ContainsKey(key))
{
edges.Add(key, new List());
}
// An endpoint that allows CORS reqests will match both CORS and non-CORS
// so we model it as both.
if (acceptCorsPreFlight)
{
key = new EdgeKey(httpMethod, false);
if (!edges.ContainsKey(key))
{
edges.Add(key, new List());
}
}
// Also if it's not the *any* method key, then track it.
if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase))
{
allHttpMethods.Add(httpMethod);
}
}
}
// Now in a second loop, add endpoints to these lists. We've enumerated all of
// the states, so we want to see which states this endpoint matches.
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint);
if (httpMethods.Count == 0)
{
// OK this means that this endpoint matches *all* HTTP methods.
// So, loop and add it to all states.
foreach (var kvp in edges)
{
if (acceptCorsPreFlight || !kvp.Key.IsCorsPreflightRequest)
{
kvp.Value.Add(endpoint);
}
}
}
else
{
// OK this endpoint matches specific methods.
for (var j = 0; j < httpMethods.Count; j++)
{
var httpMethod = httpMethods[j];
var key = new EdgeKey(httpMethod, acceptCorsPreFlight);
edges[key].Add(endpoint);
// An endpoint that allows CORS reqests will match both CORS and non-CORS
// so we model it as both.
if (acceptCorsPreFlight)
{
key = new EdgeKey(httpMethod, false);
edges[key].Add(endpoint);
}
}
}
}
// Adds a very low priority endpoint that will reject the request with
// a 405 if nothing else can handle this verb. This is only done if
// no other actions exist that handle the 'all verbs'.
//
// The rationale for this is that we want to report a 405 if none of
// the supported methods match, but we don't want to report a 405 in a
// case where an application defines an endpoint that handles all verbs, but
// a constraint rejects the request, or a complex segment fails to parse. We
// consider a case like that a 'user input validation' failure rather than
// a semantic violation of HTTP.
//
// This will make 405 much more likely in API-focused applications, and somewhat
// unlikely in a traditional MVC application. That's good.
//
// We don't bother returning a 405 when the CORS preflight method doesn't exist.
// The developer calling the API will see it as a CORS error, which is fine because
// there isn't an endpoint to check for a CORS policy.
if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches))
{
// Methods sorted for testability.
var endpoint = CreateRejectionEndpoint(allHttpMethods.OrderBy(m => m));
matches = new List() { endpoint, };
edges[new EdgeKey(AnyMethod, false)] = matches;
}
return edges
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
.ToArray();
(IReadOnlyList httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e)
{
var metadata = e.Metadata.GetMetadata();
return metadata == null ? (Array.Empty(), false) : (metadata.HttpMethods, metadata.AcceptCorsPreflight);
}
}
///
/// For framework use only.
///
///
///
///
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges)
{
var destinations = new Dictionary(StringComparer.OrdinalIgnoreCase);
var corsPreflightDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < edges.Count; i++)
{
// We create this data, so it's safe to cast it.
var key = (EdgeKey)edges[i].State;
if (key.IsCorsPreflightRequest)
{
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
}
else
{
destinations.Add(key.HttpMethod, edges[i].Destination);
}
}
int corsPreflightExitDestination = exitDestination;
if (corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
corsPreflightExitDestination = matchesAnyVerb;
corsPreflightDestinations.Remove(AnyMethod);
}
if (destinations.TryGetValue(AnyMethod, out matchesAnyVerb))
{
// If we have endpoints that match any HTTP method, use that as the exit.
exitDestination = matchesAnyVerb;
destinations.Remove(AnyMethod);
}
return new HttpMethodPolicyJumpTable(
exitDestination,
destinations,
corsPreflightExitDestination,
corsPreflightDestinations);
}
private Endpoint CreateRejectionEndpoint(IEnumerable httpMethods)
{
var allow = string.Join(", ", httpMethods);
return new Endpoint(
(context) =>
{
context.Response.StatusCode = 405;
context.Response.Headers.Add("Allow", allow);
return Task.CompletedTask;
},
EndpointMetadataCollection.Empty,
Http405EndpointDisplayName);
}
private class HttpMethodPolicyJumpTable : PolicyJumpTable
{
private readonly int _exitDestination;
private readonly Dictionary _destinations;
private readonly int _corsPreflightExitDestination;
private readonly Dictionary _corsPreflightDestinations;
private readonly bool _supportsCorsPreflight;
public HttpMethodPolicyJumpTable(
int exitDestination,
Dictionary destinations,
int corsPreflightExitDestination,
Dictionary corsPreflightDestinations)
{
_exitDestination = exitDestination;
_destinations = destinations;
_corsPreflightExitDestination = corsPreflightExitDestination;
_corsPreflightDestinations = corsPreflightDestinations;
_supportsCorsPreflight = _corsPreflightDestinations.Count > 0;
}
public override int GetDestination(HttpContext httpContext)
{
int destination;
var httpMethod = httpContext.Request.Method;
if (_supportsCorsPreflight &&
string.Equals(httpMethod, PreflightHttpMethod, StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Headers.ContainsKey(OriginHeader) &&
httpContext.Request.Headers.TryGetValue(AccessControlRequestMethod, out var accessControlRequestMethod) &&
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
return _corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination)
? destination
: _corsPreflightExitDestination;
}
return _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination;
}
}
private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer
{
protected override int CompareMetadata(IHttpMethodMetadata x, IHttpMethodMetadata y)
{
// Ignore the metadata if it has an empty list of HTTP methods.
return base.CompareMetadata(
x?.HttpMethods.Count > 0 ? x : null,
y?.HttpMethods.Count > 0 ? y : null);
}
}
internal readonly struct EdgeKey : IEquatable, IComparable, IComparable
{
// Note that in contrast with the metadata, the edge represents a possible state change
// rather than a list of what's allowed. We represent CORS and non-CORS requests as separate
// states.
public readonly bool IsCorsPreflightRequest;
public readonly string HttpMethod;
public EdgeKey(string httpMethod, bool isCorsPreflightRequest)
{
HttpMethod = httpMethod;
IsCorsPreflightRequest = isCorsPreflightRequest;
}
// These are comparable so they can be sorted in tests.
public int CompareTo(EdgeKey other)
{
var compare = HttpMethod.CompareTo(other.HttpMethod);
if (compare != 0)
{
return compare;
}
return IsCorsPreflightRequest.CompareTo(other.IsCorsPreflightRequest);
}
public int CompareTo(object obj)
{
return CompareTo((EdgeKey)obj);
}
public bool Equals(EdgeKey other)
{
return
IsCorsPreflightRequest == other.IsCorsPreflightRequest &&
string.Equals(HttpMethod, other.HttpMethod, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object obj)
{
var other = obj as EdgeKey?;
return other == null ? false : Equals(other.Value);
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(IsCorsPreflightRequest);
hash.Add(HttpMethod, StringComparer.Ordinal);
return hash;
}
// Used in GraphViz output.
public override string ToString()
{
return IsCorsPreflightRequest ? $"CORS: {HttpMethod}" : $"HTTP: {HttpMethod}";
}
}
}
}