NetworkExtensions.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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 subnets, respecting exclusions.
  138. /// </summary>
  139. /// <param name="values">Input string array to be parsed.</param>
  140. /// <param name="result">Collection of <see cref="IPNetwork"/>.</param>
  141. /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
  142. /// <returns><c>True</c> if parsing was successful.</returns>
  143. public static bool TryParseSubnets(string[] values, out List<IPNetwork> result, bool negated = false)
  144. {
  145. result = new List<IPNetwork>();
  146. if (values == null || values.Length == 0)
  147. {
  148. return false;
  149. }
  150. for (int a = 0; a < values.Length; a++)
  151. {
  152. string[] v = values[a].Trim().Split("/");
  153. var address = IPAddress.None;
  154. if (negated && v[0].StartsWith('!'))
  155. {
  156. _ = IPAddress.TryParse(v[0][1..], out address);
  157. }
  158. else if (!negated)
  159. {
  160. _ = IPAddress.TryParse(v[0][0..], out address);
  161. }
  162. if (address != IPAddress.None && address != null)
  163. {
  164. if (int.TryParse(v[1], out var netmask))
  165. {
  166. result.Add(new IPNetwork(address, netmask));
  167. }
  168. else if (address.AddressFamily == AddressFamily.InterNetwork)
  169. {
  170. result.Add(new IPNetwork(address, 32));
  171. }
  172. else if (address.AddressFamily == AddressFamily.InterNetworkV6)
  173. {
  174. result.Add(new IPNetwork(address, 128));
  175. }
  176. }
  177. }
  178. if (result.Count > 0)
  179. {
  180. return true;
  181. }
  182. return false;
  183. }
  184. /// <summary>
  185. /// Try parsing a string into a subnet, respecting exclusions.
  186. /// </summary>
  187. /// <param name="value">Input string to be parsed.</param>
  188. /// <param name="result">An <see cref="IPNetwork"/>.</param>
  189. /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
  190. /// <returns><c>True</c> if parsing was successful.</returns>
  191. public static bool TryParseSubnet(string value, out IPNetwork? result, bool negated = false)
  192. {
  193. result = null;
  194. if (string.IsNullOrEmpty(value))
  195. {
  196. return false;
  197. }
  198. string[] v = value.Trim().Split("/");
  199. var address = IPAddress.None;
  200. if (negated && v[0].StartsWith('!'))
  201. {
  202. _ = IPAddress.TryParse(v[0][1..], out address);
  203. }
  204. else if (!negated)
  205. {
  206. _ = IPAddress.TryParse(v[0][0..], out address);
  207. }
  208. if (address != IPAddress.None && address != null)
  209. {
  210. if (int.TryParse(v[1], out var netmask))
  211. {
  212. result = new IPNetwork(address, netmask);
  213. }
  214. else if (address.AddressFamily == AddressFamily.InterNetwork)
  215. {
  216. result = new IPNetwork(address, 32);
  217. }
  218. else if (address.AddressFamily == AddressFamily.InterNetworkV6)
  219. {
  220. result = new IPNetwork(address, 128);
  221. }
  222. }
  223. if (result != null)
  224. {
  225. return true;
  226. }
  227. return false;
  228. }
  229. /// <summary>
  230. /// Attempts to parse a host string.
  231. /// </summary>
  232. /// <param name="host">Host name to parse.</param>
  233. /// <param name="addresses">Object representing the string, if it has successfully been parsed.</param>
  234. /// <param name="isIpv4Enabled"><c>true</c> if IPv4 is enabled.</param>
  235. /// <param name="isIpv6Enabled"><c>true</c> if IPv6 is enabled.</param>
  236. /// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
  237. public static bool TryParseHost(string host, [NotNullWhen(true)] out IPAddress[] addresses, bool isIpv4Enabled = true, bool isIpv6Enabled = false)
  238. {
  239. if (string.IsNullOrWhiteSpace(host))
  240. {
  241. addresses = Array.Empty<IPAddress>();
  242. return false;
  243. }
  244. host = host.Trim();
  245. // See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
  246. if (host[0] == '[')
  247. {
  248. int i = host.IndexOf(']', StringComparison.Ordinal);
  249. if (i != -1)
  250. {
  251. return TryParseHost(host.Remove(i)[1..], out addresses);
  252. }
  253. addresses = Array.Empty<IPAddress>();
  254. return false;
  255. }
  256. var hosts = host.Split(':');
  257. if (hosts.Length <= 2)
  258. {
  259. // Is hostname or hostname:port
  260. if (_fqdnRegex.IsMatch(hosts[0]))
  261. {
  262. try
  263. {
  264. addresses = Dns.GetHostAddresses(hosts[0]);
  265. return true;
  266. }
  267. catch (SocketException)
  268. {
  269. // Log and then ignore socket errors, as the result value will just be an empty array.
  270. Console.WriteLine("GetHostAddresses failed.");
  271. }
  272. }
  273. // Is an IP4 or IP4:port
  274. host = hosts[0].Split('/')[0];
  275. if (IPAddress.TryParse(host, out var address))
  276. {
  277. if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIpv4Enabled && isIpv6Enabled)) ||
  278. ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIpv4Enabled && !isIpv6Enabled)))
  279. {
  280. addresses = Array.Empty<IPAddress>();
  281. return false;
  282. }
  283. addresses = new[] { address };
  284. // Host name is an ip4 address, so fake resolve.
  285. return true;
  286. }
  287. }
  288. else if (hosts.Length <= 9 && IPAddress.TryParse(host.Split('/')[0], out var address)) // 8 octets + port
  289. {
  290. addresses = new[] { address };
  291. return true;
  292. }
  293. addresses = Array.Empty<IPAddress>();
  294. return false;
  295. }
  296. /// <summary>
  297. /// Gets the broadcast address for a <see cref="IPNetwork"/>.
  298. /// </summary>
  299. /// <param name="network">The <see cref="IPNetwork"/>.</param>
  300. /// <returns>The broadcast address.</returns>
  301. public static IPAddress GetBroadcastAddress(IPNetwork network)
  302. {
  303. uint ipAddress = BitConverter.ToUInt32(network.Prefix.GetAddressBytes(), 0);
  304. uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0);
  305. uint broadCastIpAddress = ipAddress | ~ipMaskV4;
  306. return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
  307. }
  308. }
  309. }