NetworkExtensions.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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 in CIDR notation to a dotted decimal string value. IPv4 only.
  61. /// </summary>
  62. /// <param name="cidr">Subnet mask in CIDR notation.</param>
  63. /// <param name="family">IPv4 or IPv6 family.</param>
  64. /// <returns>String value of the subnet mask in dotted decimal notation.</returns>
  65. public static IPAddress CidrToMask(int cidr, AddressFamily family)
  66. {
  67. uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? 32 : 128) - cidr);
  68. addr = ((addr & 0xff000000) >> 24)
  69. | ((addr & 0x00ff0000) >> 8)
  70. | ((addr & 0x0000ff00) << 8)
  71. | ((addr & 0x000000ff) << 24);
  72. return new IPAddress(addr);
  73. }
  74. /// <summary>
  75. /// Convert a subnet mask to a CIDR. IPv4 only.
  76. /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask.
  77. /// </summary>
  78. /// <param name="mask">Subnet mask.</param>
  79. /// <returns>Byte CIDR representing the mask.</returns>
  80. public static byte MaskToCidr(IPAddress mask)
  81. {
  82. ArgumentNullException.ThrowIfNull(mask);
  83. byte cidrnet = 0;
  84. if (!mask.Equals(IPAddress.Any))
  85. {
  86. // GetAddressBytes
  87. Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
  88. mask.TryWriteBytes(bytes, out _);
  89. var zeroed = false;
  90. for (var i = 0; i < bytes.Length; i++)
  91. {
  92. for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1)
  93. {
  94. if (zeroed)
  95. {
  96. // Invalid netmask.
  97. return (byte)~cidrnet;
  98. }
  99. if ((v & 0x80) == 0)
  100. {
  101. zeroed = true;
  102. }
  103. else
  104. {
  105. cidrnet++;
  106. }
  107. }
  108. }
  109. }
  110. return cidrnet;
  111. }
  112. /// <summary>
  113. /// Converts an IPAddress into a string.
  114. /// Ipv6 addresses are returned in [ ], with their scope removed.
  115. /// </summary>
  116. /// <param name="address">Address to convert.</param>
  117. /// <returns>URI safe conversion of the address.</returns>
  118. public static string FormatIpString(IPAddress? address)
  119. {
  120. if (address == null)
  121. {
  122. return string.Empty;
  123. }
  124. var str = address.ToString();
  125. if (address.AddressFamily == AddressFamily.InterNetworkV6)
  126. {
  127. int i = str.IndexOf('%', StringComparison.Ordinal);
  128. if (i != -1)
  129. {
  130. str = str.Substring(0, i);
  131. }
  132. return $"[{str}]";
  133. }
  134. return str;
  135. }
  136. /// <summary>
  137. /// Try parsing an array of strings into <see cref="IPNetwork"/> objects, respecting exclusions.
  138. /// Elements without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP.
  139. /// </summary>
  140. /// <param name="values">Input string array to be parsed.</param>
  141. /// <param name="result">Collection of <see cref="IPNetwork"/>.</param>
  142. /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
  143. /// <returns><c>True</c> if parsing was successful.</returns>
  144. public static bool TryParseToSubnets(string[] values, out List<IPNetwork> result, bool negated = false)
  145. {
  146. result = new List<IPNetwork>();
  147. if (values == null || values.Length == 0)
  148. {
  149. return false;
  150. }
  151. for (int a = 0; a < values.Length; a++)
  152. {
  153. string[] v = values[a].Trim().Split("/");
  154. var address = IPAddress.None;
  155. if (negated && v[0].StartsWith('!'))
  156. {
  157. _ = IPAddress.TryParse(v[0][1..], out address);
  158. }
  159. else if (!negated)
  160. {
  161. _ = IPAddress.TryParse(v[0][0..], out address);
  162. }
  163. if (address != IPAddress.None && address != null)
  164. {
  165. if (v.Length > 1 && int.TryParse(v[1], out var netmask))
  166. {
  167. result.Add(new IPNetwork(address, netmask));
  168. }
  169. else if (v.Length > 1 && IPAddress.TryParse(v[1], out var netmaskAddress))
  170. {
  171. result.Add(new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress)));
  172. }
  173. else if (address.AddressFamily == AddressFamily.InterNetwork)
  174. {
  175. result.Add(new IPNetwork(address, 32));
  176. }
  177. else if (address.AddressFamily == AddressFamily.InterNetworkV6)
  178. {
  179. result.Add(new IPNetwork(address, 128));
  180. }
  181. }
  182. }
  183. if (result.Count > 0)
  184. {
  185. return true;
  186. }
  187. return false;
  188. }
  189. /// <summary>
  190. /// Try parsing a string into an <see cref="IPNetwork"/>, respecting exclusions.
  191. /// Inputs without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP.
  192. /// </summary>
  193. /// <param name="value">Input string to be parsed.</param>
  194. /// <param name="result">An <see cref="IPNetwork"/>.</param>
  195. /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
  196. /// <returns><c>True</c> if parsing was successful.</returns>
  197. public static bool TryParseToSubnet(string value, out IPNetwork result, bool negated = false)
  198. {
  199. result = new IPNetwork(IPAddress.None, 32);
  200. if (string.IsNullOrEmpty(value))
  201. {
  202. return false;
  203. }
  204. string[] v = value.Trim().Split("/");
  205. var address = IPAddress.None;
  206. if (negated && v[0].StartsWith('!'))
  207. {
  208. _ = IPAddress.TryParse(v[0][1..], out address);
  209. }
  210. else if (!negated)
  211. {
  212. _ = IPAddress.TryParse(v[0][0..], out address);
  213. }
  214. if (address != IPAddress.None && address != null)
  215. {
  216. if (v.Length > 1 && int.TryParse(v[1], out var netmask))
  217. {
  218. result = new IPNetwork(address, netmask);
  219. }
  220. else if (v.Length > 1 && IPAddress.TryParse(v[1], out var netmaskAddress))
  221. {
  222. result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress));
  223. }
  224. else if (address.AddressFamily == AddressFamily.InterNetwork)
  225. {
  226. result = new IPNetwork(address, 32);
  227. }
  228. else if (address.AddressFamily == AddressFamily.InterNetworkV6)
  229. {
  230. result = new IPNetwork(address, 128);
  231. }
  232. }
  233. if (!result.Prefix.Equals(IPAddress.None))
  234. {
  235. return true;
  236. }
  237. return false;
  238. }
  239. /// <summary>
  240. /// Attempts to parse a host string.
  241. /// </summary>
  242. /// <param name="host">Host name to parse.</param>
  243. /// <param name="addresses">Object representing the string, if it has successfully been parsed.</param>
  244. /// <param name="isIpv4Enabled"><c>true</c> if IPv4 is enabled.</param>
  245. /// <param name="isIpv6Enabled"><c>true</c> if IPv6 is enabled.</param>
  246. /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
  247. public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIpv4Enabled = true, bool isIpv6Enabled = false)
  248. {
  249. if (string.IsNullOrWhiteSpace(host))
  250. {
  251. addresses = Array.Empty<IPAddress>();
  252. return false;
  253. }
  254. host = host.Trim();
  255. // See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
  256. if (host[0] == '[')
  257. {
  258. int i = host.IndexOf(']', StringComparison.Ordinal);
  259. if (i != -1)
  260. {
  261. return TryParseHost(host.Remove(i)[1..], out addresses);
  262. }
  263. addresses = Array.Empty<IPAddress>();
  264. return false;
  265. }
  266. var hosts = host.Split(':');
  267. if (hosts.Length <= 2)
  268. {
  269. // Is hostname or hostname:port
  270. if (_fqdnRegex.IsMatch(hosts[0]))
  271. {
  272. try
  273. {
  274. addresses = Dns.GetHostAddresses(hosts[0]);
  275. return true;
  276. }
  277. catch (SocketException)
  278. {
  279. // Log and then ignore socket errors, as the result value will just be an empty array.
  280. Console.WriteLine("GetHostAddresses failed.");
  281. }
  282. }
  283. // Is an IP4 or IP4:port
  284. host = hosts[0].Split('/')[0];
  285. if (IPAddress.TryParse(host, out var address))
  286. {
  287. if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIpv4Enabled && isIpv6Enabled)) ||
  288. ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIpv4Enabled && !isIpv6Enabled)))
  289. {
  290. addresses = Array.Empty<IPAddress>();
  291. return false;
  292. }
  293. addresses = new[] { address };
  294. // Host name is an ip4 address, so fake resolve.
  295. return true;
  296. }
  297. }
  298. else if (hosts.Length <= 9 && IPAddress.TryParse(host.Split('/')[0], out var address)) // 8 octets + port
  299. {
  300. addresses = new[] { address };
  301. return true;
  302. }
  303. addresses = Array.Empty<IPAddress>();
  304. return false;
  305. }
  306. /// <summary>
  307. /// Gets the broadcast address for a <see cref="IPNetwork"/>.
  308. /// </summary>
  309. /// <param name="network">The <see cref="IPNetwork"/>.</param>
  310. /// <returns>The broadcast address.</returns>
  311. public static IPAddress GetBroadcastAddress(IPNetwork network)
  312. {
  313. uint ipAddress = BitConverter.ToUInt32(network.Prefix.GetAddressBytes(), 0);
  314. uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0);
  315. uint broadCastIpAddress = ipAddress | ~ipMaskV4;
  316. return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
  317. }
  318. }
  319. }