From 8df3dc7ae44b909a32a5f314e4acca8eb19e315a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 14 Apr 2019 13:44:36 -0700 Subject: [PATCH] Make HostMatcherPolicy implement IEndpointSelectorPolicy --- ...rosoft.AspNetCore.Routing.netcoreapp3.0.cs | 6 +- .../Routing/src/Matching/HostMatcherPolicy.cs | 170 +++++++++++++++--- ...yIEndpointSelectorPolicyIntegrationTest.cs | 11 ++ ...PolicyINodeBuilderPolicyIntegrationTest.cs | 11 ++ ...> HostMatcherPolicyIntegrationTestBase.cs} | 16 +- .../Matching/HostMatcherPolicyTest.cs | 142 +++++++++++++-- 6 files changed, 311 insertions(+), 45 deletions(-) create mode 100644 src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs create mode 100644 src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs rename src/Http/Routing/test/UnitTests/Matching/{HostMatcherPolicyIntegrationTest.cs => HostMatcherPolicyIntegrationTestBase.cs} (96%) diff --git a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs index 3827225df0..de48294c7b 100644 --- a/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs +++ b/src/Http/Routing/ref/Microsoft.AspNetCore.Routing.netcoreapp3.0.cs @@ -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 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 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 edges) { throw null; } public System.Collections.Generic.IReadOnlyList GetEdges(System.Collections.Generic.IReadOnlyList endpoints) { throw null; } + bool Microsoft.AspNetCore.Routing.Matching.IEndpointSelectorPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList endpoints) { throw null; } + bool Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy.AppliesToEndpoints(System.Collections.Generic.IReadOnlyList endpoints) { throw null; } } public sealed partial class HttpMethodMatcherPolicy : Microsoft.AspNetCore.Routing.MatcherPolicy, Microsoft.AspNetCore.Routing.Matching.IEndpointComparerPolicy, Microsoft.AspNetCore.Routing.Matching.INodeBuilderPolicy { diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index 7f00a8ead5..ff9244ad12 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -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 that implements filtering and selection by /// the host header of a request. /// - 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 Comparer { get; } = new HostMetadataEndpointComparer(); - public bool AppliesToEndpoints(IReadOnlyList endpoints) + bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) { if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); } + return !ContainsDynamicEndpoints(endpoints) && AppliesToEndpointsCore(endpoints); + } + + bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList 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 endpoints) + { return endpoints.Any(e => { var hosts = e.Metadata.GetMetadata()?.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()?.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 { 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, IComparable, 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}"; } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs new file mode 100644 index 0000000000..3d46193f6b --- /dev/null +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs @@ -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; + } +} diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs new file mode 100644 index 0000000000..36df824df1 --- /dev/null +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs @@ -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; + } +} diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs similarity index 96% rename from src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTest.cs rename to src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs index b369d2a0e5..c812ad8d4c 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs @@ -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())); } + 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; + } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs index 9b3421e908..1f30201416 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs @@ -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())), }; - 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())), + 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(() => + { + 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()), 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())), + 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())), + 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(() => @@ -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(); 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; + } } }