NetworkExtensions.cs 9.9 KB

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