diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs b/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs
index 3ac23d9c40..9496b26bac 100644
--- a/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs
+++ b/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs
@@ -2,9 +2,11 @@
// 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.Globalization;
using Microsoft.AspNetCore.Http.Abstractions;
using Microsoft.AspNetCore.Http.Internal;
+using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Http
{
@@ -73,11 +75,9 @@ namespace Microsoft.AspNetCore.Http
{
get
{
- string host, port;
+ GetParts(_value, out var host, out var port);
- GetParts(out host, out port);
-
- return host;
+ return host.ToString();
}
}
@@ -89,17 +89,15 @@ namespace Microsoft.AspNetCore.Http
{
get
{
- string host, port;
- int p;
+ GetParts(_value, out var host, out var port);
- GetParts(out host, out port);
-
- if (string.IsNullOrEmpty(port) || !int.TryParse(port, out p))
+ if (!StringSegment.IsNullOrEmpty(port)
+ && int.TryParse(port.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out var p))
{
- return null;
+ return p;
}
- return p;
+ return null;
}
}
@@ -135,15 +133,14 @@ namespace Microsoft.AspNetCore.Http
if (i != _value.Length)
{
- string host, port;
- GetParts(out host, out port);
+ GetParts(_value, out var host, out var port);
var mapping = new IdnMapping();
- host = mapping.GetAscii(host);
+ var encoded = mapping.GetAscii(host.Buffer, host.Offset, host.Length);
- return string.IsNullOrEmpty(port)
- ? host
- : string.Concat(host, ":", port);
+ return StringSegment.IsNullOrEmpty(port)
+ ? encoded
+ : string.Concat(encoded, ":", port.ToString());
}
return _value;
@@ -208,6 +205,74 @@ namespace Microsoft.AspNetCore.Http
UriComponents.HostAndPort, UriFormat.Unescaped));
}
+ ///
+ /// 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.
+ ///
+ /// Host header value with or without a port.
+ /// A set of pattern to match, without ports.
+ ///
+ /// 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.
+ ///
+ ///
+ public static bool MatchesAny(StringSegment value, IList 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;
+ }
+
///
/// Compares the equality of the Value property, ignoring case.
///
@@ -270,43 +335,43 @@ namespace Microsoft.AspNetCore.Http
///
/// Parses the current value. IPv6 addresses will have brackets added if they are missing.
///
- private void GetParts(out string host, out string port)
+ private static void GetParts(StringSegment value, out StringSegment host, out StringSegment port)
{
int index;
port = null;
host = null;
- if (string.IsNullOrEmpty(_value))
+ if (StringSegment.IsNullOrEmpty(value))
{
return;
}
- else if ((index = _value.IndexOf(']')) >= 0)
+ else if ((index = value.IndexOf(']')) >= 0)
{
// IPv6 in brackets [::1], maybe with port
- host = _value.Substring(0, index + 1);
-
- if ((index = _value.IndexOf(':', index + 1)) >= 0)
+ host = value.Subsegment(0, index + 1);
+ // Is there a colon and at least one character?
+ if (index + 2 < value.Length && value[index + 1] == ':')
{
- port = _value.Substring(index + 1);
+ port = value.Subsegment(index + 2);
}
}
- else if ((index = _value.IndexOf(':')) >= 0
- && index < _value.Length - 1
- && _value.IndexOf(':', index + 1) >= 0)
+ else if ((index = value.IndexOf(':')) >= 0
+ && index < value.Length - 1
+ && value.IndexOf(':', index + 1) >= 0)
{
// IPv6 without brackets ::1 is the only type of host with 2 or more colons
- host = $"[{_value}]";
+ host = $"[{value}]";
port = null;
}
else if (index >= 0)
{
// Has a port
- host = _value.Substring(0, index);
- port = _value.Substring(index + 1);
+ host = value.Subsegment(0, index);
+ port = value.Subsegment(index + 1);
}
else
{
- host = _value;
+ host = value;
port = null;
}
}
diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs
index d529ed76d2..85820f8ffc 100644
--- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs
+++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs
@@ -1,7 +1,9 @@
// 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.Testing;
+using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.Http
@@ -123,5 +125,51 @@ namespace Microsoft.AspNetCore.Http
// Act and Assert
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(() => HostString.MatchesAny("example.com:1abc", new StringSegment[] { "example.com" }));
+ }
}
}