Make HostMatcherPolicy implement IEndpointSelectorPolicy

This commit is contained in:
Ryan Nowak 2019-04-14 13:44:36 -07:00 committed by Ryan Nowak
parent 94fab79771
commit 8df3dc7ae4
6 changed files with 311 additions and 45 deletions

View File

@ -558,14 +558,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
protected EndpointSelector() { }
public abstract System.Threading.Tasks.Task SelectAsync(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.EndpointSelectorContext context, Microsoft.AspNetCore.Routing.Matching.CandidateSet candidates);
}
public sealed partial class HostMatcherPolicy : Microsoft.AspNetCore.Routing.MatcherPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointComparerPolicy, Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy
public sealed partial class HostMatcherPolicy : Microsoft.AspNetCore.Routing.MatcherPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointComparerPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointSelectorPolicy, Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy
{
public HostMatcherPolicy() { }
public System.Collections.Generic.IComparer<Microsoft.AspNetCore.Http.Endpoint> Comparer { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public override int Order { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public bool AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
public System.Threading.Tasks.Task ApplyAsync(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Routing.EndpointSelectorContext context, Microsoft.AspNetCore.Routing.Matching.CandidateSet candidates) { throw null; }
public Microsoft.AspNetCore.Routing.Matching.PolicyJumpTable BuildJumpTable(int exitDestination, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Routing.Matching.PolicyJumpTableEdge> edges) { throw null; }
public System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Routing.Matching.PolicyNodeEdge> GetEdges(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
bool Microsoft.AspNetCore.Routing.Matching.IEndpointSelectorPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
bool Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Http.Endpoint> endpoints) { throw null; }
}
public sealed partial class HttpMethodMatcherPolicy : Microsoft.AspNetCore.Routing.MatcherPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointComparerPolicy, Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy
{

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matching
@ -12,20 +13,41 @@ namespace Microsoft.AspNetCore.Routing.Matching
/// An <see cref="MatcherPolicy"/> that implements filtering and selection by
/// the host header of a request.
/// </summary>
public sealed class HostMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
public sealed class HostMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy
{
private const string WildcardHost = "*";
private const string WildcardPrefix = "*.";
// Run after HTTP methods, but before 'default'.
public override int Order { get; } = -100;
public IComparer<Endpoint> Comparer { get; } = new HostMetadataEndpointComparer();
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
return !ContainsDynamicEndpoints(endpoints) && AppliesToEndpointsCore(endpoints);
}
bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
{
// When the node contains dynamic endpoints we can't make any assumptions.
var applies = ContainsDynamicEndpoints(endpoints);
if (applies)
{
// Run for the side-effect of validating metadata.
AppliesToEndpointsCore(endpoints);
}
return applies;
}
private bool AppliesToEndpointsCore(IReadOnlyList<Endpoint> endpoints)
{
return endpoints.Any(e =>
{
var hosts = e.Metadata.GetMetadata<IHostMetadata>()?.Hosts;
@ -48,6 +70,97 @@ namespace Microsoft.AspNetCore.Routing.Matching
});
}
public Task ApplyAsync(HttpContext httpContext, EndpointSelectorContext context, CandidateSet candidates)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (candidates == null)
{
throw new ArgumentNullException(nameof(candidates));
}
for (var i = 0; i < candidates.Count; i++)
{
if (!candidates.IsValidCandidate(i))
{
continue;
}
var hosts = candidates[i].Endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts;
if (hosts == null || hosts.Count == 0)
{
// Can match any host.
continue;
}
var matched = false;
var (requestHost, requestPort) = GetHostAndPort(httpContext);
for (var j = 0; j < hosts.Count; j++)
{
var host = hosts[j].AsSpan();
var port = "".AsSpan();
// Split into host and port
var pivot = host.IndexOf(":");
if (pivot >= 0)
{
port = host.Slice(pivot + 1);
host = host.Slice(0, pivot);
}
if (host == null || MemoryExtensions.Equals(host, WildcardHost, StringComparison.OrdinalIgnoreCase))
{
// Can match any host
}
else if (
host.StartsWith(WildcardPrefix) &&
// Note that we only slice of the `*`. We want to match the leading `.` also.
MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase))
{
// Matches a suffix wildcard.
}
else if (MemoryExtensions.Equals(requestHost, host, StringComparison.OrdinalIgnoreCase))
{
// Matches exactly
}
else
{
// If we get here then the host doesn't match.
continue;
}
if (MemoryExtensions.Equals(port, WildcardHost, StringComparison.OrdinalIgnoreCase))
{
// Port is a wildcard, we allow any port.
}
else if (port.Length > 0 && (!int.TryParse(port, out var parsed) || parsed != requestPort))
{
// If we get here then the port doesn't match.
continue;
}
matched = true;
break;
}
if (!matched)
{
candidates.SetValidity(i, false);
}
}
return Task.CompletedTask;
}
private static EdgeKey CreateEdgeKey(string host)
{
if (host == null)
@ -71,7 +184,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
return new EdgeKey(hostParts[0], port);
}
else if (string.Equals(hostParts[1], "*", StringComparison.Ordinal))
else if (string.Equals(hostParts[1], WildcardHost, StringComparison.Ordinal))
{
return new EdgeKey(hostParts[0], null);
}
@ -211,6 +324,27 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
}
private static (string host, int? port) GetHostAndPort(HttpContext httpContext)
{
var hostString = httpContext.Request.Host;
if (hostString.Port != null)
{
return (hostString.Host, hostString.Port);
}
else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase))
{
return (hostString.Host, 443);
}
else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase))
{
return (hostString.Host, 80);
}
else
{
return (hostString.Host, null);
}
}
private class HostMetadataEndpointComparer : EndpointMetadataComparer<IHostMetadata>
{
protected override int CompareMetadata(IHostMetadata x, IHostMetadata y)
@ -237,9 +371,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
// HostString can allocate when accessing the host or port
// Store host and port locally and reuse
var requestHost = httpContext.Request.Host;
var host = requestHost.Host;
var port = ResolvePort(httpContext, requestHost);
var (host, port) = GetHostAndPort(httpContext);
var destinations = _destinations;
for (var i = 0; i < destinations.Length; i++)
@ -255,31 +387,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
return _exitDestination;
}
private static int? ResolvePort(HttpContext httpContext, HostString requestHost)
{
if (requestHost.Port != null)
{
return requestHost.Port;
}
else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase))
{
return 443;
}
else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase))
{
return 80;
}
else
{
return null;
}
}
}
private readonly struct EdgeKey : IEquatable<EdgeKey>, IComparable<EdgeKey>, IComparable
{
private const string WildcardHost = "*";
internal static readonly EdgeKey WildcardEdgeKey = new EdgeKey(null, null);
public readonly int? Port;
@ -292,7 +403,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
Host = host ?? WildcardHost;
Port = port;
HasHostWildcard = Host.StartsWith("*.", StringComparison.Ordinal);
HasHostWildcard = Host.StartsWith(WildcardPrefix, StringComparison.Ordinal);
_wildcardEndsWith = HasHostWildcard ? Host.Substring(1) : null;
}
@ -342,6 +453,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
return true;
}
public override int GetHashCode()
{
return (Host?.GetHashCode() ?? 0) ^ (Port?.GetHashCode() ?? 0);
@ -359,7 +471,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
public override string ToString()
{
return $"{Host}:{Port?.ToString() ?? "*"}";
return $"{Host}:{Port?.ToString() ?? WildcardHost}";
}
}
}

