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);
+ }
+ }
+}