NetworkExtensions.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. using Microsoft.AspNetCore.HttpOverrides;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Net;
  6. using System.Net.Sockets;
  7. using System.Text.RegularExpressions;
  8. namespace MediaBrowser.Common.Net
  9. {
  10. /// <summary>
  11. /// Defines the <see cref="NetworkExtensions" />.
  12. /// </summary>
  13. public static class NetworkExtensions
  14. {
  15. // Use regular expression as CheckHostName isn't RFC5892 compliant.
  16. // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
  17. private static readonly Regex _fqdnRegex = new Regex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$");
  18. /// <summary>
  19. /// Returns true if the IPAddress contains an IP6 Local link address.
  20. /// </summary>
  21. /// <param name="address">IPAddress object to check.</param>
  22. /// <returns>True if it is a local link address.</returns>
  23. /// <remarks>
  24. /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress
  25. /// it appears that the IPAddress.IsIPv6LinkLocal is out of date.
  26. /// </remarks>
  27. public static bool IsIPv6LinkLocal(IPAddress address)
  28. {
  29. ArgumentNullException.ThrowIfNull(address);
  30. if (address.IsIPv4MappedToIPv6)
  31. {
  32. address = address.MapToIPv4();
  33. }
  34. if (address.AddressFamily != AddressFamily.InterNetworkV6)
  35. {
  36. return false;
  37. }
  38. // GetAddressBytes
  39. Span<byte> octet = stackalloc byte[16];
  40. address.TryWriteBytes(octet, out _);
  41. uint word = (uint)(octet[0] << 8) + octet[1];
  42. return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link.
  43. }
  44. /// <summary>
  45. /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
  46. /// </summary>
  47. /// <param name="cidr">Subnet mask in CIDR notation.</param>
  48. /// <param name="family">IPv4 or IPv6 family.</param>
  49. /// <returns>String value of the subnet mask in dotted decimal notation.</returns>
  50. public static IPAddress CidrToMask(byte cidr, AddressFamily family)
  51. {
  52. uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr);
  53. addr = ((addr & 0xff000000) >> 24)
  54. | ((addr & 0x00ff0000) >> 8)
  55. | ((addr & 0x0000ff00) << 8)
  56. | ((addr & 0x000000ff) << 24);
  57. return new IPAddress(addr);
  58. }
  59. /// <summary>
  60. /// Convert a subnet mask to a CIDR. IPv4 only.
  61. /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask.
  62. /// </summary>
  63. /// <param name="mask">Subnet mask.</param>
  64. /// <returns>Byte CIDR representing the mask.</returns>
  65. public static byte MaskToCidr(IPAddress mask)
  66. {
  67. ArgumentNullException.ThrowIfNull(mask);
  68. byte cidrnet = 0;
  69. if (!mask.Equals(IPAddress.Any))
  70. {
  71. // GetAddressBytes
  72. Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
  73. mask.TryWriteBytes(bytes, out _);
  74. var zeroed = false;
  75. for (var i = 0; i < bytes.Length; i++)
  76. {
  77. for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1)
  78. {
  79. if (zeroed)
  80. {
  81. // Invalid netmask.
  82. return (byte)~cidrnet;
  83. }
  84. if ((v & 0x80) == 0)
  85. {
  86. zeroed = true;
  87. }
  88. else
  89. {
  90. cidrnet++;
  91. }
  92. }
  93. }
  94. }
  95. return cidrnet;
  96. }
  97. /// <summary>
  98. /// Converts an IPAddress into a string.
  99. /// Ipv6 addresses are returned in [ ], with their scope removed.
  100. /// </summary>
  101. /// <param name="address">Address to convert.</param>
  102. /// <returns>URI safe conversion of the address.</returns>
  103. public static string FormatIpString(IPAddress? address)
  104. {
  105. if (address == null)
  106. {
  107. return string.Empty;
  108. }
  109. var str = address.ToString();
  110. if (address.AddressFamily == AddressFamily.InterNetworkV6)
  111. {
  112. int i = str.IndexOf('%', StringComparison.Ordinal);
  113. if (i != -1)
  114. {
  115. str = str.Substring(0, i);
  116. }
  117. return $"[{str}]";
  118. }
  119. return str;
  120. }
  121. /// <summary>
  122. /// Try parsing an array of strings into subnets, respecting exclusions.
  123. /// </summary>
  124. /// <param name="values">Input string to be parsed.</param>
  125. /// <param name="result">Collection of <see cref="IPNetwork"/>.</param>
  126. /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
  127. /// <returns><c>True</c> if parsing was successful.</returns>
  128. public static bool TryParseSubnets(string[] values, out List<IPNetwork> result, bool negated = false)
  129. {
  130. result = new List<IPNetwork>();
  131. if (values == null || values.Length == 0)
  132. {
  133. return false;
  134. }
  135. for (int a = 0; a < values.Length; a++)
  136. {
  137. string[] v = values[a].Trim().Split("/");
  138. var address = IPAddress.None;
  139. if (negated && v[0].StartsWith('!'))
  140. {
  141. _ = IPAddress.TryParse(v[0][1..], out address);
  142. }
  143. else if (!negated)
  144. {
  145. _ = IPAddress.TryParse(v[0][0..], out address);
  146. }
  147. if (address != IPAddress.None && address != null)
  148. {
  149. if (int.TryParse(v[1], out var netmask))
  150. {
  151. result.Add(new IPNetwork(address, netmask));
  152. }
  153. else if (address.AddressFamily == AddressFamily.InterNetwork)
  154. {
  155. result.Add(new IPNetwork(address, 32));
  156. }
  157. else if (address.AddressFamily == AddressFamily.InterNetworkV6)
  158. {
  159. result.Add(new IPNetwork(address, 128));
  160. }
  161. }
  162. }
  163. if (result.Count > 0)
  164. {
  165. return true;
  166. }
  167. return false;
  168. }
  169. /// <summary>
  170. /// Attempts to parse a host string.
  171. /// </summary>
  172. /// <param name="host">Host name to parse.</param>
  173. /// <param name="addresses">Object representing the string, if it has successfully been parsed.</param>
  174. /// <param name="isIpv4Enabled"><c>true</c> if IPv4 is enabled.</param>
  175. /// <param name="isIpv6Enabled"><c>true</c> if IPv6 is enabled.</param>
  176. /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
  177. public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIpv4Enabled = true, bool isIpv6Enabled = false)
  178. {
  179. if (string.IsNullOrWhiteSpace(host))
  180. {
  181. addresses = Array.Empty<IPAddress>();
  182. return false;
  183. }
  184. host = host.Trim();
  185. // See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
  186. if (host[0] == '[')
  187. {
  188. int i = host.IndexOf(']', StringComparison.Ordinal);
  189. if (i != -1)
  190. {
  191. return TryParseHost(host.Remove(i)[1..], out addresses);
  192. }
  193. addresses = Array.Empty<IPAddress>();
  194. return false;
  195. }
  196. var hosts = host.Split(':');
  197. if (hosts.Length <= 2)
  198. {
  199. // Is hostname or hostname:port
  200. if (_fqdnRegex.IsMatch(hosts[0]))
  201. {
  202. try
  203. {
  204. addresses = Dns.GetHostAddresses(hosts[0]);
  205. return true;
  206. }
  207. catch (SocketException)
  208. {
  209. // Log and then ignore socket errors, as the result value will just be an empty array.
  210. Console.WriteLine("GetHostAddresses failed.");
  211. }
  212. }
  213. // Is an IP4 or IP4:port
  214. host = hosts[0].Split('/')[0];
  215. if (IPAddress.TryParse(host, out var address))
  216. {
  217. if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIpv4Enabled && isIpv6Enabled)) ||
  218. ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIpv4Enabled && !isIpv6Enabled)))
  219. {
  220. addresses = Array.Empty<IPAddress>();
  221. return false;
  222. }
  223. addresses = new[] { address };
  224. // Host name is an ip4 address, so fake resolve.
  225. return true;
  226. }
  227. }
  228. else if (hosts.Length <= 9 && IPAddress.TryParse(host.Split('/')[0], out var address)) // 8 octets + port
  229. {
  230. addresses = new[] { address };
  231. return true;
  232. }
  233. addresses = Array.Empty<IPAddress>();
  234. return false;
  235. }
  236. }
  237. }