PageRenderTime 53ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/branch/Salient.WebTest/CassiniDev/Rules.cs

#
C# | 358 lines | 230 code | 43 blank | 85 comment | 35 complexity | 0d542d4b9e52795b11c068f5b62cdb23 MD5 | raw file
Possible License(s): CC-BY-SA-3.0, AGPL-3.0, Apache-2.0
  1. // /* **********************************************************************************
  2. // *
  3. // * Copyright (c) Sky Sanders. All rights reserved.
  4. // *
  5. // * This source code is subject to terms and conditions of the Microsoft Public
  6. // * License (Ms-PL). A copy of the license can be found in the license.htm file
  7. // * included in this distribution.
  8. // *
  9. // * You must not remove this notice, or any other, from this software.
  10. // *
  11. // * **********************************************************************************/
  12. using System;
  13. using System.Collections.Generic;
  14. using System.Diagnostics;
  15. using System.IO;
  16. using System.Linq;
  17. using System.Net;
  18. using System.Net.NetworkInformation;
  19. using System.Reflection;
  20. using System.Text.RegularExpressions;
  21. using System.Threading;
  22. namespace CassiniDev
  23. {
  24. /// <summary>
  25. /// Business Rules Service - Needs to be broken into coherent classes
  26. /// </summary>
  27. public class Rules : IRules
  28. {
  29. private readonly string _executablePath = Assembly.GetAssembly(typeof (IServer)).Location;
  30. #region Hosts file
  31. /// <summary>
  32. ///
  33. /// </summary>
  34. /// <param name="ipAddress"></param>
  35. /// <param name="hostname"></param>
  36. /// <returns></returns>
  37. public int RemoveHostEntry(string ipAddress, string hostname)
  38. {
  39. try
  40. {
  41. SetHostsEntry(false, ipAddress, hostname);
  42. return 0;
  43. }
  44. catch
  45. {
  46. }
  47. return StartElevated(_executablePath, string.Format("Hostsfile /ah- /h:{0} /i:{1}", hostname, ipAddress));
  48. }
  49. /// <summary>
  50. ///
  51. /// </summary>
  52. /// <param name="ipAddress"></param>
  53. /// <param name="hostname"></param>
  54. /// <returns></returns>
  55. public int AddHostEntry(string ipAddress, string hostname)
  56. {
  57. try
  58. {
  59. SetHostsEntry(true, ipAddress, hostname);
  60. return 0;
  61. }
  62. catch
  63. {
  64. }
  65. return StartElevated(_executablePath, string.Format("Hostsfile /ah+ /h:{0} /i:{1}", hostname, ipAddress));
  66. }
  67. private void SetHostsEntry(bool addHost, string ipAddress, string hostname)
  68. {
  69. // limitation: while windows allows mulitple entries for a single host, we currently allow only one
  70. string windir = Environment.GetEnvironmentVariable("SystemRoot") ?? @"c:\windows";
  71. string hostsFilePath = Path.Combine(windir, @"system32\drivers\etc\hosts");
  72. string hostsFileContent = GetFileText(hostsFilePath);
  73. hostsFileContent = Regex.Replace(hostsFileContent,
  74. string.Format(@"\r\n^\s*[\d\w\.:]+\s{0}\s#\sadded\sby\scassini$",
  75. hostname), "", RegexOptions.Multiline);
  76. if (addHost)
  77. {
  78. hostsFileContent += string.Format("\r\n{0} {1} # added by cassini", ipAddress, hostname);
  79. }
  80. SetFileText(hostsFilePath, hostsFileContent);
  81. }
  82. private static int StartElevated(string filename, string args)
  83. {
  84. ProcessStartInfo startInfo = new ProcessStartInfo();
  85. startInfo.UseShellExecute = true;
  86. startInfo.WorkingDirectory = Environment.CurrentDirectory;
  87. startInfo.FileName = filename;
  88. startInfo.Arguments = args;
  89. startInfo.Verb = "runas";
  90. try
  91. {
  92. Process p = Process.Start(startInfo);
  93. if (p != null)
  94. {
  95. p.WaitForExit();
  96. return p.ExitCode;
  97. }
  98. return -2;
  99. }
  100. catch
  101. {
  102. return -2;
  103. }
  104. }
  105. // Testing Seam
  106. protected string GetFileText(string path)
  107. {
  108. return File.ReadAllText(path);
  109. }
  110. // Testing Seam
  111. protected void SetFileText(string path, string contents)
  112. {
  113. File.WriteAllText(path, contents);
  114. }
  115. #endregion
  116. #region Network
  117. /// <summary>
  118. /// Returns first available port on the specified IP address. The port scan excludes ports that are open on ANY loopback adapter.
  119. /// If the address upon which a port is requested is an 'ANY' address all ports that are open on ANY IP are excluded.
  120. /// </summary>
  121. /// <param name="rangeStart"></param>
  122. /// <param name="rangeEnd"></param>
  123. /// <param name="ip">The IP address upon which to search for available port.</param>
  124. /// <param name="includeIdlePorts">If true includes ports in TIME_WAIT state in results. TIME_WAIT state is typically cool down period for recently released ports.</param>
  125. /// <returns></returns>
  126. public ushort GetAvailablePort(UInt16 rangeStart, UInt16 rangeEnd, IPAddress ip, bool includeIdlePorts)
  127. {
  128. IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();
  129. // if the ip we want a port on is an 'any' or loopback port we need to exclude all ports that are active on any IP
  130. Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
  131. IPAddress.IPv6Any.Equals(i) ||
  132. IPAddress.Loopback.Equals(i) ||
  133. IPAddress.IPv6Loopback.
  134. Equals(i);
  135. // get all active ports on specified IP.
  136. List<ushort> excludedPorts = new List<ushort>();
  137. // if a port is open on an 'any' or 'loopback' interface then include it in the excludedPorts
  138. excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
  139. where
  140. n.LocalEndPoint.Port >= rangeStart
  141. && n.LocalEndPoint.Port <= rangeEnd
  142. &&
  143. (isIpAnyOrLoopBack(ip) || n.LocalEndPoint.Address.Equals(ip) ||
  144. isIpAnyOrLoopBack(n.LocalEndPoint.Address))
  145. && (!includeIdlePorts || n.State != TcpState.TimeWait)
  146. select (ushort) n.LocalEndPoint.Port);
  147. excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
  148. where n.Port >= rangeStart && n.Port <= rangeEnd
  149. &&
  150. (isIpAnyOrLoopBack(ip) || n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
  151. select (ushort) n.Port);
  152. excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
  153. where n.Port >= rangeStart && n.Port <= rangeEnd
  154. &&
  155. (isIpAnyOrLoopBack(ip) || n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
  156. select (ushort) n.Port);
  157. excludedPorts.Sort();
  158. for (ushort port = rangeStart; port <= rangeEnd; port++)
  159. {
  160. if (!excludedPorts.Contains(port))
  161. {
  162. return port;
  163. }
  164. }
  165. return 0;
  166. }
  167. public IPAddress ParseIPString(string ipString)
  168. {
  169. if (string.IsNullOrEmpty(ipString))
  170. {
  171. ipString = "loopback";
  172. }
  173. ipString = ipString.Trim().ToLower();
  174. switch (ipString)
  175. {
  176. case "any":
  177. return IPAddress.Any;
  178. case "loopback":
  179. return IPAddress.Loopback;
  180. case "ipv6any":
  181. return IPAddress.IPv6Any;
  182. case "ipv6loopback":
  183. return IPAddress.IPv6Loopback;
  184. default:
  185. IPAddress result;
  186. IPAddress.TryParse(ipString, out result);
  187. return result;
  188. }
  189. }
  190. /// <summary>
  191. /// <para>
  192. /// Hostnames are composed of series of labels concatenated with dots, as are all domain names[1].
  193. /// For example, "en.wikipedia.org" is a hostname. Each label must be between 1 and 63 characters long,
  194. /// and the entire hostname has a maximum of 255 characters.</para>
  195. /// <para>
  196. /// The Internet standards (Request for Comments) for protocols mandate that component hostname
  197. /// labels may contain only the ASCII letters 'a' through 'z' (in a case-insensitive manner), the digits
  198. /// '0' through '9', and the hyphen. The original specification of hostnames in RFC 952, mandated that
  199. /// labels could not start with a digit or with a hyphen, and must not end with a hyphen. However, a
  200. /// subsequent specification (RFC 1123) permitted hostname labels to start with digits. No other symbols,
  201. /// punctuation characters, or blank spaces are permitted.</para>
  202. /// </summary>
  203. /// <param name="hostname"></param>
  204. /// <returns></returns>
  205. /// http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
  206. public bool ValidateHostName(string hostname)
  207. {
  208. Regex hostnameRx =
  209. new Regex(
  210. @"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$");
  211. return hostnameRx.IsMatch(hostname);
  212. }
  213. /// <summary>
  214. /// Converts CommandLineArgument values to an IP address if possible.
  215. /// Throws Exception if not.
  216. /// </summary>
  217. /// <param name="ipmode"></param>
  218. /// <param name="v6"></param>
  219. /// <param name="ipString"></param>
  220. /// <returns></returns>
  221. /// <exception cref="CassiniException">If IPMode is invalid</exception>
  222. /// <exception cref="CassiniException">If IPMode is 'Specific' and ipString is invalid</exception>
  223. public IPAddress ParseIP(IPMode ipmode, bool v6, string ipString)
  224. {
  225. IPAddress ip;
  226. switch (ipmode)
  227. {
  228. case IPMode.Loopback:
  229. ip = v6 ? IPAddress.IPv6Loopback : IPAddress.Loopback;
  230. break;
  231. case IPMode.Any:
  232. ip = v6 ? IPAddress.IPv6Any : IPAddress.Any;
  233. break;
  234. case IPMode.Specific:
  235. if (!IPAddress.TryParse(ipString, out ip))
  236. {
  237. throw new CassiniException("Invalid IP Address", ErrorField.IPAddress);
  238. }
  239. break;
  240. default:
  241. throw new CassiniException("Invalid IPMode", ErrorField.None);
  242. }
  243. return ip;
  244. }
  245. #endregion
  246. #region Arguments
  247. /// <summary>
  248. /// Validates all but application path
  249. /// </summary>
  250. /// <param name="args"></param>
  251. /// <exception cref="CassiniException">If vpath is null or does not begin with '/'</exception>
  252. /// <exception cref="CassiniException">If an invalid hostname is specified</exception>
  253. /// <exception cref="CassiniException">If AddHost is true and a null or invalid hostname is specified</exception>
  254. /// <exception cref="CassiniException">If either port range is less than 1</exception>
  255. /// <exception cref="CassiniException">If PortRangeStart is greater than PortRangeEnd</exception>
  256. /// <exception cref="CassiniException">If no available port within specified range is found.</exception>
  257. /// <exception cref="CassiniException">If specified port is in use.</exception>
  258. /// <exception cref="CassiniException">If PortMode is invalid</exception>
  259. /// <exception cref="CassiniException">If PortMode is invalid</exception>
  260. /// <exception cref="CassiniException">If IPMode is 'Specific' and IPAddress is invalid</exception>
  261. public void ValidateArgs(CommandLineArguments args)
  262. {
  263. if (string.IsNullOrEmpty(args.VirtualPath) || !args.VirtualPath.StartsWith("/"))
  264. {
  265. throw new CassiniException("Invalid VPath", ErrorField.VirtualPath);
  266. }
  267. if (args.AddHost && (string.IsNullOrEmpty(args.HostName) || !ValidateHostName(args.HostName)))
  268. {
  269. throw new CassiniException("Invalid Hostname", ErrorField.HostName);
  270. }
  271. IPAddress ip = ParseIP(args.IPMode, args.IPv6, args.IPAddress);
  272. switch (args.PortMode)
  273. {
  274. case PortMode.FirstAvailable:
  275. if (args.PortRangeStart < 1)
  276. {
  277. throw new CassiniException("Invalid port.", ErrorField.PortRangeStart);
  278. }
  279. if (args.PortRangeEnd < 1)
  280. {
  281. throw new CassiniException("Invalid port.", ErrorField.PortRangeEnd);
  282. }
  283. if (args.PortRangeStart > args.PortRangeEnd)
  284. {
  285. throw new CassiniException("Port range end must be equal or greater than port range start.",
  286. ErrorField.PortRange);
  287. }
  288. if (GetAvailablePort(args.PortRangeStart, args.PortRangeEnd, ip, true) == 0)
  289. {
  290. throw new CassiniException("No available port found.", ErrorField.PortRange);
  291. }
  292. break;
  293. case PortMode.Specific:
  294. // start waiting....
  295. //TODO: design this hack away.... why am I waiting in a validation method?
  296. int now = Environment.TickCount;
  297. // wait until either 1) the specified port is available or 2) the specified amount of time has passed
  298. while (Environment.TickCount < now + args.WaitForPort &&
  299. GetAvailablePort(args.Port, args.Port, ip, true) != args.Port)
  300. {
  301. Thread.Sleep(100);
  302. }
  303. // is the port available?
  304. if (GetAvailablePort(args.Port, args.Port, ip, true) != args.Port)
  305. {
  306. throw new CassiniException("Port is in use.", ErrorField.Port);
  307. }
  308. break;
  309. default:
  310. throw new CassiniException("Invalid PortMode", ErrorField.None);
  311. }
  312. }
  313. #endregion
  314. }
  315. }