Skip to content

Commit e6db606

Browse files
committed
Add HostPolicyMatcher
1 parent 02a8afe commit e6db606

File tree

9 files changed

+688
-2
lines changed

9 files changed

+688
-2
lines changed

src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public static IServiceCollection AddRouting(this IServiceCollection services)
8686
//
8787
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
8888
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
89+
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HostMatcherPolicy>());
8990

9091
//
9192
// Misc infrastructure
@@ -121,4 +122,4 @@ public static IServiceCollection AddRouting(
121122
return services;
122123
}
123124
}
124-
}
125+
}

src/Http/Routing/src/HostAttribute.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace Microsoft.AspNetCore.Routing
9+
{
10+
public class HostAttribute : Attribute, IHostMetadata
11+
{
12+
public IReadOnlyList<string> Hosts { get; }
13+
14+
public HostAttribute(params string[] hosts)
15+
{
16+
if (hosts == null)
17+
{
18+
throw new ArgumentNullException(nameof(hosts));
19+
}
20+
21+
Hosts = hosts.ToArray();
22+
}
23+
}
24+
}

src/Http/Routing/src/IHostMetadata.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.AspNetCore.Routing
7+
{
8+
/// <summary>
9+
/// Represents host metadata used during routing.
10+
/// </summary>
11+
public interface IHostMetadata
12+
{
13+
IReadOnlyList<string> Hosts { get; }
14+
}
15+
}
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace Microsoft.AspNetCore.Routing.Matching
10+
{
11+
public class HostMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy
12+
{
13+
// Run after HTTP methods, but before 'default'.
14+
public override int Order { get; } = -100;
15+
16+
public IComparer<Endpoint> Comparer { get; } = new HostMetadataEndpointComparer();
17+
18+
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
19+
{
20+
if (endpoints == null)
21+
{
22+
throw new ArgumentNullException(nameof(endpoints));
23+
}
24+
25+
return endpoints.Any(e =>
26+
{
27+
var hosts = e.Metadata.GetMetadata<IHostMetadata>()?.Hosts;
28+
if (hosts == null || hosts.Count == 0)
29+
{
30+
return false;
31+
}
32+
33+
foreach (var host in hosts)
34+
{
35+
// Don't run policy on endpoints that match everything
36+
var key = CreateEdgeKey(host);
37+
if (!key.MatchesAll)
38+
{
39+
return true;
40+
}
41+
}
42+
43+
return false;
44+
});
45+
}
46+
47+
private static EdgeKey CreateEdgeKey(string host)
48+
{
49+
if (host == null)
50+
{
51+
return EdgeKey.WildcardEdgeKey;
52+
}
53+
54+
var hostParts = host.Split(':');
55+
if (hostParts.Length == 1)
56+
{
57+
return new EdgeKey(hostParts[0], null);
58+
}
59+
if (hostParts.Length == 2)
60+
{
61+
if (int.TryParse(hostParts[1], out var port))
62+
{
63+
return new EdgeKey(hostParts[0], port);
64+
}
65+
else if (string.Equals(hostParts[1], "*", StringComparison.Ordinal))
66+
{
67+
return new EdgeKey(hostParts[0], null);
68+
}
69+
}
70+
71+
throw new InvalidOperationException($"Could not parse host: {host}");
72+
}
73+
74+
public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
75+
{
76+
if (endpoints == null)
77+
{
78+
throw new ArgumentNullException(nameof(endpoints));
79+
}
80+
81+
// The algorithm here is designed to be preserve the order of the endpoints
82+
// while also being relatively simple. Preserving order is important.
83+
84+
// First, build a dictionary of all of the hosts that are included
85+
// at this node.
86+
//
87+
// For now we're just building up the set of keys. We don't add any endpoints
88+
// to lists now because we don't want ordering problems.
89+
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
90+
for (var i = 0; i < endpoints.Count; i++)
91+
{
92+
var endpoint = endpoints[i];
93+
var hosts = endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray();
94+
if (hosts == null || hosts.Length == 0)
95+
{
96+
hosts = new[] { EdgeKey.WildcardEdgeKey };
97+
}
98+
99+
for (var j = 0; j < hosts.Length; j++)
100+
{
101+
var contentType = hosts[j];
102+
if (!edges.ContainsKey(contentType))
103+
{
104+
edges.Add(contentType, new List<Endpoint>());
105+
}
106+
}
107+
}
108+
109+
// Now in a second loop, add endpoints to these lists. We've enumerated all of
110+
// the states, so we want to see which states this endpoint matches.
111+
for (var i = 0; i < endpoints.Count; i++)
112+
{
113+
var endpoint = endpoints[i];
114+
115+
var endpointKeys = endpoint.Metadata.GetMetadata<IHostMetadata>()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray() ?? Array.Empty<EdgeKey>();
116+
if (endpointKeys.Length == 0)
117+
{
118+
// OK this means that this endpoint matches *all* hosts.
119+
// So, loop and add it to all states.
120+
foreach (var kvp in edges)
121+
{
122+
kvp.Value.Add(endpoint);
123+
}
124+
}
125+
else
126+
{
127+
// OK this endpoint matches specific hosts
128+
foreach (var kvp in edges)
129+
{
130+
// The edgeKey maps to a possible request header value
131+
var edgeKey = kvp.Key;
132+
133+
for (var j = 0; j < endpointKeys.Length; j++)
134+
{
135+
var endpointKey = endpointKeys[j];
136+
137+
if (edgeKey.Equals(endpointKey))
138+
{
139+
kvp.Value.Add(endpoint);
140+
break;
141+
}
142+
else if (edgeKey.HasHostWildcard && endpointKey.HasHostWildcard &&
143+
edgeKey.Port == endpointKey.Port && edgeKey.MatchHost(endpointKey.Host))
144+
{
145+
kvp.Value.Add(endpoint);
146+
break;
147+
}
148+
}
149+
}
150+
}
151+
}
152+
153+
return edges
154+
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
155+
.ToArray();
156+
}
157+
158+
public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJumpTableEdge> edges)
159+
{
160+
if (edges == null)
161+
{
162+
throw new ArgumentNullException(nameof(edges));
163+
}
164+
165+
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
166+
// are then then execute them in linear order.
167+
var ordered = edges
168+
.Select(e => (host: (EdgeKey)e.State, destination: e.Destination))
169+
.OrderBy(e => GetScore(e.host))
170+
.ToArray();
171+
172+
return new HostPolicyJumpTable(exitDestination, ordered);
173+
}
174+
175+
private int GetScore(in EdgeKey key)
176+
{
177+
// Higher score == lower priority.
178+
if (key.MatchesHost && !key.HasHostWildcard && key.MatchesPort)
179+
{
180+
return 1; // Has host AND port
181+
}
182+
else if (key.MatchesHost && !key.HasHostWildcard)
183+
{
184+
return 2; // Has host
185+
}
186+
else if (key.MatchesHost && key.MatchesPort)
187+
{
188+
return 3; // Has wildcard host AND port
189+
}
190+
else if (key.MatchesHost)
191+
{
192+
return 4; // Has wildcard host
193+
}
194+
else if (key.MatchesPort)
195+
{
196+
return 5; // Has port
197+
}
198+
else
199+
{
200+
return 6; // Has neither
201+
}
202+
}
203+
204+
private class HostMetadataEndpointComparer : EndpointMetadataComparer<IHostMetadata>
205+
{
206+
protected override int CompareMetadata(IHostMetadata x, IHostMetadata y)
207+
{
208+
// Ignore the metadata if it has an empty list of hosts.
209+
return base.CompareMetadata(
210+
x?.Hosts.Count > 0 ? x : null,
211+
y?.Hosts.Count > 0 ? y : null);
212+
}
213+
}
214+
215+
private class HostPolicyJumpTable : PolicyJumpTable
216+
{
217+
private (EdgeKey host, int destination)[] _destinations;
218+
private int _exitDestination;
219+
220+
public HostPolicyJumpTable(int exitDestination, (EdgeKey host, int destination)[] destinations)
221+
{
222+
_exitDestination = exitDestination;
223+
_destinations = destinations;
224+
}
225+
226+
public override int GetDestination(HttpContext httpContext)
227+
{
228+
var destinations = _destinations;
229+
for (var i = 0; i < destinations.Length; i++)
230+
{
231+
var destination = destinations[i];
232+
233+
if ((!destination.host.MatchesPort || destination.host.Port == httpContext.Request.Host.Port) &&
234+
destination.host.MatchHost(httpContext.Request.Host.Host))
235+
{
236+
return destination.destination;
237+
}
238+
}
239+
240+
return _exitDestination;
241+
}
242+
}
243+
244+
private readonly struct EdgeKey : IEquatable<EdgeKey>, IComparable<EdgeKey>, IComparable
245+
{
246+
private const string WildcardHost = "*";
247+
internal static readonly EdgeKey WildcardEdgeKey = new EdgeKey(null, null);
248+
249+
public readonly int? Port;
250+
public readonly string Host;
251+
252+
private readonly string _wildcardEndsWith;
253+
254+
public EdgeKey(string host, int? port)
255+
{
256+
Host = host ?? WildcardHost;
257+
Port = port;
258+
259+
HasHostWildcard = Host.StartsWith("*.", StringComparison.Ordinal);
260+
_wildcardEndsWith = HasHostWildcard ? Host.Substring(1) : null;
261+
}
262+
263+
public bool HasHostWildcard { get; }
264+
265+
public bool MatchesHost => !string.Equals(Host, WildcardHost, StringComparison.Ordinal);
266+
267+
public bool MatchesPort => Port != null;
268+
269+
public bool MatchesAll => !MatchesHost && !MatchesPort;
270+
271+
public int CompareTo(EdgeKey other)
272+
{
273+
var result = Comparer<string>.Default.Compare(Host, other.Host);
274+
if (result != 0)
275+
{
276+
return result;
277+
}
278+
279+
return Comparer<int?>.Default.Compare(Port, other.Port);
280+
}
281+
282+
public int CompareTo(object obj)
283+
{
284+
return CompareTo((EdgeKey)obj);
285+
}
286+
287+
public bool Equals(EdgeKey other)
288+
{
289+
return string.Equals(Host, other.Host, StringComparison.Ordinal) && Port == other.Port;
290+
}
291+
292+
public bool MatchHost(string host)
293+
{
294+
if (MatchesHost)
295+
{
296+
if (HasHostWildcard)
297+
{
298+
return host.EndsWith(_wildcardEndsWith, StringComparison.OrdinalIgnoreCase);
299+
}
300+
else
301+
{
302+
return string.Equals(host, Host, StringComparison.OrdinalIgnoreCase);
303+
}
304+
}
305+
306+
return true;
307+
}
308+
309+
public override int GetHashCode()
310+
{
311+
return (Host?.GetHashCode() ?? 0) ^ (Port?.GetHashCode() ?? 0);
312+
}
313+
314+
public override bool Equals(object obj)
315+
{
316+
if (obj is EdgeKey key)
317+
{
318+
return Equals(key);
319+
}
320+
321+
return false;
322+
}
323+
324+
public override string ToString()
325+
{
326+
return $"{Host}:{Port?.ToString() ?? "*"}";
327+
}
328+
}
329+
}
330+
}

0 commit comments

Comments
 (0)