diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs b/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs index f767255b1f..2641f33540 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/HostString.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Microsoft.AspNetCore.Http.Abstractions; namespace Microsoft.AspNetCore.Http { @@ -24,6 +25,31 @@ namespace Microsoft.AspNetCore.Http _value = value; } + /// + /// Creates a new HostString from its host and port parts. + /// + /// The value should be Unicode rather than punycode. IPv6 addresses must use square braces. + /// A positive, greater than 0 value representing the port in the host string. + public HostString(string host, int port) + { + if(port <= 0) + { + throw new ArgumentOutOfRangeException(nameof(port), Resources.Exception_PortMustBeGreaterThanZero); + } + + int index; + if (host.IndexOf('[') == -1 + && (index = host.IndexOf(':')) >= 0 + && index < host.Length - 1 + && host.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{host}]"; + } + + _value = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + /// /// Returns the original value from the constructor. /// @@ -37,6 +63,45 @@ namespace Microsoft.AspNetCore.Http get { return !string.IsNullOrEmpty(_value); } } + /// + /// Returns the value of the host part of the value. The port is removed if it was present. + /// IPv6 addresses will have brackets added if they are missing. + /// + /// + public string Host + { + get + { + string host, port; + + GetParts(out host, out port); + + return host; + } + } + + /// + /// Returns the value of the port part of the host, or null if none is found. + /// + /// + public int? Port + { + get + { + string host, port; + int p; + + GetParts(out host, out port); + + if (string.IsNullOrEmpty(port) || !int.TryParse(port, out p)) + { + return null; + } + + return p; + } + } + /// /// Returns the value as normalized by ToUriComponent(). /// @@ -53,35 +118,25 @@ namespace Microsoft.AspNetCore.Http /// public string ToUriComponent() { - int index; if (string.IsNullOrEmpty(_value)) { return string.Empty; } - else if (_value.IndexOf('[') >= 0) - { - // IPv6 in brackets [::1], maybe with port - return _value; - } - 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 - return $"[{_value}]"; - } - else if (index >= 0) - { - // Has a port - string port = _value.Substring(index); + + string host, port; + + GetParts(out host, out port); + + if (host.IndexOf('[') == -1) + { var mapping = new IdnMapping(); - return mapping.GetAscii(_value, 0, index) + port; - } - else - { - var mapping = new IdnMapping(); - return mapping.GetAscii(_value); + host = mapping.GetAscii(host); } + + return string.IsNullOrEmpty(port) + ? host + : string.Concat(host, ":", port); + } /// @@ -197,5 +252,49 @@ namespace Microsoft.AspNetCore.Http { return !left.Equals(right); } + + /// + /// Parses the current value. IPv6 addresses will have brackets added if they are missing. + /// + private void GetParts(out string host, out string port) + { + int index; + port = null; + host = null; + + if (string.IsNullOrEmpty(_value)) + { + return; + } + 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) + { + port = _value.Substring(index + 1); + } + } + 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}]"; + port = null; + } + else if (index >= 0) + { + // Has a port + host = _value.Substring(0, index); + port = _value.Substring(index + 1); + } + else + { + host = _value; + port = null; + } + } } } diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs index 438644852a..40e4255667 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs @@ -138,6 +138,22 @@ namespace Microsoft.AspNetCore.Http.Abstractions return string.Format(CultureInfo.CurrentCulture, GetString("Exception_InvokeDoesNotSupportRefOrOutParams"), p0); } + /// + /// The value must be greater than zero. + /// + internal static string Exception_PortMustBeGreaterThanZero + { + get { return GetString("Exception_PortMustBeGreaterThanZero"); } + } + + /// + /// The value must be greater than zero. + /// + internal static string FormatException_PortMustBeGreaterThanZero() + { + return GetString("Exception_PortMustBeGreaterThanZero"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx b/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx index 414bad847d..19ce173d19 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx @@ -141,4 +141,7 @@ The '{0}' method must not have ref or out parameters. + + The value must be greater than zero. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs new file mode 100644 index 0000000000..6e5a684e0b --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/HostStringTest.cs @@ -0,0 +1,96 @@ +// 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 Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Http +{ + public class HostStringTests + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void CtorThrows_IfPortIsNotGreaterThanZero(int port) + { + // Act and Assert + ExceptionAssert.ThrowsArgumentOutOfRange(() => new HostString("localhost", port), "port", "The value must be greater than zero."); + } + + [Theory] + [InlineData("localhost", "localhost")] + [InlineData("1.2.3.4", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機", "本地主機")] + [InlineData("localhost:5000", "localhost")] + [InlineData("1.2.3.4:5000", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]:5000", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機:5000", "本地主機")] + public void Domain_ExtractsHostFromValue(string sourceValue, string expectedDomain) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Host; + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Theory] + [InlineData("localhost", null)] + [InlineData("1.2.3.4", null)] + [InlineData("[2001:db8:a0b:12f0::1]", null)] + [InlineData("本地主機", null)] + [InlineData("localhost:5000", 5000)] + [InlineData("1.2.3.4:5000", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]:5000", 5000)] + [InlineData("本地主機:5000", 5000)] + public void Port_ExtractsPortFromValue(string sourceValue, int? expectedPort) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Equal(expectedPort, result); + } + + [Theory] + [InlineData("localhost:BLAH")] + public void Port_ExtractsInvalidPortFromValue(string sourceValue) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Equal(null, result); + } + + [Theory] + [InlineData("localhost", 5000, "localhost", 5000)] + [InlineData("1.2.3.4", 5000, "1.2.3.4", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("2001:db8:a0b:12f0::1", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("本地主機", 5000, "本地主機", 5000)] + public void Ctor_CreatesFromHostAndPort(string sourceHost, int sourcePort, string expectedHost, int expectedPort) + { + // Arrange + var hostString = new HostString(sourceHost, sourcePort); + + // Act + var host = hostString.Host; + var port = hostString.Port; + + // Assert + Assert.Equal(expectedHost, host); + Assert.Equal(expectedPort, port); + } + } +}