aspnetcore/src/Microsoft.AspNetCore.Mvc.Core/Routing/ConsumesMatcherPolicy.cs

244 lines
9.4 KiB
C#

// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Mvc.Routing
{
internal class ConsumesMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
{
internal const string Http415EndpointDisplayName = "415 HTTP Unsupported Media Type";
internal const string AnyContentType = "*/*";
// Run after HTTP methods, but before 'default'.
public override int Order { get; } = -100;
public IComparer<Endpoint> Comparer { get; } = new ConsumesMetadataEndpointComparer();
public bool AppliesToNode(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
return endpoints.Any(e => e.Metadata.GetMetadata<IConsumesMetadata>()?.ContentTypes.Count > 0);
}
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(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 of the content-type patterns that are included
// at this node.
//
// 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 edges = new Dictionary<string, List<Endpoint>>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < endpoints.Count; i++)
{
var endpoint = endpoints[i];
var contentTypes = endpoint.Metadata.GetMetadata<IConsumesMetadata>()?.ContentTypes;
if (contentTypes == null || contentTypes.Count == 0)
{
contentTypes = new string[] { AnyContentType, };
}
for (var j = 0; j < contentTypes.Count; j++)
{
var contentType = contentTypes[j];
if (!edges.ContainsKey(contentType))
{
edges.Add(contentType, new List<Endpoint>());
}
}
}
// 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 contentTypes = endpoint.Metadata.GetMetadata<IConsumesMetadata>()?.ContentTypes ?? Array.Empty<string>();
if (contentTypes.Count == 0)
{
// OK this means that this endpoint matches *all* content methods.
// So, loop and add it to all states.
foreach (var kvp in edges)
{
kvp.Value.Add(endpoint);
}
}
else
{
// OK this endpoint matches specific content types -- we have to loop through edges here
// because content types could either be exact (like 'application/json') or they
// could have wildcards (like 'text/*'). We don't expect wildcards to be especially common
// with consumes, but we need to support it.
foreach (var kvp in edges)
{
// The edgeKey maps to a possible request header value
var edgeKey = new MediaType(kvp.Key);
for (var j = 0; j < contentTypes.Count; j++)
{
var contentType = contentTypes[j];
var mediaType = new MediaType(contentType);
// Example: 'application/json' is subset of 'application/*'
//
// This means that when the request has content-type 'application/json' an endpoint
// what consumes 'application/*' should match.
if (edgeKey.IsSubsetOf(mediaType))
{
kvp.Value.Add(endpoint);
// It's possible that a ConsumesMetadata defines overlapping wildcards. Don't add an endpoint
// to any edge twice
break;
}
}
}
}
}
// If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an
// endpoint that always returns a 415.
if (!edges.ContainsKey(AnyContentType))
{
edges.Add(AnyContentType, new List<Endpoint>()
{
CreateRejectionEndpoint(),
});
}
return edges
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
.ToArray();
}
private Endpoint CreateRejectionEndpoint()
{
return new RouteEndpoint(
(context) =>
{
context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
return Task.CompletedTask;
},
RoutePatternFactory.Parse("/"),
0,
EndpointMetadataCollection.Empty,
Http415EndpointDisplayName);
}
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
{
if (edges == null)
{
throw new ArgumentNullException(nameof(edges));
}
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
// are then then execute them in linear order.
var ordered = edges
.Select(e => (mediaType: new MediaType((string)e.State), destination: e.Destination))
.OrderBy(e => GetScore(e.mediaType))
.ToArray();
// If any edge matches all content types, then treat that as the 'exit'. This will
// always happen because we insert a 415 endpoint.
for (var i = 0; i < ordered.Length; i++)
{
if (ordered[i].mediaType.MatchesAllTypes)
{
exitDestination = ordered[i].destination;
break;
}
}
return new ConsumesPolicyJumpTable(exitDestination, ordered);
}
private int GetScore(MediaType mediaType)
{
// Higher score == lower priority - see comments on MediaType.
if (mediaType.MatchesAllTypes)
{
return 4;
}
else if (mediaType.MatchesAllSubTypes)
{
return 3;
}
else if (mediaType.MatchesAllSubTypesWithoutSuffix)
{
return 2;
}
else
{
return 1;
}
}
private class ConsumesMetadataEndpointComparer : EndpointMetadataComparer<IConsumesMetadata>
{
protected override int CompareMetadata(IConsumesMetadata x, IConsumesMetadata y)
{
// Ignore the metadata if it has an empty list of content types.
return base.CompareMetadata(
x?.ContentTypes.Count > 0 ? x : null,
y?.ContentTypes.Count > 0 ? y : null);
}
}
private class ConsumesPolicyJumpTable : PolicyJumpTable
{
private (MediaType mediaType, int destination)[] _destinations;
private int _exitDestination;
public ConsumesPolicyJumpTable(int exitDestination, (MediaType mediaType, int destination)[] destinations)
{
_exitDestination = exitDestination;
_destinations = destinations;
}
public override int GetDestination(HttpContext httpContext)
{
var contentType = httpContext.Request.ContentType;
if (string.IsNullOrEmpty(contentType))
{
return _exitDestination;
}
var requestMediaType = new MediaType(contentType);
var destinations = _destinations;
for (var i = 0; i < destinations.Length; i++)
{
if (requestMediaType.IsSubsetOf(destinations[i].mediaType))
{
return destinations[i].destination;
}
}
return _exitDestination;
}
}
}
}