using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; using Jellyfin.Networking.Constants; using Microsoft.AspNetCore.HttpOverrides; namespace Jellyfin.Networking.Extensions; /// /// Defines the . /// public static partial class NetworkExtensions { // 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 [GeneratedRegex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$", RegexOptions.IgnoreCase, "en-US")] private static partial Regex FqdnGeneratedRegex(); /// /// Returns true if the IPAddress contains an IP6 Local link address. /// /// IPAddress object to check. /// True if it is a local link address. /// /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. /// public static bool IsIPv6LinkLocal(IPAddress address) { ArgumentNullException.ThrowIfNull(address); if (address.IsIPv4MappedToIPv6) { address = address.MapToIPv4(); } if (address.AddressFamily != AddressFamily.InterNetworkV6) { return false; } // GetAddressBytes Span octet = stackalloc byte[16]; address.TryWriteBytes(octet, out _); uint word = (uint)(octet[0] << 8) + octet[1]; return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. } /// /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. /// /// Subnet mask in CIDR notation. /// IPv4 or IPv6 family. /// String value of the subnet mask in dotted decimal notation. public static IPAddress CidrToMask(byte cidr, AddressFamily family) { uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) | ((addr & 0x000000ff) << 24); return new IPAddress(addr); } /// /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. /// /// Subnet mask in CIDR notation. /// IPv4 or IPv6 family. /// String value of the subnet mask in dotted decimal notation. public static IPAddress CidrToMask(int cidr, AddressFamily family) { uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr); addr = ((addr & 0xff000000) >> 24) | ((addr & 0x00ff0000) >> 8) | ((addr & 0x0000ff00) << 8) | ((addr & 0x000000ff) << 24); return new IPAddress(addr); } /// /// Convert a subnet mask to a CIDR. IPv4 only. /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. /// /// Subnet mask. /// Byte CIDR representing the mask. public static byte MaskToCidr(IPAddress mask) { ArgumentNullException.ThrowIfNull(mask); byte cidrnet = 0; if (mask.Equals(IPAddress.Any)) { return cidrnet; } // GetAddressBytes Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes]; if (!mask.TryWriteBytes(bytes, out var bytesWritten)) { Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written."); } var zeroed = false; for (var i = 0; i < bytes.Length; i++) { for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) { if (zeroed) { // Invalid netmask. return (byte)~cidrnet; } if ((v & 0x80) == 0) { zeroed = true; } else { cidrnet++; } } } return cidrnet; } /// /// Converts an IPAddress into a string. /// IPv6 addresses are returned in [ ], with their scope removed. /// /// Address to convert. /// URI safe conversion of the address. public static string FormatIPString(IPAddress? address) { if (address is null) { return string.Empty; } var str = address.ToString(); if (address.AddressFamily == AddressFamily.InterNetworkV6) { int i = str.IndexOf('%', StringComparison.Ordinal); if (i != -1) { str = str.Substring(0, i); } return $"[{str}]"; } return str; } /// /// Try parsing an array of strings into objects, respecting exclusions. /// Elements without a subnet mask will be represented as with a single IP. /// /// Input string array to be parsed. /// Collection of . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList? result, bool negated = false) { if (values is null || values.Length == 0) { result = null; return false; } var tmpResult = new List(); for (int a = 0; a < values.Length; a++) { if (TryParseToSubnet(values[a], out var innerResult, negated)) { tmpResult.Add(innerResult); } } result = tmpResult; return tmpResult.Count > 0; } /// /// Try parsing a string into an , respecting exclusions. /// Inputs without a subnet mask will be represented as with a single IP. /// /// Input string to be parsed. /// An . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. public static bool TryParseToSubnet(ReadOnlySpan value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) { var splitString = value.Trim().Split('/'); if (splitString.MoveNext()) { var ipBlock = splitString.Current; var address = IPAddress.None; if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) { address = tmpAddress; } else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress)) { address = tmpAddress; } if (address != IPAddress.None) { if (splitString.MoveNext()) { var subnetBlock = splitString.Current; if (int.TryParse(subnetBlock, out var netmask)) { result = new IPNetwork(address, netmask); return true; } else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress)) { result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)); return true; } } else if (address.AddressFamily == AddressFamily.InterNetwork) { result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); return true; } } } result = null; return false; } /// /// Attempts to parse a host span. /// /// Host name to parse. /// Object representing the span, if it has successfully been parsed. /// true if IPv4 is enabled. /// true if IPv6 is enabled. /// true if the parsing is successful, false if not. public static bool TryParseHost(ReadOnlySpan host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false) { host = host.Trim(); if (host.IsEmpty) { addresses = null; return false; } // See if it's an IPv6 with port address e.g. [::1] or [::1]:120. if (host[0] == '[') { int i = host.IndexOf(']'); if (i != -1) { return TryParseHost(host[1..(i - 1)], out addresses); } addresses = Array.Empty(); return false; } var hosts = new List(); foreach (var splitSpan in host.Split(':')) { hosts.Add(splitSpan.ToString()); } if (hosts.Count <= 2) { // Is hostname or hostname:port if (FqdnGeneratedRegex().IsMatch(hosts[0])) { try { addresses = Dns.GetHostAddresses(hosts[0]); return true; } catch (SocketException) { // Ignore socket errors, as the result value will just be an empty array. } } // Is an IPv4 or IPv4:port if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) { if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) { addresses = Array.Empty(); return false; } addresses = new[] { address }; // Host name is an IPv4 address, so fake resolve. return true; } } else if (hosts.Count > 0 && hosts.Count <= 9) // 8 octets + port { if (IPAddress.TryParse(host.LeftPart('/'), out var address)) { addresses = new[] { address }; return true; } } addresses = Array.Empty(); return false; } /// /// Gets the broadcast address for a . /// /// The . /// The broadcast address. public static IPAddress GetBroadcastAddress(IPNetwork network) { var addressBytes = network.Prefix.GetAddressBytes(); uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); uint broadCastIPAddress = ipAddress | ~ipMaskV4; return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); } }