Add HostPolicyMatcher (#6214)
This commit is contained in:
parent
55ec35bb80
commit
90511e6039
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for adding routing metadata to endpoint instances using <see cref="IEndpointConventionBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class RoutingEndpointConventionBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires that endpoints match one of the specified hosts during routing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/> to add the metadata to.</param>
|
||||||
|
/// <param name="hosts">
|
||||||
|
/// The hosts used during routing.
|
||||||
|
/// Hosts should be Unicode rather than punycode, and may have a port.
|
||||||
|
/// An empty collection means any host will be accepted.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||||
|
public static IEndpointConventionBuilder RequireHost(this IEndpointConventionBuilder builder, params string[] hosts)
|
||||||
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hosts == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(hosts));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Add(endpointBuilder =>
|
||||||
|
{
|
||||||
|
endpointBuilder.Metadata.Add(new HostAttribute(hosts));
|
||||||
|
});
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,6 +87,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
//
|
//
|
||||||
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
|
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
|
||||||
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HostMatcherPolicy>());
|
||||||
|
|
||||||
//
|
//
|
||||||
// Misc infrastructure
|
// Misc infrastructure
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute for providing host metdata that is used during routing.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||||
|
public sealed class HostAttribute : Attribute, IHostMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HostAttribute" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host">
|
||||||
|
/// The host used during routing.
|
||||||
|
/// Host should be Unicode rather than punycode, and may have a port.
|
||||||
|
/// </param>
|
||||||
|
public HostAttribute(string host) : this(new[] { host })
|
||||||
|
{
|
||||||
|
if (host == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(host));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HostAttribute" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hosts">
|
||||||
|
/// The hosts used during routing.
|
||||||
|
/// Hosts should be Unicode rather than punycode, and may have a port.
|
||||||
|
/// An empty collection means any host will be accepted.
|
||||||
|
/// </param>
|
||||||
|
public HostAttribute(params string[] hosts)
|
||||||
|
{
|
||||||
|
if (hosts == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(hosts));
|
||||||
|
}
|
||||||
|
|
||||||
|
Hosts = hosts.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only collection of hosts used during routing.
|
||||||
|
/// Hosts will be Unicode rather than punycode, and may have a port.
|
||||||
|
/// An empty collection means any host will be accepted.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Hosts { get; }
|
||||||
|
|
||||||
|
private string DebuggerToString()
|
||||||
|
{
|
||||||
|
var hostsDisplay = (Hosts.Count == 0)
|
||||||
|
? "*:*"
|
||||||
|
: string.Join(",", Hosts.Select(h => h.Contains(':') ? h : h + ":*"));
|
||||||
|
|
||||||
|
return $"Hosts: {hostsDisplay}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents host metadata used during routing.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHostMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only collection of hosts used during routing.
|
||||||
|
/// Hosts will be Unicode rather than punycode, and may have a port.
|
||||||
|
/// An empty collection means any host will be accepted.
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<string> Hosts { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,366 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="MatcherPolicy"/> that implements filtering and selection by
|
||||||
|
/// the host header of a request.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HostMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
if (endpoints == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(endpoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints.Any(e =>
|
||||||
|
{
|
||||||
|
var hosts = e.Metadata.GetMetadata<IHostMetadata>()?.Hosts;
|
||||||
|
if (hosts == null || hosts.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var host in hosts)
|
||||||
|
{
|
||||||
|
// Don't run policy on endpoints that match everything
|
||||||
|
var key = CreateEdgeKey(host);
|
||||||
|
if (!key.MatchesAll)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EdgeKey CreateEdgeKey(string host)
|
||||||
|
{
|
||||||
|
if (host == null)
|
||||||
|
{
|
||||||
|
return EdgeKey.WildcardEdgeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostParts = host.Split(':');
|
||||||
|
if (hostParts.Length == 1)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(hostParts[0]))
|
||||||
|
{
|
||||||
|
return new EdgeKey(hostParts[0], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hostParts.Length == 2)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(hostParts[0]))
|
||||||
|
{
|
||||||
|
if (int.TryParse(hostParts[1], out var port))
|
||||||
|
{
|
||||||
|
return new EdgeKey(hostParts[0], port);
|
||||||
|
}
|
||||||
|
else if (string.Equals(hostParts[1], "*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return new EdgeKey(hostParts[0], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Could not parse host: {host}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
|
||||||
|
{
|
||||||
|
if (endpoints == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(endpoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The algorithm here is designed to be preserve the order of the endpoints
|
||||||
|
// while also being relatively simple. Preserving order is important.
|
||||||
|
|
||||||
|
// First, build a dictionary of all of the hosts that are included
|
||||||
|
// at this node.
|
||||||
|
//
|
||||||
|
// For now we're just building up the set of keys. We don't add any endpoints
|
||||||
|
// to lists now because we don't want ordering problems.
|
||||||
|
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
|
||||||
|
for (var i = 0; i < endpoints.Count; i++)
|
||||||
|
{
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
var hosts = endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray();
|
||||||
|
if (hosts == null || hosts.Length == 0)
|
||||||
|
{
|
||||||
|
hosts = new[] { EdgeKey.WildcardEdgeKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < hosts.Length; j++)
|
||||||
|
{
|
||||||
|
var host = hosts[j];
|
||||||
|
if (!edges.ContainsKey(host))
|
||||||
|
{
|
||||||
|
edges.Add(host, new List<Endpoint>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now in a second loop, add endpoints to these lists. We've enumerated all of
|
||||||
|
// the states, so we want to see which states this endpoint matches.
|
||||||
|
for (var i = 0; i < endpoints.Count; i++)
|
||||||
|
{
|
||||||
|
var endpoint = endpoints[i];
|
||||||
|
|
||||||
|
var endpointKeys = endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray() ?? Array.Empty<EdgeKey>();
|
||||||
|
if (endpointKeys.Length == 0)
|
||||||
|
{
|
||||||
|
// OK this means that this endpoint matches *all* hosts.
|
||||||
|
// So, loop and add it to all states.
|
||||||
|
foreach (var kvp in edges)
|
||||||
|
{
|
||||||
|
kvp.Value.Add(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// OK this endpoint matches specific hosts
|
||||||
|
foreach (var kvp in edges)
|
||||||
|
{
|
||||||
|
// The edgeKey maps to a possible request header value
|
||||||
|
var edgeKey = kvp.Key;
|
||||||
|
|
||||||
|
for (var j = 0; j < endpointKeys.Length; j++)
|
||||||
|
{
|
||||||
|
var endpointKey = endpointKeys[j];
|
||||||
|
|
||||||
|
if (edgeKey.Equals(endpointKey))
|
||||||
|
{
|
||||||
|
kvp.Value.Add(endpoint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (edgeKey.HasHostWildcard && endpointKey.HasHostWildcard &&
|
||||||
|
edgeKey.Port == endpointKey.Port && edgeKey.MatchHost(endpointKey.Host))
|
||||||
|
{
|
||||||
|
kvp.Value.Add(endpoint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return edges
|
||||||
|
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
|
||||||
|
{
|
||||||
|
if (edges == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(edges));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
|
||||||
|
// are then then execute them in linear order.
|
||||||
|
var ordered = edges
|
||||||
|
.Select(e => (host: (EdgeKey)e.State, destination: e.Destination))
|
||||||
|
.OrderBy(e => GetScore(e.host))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new HostPolicyJumpTable(exitDestination, ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetScore(in EdgeKey key)
|
||||||
|
{
|
||||||
|
// Higher score == lower priority.
|
||||||
|
if (key.MatchesHost && !key.HasHostWildcard && key.MatchesPort)
|
||||||
|
{
|
||||||
|
return 1; // Has host AND port, e.g. www.consoto.com:8080
|
||||||
|
}
|
||||||
|
else if (key.MatchesHost && !key.HasHostWildcard)
|
||||||
|
{
|
||||||
|
return 2; // Has host, e.g. www.consoto.com
|
||||||
|
}
|
||||||
|
else if (key.MatchesHost && key.MatchesPort)
|
||||||
|
{
|
||||||
|
return 3; // Has wildcard host AND port, e.g. *.consoto.com:8080
|
||||||
|
}
|
||||||
|
else if (key.MatchesHost)
|
||||||
|
{
|
||||||
|
return 4; // Has wildcard host, e.g. *.consoto.com
|
||||||
|
}
|
||||||
|
else if (key.MatchesPort)
|
||||||
|
{
|
||||||
|
return 5; // Has port, e.g. *:8080
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 6; // Has neither, e.g. *:* (or no metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HostMetadataEndpointComparer : EndpointMetadataComparer<IHostMetadata>
|
||||||
|
{
|
||||||
|
protected override int CompareMetadata(IHostMetadata x, IHostMetadata y)
|
||||||
|
{
|
||||||
|
// Ignore the metadata if it has an empty list of hosts.
|
||||||
|
return base.CompareMetadata(
|
||||||
|
x?.Hosts.Count > 0 ? x : null,
|
||||||
|
y?.Hosts.Count > 0 ? y : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HostPolicyJumpTable : PolicyJumpTable
|
||||||
|
{
|
||||||
|
private (EdgeKey host, int destination)[] _destinations;
|
||||||
|
private int _exitDestination;
|
||||||
|
|
||||||
|
public HostPolicyJumpTable(int exitDestination, (EdgeKey host, int destination)[] destinations)
|
||||||
|
{
|
||||||
|
_exitDestination = exitDestination;
|
||||||
|
_destinations = destinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetDestination(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
// 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 destinations = _destinations;
|
||||||
|
for (var i = 0; i < destinations.Length; i++)
|
||||||
|
{
|
||||||
|
var destination = destinations[i];
|
||||||
|
|
||||||
|
if ((!destination.host.MatchesPort || destination.host.Port == port) &&
|
||||||
|
destination.host.MatchHost(host))
|
||||||
|
{
|
||||||
|
return destination.destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
public readonly string Host;
|
||||||
|
|
||||||
|
private readonly string _wildcardEndsWith;
|
||||||
|
|
||||||
|
public EdgeKey(string host, int? port)
|
||||||
|
{
|
||||||
|
Host = host ?? WildcardHost;
|
||||||
|
Port = port;
|
||||||
|
|
||||||
|
HasHostWildcard = Host.StartsWith("*.", StringComparison.Ordinal);
|
||||||
|
_wildcardEndsWith = HasHostWildcard ? Host.Substring(1) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasHostWildcard { get; }
|
||||||
|
|
||||||
|
public bool MatchesHost => !string.Equals(Host, WildcardHost, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public bool MatchesPort => Port != null;
|
||||||
|
|
||||||
|
public bool MatchesAll => !MatchesHost && !MatchesPort;
|
||||||
|
|
||||||
|
public int CompareTo(EdgeKey other)
|
||||||
|
{
|
||||||
|
var result = Comparer<string>.Default.Compare(Host, other.Host);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Comparer<int?>.Default.Compare(Port, other.Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(object obj)
|
||||||
|
{
|
||||||
|
return CompareTo((EdgeKey)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(EdgeKey other)
|
||||||
|
{
|
||||||
|
return string.Equals(Host, other.Host, StringComparison.Ordinal) && Port == other.Port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchHost(string host)
|
||||||
|
{
|
||||||
|
if (MatchesHost)
|
||||||
|
{
|
||||||
|
if (HasHostWildcard)
|
||||||
|
{
|
||||||
|
return host.EndsWith(_wildcardEndsWith, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return string.Equals(host, Host, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return (Host?.GetHashCode() ?? 0) ^ (Port?.GetHashCode() ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj is EdgeKey key)
|
||||||
|
{
|
||||||
|
return Equals(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Host}:{Port?.ToString() ?? "*"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using RoutingWebSite;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.FunctionalTests
|
||||||
|
{
|
||||||
|
public class HostMatchingTests : IClassFixture<RoutingTestFixture<UseEndpointRoutingStartup>>
|
||||||
|
{
|
||||||
|
private readonly RoutingTestFixture<UseEndpointRoutingStartup> _fixture;
|
||||||
|
|
||||||
|
public HostMatchingTests(RoutingTestFixture<UseEndpointRoutingStartup> fixture)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient CreateClient(string baseAddress)
|
||||||
|
{
|
||||||
|
var client = _fixture.CreateClient(baseAddress);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("http://localhost")]
|
||||||
|
[InlineData("http://localhost:5001")]
|
||||||
|
public async Task Get_CatchAll(string baseAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var client = CreateClient(baseAddress);
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("*:*", responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("http://9000.0.0.1")]
|
||||||
|
[InlineData("http://9000.0.0.1:8888")]
|
||||||
|
public async Task Get_MatchWildcardDomain(string baseAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var client = CreateClient(baseAddress);
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("*.0.0.1:*", responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("http://127.0.0.1")]
|
||||||
|
[InlineData("http://127.0.0.1:8888")]
|
||||||
|
public async Task Get_MatchDomain(string baseAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var client = CreateClient(baseAddress);
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("127.0.0.1:*", responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("http://9000.0.0.1:5000")]
|
||||||
|
[InlineData("http://9000.0.0.1:5001")]
|
||||||
|
public async Task Get_MatchWildcardDomainAndPort(string baseAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var client = CreateClient(baseAddress);
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("*.0.0.1:5000,*.0.0.1:5001", responseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("http://www.contoso.com")]
|
||||||
|
[InlineData("http://contoso.com")]
|
||||||
|
public async Task Get_MatchWildcardDomainAndSubdomain(string baseAddress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var client = CreateClient(baseAddress);
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
Assert.Equal("contoso.com:*,*.contoso.com:*", responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests
|
||||||
|
|
||||||
public HttpClient Client { get; }
|
public HttpClient Client { get; }
|
||||||
|
|
||||||
|
public HttpClient CreateClient(string baseAddress)
|
||||||
|
{
|
||||||
|
var client = _server.CreateClient();
|
||||||
|
client.BaseAddress = new Uri(baseAddress);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Client.Dispose();
|
Client.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,339 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
{
|
||||||
|
// End-to-end tests for the host matching functionality
|
||||||
|
public class HostMatcherPolicyIntegrationTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_Host()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithPort()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com:8080");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_Host_Unicode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "æon.contoso.com", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "æon.contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithPort_IncorrectPort()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com:1111");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertNotMatch(context, httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithPort_IncorrectHost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "www.contoso.com:8080");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertNotMatch(context, httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithWildcard()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "æon.contoso.com:8080");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithWildcard_Unicode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "www.contoso.com:8080");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_Host_CaseInsensitive()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "Contoso.COM", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithPort_InferHttpPort()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:80", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com", "http");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithPort_InferHttpsPort()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:443", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com", "https");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_HostWithPort_NoHostHeader()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:443", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", null, "https");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertNotMatch(context, httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_Port_NoHostHeader_InferHttpsPort()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*:443", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", null, "https");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_NoMetadata_MatchesAnyHost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello");
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_EmptyHostList_MatchesAnyHost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_WildcardHost_MatchesAnyHost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Match_WildcardHostAndWildcardPort_MatchesAnyHost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*:*", });
|
||||||
|
|
||||||
|
var matcher = CreateMatcher(endpoint);
|
||||||
|
var (httpContext, context) = CreateContext("/hello", "contoso.com");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await matcher.MatchAsync(httpContext, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
MatcherAssert.AssertMatch(context, httpContext, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Matcher CreateMatcher(params RouteEndpoint[] endpoints)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection()
|
||||||
|
.AddOptions()
|
||||||
|
.AddLogging()
|
||||||
|
.AddRouting()
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
var builder = services.GetRequiredService<DfaMatcherBuilder>();
|
||||||
|
for (var i = 0; i < endpoints.Length; i++)
|
||||||
|
{
|
||||||
|
builder.AddEndpoint(endpoints[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static (HttpContext httpContext, EndpointSelectorContext context) CreateContext(
|
||||||
|
string path,
|
||||||
|
string host,
|
||||||
|
string scheme = null)
|
||||||
|
{
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
if (host != null)
|
||||||
|
{
|
||||||
|
httpContext.Request.Host = new HostString(host);
|
||||||
|
}
|
||||||
|
httpContext.Request.Path = path;
|
||||||
|
httpContext.Request.Scheme = scheme;
|
||||||
|
|
||||||
|
var context = new EndpointSelectorContext();
|
||||||
|
httpContext.Features.Set<IEndpointFeature>(context);
|
||||||
|
httpContext.Features.Set<IRouteValuesFeature>(context);
|
||||||
|
|
||||||
|
return (httpContext, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static RouteEndpoint CreateEndpoint(
|
||||||
|
string template,
|
||||||
|
object defaults = null,
|
||||||
|
object constraints = null,
|
||||||
|
int order = 0,
|
||||||
|
string[] hosts = null)
|
||||||
|
{
|
||||||
|
var metadata = new List<object>();
|
||||||
|
if (hosts != null)
|
||||||
|
{
|
||||||
|
metadata.Add(new HostAttribute(hosts ?? Array.Empty<string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName = "endpoint: " + template + " " + string.Join(", ", hosts ?? new[] { "*:*" });
|
||||||
|
return new RouteEndpoint(
|
||||||
|
TestConstants.EmptyRequestDelegate,
|
||||||
|
RoutePatternFactory.Parse(template, defaults, constraints),
|
||||||
|
order,
|
||||||
|
new EndpointMetadataCollection(metadata),
|
||||||
|
displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template)
|
||||||
|
{
|
||||||
|
var endpoint = CreateEndpoint(template);
|
||||||
|
return (CreateMatcher(endpoint), endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
{
|
||||||
|
public class HostMatcherPolicyTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[] { CreateEndpoint("/", null), };
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = policy.AppliesToEndpoints(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppliesToEndpoints_EndpointWithoutHosts_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = policy.AppliesToEndpoints(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppliesToEndpoints_EndpointHasHosts_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
|
||||||
|
CreateEndpoint("/", new HostAttribute(new[] { "localhost", })),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = policy.AppliesToEndpoints(endpoints);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(":")]
|
||||||
|
[InlineData(":80")]
|
||||||
|
[InlineData("80:")]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData("::")]
|
||||||
|
[InlineData("*:test")]
|
||||||
|
public void AppliesToEndpoints_InvalidHosts(string host)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host })), };
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
policy.AppliesToEndpoints(endpoints);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetEdges_GroupsByHost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoints = new[]
|
||||||
|
{
|
||||||
|
CreateEndpoint("/", new HostAttribute(new[] { "*:5000", "*:5001", })),
|
||||||
|
CreateEndpoint("/", new HostAttribute(Array.Empty<string>())),
|
||||||
|
CreateEndpoint("/", hostMetadata: null),
|
||||||
|
CreateEndpoint("/", new HostAttribute("*.contoso.com:*")),
|
||||||
|
CreateEndpoint("/", new HostAttribute("*.sub.contoso.com:*")),
|
||||||
|
CreateEndpoint("/", new HostAttribute("www.contoso.com:*")),
|
||||||
|
CreateEndpoint("/", new HostAttribute("www.contoso.com:5000")),
|
||||||
|
CreateEndpoint("/", new HostAttribute("*:*")),
|
||||||
|
};
|
||||||
|
|
||||||
|
var policy = CreatePolicy();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var edges = policy.GetEdges(endpoints);
|
||||||
|
|
||||||
|
var data = edges.OrderBy(e => e.State).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
data,
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*:*", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[7], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*:5000", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*:5001", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*.contoso.com:*", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("*.sub.contoso.com:*", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("www.contoso.com:*", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[5], }, e.Endpoints.ToArray());
|
||||||
|
},
|
||||||
|
e =>
|
||||||
|
{
|
||||||
|
Assert.Equal("www.contoso.com:5000", e.State.ToString());
|
||||||
|
Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[6], }, e.Endpoints.ToArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RouteEndpoint CreateEndpoint(string template, IHostMetadata hostMetadata)
|
||||||
|
{
|
||||||
|
var metadata = new List<object>();
|
||||||
|
if (hostMetadata != null)
|
||||||
|
{
|
||||||
|
metadata.Add(hostMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteEndpoint(
|
||||||
|
(context) => Task.CompletedTask,
|
||||||
|
RoutePatternFactory.Parse(template),
|
||||||
|
0,
|
||||||
|
new EndpointMetadataCollection(metadata),
|
||||||
|
$"test: {template} - {string.Join(", ", hostMetadata?.Hosts ?? Array.Empty<string>())}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HostMatcherPolicy CreatePolicy()
|
||||||
|
{
|
||||||
|
return new HostMatcherPolicy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -356,6 +356,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
||||||
|
|
||||||
return (httpContext, context);
|
return (httpContext, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static RouteEndpoint CreateEndpoint(
|
internal static RouteEndpoint CreateEndpoint(
|
||||||
string template,
|
string template,
|
||||||
object defaults = null,
|
object defaults = null,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
public class RoutingEndpointConventionBuilderExtensionsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RequireHost_HostNames()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var builder = new TestEndpointConventionBuilder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
builder.RequireHost("contoso.com:8080");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var convention = Assert.Single(builder.Conventions);
|
||||||
|
|
||||||
|
var endpointModel = new RouteEndpointBuilder((context) => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0);
|
||||||
|
convention(endpointModel);
|
||||||
|
|
||||||
|
var hostMetadata = Assert.IsType<HostAttribute>(Assert.Single(endpointModel.Metadata));
|
||||||
|
|
||||||
|
Assert.Equal("contoso.com:8080", hostMetadata.Hosts.Single());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestEndpointConventionBuilder : IEndpointConventionBuilder
|
||||||
|
{
|
||||||
|
public IList<Action<EndpointBuilder>> Conventions { get; } = new List<Action<EndpointBuilder>>();
|
||||||
|
|
||||||
|
public void Add(Action<EndpointBuilder> convention)
|
||||||
|
{
|
||||||
|
Conventions.Add(convention);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Endpoints;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Internal;
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
@ -112,6 +113,12 @@ namespace RoutingWebSite
|
||||||
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
|
"Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { }));
|
||||||
},
|
},
|
||||||
new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll"));
|
new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll"));
|
||||||
|
|
||||||
|
MapHostEndpoint(routes);
|
||||||
|
MapHostEndpoint(routes, "*.0.0.1");
|
||||||
|
MapHostEndpoint(routes, "127.0.0.1");
|
||||||
|
MapHostEndpoint(routes, "*.0.0.1:5000", "*.0.0.1:5001");
|
||||||
|
MapHostEndpoint(routes, "contoso.com:*", "*.contoso.com:*");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
|
app.Map("/Branch1", branch => SetupBranch(branch, "Branch1"));
|
||||||
|
|
@ -124,6 +131,31 @@ namespace RoutingWebSite
|
||||||
app.UseEndpoint();
|
app.UseEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IEndpointConventionBuilder MapHostEndpoint(IEndpointRouteBuilder routes, params string[] hosts)
|
||||||
|
{
|
||||||
|
var hostsDisplay = (hosts == null || hosts.Length == 0)
|
||||||
|
? "*:*"
|
||||||
|
: string.Join(",", hosts.Select(h => h.Contains(':') ? h : h + ":*"));
|
||||||
|
|
||||||
|
var conventionBuilder = routes.MapGet(
|
||||||
|
"api/DomainWildcard",
|
||||||
|
httpContext =>
|
||||||
|
{
|
||||||
|
var response = httpContext.Response;
|
||||||
|
response.StatusCode = 200;
|
||||||
|
response.ContentType = "text/plain";
|
||||||
|
return response.WriteAsync(hostsDisplay);
|
||||||
|
});
|
||||||
|
|
||||||
|
conventionBuilder.Add(endpointBuilder =>
|
||||||
|
{
|
||||||
|
endpointBuilder.Metadata.Add(new HostAttribute(hosts));
|
||||||
|
endpointBuilder.DisplayName += " HOST: " + hostsDisplay;
|
||||||
|
});
|
||||||
|
|
||||||
|
return conventionBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
private void SetupBranch(IApplicationBuilder app, string name)
|
private void SetupBranch(IApplicationBuilder app, string name)
|
||||||
{
|
{
|
||||||
app.UseRouting(routes =>
|
app.UseRouting(routes =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue