/// <summary> /// Central store path for certificates. Returns exception if not configured or cannot be returned. /// </summary> public static string CentralStorePath(ILoggerInterface logger) { if (!CentralStoreEnabled()) { throw new Exception( "IIS Central store path not enabled or installed. Please check https://blogs.msdn.microsoft.com/kaushal/2012/10/11/central-certificate-store-ccs-with-iis-8-windows-server-2012/"); } string certStoreLocation = Convert.ToString(UtilsRegistry.GetRegistryKeyValue64( RegistryHive.LocalMachine, "SOFTWARE\\Microsoft\\IIS\\CentralCertProvider", "CertStoreLocation", string.Empty)); if (string.IsNullOrWhiteSpace(certStoreLocation)) { throw new Exception("IIS Central store location not configured"); } var resolvedCertStoreLocation = certStoreLocation; if (UtilsJunction.IsJunctionOrSymlink(certStoreLocation)) { resolvedCertStoreLocation = UtilsJunction.ResolvePath(resolvedCertStoreLocation); } if (UtilsSystem.IsNetworkPath(resolvedCertStoreLocation)) { logger.LogWarning(true, "Central Certificate Store Path is located on a network share [{0}]. This has proven to be unstable as CCS will cache corrupted certificates when it is unable to read from the network share.", certStoreLocation); } return(certStoreLocation); }
/// <summary> /// Find a site with a specific name in a resilient way /// </summary> /// <param name="manager"></param> /// <param name="siteName"></param> /// <param name="logger"></param> /// <returns></returns> public static List <Site> FindSiteWithName(ServerManager manager, string siteName, ILoggerInterface logger) { return(UtilsSystem.QueryEnumerable( manager.Sites, (s) => s.Name == siteName, (s) => s, (s) => s.Name, logger)); }
/// <summary> /// Write contents of a web.config. /// /// Takes into consideration maximum web.config file size as defined at the OS level. /// /// Throws an exception if size is exceeded. /// </summary> /// <param name="webconfigfilepath"></param> /// <param name="webConfigContents"></param> /// <returns></returns> public static void WriteWebConfig(string webconfigfilepath, string webConfigContents) { long requiredSizeKb = UtilsSystem.GetStringSizeInDiskBytes(webConfigContents) / 1024; int maxSizeKb = UtilsIis.GetMaxWebConfigFileSizeInKb() - 1; if (requiredSizeKb >= maxSizeKb) { throw new Exception($"Required web.config size of {requiredSizeKb}Kb for CDN chef feature exceeds current limit of {maxSizeKb}Kb. Please, review this in 'HKLM\\SOFTWARE\\Microsoft\\InetStp\\Configuration\\MaxWebConfigFileSizeInKB'"); } File.WriteAllText(webconfigfilepath, webConfigContents); }
/// <summary> /// Get an instance of FqdnNameParser /// </summary> /// <param name="name"></param> /// <param name="autoCalculateSamAccountName">Usernames used by CHEF al have an autocalculated sam account name</param> public FqdnNameParser(string name, bool autoCalculateSamAccountName = true) { // Backwards compatibility fix if (name.StartsWith("sid:")) { name = name.Replace("sid:", string.Empty).Trim(); } if (name.Contains("@")) { this.DomainName = name.Split("@".ToCharArray()).Last(); name = name.Split("@".ToCharArray()).First(); } if (name.Contains("\\")) { this.DomainName = name.Split("\\".ToCharArray()).First(); name = name.Split("\\".ToCharArray()).Last(); } if (UtilsSystem.IsValidSid(name)) { // Assume we are using the user principal name, we cannot determine the context here... this.Sid = new SecurityIdentifier(name); } else { this.UserPrincipalName = name; if (autoCalculateSamAccountName) { this.SamAccountName = UtilsWindowsAccounts.SamAccountNameFromUserPrincipalName(this.UserPrincipalName); } } if (name.StartsWith("#")) { throw new Exception("Unsupported prefix # used in identity definition."); // Assume we are using the user principal name, we cannot determine the context here... // this.UserPrincipalName = name.Replace("#", string.Empty); // this.SamAccountName = name.Replace("#", string.Empty); } if (!string.IsNullOrWhiteSpace(this.DomainName)) { this.ContextType = (Environment.MachineName == this.DomainName || this.DomainName.Equals("localhost", StringComparison.CurrentCultureIgnoreCase)) ? System.DirectoryServices.AccountManagement.ContextType.Machine : System.DirectoryServices.AccountManagement.ContextType.Domain; } }
/// <summary> /// Delete a site from the server manager. /// /// The behaviour has been enhanced to prevent deleting a site from affecting bindings from /// other sites. See related article. Doing a simple sites.Remove() call can totally mess up bindings from /// other websites, which becomes even worse when using CCS (central certificate store). /// /// @see https://stackoverflow.com/questions/37792421/removing-secured-site-programmatically-spoils-other-bindings /// </summary> /// <param name="site"></param> /// <param name="manager"></param> /// <param name="criteria"></param> /// <param name="logger"></param> public static void RemoveSiteBindings( Site site, ServerManager manager, Func <Binding, bool> criteria, ILoggerInterface logger) { // Site bindings var siteBindingsToRemove = site.Bindings.Where(criteria).ToList(); // Make sure that this is "resilient" var allBindings = UtilsSystem.QueryEnumerable( manager.Sites, (s) => true, (s) => s.Bindings, (s) => s.Name, logger); // We have to collect all existing bindings from other sites, including ourse var existingBindings = (from p in allBindings select p.Where((i) => i.Protocol == "https")).SelectMany((i) => i) .ToList(); // Remove all bindings for our site, we only care about SSL bindings,the other onse // will be removed with the site itself foreach (var b in siteBindingsToRemove) { existingBindings.Remove(b); // The central certificate store bool bindingUsedInAnotherSite = (from p in existingBindings where (p.Host == b.Host && p.BindingInformation == b.BindingInformation) || // A combination of port and usage of CCS is a positive, even if in different IP addresses (p.SslFlags.HasFlag(SslFlags.CentralCertStore) && b.SslFlags.HasFlag(SslFlags.CentralCertStore) && p.EndPoint.Port.ToString() == b.EndPoint.Port.ToString()) select 1).Any(); site.Bindings.Remove(b, bindingUsedInAnotherSite); } }
/// <summary> /// There is a delay between a serverManager.CommitChanges() and the actual /// materialization of the configuration. /// /// This methods waits for a specific site to be available. /// </summary> public static void WaitForSiteToBeAvailable(string siteName, ILoggerInterface logger) { UtilsSystem.RetryWhile( () => { using (ServerManager sm = new ServerManager()) { // Site is ready when state is available var state = UtilsSystem.QueryEnumerable( sm.Sites, (s) => s.Name == siteName, (s) => s, (s) => s.Name, logger).Single().State; } }, (e) => true, 3000, logger); }
/// <summary> /// Busca la carpeta de user settings para application pools /// que están configurados como "ApplicationPoolIdentity". /// </summary> /// <param name="pool"></param> /// <returns></returns> public static string FindStorageFolderForAppPoolWithDefaultIdentity(ApplicationPool pool) { // Peligroso... este método solo para las identidades de defecto. if (pool.ProcessModel.IdentityType != ProcessModelIdentityType.ApplicationPoolIdentity) { return(null); } // If the user profile is not being loaded, then no need // to handle this. if (pool.ProcessModel.LoadUserProfile == false) { return(null); } var suffix = "\\" + Environment.UserName; var pathWithEnv = Environment.ExpandEnvironmentVariables(@"%USERPROFILE%"); if (!pathWithEnv.EndsWith(suffix)) { return(null); } var subpath = pathWithEnv.Substring(0, pathWithEnv.Length - suffix.Length); var userpath = UtilsSystem.CombinePaths(subpath, pool.Name + ".IIS APPPOOL"); if (Directory.Exists(userpath)) { return(userpath); } userpath = UtilsSystem.CombinePaths(subpath, pool.Name); if (Directory.Exists(userpath)) { return(userpath); } return(null); }
/// <summary> /// Stop a site /// </summary> /// <param name="p"></param> /// <param name="maxWait"></param> /// <returns></returns> private bool StopSite(Site p, int maxWait = WaitMaxForProcessMs) { var state = p.State; if (state == ObjectState.Stopped) { return(true); } if (state != ObjectState.Started) { return(false); } UtilsSystem.RetryWhile(() => p.Stop(), (e) => true, 2000, this.Logger); Stopwatch sw = Stopwatch.StartNew(); sw.Start(); while (true) { if (sw.ElapsedMilliseconds > maxWait) { break; } if (p.State == ObjectState.Stopped) { return(true); } Thread.Sleep(WaitPauseMs); } return(false); }
/// <summary> /// /// </summary> /// <param name="type"></param> /// <param name="path"></param> /// <returns></returns> public static string FindResourcePhysicalPath(Type type, string path) { string codeBasePath = UtilsSystem.GetCodeBaseDir(); var finalparts = NormalizeResourcePath(type, path); // There is a problem with the root path of the assembly, // so start removing leading namespaces until we find a physical match. for (int x = 0; x < 5; x++) { List <string> pathParts = new List <string>(); pathParts.Add(codeBasePath); pathParts.AddRange(finalparts.Skip(x).Take(finalparts.Count - x)); string pathAsFile = CombinePaths(pathParts.ToArray()); if (File.Exists(pathAsFile) || Directory.Exists(pathAsFile)) { return(pathAsFile); } } return(null); }
/// <summary> /// Creates a link (symlink or junction) /// </summary> /// <param name="mountPath">Path where the symlink or junction will be created.</param> /// <param name="mountDestination">Path the JUNCTION points to.</param> /// <param name="logger"></param> /// <param name="persistOnDeploy">If true, any files in the repo are synced to the content folder.</param> /// <param name="overWrite">If a junction or link already exists, overwrite it</param> /// <param name="linkType">Use this to force usage of symlinks. Otherwise junction/symlink is chosen by the method internally.</param> /// <returns></returns> public static void EnsureLink( string mountPath, string mountDestination, ILoggerInterface logger, bool persistOnDeploy, bool overWrite = false, LinkTypeRequest linkType = LinkTypeRequest.Auto) { var linkmanager = ReparsePointFactory.Create(); // On a local folder based deployment we might be redeploying on top of same application... if (Directory.Exists(mountPath)) { logger.LogWarning(true, "Mount destination already exists: {0}", mountPath); if (linkmanager.GetLinkType(mountPath) == LinkType.Junction || linkmanager.GetLinkType(mountPath) == LinkType.Symbolic) { logger.LogInfo(true, "Mount destination is junction, grabbing attributes."); var atts = linkmanager.GetLink(mountPath); logger.LogInfo(true, "Mount destination attributes: {0}", Newtonsoft.Json.JsonConvert.SerializeObject(atts)); var currentTarget = new DirectoryInfo(atts.Target); var requiredTarget = new DirectoryInfo(mountDestination); if (currentTarget.FullName == requiredTarget.FullName || overWrite) { // Remove it, it will be recreated anyways. Directory.Delete(mountPath); } else { // Something already exists. And it is NOT a junction equivalent to what // we are asking for. throw new Exception($"Could not mount junction because a directory or junction already exists at the junction source path: {mountPath}"); } } else { bool existingDirectoryHasFiles = Directory.EnumerateFiles(mountPath, "*", SearchOption.AllDirectories).Any(); // If the mountpath exists, but has nothing in it, delete it to make this process more error-proof. if (Directory.Exists(mountPath)) { if (persistOnDeploy) { // Copy any files, and then delete UtilsSystem.CopyFilesRecursivelyFast(mountPath, mountDestination, true, null, logger); Directory.Delete(mountPath, true); } else { if (!existingDirectoryHasFiles) { // Delete so we can junction Directory.Delete(mountPath, true); } } } } } // Create junction will fail if the physicial folder exists at the junction target // so the previous logic takes care of that...7 bool useSymlinkInsteadOfJunction; switch (linkType) { case LinkTypeRequest.Auto: useSymlinkInsteadOfJunction = mountDestination.StartsWith("\\"); break; case LinkTypeRequest.Junction: useSymlinkInsteadOfJunction = false; break; case LinkTypeRequest.Symlink: useSymlinkInsteadOfJunction = true; break; default: throw new NotSupportedException(); } logger.LogInfo(true, $"Creating {(useSymlinkInsteadOfJunction ? "symlink" : "junction")} '{mountPath}' => '{mountDestination}'"); // For remote drives, junctions will not work // https://helpcenter.netwrix.com/Configure_IT_Infrastructure/File_Servers/Enable_Symlink.html linkmanager.CreateLink(mountPath, mountDestination, useSymlinkInsteadOfJunction ? LinkType.Symbolic : LinkType.Junction); }
/// <summary> /// IIS is very bad at detecting and handling changes in certificates stored in the /// central certificate store, use this method to ensure that a hostname bound /// to a SSL termination is properly updated throughout IIS /// /// https://docs.microsoft.com/en-us/iis/get-started/whats-new-in-iis-85/certificate-rebind-in-iis85 /// https://delpierosysadmin.wordpress.com/2015/02/23/iis-8-5-enable-automatic-rebind-of-renewed-certificate-via-command-line/ /// </summary> public static void EnsureCertificateInCentralCertificateStoreIsRebound(string hostname, ILoggerInterface logger) { Dictionary <string, List <Binding> > temporaryBindings = new Dictionary <string, List <Binding> >(); using (var sm = new ServerManager()) { // Al sites that have an SSL termination bound to this hostname var sites = UtilsSystem.QueryEnumerable( sm.Sites, (s) => s.Bindings.Any(i => i.Protocol == "https" && hostname.Equals(i.Host, StringComparison.CurrentCultureIgnoreCase)), (s) => s, (s) => s.Name, logger).ToList(); // Remove temporarily foreach (var site in sites) { foreach (var binding in site.Bindings.Where((i) => i.Protocol == "https" && hostname.Equals(i.Host, StringComparison.CurrentCultureIgnoreCase)).ToList()) { if (!temporaryBindings.ContainsKey(site.Name)) { temporaryBindings[site.Name] = new List <Binding>(); } logger.LogInfo(true, "Removed binding {0} from site {1}", binding.BindingInformation, site.Name); temporaryBindings[site.Name].Add(binding); site.Bindings.Remove(binding); } } CommitChanges(sm); } // This wait here helps... Thread.Sleep(2000); // Now restore... using (var sm = new ServerManager()) { foreach (var siteName in temporaryBindings.Keys) { var site = FindSiteWithName(sm, siteName, logger).Single(); foreach (var binding in temporaryBindings[siteName]) { var b = site.Bindings.Add(binding.BindingInformation, binding.Protocol); b.SslFlags = binding.SslFlags; b.CertificateStoreName = binding.CertificateStoreName; b.UseDsMapper = binding.UseDsMapper; logger.LogInfo(true, "Restored binding {0} to site {1}", binding.BindingInformation, site.Name); } } CommitChanges(sm); } // This wait here helps also... Thread.Sleep(2000); }
/// <summary> /// Remove a host mapping /// </summary> /// <param name="hostname">Use null to remove all host mappings for this application Id</param> /// <param name="owner"></param> public void RemoveHostsMapping(string owner, string hostname = null) { UtilsSystem.RetryWhile(() => this.DoRemoveHostsMapping(owner, hostname), (e) => e is IOException, 2000, this.Logger); }
/// <summary> /// Añade un mapping al fichero hosts, o lo actualiza en función del hostname. /// Evita añadir duplicados o conflictivos. /// </summary> /// <param name="address"></param> /// <param name="hostname"></param> /// <param name="owner"></param> public void AddHostsMapping(string address, string hostname, string owner) { UtilsSystem.RetryWhile(() => this.DoAddHostsMapping(address, hostname, owner), (e) => e is IOException, 2000, this.Logger); }
private static List <Handle> GetProcessesThatBlockPathHandle(string path, ILoggerInterface logger, bool logDetails = false) { if (!File.Exists(path) && !Directory.Exists(path)) { return(new List <Handle>()); } string key = "SOFTWARE\\Sysinternals\\Handle"; string name = "EulaAccepted"; // This Utility has an EULA GUI on first run... try to avoid that // by manually setting the registry int?eulaaccepted64 = (int?)UtilsRegistry.GetRegistryKeyValue64(RegistryHive.CurrentUser, key, name, null); int?eulaaccepted32 = (int?)UtilsRegistry.GetRegistryKeyValue32(RegistryHive.CurrentUser, key, name, null); bool eulaaccepted = (eulaaccepted32 == 1 && eulaaccepted64 == 1); if (!eulaaccepted) { UtilsRegistry.SetRegistryValue(RegistryHive.CurrentUser, key, name, 1, RegistryValueKind.DWord); } // Normalize the path, to ensure that long path is not used, otherwise handle.exe won't work as expected string fileName = UtilsSystem.RemoveLongPathSupport(path); List <Handle> result = new List <Handle>(); string outputTool = string.Empty; // Gather the handle.exe from the embeded resource and into a temp file var handleexe = UtilsSystem.GetTempPath("handle") + Guid.NewGuid().ToString().Replace("-", "_") + ".exe"; UtilsSystem.EmbededResourceToFile(Assembly.GetExecutingAssembly(), "_Resources.Handle.exe", handleexe); try { using (Process tool = new Process()) { tool.StartInfo.FileName = handleexe; tool.StartInfo.Arguments = fileName; tool.StartInfo.UseShellExecute = false; tool.StartInfo.Verb = "runas"; tool.StartInfo.RedirectStandardOutput = true; tool.Start(); outputTool = tool.StandardOutput.ReadToEnd(); tool.WaitForExit(1000); if (!tool.HasExited) { tool.Kill(); } } } catch (Exception e) { logger.LogException(e, EventLogEntryType.Warning); } finally { UtilsSystem.DeleteFile(handleexe, logger, 5); } string matchPattern = @"(?<=\s+pid:\s+)\b(\d+)\b(?=\s+)"; foreach (Match match in Regex.Matches(outputTool, matchPattern)) { if (int.TryParse(match.Value, out var pid)) { if (result.All(i => i.pid != pid)) { result.Add(new Handle() { pid = pid }); } } } if (result.Any() && logDetails) { logger?.LogInfo(true, outputTool); } return(result); }
/// <summary> /// Closes all the handles that block any files in the specified path /// </summary> /// <param name="path"></param> /// <param name="allowedProcesses">List of whitelisted processes</param> /// <param name="logger"></param> public static void ClosePathProcesses( string path, List <string> allowedProcesses, ILoggerInterface logger) { if (string.IsNullOrWhiteSpace(path)) { return; } // Make sure the path exists if (!File.Exists(path) && !Directory.Exists(path)) { return; } // Load list of processes that block directory var processes = GetPathProcessesInfo(path, logger); // Filter the whitelisted string regex = string.Join("|", allowedProcesses); var processesThatWillBeClosed = processes.Where((i) => i.MainModulePath != null && Regex.IsMatch(i.MainModulePath, regex)).ToList(); if (!processesThatWillBeClosed.Any()) { return; } // Message of processes that will not be closed var processesThatWillNotBeClosed = processes.Except(processesThatWillBeClosed).ToList(); if (processesThatWillNotBeClosed.Any()) { logger.LogWarning(true, "The following processes are not whitelisted and will not be closed {0}", string.Join(", ", processesThatWillNotBeClosed.Select((i) => i.ProcessName))); } // Grab the actual process instances var processesInstances = GetProcessInstance(processesThatWillBeClosed); // First kill al the processes. foreach (var p in processesInstances) { try { logger.LogInfo(true, "Killing process: {0}", p.ProcessName); if (!p.HasExited) { p.Kill(); p.WaitForExit(3000); } } catch (Exception e) { logger.LogException(e, EventLogEntryType.Warning); } } // Even though the processes have exited, handles take a while to be released Thread.Sleep(500); foreach (var p in processesInstances) { bool hasClosed = UtilsSystem.WaitWhile(() => !p.HasExited, 15000, $"Waiting for process {p.ProcessName} to close.", logger); logger.LogInfo(true, "Process {0} has closed: {1}", p.ProcessName, hasClosed); } }
/// <summary> /// Start a site /// </summary> /// <param name="p"></param> /// <returns></returns> private bool StartSite(Site p) { if (p.State == ObjectState.Started) { return(true); } if (p.State != ObjectState.Stopped) { return(false); } try { // Try a couple of times before actually handling an exception UtilsSystem.RetryWhile(() => p.Start(), (e) => true, 3000, this.Logger); } catch (Exception e) { // This usually happens when a port is in-use if (Convert.ToString(e.HResult) == "-2147024864") { List <string> ports = new List <string>(); foreach (var binding in p.Bindings) { if (ports.Contains(binding.EndPoint.Port.ToString())) { continue; } ports.Add(binding.EndPoint.Port.ToString()); } // Let's give a hint into what ports might be in use.... var bindings = (from binding in p.Bindings select binding.Host + "@" + Convert.ToString(binding.EndPoint)); // Try to figure out what process is using the port... string process = null; try { var processes = UtilsProcessPort.GetNetStatPorts(); process = string.Join( ", " + Environment.NewLine, processes.Where((i) => ports.Contains(i.port_number)) .Select((i) => $"{i.process_name}:{i.port_number}")); } catch { // ignored } throw new Exception( "Cannot start website, a port or binding might be already in use by another application or website: " + Environment.NewLine + string.Join(", " + Environment.NewLine, bindings) + Environment.NewLine + "The following port usages have been detected:" + Environment.NewLine + process, e); } else if (Convert.ToString(e.HResult) == "-2146233088") { this.Logger.LogInfo(false, "Cannot start website: " + Environment.NewLine + e.Message + Environment.NewLine + e.InnerException?.Message); return(true); } else { throw; } } Stopwatch sw = Stopwatch.StartNew(); sw.Start(); while (true) { if (sw.ElapsedMilliseconds > WaitMaxForProcessMs) { break; } if (p.State == ObjectState.Started) { return(true); } Thread.Sleep(WaitPauseMs); } return(false); }