using System; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; namespace MediaBrowser.Common.Net { /// /// Object that holds a host name. /// public class IPHost : IPObject { /// /// Gets or sets timeout value before resolve required, in minutes. /// public const int Timeout = 30; /// /// Represents an IPHost that has no value. /// public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); /// /// Time when last resolved in ticks. /// private DateTime? _lastResolved = null; /// /// Gets the IP Addresses, attempting to resolve the name, if there are none. /// private IPAddress[] _addresses; /// /// Initializes a new instance of the class. /// /// Host name to assign. public IPHost(string name) { HostName = name ?? throw new ArgumentNullException(nameof(name)); _addresses = Array.Empty(); Resolved = false; } /// /// Initializes a new instance of the class. /// /// Host name to assign. /// Address to assign. private IPHost(string name, IPAddress address) { HostName = name ?? throw new ArgumentNullException(nameof(name)); _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) }; Resolved = !address.Equals(IPAddress.None); } /// /// Gets or sets the object's first IP address. /// public override IPAddress Address { get { return ResolveHost() ? this[0] : IPAddress.None; } set { // Not implemented, as a host's address is determined by DNS. throw new NotImplementedException("The address of a host is determined by DNS."); } } /// /// Gets or sets the object's first IP's subnet prefix. /// The setter does nothing, but shouldn't raise an exception. /// public override byte PrefixLength { get => (byte)(ResolveHost() ? 128 : 32); // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length, // which is automatically determined by it's IP type. Anything else is meaningless. set => throw new NotImplementedException(); } /// /// Gets a value indicating whether the address has a value. /// public bool HasAddress => _addresses.Length != 0; /// /// Gets the host name of this object. /// public string HostName { get; } /// /// Gets a value indicating whether this host has attempted to be resolved. /// public bool Resolved { get; private set; } /// /// Gets or sets the IP Addresses associated with this object. /// /// Index of address. public IPAddress this[int index] { get { ResolveHost(); return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None; } } /// /// Attempts to parse the host string. /// /// Host name to parse. /// Object representing the string, if it has successfully been parsed. /// true if the parsing is successful, false if not. public static bool TryParse(string host, out IPHost hostObj) { if (string.IsNullOrWhiteSpace(host)) { hostObj = IPHost.None; return false; } // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. int i = host.IndexOf(']', StringComparison.Ordinal); if (i != -1) { return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); } if (IPNetAddress.TryParse(host, out var netAddress)) { // Host name is an ip address, so fake resolve. hostObj = new IPHost(host, netAddress.Address); return true; } // Is it a host, IPv4/6 with/out port? string[] hosts = host.Split(':'); if (hosts.Length <= 2) { // This is either a hostname: port, or an IP4:port. host = hosts[0]; if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase)) { hostObj = new IPHost(host); return true; } if (IPAddress.TryParse(host, out var netIP)) { // Host name is an ip address, so fake resolve. hostObj = new IPHost(host, netIP); return true; } } else { // Invalid host name, as it cannot contain : hostObj = new IPHost(string.Empty, IPAddress.None); return false; } // Use regular expression as CheckHostName isn't RFC5892 compliant. // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation string pattern = @"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$"; if (Regex.IsMatch(host, pattern)) { hostObj = new IPHost(host); return true; } hostObj = IPHost.None; return false; } /// /// Attempts to parse the host string. /// /// Host name to parse. /// Object representing the string, if it has successfully been parsed. public static IPHost Parse(string host) { if (IPHost.TryParse(host, out IPHost res)) { return res; } throw new InvalidCastException($"Host does not contain a valid value. {host}"); } /// /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type. /// /// Host name to parse. /// Addressfamily filter. /// Object representing the string, if it has successfully been parsed. public static IPHost Parse(string host, AddressFamily family) { if (IPHost.TryParse(host, out IPHost res)) { if (family == AddressFamily.InterNetwork) { res.Remove(AddressFamily.InterNetworkV6); } else { res.Remove(AddressFamily.InterNetwork); } return res; } throw new InvalidCastException($"Host does not contain a valid value. {host}"); } /// /// Returns the Addresses that this item resolved to. /// /// IPAddress Array. public IPAddress[] GetAddresses() { ResolveHost(); return _addresses; } /// public override bool Contains(IPAddress address) { if (address is not null && !Address.Equals(IPAddress.None)) { if (address.IsIPv4MappedToIPv6) { address = address.MapToIPv4(); } foreach (var addr in GetAddresses()) { if (address.Equals(addr)) { return true; } } } return false; } /// public override bool Equals(IPObject? other) { if (other is IPHost otherObj) { // Do we have the name Hostname? if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase)) { return true; } if (!ResolveHost() || !otherObj.ResolveHost()) { return false; } // Do any of our IP addresses match? foreach (IPAddress addr in _addresses) { foreach (IPAddress otherAddress in otherObj._addresses) { if (addr.Equals(otherAddress)) { return true; } } } } return false; } /// public override bool IsIP6() { // Returns true if interfaces are only IP6. if (ResolveHost()) { foreach (IPAddress i in _addresses) { if (i.AddressFamily != AddressFamily.InterNetworkV6) { return false; } } return true; } return false; } /// public override string ToString() { // StringBuilder not optimum here. string output = string.Empty; if (_addresses.Length > 0) { bool moreThanOne = _addresses.Length > 1; if (moreThanOne) { output = "["; } foreach (var i in _addresses) { if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified) { output += HostName + ","; } else if (i.Equals(IPAddress.Any)) { output += "Any IP4 Address,"; } else if (Address.Equals(IPAddress.IPv6Any)) { output += "Any IP6 Address,"; } else if (i.Equals(IPAddress.Broadcast)) { output += "Any Address,"; } else if (i.AddressFamily == AddressFamily.InterNetwork) { output += $"{i}/32,"; } else { output += $"{i}/128,"; } } output = output[..^1]; if (moreThanOne) { output += "]"; } } else { output = HostName; } return output; } /// public override void Remove(AddressFamily family) { if (ResolveHost()) { _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray(); } } /// public override bool Contains(IPObject address) { // An IPHost cannot contain another IPObject, it can only be equal. return Equals(address); } /// protected override IPObject CalculateNetworkAddress() { var (address, prefixLength) = NetworkAddressOf(this[0], PrefixLength); return new IPNetAddress(address, prefixLength); } /// /// Attempt to resolve the ip address of a host. /// /// true if any addresses have been resolved, otherwise false. private bool ResolveHost() { // When was the last time we resolved? _lastResolved ??= DateTime.UtcNow; // If we haven't resolved before, or our timer has run out... if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved.Value.AddMinutes(Timeout))) { _lastResolved = DateTime.UtcNow; ResolveHostInternal(); Resolved = true; } return _addresses.Length > 0; } /// /// Task that looks up a Host name and returns its IP addresses. /// private void ResolveHostInternal() { var hostName = HostName; if (string.IsNullOrEmpty(hostName)) { return; } // Resolves the host name - so save a DNS lookup. if (string.Equals(hostName, "localhost", StringComparison.OrdinalIgnoreCase)) { _addresses = new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }; return; } if (Uri.CheckHostName(hostName) == UriHostNameType.Dns) { try { _addresses = Dns.GetHostEntry(hostName).AddressList; } catch (SocketException ex) { // Log and then ignore socket errors, as the result value will just be an empty array. Debug.WriteLine("GetHostAddresses failed with {Message}.", ex.Message); } } } } }