// 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 System.Globalization; using Microsoft.AspNetCore.Http.Abstractions; namespace Microsoft.AspNetCore.Http { /// /// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in /// HTTP headers. /// public struct HostString : IEquatable { private readonly string _value; /// /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. /// IPv4 and IPv6 addresses are also allowed, and also may have ports. /// /// public HostString(string value) { _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. /// public string Value { get { return _value; } } public bool HasValue { 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(). /// /// public override string ToString() { return ToUriComponent(); } /// /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. /// /// public string ToUriComponent() { if (string.IsNullOrEmpty(_value)) { return string.Empty; } string host, port; GetParts(out host, out port); if (host.IndexOf('[') == -1) { var mapping = new IdnMapping(); host = mapping.GetAscii(host); } return string.IsNullOrEmpty(port) ? host : string.Concat(host, ":", port); } /// /// Creates a new HostString from the given URI component. /// Any punycode will be converted to Unicode. /// /// /// public static HostString FromUriComponent(string uriComponent) { if (!string.IsNullOrEmpty(uriComponent)) { int index; if (uriComponent.IndexOf('[') >= 0) { // IPv6 in brackets [::1], maybe with port } else if ((index = uriComponent.IndexOf(':')) >= 0 && index < uriComponent.Length - 1 && uriComponent.IndexOf(':', index + 1) >= 0) { // IPv6 without brackets ::1 is the only type of host with 2 or more colons } else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) { // Contains punycode if (index >= 0) { // Has a port string port = uriComponent.Substring(index); var mapping = new IdnMapping(); uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; } else { var mapping = new IdnMapping(); uriComponent = mapping.GetUnicode(uriComponent); } } } return new HostString(uriComponent); } /// /// Creates a new HostString from the host and port of the give Uri instance. /// Punycode will be converted to Unicode. /// /// /// public static HostString FromUriComponent(Uri uri) { if (uri == null) { throw new ArgumentNullException(nameof(uri)); } return new HostString(uri.GetComponents( UriComponents.NormalizedHost | // Always convert punycode to Unicode. UriComponents.HostAndPort, UriFormat.Unescaped)); } /// /// Compares the equality of the Value property, ignoring case. /// /// /// public bool Equals(HostString other) { return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); } /// /// Compares against the given object only if it is a HostString. /// /// /// public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } return obj is HostString && Equals((HostString)obj); } /// /// Gets a hash code for the value. /// /// public override int GetHashCode() { return (_value != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); } /// /// Compares the two instances for equality. /// /// /// /// public static bool operator ==(HostString left, HostString right) { return left.Equals(right); } /// /// Compares the two instances for inequality. /// /// /// /// public static bool operator !=(HostString left, HostString right) { 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; } } } }