// 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.Collections.Generic; using System.Globalization; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Primitives; 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 (host == null) { throw new ArgumentNullException(nameof(host)); } 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 { GetParts(_value, out var host, out var port); return host.ToString(); } } /// /// Returns the value of the port part of the host, or null if none is found. /// /// public int? Port { get { GetParts(_value, out var host, out var port); if (!StringSegment.IsNullOrEmpty(port) && int.TryParse(port.ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) { return p; } return null; } } /// /// 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; } int i; for (i = 0; i < _value.Length; ++i) { if (!HostStringHelper.IsSafeHostStringChar(_value[i])) { break; } } if (i != _value.Length) { GetParts(_value, out var host, out var port); var mapping = new IdnMapping(); var encoded = mapping.GetAscii(host.Buffer, host.Offset, host.Length); return StringSegment.IsNullOrEmpty(port) ? encoded : string.Concat(encoded, ":", port.ToString()); } return _value; } /// /// 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)); } /// /// 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. /// /// /// public bool Equals(HostString other) { if (!HasValue && !other.HasValue) { return true; } 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 !HasValue; } return obj is HostString && Equals((HostString)obj); } /// /// Gets a hash code for the value. /// /// public override int GetHashCode() { return (HasValue ? 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 static void GetParts(StringSegment value, out StringSegment host, out StringSegment port) { int index; port = null; host = null; if (StringSegment.IsNullOrEmpty(value)) { return; } else if ((index = value.IndexOf(']')) >= 0) { // IPv6 in brackets [::1], maybe with port 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.Subsegment(index + 2); } } 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.Subsegment(0, index); port = value.Subsegment(index + 1); } else { host = value; port = null; } } } }