View File

@ -0,0 +1,11 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matching
{
// End-to-end tests for the host matching functionality
public class HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest : HostMatcherPolicyIntegrationTestBase
{
protected override bool HasDynamicMetadata => true;
}
}

View File

@ -0,0 +1,11 @@
// 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.
namespace Microsoft.AspNetCore.Routing.Matching
{
// End-to-end tests for the host matching functionality
public class HostMatcherPolicyINodeBuilderPolicyIntegrationTest : HostMatcherPolicyIntegrationTestBase
{
protected override bool HasDynamicMetadata => false;
}
}

View File

@ -13,8 +13,10 @@ using Xunit;
namespace Microsoft.AspNetCore.Routing.Matching
{
// End-to-end tests for the host matching functionality
public class HostMatcherPolicyIntegrationTest
public abstract class HostMatcherPolicyIntegrationTestBase
{
protected abstract bool HasDynamicMetadata { get; }
[Fact]
public async Task Match_Host()
{
@ -308,7 +310,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
return (httpContext, context);
}
internal static RouteEndpoint CreateEndpoint(
internal RouteEndpoint CreateEndpoint(
string template,
object defaults = null,
object constraints = null,
@ -321,6 +323,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
metadata.Add(new HostAttribute(hosts ?? Array.Empty<string>()));
}
if (HasDynamicMetadata)
{
metadata.Add(new DynamicEndpointMetadata());
}
var displayName = "endpoint: " + template + " " + string.Join(", ", hosts ?? new[] { "*:*" });
return new RouteEndpoint(
TestConstants.EmptyRequestDelegate,
@ -335,5 +342,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
var endpoint = CreateEndpoint(template);
return (CreateMatcher(endpoint), endpoint);
}
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
{
public bool IsDynamic => true;
}
}
}

View File

@ -4,11 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;
@ -17,12 +14,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
public class HostMatcherPolicyTest
{
[Fact]
public void AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse()
public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", null), };
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
@ -32,7 +29,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
[Fact]
public void AppliesToEndpoints_EndpointWithoutHosts_ReturnsFalse()
public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutHosts_ReturnsFalse()
{
// Arrange
var endpoints = new[]
@ -40,7 +37,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
};
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
@ -50,7 +47,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
[Fact]
public void AppliesToEndpoints_EndpointHasHosts_ReturnsTrue()
public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasHosts_ReturnsTrue()
{
// Arrange
var endpoints = new[]
@ -59,7 +56,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
CreateEndpoint("/", new HostAttribute(new[] { "localhost", })),
};
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
@ -68,6 +65,25 @@ namespace Microsoft.AspNetCore.Routing.Matching
Assert.True(result);
}
[Fact]
public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasDynamicMetadata_ReturnsFalse()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
CreateEndpoint("/", new HostAttribute(new[] { "localhost", }), new DynamicEndpointMetadata()),
};
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.False(result);
}
[Theory]
[InlineData(":")]
[InlineData(":80")]
@ -75,12 +91,104 @@ namespace Microsoft.AspNetCore.Routing.Matching
[InlineData("")]
[InlineData("::")]
[InlineData("*:test")]
public void AppliesToEndpoints_InvalidHosts(string host)
public void INodeBuilderPolicy_AppliesToEndpoints_InvalidHosts(string host)
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host })), };
var policy = CreatePolicy();
var policy = (INodeBuilderPolicy)CreatePolicy();
// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
{
policy.AppliesToEndpoints(endpoints);
});
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsTrue()
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), };
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutHosts_ReturnsTrue()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HostAttribute(Array.Empty<string>()), new DynamicEndpointMetadata()),
};
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasHosts_ReturnsTrue()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
CreateEndpoint("/", new HostAttribute(new[] { "localhost", }), new DynamicEndpointMetadata()),
};
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.True(result);
}
[Fact]
public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasNoDynamicMetadata_ReturnsFalse()
{
// Arrange
var endpoints = new[]
{
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
CreateEndpoint("/", new HostAttribute(new[] { "localhost", })),
};
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act
var result = policy.AppliesToEndpoints(endpoints);
// Assert
Assert.False(result);
}
[Theory]
[InlineData(":")]
[InlineData(":80")]
[InlineData("80:")]
[InlineData("")]
[InlineData("::")]
[InlineData("*:test")]
public void IEndpointSelectorPolicy_AppliesToEndpoints_InvalidHosts(string host)
{
// Arrange
var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host }), new DynamicEndpointMetadata()), };
var policy = (IEndpointSelectorPolicy)CreatePolicy();
// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
@ -152,7 +260,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
});
}
private static RouteEndpoint CreateEndpoint(string template, IHostMetadata hostMetadata)
private static RouteEndpoint CreateEndpoint(string template, IHostMetadata hostMetadata, params object[] more)
{
var metadata = new List<object>();
if (hostMetadata != null)
@ -160,6 +268,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
metadata.Add(hostMetadata);
}
if (more != null)
{
metadata.AddRange(more);
}
return new RouteEndpoint(
(context) => Task.CompletedTask,
RoutePatternFactory.Parse(template),
@ -172,5 +285,10 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
return new HostMatcherPolicy();
}
private class DynamicEndpointMetadata : IDynamicEndpointMetadata
{
public bool IsDynamic => true;
}
}
}