Add CORS support to HttpMethodMatcherPolicy
Also removes HttpMethodEndpointConstraint, since it's been fully replaced.
This commit is contained in:
parent
c68c5befc7
commit
54e5370e8f
|
|
@ -13,7 +13,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
private BarebonesMatcher _baseline;
|
private BarebonesMatcher _baseline;
|
||||||
private Matcher _dfa;
|
private Matcher _dfa;
|
||||||
private Matcher _tree;
|
|
||||||
|
|
||||||
private int[] _samples;
|
private int[] _samples;
|
||||||
private EndpointFeature _feature;
|
private EndpointFeature _feature;
|
||||||
|
|
@ -31,7 +30,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
|
||||||
|
|
||||||
_feature = new EndpointFeature();
|
_feature = new EndpointFeature();
|
||||||
}
|
}
|
||||||
|
|
@ -61,22 +59,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
Validate(httpContext, Endpoints[sample], feature.Endpoint);
|
Validate(httpContext, Endpoints[sample], feature.Endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark(OperationsPerInvoke = SampleCount)]
|
|
||||||
public async Task LegacyTreeRouter()
|
|
||||||
{
|
|
||||||
var feature = _feature;
|
|
||||||
for (var i = 0; i < SampleCount; i++)
|
|
||||||
{
|
|
||||||
var sample = _samples[i];
|
|
||||||
var httpContext = Requests[sample];
|
|
||||||
|
|
||||||
// This is required to make the legacy router implementation work with global routing.
|
|
||||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
|
||||||
|
|
||||||
await _tree.MatchAsync(httpContext, feature);
|
|
||||||
Validate(httpContext, Endpoints[sample], feature.Endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
var metadata = new List<object>();
|
var metadata = new List<object>();
|
||||||
if (httpMethod != null)
|
if (httpMethod != null)
|
||||||
{
|
{
|
||||||
metadata.Add(new HttpMethodEndpointConstraint(new string[] { httpMethod, }));
|
metadata.Add(new HttpMethodMetadata(new string[] { httpMethod, }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MatcherEndpoint(
|
return new MatcherEndpoint(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
private BarebonesMatcher _baseline;
|
private BarebonesMatcher _baseline;
|
||||||
private Matcher _dfa;
|
private Matcher _dfa;
|
||||||
private Matcher _tree;
|
|
||||||
|
|
||||||
private EndpointFeature _feature;
|
private EndpointFeature _feature;
|
||||||
|
|
||||||
|
|
@ -25,7 +24,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
||||||
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
_baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder());
|
||||||
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
_dfa = SetupMatcher(CreateDfaMatcherBuilder());
|
||||||
_tree = SetupMatcher(new TreeRouterMatcherBuilder());
|
|
||||||
|
|
||||||
_feature = new EndpointFeature();
|
_feature = new EndpointFeature();
|
||||||
}
|
}
|
||||||
|
|
@ -53,21 +51,5 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
Validate(httpContext, Endpoints[i], feature.Endpoint);
|
Validate(httpContext, Endpoints[i], feature.Endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Benchmark(OperationsPerInvoke = EndpointCount)]
|
|
||||||
public async Task LegacyTreeRouter()
|
|
||||||
{
|
|
||||||
var feature = _feature;
|
|
||||||
for (var i = 0; i < EndpointCount; i++)
|
|
||||||
{
|
|
||||||
var httpContext = Requests[i];
|
|
||||||
|
|
||||||
// This is required to make the legacy router implementation work with global routing.
|
|
||||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
|
||||||
|
|
||||||
await _tree.MatchAsync(httpContext, feature);
|
|
||||||
Validate(httpContext, Endpoints[i], feature.Endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
|
using Microsoft.AspNetCore.Routing.Metadata;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
@ -134,15 +135,16 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
sb.Append(", Order:");
|
sb.Append(", Order:");
|
||||||
sb.Append(matcherEndpoint.Order);
|
sb.Append(matcherEndpoint.Order);
|
||||||
|
|
||||||
var httpEndpointConstraints = matcherEndpoint.Metadata.GetOrderedMetadata<IEndpointConstraintMetadata>()
|
var httpMethodMetadata = matcherEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||||
.OfType<HttpMethodEndpointConstraint>();
|
if (httpMethodMetadata != null)
|
||||||
foreach (var constraint in httpEndpointConstraints)
|
|
||||||
{
|
{
|
||||||
sb.Append(", Http Methods: ");
|
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
|
||||||
sb.Append(string.Join(", ", constraint.HttpMethods));
|
{
|
||||||
sb.Append(", Constraint Order:");
|
sb.Append(", Http Methods: ");
|
||||||
sb.Append(constraint.Order);
|
sb.Append(string.Join(", ", httpMethod));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
|
services.TryAddSingleton<EndpointSelector, EndpointConstraintEndpointSelector>();
|
||||||
services.TryAddSingleton<EndpointConstraintCache>();
|
services.TryAddSingleton<EndpointConstraintCache>();
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodEndpointSelectorPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||||
|
|
||||||
// Will be cached by the EndpointSelector
|
// Will be cached by the EndpointSelector
|
||||||
services.TryAddEnumerable(
|
services.TryAddEnumerable(
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ namespace Microsoft.AspNetCore.Routing.EndpointConstraints
|
||||||
|
|
||||||
IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
|
IReadOnlyList<string> IHttpMethodMetadata.HttpMethods => _httpMethods;
|
||||||
|
|
||||||
|
bool IHttpMethodMetadata.AcceptCorsPreflight => false;
|
||||||
|
|
||||||
public virtual bool Accept(EndpointConstraintContext context)
|
public virtual bool Accept(EndpointConstraintContext context)
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,18 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Metadata;
|
using Microsoft.AspNetCore.Routing.Metadata;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
public sealed class HttpMethodEndpointSelectorPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
|
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
|
// Used in tests
|
||||||
internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported";
|
internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported";
|
||||||
|
|
||||||
|
|
@ -47,38 +54,94 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
// The algorithm here is designed to be preserve the order of the endpoints
|
// The algorithm here is designed to be preserve the order of the endpoints
|
||||||
// while also being relatively simple. Preserving order is important.
|
// while also being relatively simple. Preserving order is important.
|
||||||
var allHttpMethods = endpoints
|
|
||||||
.SelectMany(e => GetHttpMethods(e))
|
|
||||||
.Distinct()
|
|
||||||
.OrderBy(m => m); // Sort for testability
|
|
||||||
|
|
||||||
var dictionary = new Dictionary<string, List<Endpoint>>();
|
|
||||||
foreach (var httpMethod in allHttpMethods)
|
|
||||||
{
|
|
||||||
dictionary.Add(httpMethod, new List<Endpoint>());
|
|
||||||
}
|
|
||||||
|
|
||||||
dictionary.Add(AnyMethod, new List<Endpoint>());
|
|
||||||
|
|
||||||
|
// 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<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
|
||||||
for (var i = 0; i < endpoints.Count; i++)
|
for (var i = 0; i < endpoints.Count; i++)
|
||||||
{
|
{
|
||||||
var endpoint = endpoints[i];
|
var endpoint = endpoints[i];
|
||||||
|
var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint);
|
||||||
|
|
||||||
var httpMethods = 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)
|
if (httpMethods.Count == 0)
|
||||||
{
|
{
|
||||||
// This endpoint suports all HTTP methods.
|
httpMethods = new[] { AnyMethod, };
|
||||||
foreach (var kvp in dictionary)
|
|
||||||
{
|
|
||||||
kvp.Value.Add(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var j = 0; j < httpMethods.Count; j++)
|
for (var j = 0; j < httpMethods.Count; j++)
|
||||||
{
|
{
|
||||||
dictionary[httpMethods[j]].Add(endpoint);
|
// 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<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);
|
||||||
|
if (!edges.ContainsKey(key))
|
||||||
|
{
|
||||||
|
edges.Add(key, new List<Endpoint>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,42 +158,67 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
//
|
//
|
||||||
// This will make 405 much more likely in API-focused applications, and somewhat
|
// This will make 405 much more likely in API-focused applications, and somewhat
|
||||||
// unlikely in a traditional MVC application. That's good.
|
// unlikely in a traditional MVC application. That's good.
|
||||||
if (dictionary[AnyMethod].Count == 0)
|
//
|
||||||
|
// 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))
|
||||||
{
|
{
|
||||||
dictionary[AnyMethod].Add(CreateRejectionEndpoint(allHttpMethods));
|
// Methods sorted for testability.
|
||||||
|
var endpoint = CreateRejectionEndpoint(allHttpMethods.OrderBy(m => m));
|
||||||
|
matches = new List<Endpoint>() { endpoint, };
|
||||||
|
edges[new EdgeKey(AnyMethod, false)] = matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
var edges = new List<PolicyNodeEdge>();
|
return edges
|
||||||
foreach (var kvp in dictionary)
|
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
|
||||||
{
|
.ToArray();
|
||||||
edges.Add(new PolicyNodeEdge(kvp.Key, kvp.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return edges;
|
(IReadOnlyList<string> httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e)
|
||||||
|
|
||||||
IReadOnlyList<string> GetHttpMethods(Endpoint e)
|
|
||||||
{
|
{
|
||||||
return e.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods ?? Array.Empty<string>();
|
var metadata = e.Metadata.GetMetadata<IHttpMethodMetadata>();
|
||||||
|
return metadata == null ? (Array.Empty<string>(), false) : (metadata.HttpMethods, metadata.AcceptCorsPreflight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
||||||
{
|
{
|
||||||
var dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
var destinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var corsPreflightDestinations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
for (var i = 0; i < edges.Count; i++)
|
for (var i = 0; i < edges.Count; i++)
|
||||||
{
|
{
|
||||||
// We create this data, so it's safe to cast it to a string.
|
// We create this data, so it's safe to cast it.
|
||||||
dictionary.Add((string)edges[i].State, edges[i].Destination);
|
var key = (EdgeKey)edges[i].State;
|
||||||
|
if (key.IsCorsPreflightRequest)
|
||||||
|
{
|
||||||
|
corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destinations.Add(key.HttpMethod, edges[i].Destination);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dictionary.TryGetValue(AnyMethod, out var matchesAnyVerb))
|
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.
|
// If we have endpoints that match any HTTP method, use that as the exit.
|
||||||
exitDestination = matchesAnyVerb;
|
exitDestination = matchesAnyVerb;
|
||||||
dictionary.Remove(AnyMethod);
|
destinations.Remove(AnyMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DictionaryPolicyJumpTable(exitDestination, dictionary);
|
return new HttpMethodPolicyJumpTable(
|
||||||
|
exitDestination,
|
||||||
|
destinations,
|
||||||
|
corsPreflightExitDestination,
|
||||||
|
corsPreflightDestinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
|
private Endpoint CreateRejectionEndpoint(IEnumerable<string> httpMethods)
|
||||||
|
|
@ -150,21 +238,46 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
Http405EndpointDisplayName);
|
Http405EndpointDisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DictionaryPolicyJumpTable : PolicyJumpTable
|
private class HttpMethodPolicyJumpTable : PolicyJumpTable
|
||||||
{
|
{
|
||||||
private readonly int _exitDestination;
|
private readonly int _exitDestination;
|
||||||
private readonly Dictionary<string, int> _destinations;
|
private readonly Dictionary<string, int> _destinations;
|
||||||
|
private readonly int _corsPreflightExitDestination;
|
||||||
|
private readonly Dictionary<string, int> _corsPreflightDestinations;
|
||||||
|
|
||||||
public DictionaryPolicyJumpTable(int exitDestination, Dictionary<string, int> destinations)
|
private readonly bool _supportsCorsPreflight;
|
||||||
|
|
||||||
|
public HttpMethodPolicyJumpTable(
|
||||||
|
int exitDestination,
|
||||||
|
Dictionary<string, int> destinations,
|
||||||
|
int corsPreflightExitDestination,
|
||||||
|
Dictionary<string, int> corsPreflightDestinations)
|
||||||
{
|
{
|
||||||
_exitDestination = exitDestination;
|
_exitDestination = exitDestination;
|
||||||
_destinations = destinations;
|
_destinations = destinations;
|
||||||
|
_corsPreflightExitDestination = corsPreflightExitDestination;
|
||||||
|
_corsPreflightDestinations = corsPreflightDestinations;
|
||||||
|
|
||||||
|
_supportsCorsPreflight = _corsPreflightDestinations.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetDestination(HttpContext httpContext)
|
public override int GetDestination(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
|
int destination;
|
||||||
|
|
||||||
var httpMethod = httpContext.Request.Method;
|
var httpMethod = httpContext.Request.Method;
|
||||||
return _destinations.TryGetValue(httpMethod, out var destination) ? destination : _exitDestination;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,5 +291,58 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
y?.HttpMethods.Count > 0 ? y : null);
|
y?.HttpMethods.Count > 0 ? y : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal readonly struct EdgeKey : IEquatable<EdgeKey>, IComparable<EdgeKey>, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,20 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Metadata
|
namespace Microsoft.AspNetCore.Routing.Metadata
|
||||||
{
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerToString,nq}")]
|
||||||
public sealed class HttpMethodMetadata : IHttpMethodMetadata
|
public sealed class HttpMethodMetadata : IHttpMethodMetadata
|
||||||
{
|
{
|
||||||
public HttpMethodMetadata(IEnumerable<string> httpMethods)
|
public HttpMethodMetadata(IEnumerable<string> httpMethods)
|
||||||
|
: this(httpMethods, acceptCorsPreflight: false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpMethodMetadata(IEnumerable<string> httpMethods, bool acceptCorsPreflight)
|
||||||
{
|
{
|
||||||
if (httpMethods == null)
|
if (httpMethods == null)
|
||||||
{
|
{
|
||||||
|
|
@ -17,8 +24,16 @@ namespace Microsoft.AspNetCore.Routing.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpMethods = httpMethods.ToArray();
|
HttpMethods = httpMethods.ToArray();
|
||||||
|
AcceptCorsPreflight = acceptCorsPreflight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool AcceptCorsPreflight { get; }
|
||||||
|
|
||||||
public IReadOnlyList<string> HttpMethods { get; }
|
public IReadOnlyList<string> HttpMethods { get; }
|
||||||
|
|
||||||
|
private string DebuggerToString()
|
||||||
|
{
|
||||||
|
return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Routing.Metadata
|
||||||
{
|
{
|
||||||
public interface IHttpMethodMetadata
|
public interface IHttpMethodMetadata
|
||||||
{
|
{
|
||||||
|
bool AcceptCorsPreflight { get; }
|
||||||
|
|
||||||
IReadOnlyList<string> HttpMethods { get; }
|
IReadOnlyList<string> HttpMethods { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
// 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 Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
|
||||||
using Microsoft.AspNetCore.Routing.TestObjects;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Internal
|
|
||||||
{
|
|
||||||
public class HttpMethodEndpointConstraintTest
|
|
||||||
{
|
|
||||||
public static TheoryData AcceptCaseInsensitiveData =
|
|
||||||
new TheoryData<IEnumerable<string>, string>
|
|
||||||
{
|
|
||||||
{ new string[] { "get", "Get", "GET", "GEt"}, "gEt" },
|
|
||||||
{ new string[] { "POST", "PoSt", "GEt"}, "GET" },
|
|
||||||
{ new string[] { "get" }, "get" },
|
|
||||||
{ new string[] { "post" }, "POST" },
|
|
||||||
{ new string[] { "gEt" }, "get" },
|
|
||||||
{ new string[] { "get", "PoST" }, "pOSt" }
|
|
||||||
};
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(AcceptCaseInsensitiveData))]
|
|
||||||
public void HttpMethodEndpointConstraint_IgnoresPreflightRequests(IEnumerable<string> httpMethods, string accessControlMethod)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var constraint = new HttpMethodEndpointConstraint(httpMethods);
|
|
||||||
var context = CreateEndpointConstraintContext(constraint);
|
|
||||||
context.HttpContext = CreateHttpContext("oPtIoNs", accessControlMethod);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = constraint.Accept(context);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result, "Request should have been rejected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(AcceptCaseInsensitiveData))]
|
|
||||||
public void HttpMethodEndpointConstraint_Accept_CaseInsensitive(IEnumerable<string> httpMethods, string expectedMethod)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var constraint = new HttpMethodEndpointConstraint(httpMethods);
|
|
||||||
var context = CreateEndpointConstraintContext(constraint);
|
|
||||||
context.HttpContext = CreateHttpContext(expectedMethod);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = constraint.Accept(context);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result, "Request should have been accepted.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EndpointConstraintContext CreateEndpointConstraintContext(HttpMethodEndpointConstraint constraint)
|
|
||||||
{
|
|
||||||
var context = new EndpointConstraintContext();
|
|
||||||
|
|
||||||
var endpointSelectorCandidate = new EndpointSelectorCandidate(
|
|
||||||
new TestEndpoint(EndpointMetadataCollection.Empty, string.Empty),
|
|
||||||
0,
|
|
||||||
new RouteValueDictionary(),
|
|
||||||
new List<IEndpointConstraint> { constraint });
|
|
||||||
|
|
||||||
context.Candidates = new List<EndpointSelectorCandidate> { endpointSelectorCandidate };
|
|
||||||
context.CurrentCandidate = context.Candidates[0];
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpContext CreateHttpContext(string requestedMethod, string accessControlMethod = null)
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
|
|
||||||
httpContext.Request.Method = requestedMethod;
|
|
||||||
|
|
||||||
if (accessControlMethod != null)
|
|
||||||
{
|
|
||||||
httpContext.Request.Headers.Add("Origin", StringValues.Empty);
|
|
||||||
httpContext.Request.Headers.Add("Access-Control-Request-Method", accessControlMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
return httpContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
|
|
@ -26,13 +26,19 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
template);
|
template);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Matcher CreateDfaMatcher(EndpointDataSource dataSource)
|
private Matcher CreateDfaMatcher(EndpointDataSource dataSource, EndpointSelector endpointSelector = null)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection()
|
var serviceCollection = new ServiceCollection()
|
||||||
.AddLogging()
|
.AddLogging()
|
||||||
.AddOptions()
|
.AddOptions()
|
||||||
.AddRouting()
|
.AddRouting();
|
||||||
.BuildServiceProvider();
|
|
||||||
|
if (endpointSelector != null)
|
||||||
|
{
|
||||||
|
serviceCollection.AddSingleton<EndpointSelector>(endpointSelector);
|
||||||
|
}
|
||||||
|
|
||||||
|
var services = serviceCollection.BuildServiceProvider();
|
||||||
|
|
||||||
var factory = services.GetRequiredService<MatcherFactory>();
|
var factory = services.GetRequiredService<MatcherFactory>();
|
||||||
return Assert.IsType<DataSourceDependentMatcher>(factory.CreateMatcher(dataSource));
|
return Assert.IsType<DataSourceDependentMatcher>(factory.CreateMatcher(dataSource));
|
||||||
|
|
@ -115,22 +121,39 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled()
|
public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpointWithoutConstraint = CreateEndpoint("/Teams", 0);
|
var endpoint1 = CreateEndpoint("/Teams", 0);
|
||||||
var endpointWithConstraint = CreateEndpoint(
|
var endpoint2 = CreateEndpoint("/Teams", 1);
|
||||||
"/Teams",
|
|
||||||
0,
|
var endpointSelector = new Mock<EndpointSelector>();
|
||||||
metadata: new EndpointMetadataCollection(new object[] { new HttpMethodEndpointConstraint(new[] { "POST" }) }));
|
endpointSelector
|
||||||
|
.Setup(s => s.SelectAsync(It.IsAny<HttpContext>(), It.IsAny<IEndpointFeature>(), It.IsAny<CandidateSet>()))
|
||||||
|
.Callback<HttpContext, IEndpointFeature, CandidateSet>((c, f, cs) =>
|
||||||
|
{
|
||||||
|
Assert.Equal(2, cs.Count);
|
||||||
|
|
||||||
|
Assert.Same(endpoint1, cs[0].Endpoint);
|
||||||
|
Assert.True(cs[0].IsValidCandidate);
|
||||||
|
Assert.Equal(0, cs[0].Score);
|
||||||
|
Assert.Empty(cs[0].Values);
|
||||||
|
|
||||||
|
Assert.Same(endpoint2, cs[1].Endpoint);
|
||||||
|
Assert.True(cs[1].IsValidCandidate);
|
||||||
|
Assert.Equal(1, cs[1].Score);
|
||||||
|
Assert.Empty(cs[1].Values);
|
||||||
|
|
||||||
|
f.Endpoint = endpoint2;
|
||||||
|
})
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
|
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
|
||||||
{
|
{
|
||||||
endpointWithoutConstraint,
|
endpoint1,
|
||||||
endpointWithConstraint
|
endpoint2
|
||||||
});
|
});
|
||||||
|
|
||||||
var matcher = CreateDfaMatcher(endpointDataSource);
|
var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector.Object);
|
||||||
|
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Request.Method = "POST";
|
|
||||||
httpContext.Request.Path = "/Teams";
|
httpContext.Request.Path = "/Teams";
|
||||||
|
|
||||||
var endpointFeature = new EndpointFeature();
|
var endpointFeature = new EndpointFeature();
|
||||||
|
|
@ -139,7 +162,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
await matcher.MatchAsync(httpContext, endpointFeature);
|
await matcher.MatchAsync(httpContext, endpointFeature);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(endpointWithConstraint, endpointFeature.Endpoint);
|
Assert.Equal(endpoint2, endpointFeature.Endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Routing.Metadata;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using static Microsoft.AspNetCore.Routing.Matchers.HttpMethodMatcherPolicy;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
|
|
@ -31,6 +32,56 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
MatcherAssert.AssertMatch(feature, endpoint);
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HttpMethod_CORS()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true);
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "GET");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HttpMethod_CORS_Preflight()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true);
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact] // Nothing here supports OPTIONS, so it goes to a 405.
|
||||||
|
public async Task NotMatch_HttpMethod_CORS_Preflight()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: false);
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotSame(endpoint, feature.Endpoint);
|
||||||
|
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Match_HttpMethod_CaseInsensitive()
|
public async Task Match_HttpMethod_CaseInsensitive()
|
||||||
{
|
{
|
||||||
|
|
@ -47,6 +98,22 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
MatcherAssert.AssertMatch(feature, endpoint);
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }, acceptCorsPreflight: true);
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Match_NoMetadata_MatchesAnyHttpMethod()
|
public async Task Match_NoMetadata_MatchesAnyHttpMethod()
|
||||||
{
|
{
|
||||||
|
|
@ -63,6 +130,38 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
MatcherAssert.AssertMatch(feature, endpoint);
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: true);
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] // This matches because the endpoint accepts OPTIONS
|
||||||
|
public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight_DoesNotSupportPreflight()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: false);
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "GET", corsPreflight: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(feature, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Match_EmptyMethodList_MatchesAnyHttpMethod()
|
public async Task Match_EmptyMethodList_MatchesAnyHttpMethod()
|
||||||
{
|
{
|
||||||
|
|
@ -96,7 +195,7 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
Assert.NotSame(endpoint1, feature.Endpoint);
|
Assert.NotSame(endpoint1, feature.Endpoint);
|
||||||
Assert.NotSame(endpoint2, feature.Endpoint);
|
Assert.NotSame(endpoint2, feature.Endpoint);
|
||||||
|
|
||||||
Assert.Same(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
|
Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, feature.Endpoint.DisplayName);
|
||||||
|
|
||||||
// Invoke the endpoint
|
// Invoke the endpoint
|
||||||
await feature.Invoker((c) => Task.CompletedTask)(httpContext);
|
await feature.Invoker((c) => Task.CompletedTask)(httpContext);
|
||||||
|
|
@ -104,6 +203,23 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]);
|
Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] // When all of the candidates handles specific verbs, use a 405 endpoint
|
||||||
|
public async Task NotMatch_HttpMethod_CORS_DoesNotReturn405()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }, acceptCorsPreflight: true);
|
||||||
|
var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint1, endpoint2);
|
||||||
|
var (httpContext, feature) = CreateContext("/hello", "POST", corsPreflight: true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertNotMatch(feature);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint
|
[Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint
|
||||||
public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405()
|
public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405()
|
||||||
{
|
{
|
||||||
|
|
@ -189,12 +305,21 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path, string httpMethod)
|
internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(
|
||||||
|
string path,
|
||||||
|
string httpMethod,
|
||||||
|
bool corsPreflight = false)
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Request.Method = httpMethod;
|
httpContext.Request.Method = corsPreflight ? PreflightHttpMethod : httpMethod;
|
||||||
httpContext.Request.Path = path;
|
httpContext.Request.Path = path;
|
||||||
|
|
||||||
|
if (corsPreflight)
|
||||||
|
{
|
||||||
|
httpContext.Request.Headers[OriginHeader] = "example.com";
|
||||||
|
httpContext.Request.Headers[AccessControlRequestMethod] = httpMethod;
|
||||||
|
}
|
||||||
|
|
||||||
var feature = new EndpointFeature();
|
var feature = new EndpointFeature();
|
||||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||||
|
|
||||||
|
|
@ -205,12 +330,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
object defaults = null,
|
object defaults = null,
|
||||||
object constraints = null,
|
object constraints = null,
|
||||||
int order = 0,
|
int order = 0,
|
||||||
string[] httpMethods = null)
|
string[] httpMethods = null,
|
||||||
|
bool acceptCorsPreflight = false)
|
||||||
{
|
{
|
||||||
var metadata = new List<object>();
|
var metadata = new List<object>();
|
||||||
if (httpMethods != null)
|
if (httpMethods != null)
|
||||||
{
|
{
|
||||||
metadata.Add(new HttpMethodMetadata(httpMethods));
|
metadata.Add(new HttpMethodMetadata(httpMethods ?? Array.Empty<string>(), acceptCorsPreflight));
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" });
|
var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" });
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Routing.Metadata;
|
using Microsoft.AspNetCore.Routing.Metadata;
|
||||||
using Microsoft.AspNetCore.Routing.Patterns;
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using static Microsoft.AspNetCore.Routing.Matchers.HttpMethodMatcherPolicy;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Matchers
|
namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
|
|
@ -32,7 +32,10 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
public void AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
|
public void AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpoints = new[] { CreateEndpoint("/", Array.Empty<string>()), };
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||||
|
};
|
||||||
|
|
||||||
var policy = CreatePolicy();
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
|
@ -47,7 +50,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
public void AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
|
public void AppliesToNode_EndpointHasHttpMethods_ReturnsTrue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var endpoints = new[] { CreateEndpoint("/", Array.Empty<string>()), CreateEndpoint("/", new[] { "GET", })};
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||||
|
};
|
||||||
|
|
||||||
var policy = CreatePolicy();
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
|
@ -66,11 +73,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||||
// this way so we can verify that ordering is preserved by GetEdges.
|
// this way so we can verify that ordering is preserved by GetEdges.
|
||||||
CreateEndpoint("/", new[] { "GET", }),
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||||
CreateEndpoint("/", Array.Empty<string>()),
|
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||||
CreateEndpoint("/", new[] { "GET", "PUT", "POST" }),
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" })),
|
||||||
CreateEndpoint("/", new[] { "PUT", "POST" }),
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||||
CreateEndpoint("/", Array.Empty<string>()),
|
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||||
};
|
};
|
||||||
|
|
||||||
var policy = CreatePolicy();
|
var policy = CreatePolicy();
|
||||||
|
|
@ -83,26 +90,91 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
edges.OrderBy(e => e.State),
|
edges.OrderBy(e => e.State),
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State);
|
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal("GET", e.State);
|
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal("POST", e.State);
|
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal("PUT", e.State);
|
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEdges_GroupsByHttpMethod_Cors()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||||
|
// this way so we can verify that ordering is preserved by GetEdges.
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>())),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" }, acceptCorsPreflight: true)),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(Array.Empty<string>(), acceptCorsPreflight: true)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var edges = policy.GetEdges(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
edges.OrderBy(e => e.State),
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] // See explanation in GetEdges for how this case is different
|
[Fact] // See explanation in GetEdges for how this case is different
|
||||||
public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint()
|
public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint()
|
||||||
{
|
{
|
||||||
|
|
@ -111,9 +183,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
{
|
{
|
||||||
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||||
// this way so we can verify that ordering is preserved by GetEdges.
|
// this way so we can verify that ordering is preserved by GetEdges.
|
||||||
CreateEndpoint("/", new[] { "GET", }),
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||||
CreateEndpoint("/", new[] { "GET", "PUT", "POST" }),
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" })),
|
||||||
CreateEndpoint("/", new[] { "PUT", "POST" }),
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||||
};
|
};
|
||||||
|
|
||||||
var policy = CreatePolicy();
|
var policy = CreatePolicy();
|
||||||
|
|
@ -126,32 +198,91 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
edges.OrderBy(e => e.State),
|
edges.OrderBy(e => e.State),
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal(HttpMethodEndpointSelectorPolicy.AnyMethod, e.State);
|
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(HttpMethodEndpointSelectorPolicy.Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
|
Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal("GET", e.State);
|
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal("POST", e.State);
|
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
},
|
},
|
||||||
e =>
|
e =>
|
||||||
{
|
{
|
||||||
Assert.Equal("PUT", e.State);
|
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||||
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] // See explanation in GetEdges for how this case is different
|
||||||
|
public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint_CORS()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
// These are arrange in an order that we won't actually see in a product scenario. It's done
|
||||||
|
// this way so we can verify that ordering is preserved by GetEdges.
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", "PUT", "POST" }, acceptCorsPreflight: true)),
|
||||||
|
CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var edges = policy.GetEdges(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
edges.OrderBy(e => e.State),
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName);
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State);
|
||||||
|
Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MatcherEndpoint CreateEndpoint(string template, string[] httpMethods)
|
private static MatcherEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata)
|
||||||
{
|
{
|
||||||
var metadata = new List<object>();
|
var metadata = new List<object>();
|
||||||
if (httpMethods != null)
|
if (httpMethodMetadata != null)
|
||||||
{
|
{
|
||||||
metadata.Add(new HttpMethodMetadata(httpMethods));
|
metadata.Add(httpMethodMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MatcherEndpoint(
|
return new MatcherEndpoint(
|
||||||
|
|
@ -163,9 +294,9 @@ namespace Microsoft.AspNetCore.Routing.Matchers
|
||||||
$"test: {template}");
|
$"test: {template}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpMethodEndpointSelectorPolicy CreatePolicy()
|
private static HttpMethodMatcherPolicy CreatePolicy()
|
||||||
{
|
{
|
||||||
return new HttpMethodEndpointSelectorPolicy();
|
return new HttpMethodMatcherPolicy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue