Add HostString.MatchesAny #2863

This commit is contained in:
Chris Ross (ASP.NET) 2018-02-09 09:48:22 -08:00
parent f152b9863a
commit d20d47924c
2 changed files with 144 additions and 31 deletions

View File

@ -2,9 +2,11 @@
// 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;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Abstractions;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Http namespace Microsoft.AspNetCore.Http
{ {
@ -73,11 +75,9 @@ namespace Microsoft.AspNetCore.Http
{ {
get get
{ {
string host, port; GetParts(_value, out var host, out var port);
GetParts(out host, out port); return host.ToString();
return host;
} }
} }
@ -89,17 +89,15 @@ namespace Microsoft.AspNetCore.Http
{ {
get get
{ {
string host, port; GetParts(_value, out var host, out var port);
int p;
GetParts(out host, out port); if (!StringSegment.IsNullOrEmpty(port)
&& int.TryParse(port.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out var p))
if (string.IsNullOrEmpty(port) || !int.TryParse(port, out p))
{ {
return null; return p;
} }
return p; return null;
} }
} }
@ -135,15 +133,14 @@ namespace Microsoft.AspNetCore.Http
if (i != _value.Length) if (i != _value.Length)
{ {
string host, port; GetParts(_value, out var host, out var port);
GetParts(out host, out port);
var mapping = new IdnMapping(); var mapping = new IdnMapping();
host = mapping.GetAscii(host); var encoded = mapping.GetAscii(host.Buffer, host.Offset, host.Length);
return string.IsNullOrEmpty(port) return StringSegment.IsNullOrEmpty(port)
? host ? encoded
: string.Concat(host, ":", port); : string.Concat(encoded, ":", port.ToString());
} }
return _value; return _value;
@ -208,6 +205,74 @@ namespace Microsoft.AspNetCore.Http
UriComponents.HostAndPort, UriFormat.Unescaped)); UriComponents.HostAndPort, UriFormat.Unescaped));
} }
/// <summary>
/// Matches the host portion of a host header value against a list of patterns.
/// The host may be the encoded punycode or decoded unicode form so long as the pattern
/// uses the same format.
/// </summary>
/// <param name="value">Host header value with or without a port.</param>
/// <param name="patterns">A set of pattern to match, without ports.</param>
/// <remarks>
/// The port on the given value is ignored. The patterns should not have ports.
/// The patterns may be exact matches like "example.com", a top level wildcard "*"
/// that matches all hosts, or a subdomain wildcard like "*.example.com" that matches
/// "abc.example.com:443" but not "example.com:443".
/// Matching is case insensitive.
/// </remarks>
/// <returns></returns>
public static bool MatchesAny(StringSegment value, IList<StringSegment> patterns)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (patterns == null)
{
throw new ArgumentNullException(nameof(patterns));
}
// Drop the port
GetParts(value, out var host, out var port);
for (int i = 0; i < port.Length; i++)
{
if (port[i] < '0' || '9' < port[i])
{
throw new FormatException($"The given host value '{value}' has a malformed port.");
}
}
for (int i = 0; i < patterns.Count; i++)
{
var pattern = patterns[i];
if (pattern == "*")
{
return true;
}
if (StringSegment.Equals(pattern, host, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Sub-domain wildcards: *.example.com
if (pattern.StartsWith("*.", StringComparison.Ordinal) && host.Length >= pattern.Length)
{
// .example.com
var allowedRoot = pattern.Subsegment(1);
var hostRoot = host.Subsegment(host.Length - allowedRoot.Length);
if (hostRoot.Equals(allowedRoot, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
/// <summary> /// <summary>
/// Compares the equality of the Value property, ignoring case. /// Compares the equality of the Value property, ignoring case.
/// </summary> /// </summary>
@ -270,43 +335,43 @@ namespace Microsoft.AspNetCore.Http
/// <summary> /// <summary>
/// Parses the current value. IPv6 addresses will have brackets added if they are missing. /// Parses the current value. IPv6 addresses will have brackets added if they are missing.
/// </summary> /// </summary>
private void GetParts(out string host, out string port) private static void GetParts(StringSegment value, out StringSegment host, out StringSegment port)
{ {
int index; int index;
port = null; port = null;
host = null; host = null;
if (string.IsNullOrEmpty(_value)) if (StringSegment.IsNullOrEmpty(value))
{ {
return; return;
} }
else if ((index = _value.IndexOf(']')) >= 0) else if ((index = value.IndexOf(']')) >= 0)
{ {
// IPv6 in brackets [::1], maybe with port // IPv6 in brackets [::1], maybe with port
host = _value.Substring(0, index + 1); host = value.Subsegment(0, index + 1);
// Is there a colon and at least one character?
if ((index = _value.IndexOf(':', index + 1)) >= 0) if (index + 2 < value.Length && value[index + 1] == ':')
{ {
port = _value.Substring(index + 1); port = value.Subsegment(index + 2);
} }
} }
else if ((index = _value.IndexOf(':')) >= 0 else if ((index = value.IndexOf(':')) >= 0
&& index < _value.Length - 1 && index < value.Length - 1
&& _value.IndexOf(':', index + 1) >= 0) && value.IndexOf(':', index + 1) >= 0)
{ {
// IPv6 without brackets ::1 is the only type of host with 2 or more colons // IPv6 without brackets ::1 is the only type of host with 2 or more colons
host = $"[{_value}]"; host = $"[{value}]";
port = null; port = null;
} }
else if (index >= 0) else if (index >= 0)
{ {
// Has a port // Has a port
host = _value.Substring(0, index); host = value.Subsegment(0, index);
port = _value.Substring(index + 1); port = value.Subsegment(index + 1);
} }
else else
{ {
host = _value; host = value;
port = null; port = null;
} }
} }

View File

@ -1,7 +1,9 @@
// 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 Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Primitives;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Http namespace Microsoft.AspNetCore.Http
@ -123,5 +125,51 @@ namespace Microsoft.AspNetCore.Http
// Act and Assert // Act and Assert
Assert.NotEqual(hostString, new HostString(string.Empty)); Assert.NotEqual(hostString, new HostString(string.Empty));
} }
[Theory]
[InlineData("localHost", "localhost")]
[InlineData("localHost", "*")] // Any - Used by HttpSys
[InlineData("localhost:9090", "localHost")]
[InlineData("example.com:443", "example.com")]
[InlineData("foo.eXample.com:443", "*.exampLe.com")]
[InlineData("f.eXample.com:443", "*.exampLe.com")]
[InlineData("a.b.c.eXample.com:443", "*.exampLe.com")]
[InlineData("127.0.0.1", "127.0.0.1")]
[InlineData("127.0.0.1:443", "127.0.0.1")]
[InlineData("xn--c1yn36f:443", "xn--c1yn36f")]
[InlineData("點看", "點看")]
[InlineData("[::ABC]", "[::aBc]")]
[InlineData("[::1]:80", "[::1]")]
[InlineData("[::1]:", "[::1]")]
[InlineData("::1", "[::1]")]
public void HostMatches(string host, string pattern)
{
Assert.True(HostString.MatchesAny(host, new StringSegment[] { pattern }));
}
[Theory]
[InlineData("example.com", "localhost")]
[InlineData("localhost:9090", "example.com")]
[InlineData(":80", "localhost")]
[InlineData(":", "localhost")]
[InlineData("example.com:443", "*.example.com")]
[InlineData(".example.com:443", "*.example.com")]
[InlineData("foo.com:443", "*.example.com")]
[InlineData("foo.example.com.bar:443", "*.example.com")]
[InlineData(".com:443", "*.com")]
[InlineData("xn--c1yn36f:443", "點看")]
[InlineData("[::1", "[::1]")]
[InlineData("[::1:80", "[::1]")]
[InlineData("::1", "::1")] // Brackets are added to the host before the comparison
public void HostDoesntMatch(string host, string pattern)
{
Assert.False(HostString.MatchesAny(host, new StringSegment[] { pattern }));
}
[Fact]
public void HostMatchThrowsForBadPort()
{
Assert.Throws<FormatException>(() => HostString.MatchesAny("example.com:1abc", new StringSegment[] { "example.com" }));
}
} }
} }