/// <summary> /// Delete a directory /// </summary> /// <param name="strDir">Directory to delete</param> /// <param name="logger"></param> /// <param name="closeProcesses">Force a process close if it cannot be deleted (i.e. in use)</param> /// <param name="waitTimeIfInUse">If in-use, time to wait (in seconds) before either failing or closing all processes if forceCloseProcesses is true.</param> private static void DoDeleteDirectory( string strDir, ILoggerInterface logger, List <string> closeProcesses = null, int waitTimeIfInUse = 10) { if (string.IsNullOrWhiteSpace(strDir)) { logger.LogWarning(true, "Empty directory name provided DoDeleteDirectory, skipping."); return; } ValidateDirectoryDepthDeletion(strDir); strDir = EnsureLongPathSupportIfAvailable(strDir); if (!Directory.Exists(strDir)) { return; } logger.LogInfo(true, "Removing directory {0} with close processes {1}", strDir, closeProcesses == null ? string.Empty : string.Join(", ", closeProcesses)); if (closeProcesses?.Any() == true) { var processes = UtilsProcess.GetPathProcessesInfo(strDir, logger, true); foreach (var p in processes.AsIterable()) { logger.LogWarning( false, "The following process might be blocking files in the directory: {0}", p.CommandLine); } if (processes.Any()) { UtilsProcess.ClosePathProcesses(strDir, closeProcesses, logger); } } RetryWhile( () => { DeleteDirectoryAndRemovePermissionsIfNeeded(strDir); }, ExceptionIsAccessDeniedOrFileInUse, waitTimeIfInUse * 1000, logger); if (Directory.Exists(strDir)) { throw new Exception($"Could not completely delete directory '{strDir}', see log for details."); } }
/// <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> /// Get the executable path of a process from it's process id /// </summary> /// <param name="processId"></param> /// <returns></returns> public static ProcessInfo GetProcessInfo(int processId, ILoggerInterface logger) { ProcessInfo result = new ProcessInfo(); result.ProcessId = processId; try { string query = "SELECT ExecutablePath, Name, CommandLine FROM Win32_Process WHERE ProcessId = " + processId; using (ManagementObjectSearcher mos = new ManagementObjectSearcher(query)) { using (ManagementObjectCollection moc = mos.Get()) { result.MainModulePath = (from mo in moc.Cast <ManagementObject>() select mo["ExecutablePath"]).First().ToString(); result.ProcessName = (from mo in moc.Cast <ManagementObject>() select mo["Name"]).First().ToString(); result.CommandLine = (from mo in moc.Cast <ManagementObject>() select mo["CommandLine"]).First().ToString(); } } } catch (Exception e) { logger.LogWarning(true, e.Message); } return(result); }
/// <summary> /// Ensure user is in group /// </summary> /// <param name="userPrincipalName"></param> /// <param name="groupname"></param> /// <param name="logger"></param> /// <param name="acp"></param> public static void EnsureUserInGroup( string userPrincipalName, string groupname, ILoggerInterface logger, AccountManagementPrincipalContext acp) { logger.LogInfo(true, $"Ensure user '{userPrincipalName}' in group '{groupname}'"); UserPrincipal user = SearchUser(userPrincipalName, acp, out var userContext); if (user == null) { userContext?.Dispose(); throw new Exception($"User '{userPrincipalName}' not found."); } GroupPrincipal group = SearchGroup(groupname, acp, out var groupContext); if (group == null) { userContext?.Dispose(); groupContext?.Dispose(); throw new Exception($"Group '{groupname}' not found."); } logger.LogWarning(false, $"Found group '{group.Name}' '{group.Sid}' in context '{groupContext.ConnectedServer}' and '{groupContext.ContextType}'"); foreach (Principal member in group.GetMembers(true)) { if (member.SamAccountName == user.SamAccountName) { logger.LogInfo(true, $"User already in group '{groupname}'"); return; } } group.Members.Add(user); group.Save(); userContext.Dispose(); groupContext.Dispose(); logger.LogInfo(true, $"Added user '{userPrincipalName}' to group '{groupname}'"); }
/// <summary> /// /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="T2"></typeparam> /// <param name="source"></param> /// <param name="condition"></param> /// <param name="selector"></param> /// <param name="name"></param> /// <param name="logger"></param> /// <returns></returns> public static List <T2> QueryEnumerable <T, T2>(IEnumerable <T> source, Func <T, bool> condition, Func <T, T2> selector, Func <T, string> name, ILoggerInterface logger) { var results = new List <T2>(); foreach (var s in source) { bool isMatch = false; try { isMatch = condition(s); } catch (Exception e) { string displayName = null; try { displayName = name(s); } catch { // ignored } logger.LogWarning(true, "Error while inspecting condition on object {0}: {1}", displayName, e.Message); } if (!isMatch) { continue; } results.Add(selector(s)); } return(results); }
/// <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> /// Find a certificate in IIS central certificate store /// </summary> /// <param name="hostName"></param> /// <param name="logger"></param> /// <param name="certificatePath"></param> /// <returns></returns> public static X509Certificate2 FindCertificateInCentralCertificateStore( string hostName, ILoggerInterface logger, out string certificatePath) { string centralStorePath = UtilsIis.CentralStorePath(logger); FileInfo certificateFile = null; // Look for a certificate file that includes wildcard matching logic // https://serverfault.com/questions/901494/iis-wildcard-https-binding-with-centralized-certificate-store var hostNameParts = hostName.Split(".".ToCharArray()).Reverse().ToList(); foreach (var f in new DirectoryInfo(centralStorePath).EnumerateFiles()) { // Check if this certificate file is valid for the hostname... var certNameParts = Path.GetFileNameWithoutExtension(f.FullName).Split(".".ToCharArray()).Reverse() .ToList(); // This won't allow for nested subdomain with wildcards, but it's a good starting point // i.e. a hostname such as "a.mytest.mydomain.com" won't be matched to a certifica // such as "_.mydomain.com" // but "mytest.mydomain.com" will match to "_.mydomain.com". if (certNameParts.Count != hostNameParts.Count) { continue; } bool isMatch = true; for (int x = 0; x < hostNameParts.Count; x++) { if (hostNameParts[x] == "*" || certNameParts[x] == "_") { continue; } if (hostNameParts[x] != certNameParts[x]) { isMatch = false; break; } } if (isMatch) { certificateFile = f; break; } } certificatePath = certificateFile?.FullName; // This is null on purpose. string certificatePassword = null; X509Certificate2Collection collection = new X509Certificate2Collection(); if (certificateFile != null) { logger.LogInfo(true, "Found potential certificate matching file at {0}", certificateFile.FullName); try { // Usamos ephemeral keyset para que no almacene claves en la máquina todo el tiempo... collection.Import(certificateFile.FullName, certificatePassword, X509KeyStorageFlags.EphemeralKeySet); var originalCert = collection[0]; logger.LogInfo(true, "Certificate IssuerName '{0}'", originalCert.IssuerName.Name); logger.LogInfo(true, "Certificate FriendlyName '{0}'", originalCert.FriendlyName); logger.LogInfo(true, "Certificate SubjectName '{0}'", originalCert.SubjectName.Name); logger.LogInfo(true, "Certificate NotBefore '{0}'", originalCert.NotBefore.ToString("HH:mm:ss yyyy/MM/dd")); return(originalCert); } catch (Exception e) { logger.LogWarning(false, $"Error importing certificate: '{certificateFile.FullName}'." + e.Message); } } return(null); }
public static X509Certificate2 CreateSelfSignedCertificateOldImplementationNetFramework( string subjectName, string friendlyName, ILoggerInterface logger, int expirationDays = 90) { // create DN for subject var dn = new CX500DistinguishedName(); dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE); // create DN for the issuer var issuer = new CX500DistinguishedName(); issuer.Encode("CN=ChefCertificate", X500NameFlags.XCN_CERT_NAME_STR_NONE); // create a new private key for the certificate CX509PrivateKey privateKey = new CX509PrivateKey(); privateKey.ProviderName = "Microsoft Base Cryptographic Provider v1.0"; privateKey.MachineContext = true; privateKey.Length = 2048; privateKey.KeySpec = X509KeySpec.XCN_AT_SIGNATURE; // use is not limited privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG; privateKey.Create(); // Use the stronger SHA512 hashing algorithm var hashobj = new CObjectId(); hashobj.InitializeFromAlgorithmName( ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID, ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY, AlgorithmFlags.AlgorithmFlagsNone, "SHA512"); // add extended key usage if you want - look at MSDN for a list of possible OIDs var oid = new CObjectId(); oid.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // SSL server var oidlist = new CObjectIds(); oidlist.Add(oid); var eku = new CX509ExtensionEnhancedKeyUsage(); eku.InitializeEncode(oidlist); // Create the self signing request var cert = new CX509CertificateRequestCertificate(); cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, string.Empty); cert.Subject = dn; cert.Issuer = issuer; cert.NotBefore = DateTime.Now; // this cert expires immediately. Change to whatever makes sense for you cert.NotAfter = DateTime.Now.AddDays(expirationDays); cert.X509Extensions.Add((CX509Extension)eku); // add the EKU cert.HashAlgorithm = hashobj; // Specify the hashing algorithm cert.Encode(); // encode the certificate // Do the final enrollment process var enroll = new CX509Enrollment(); enroll.InitializeFromRequest(cert); // load the certificate enroll.CertificateFriendlyName = friendlyName; // Optional: add a friendly name string csr = enroll.CreateRequest(); // Output the request in base64 // and install it back as the response enroll.InstallResponse( InstallResponseRestrictionFlags.AllowUntrustedCertificate, csr, EncodingType.XCN_CRYPT_STRING_BASE64, string.Empty); // no password // output a base64 encoded PKCS#12 so we can import it back to the .Net security classes var base64Encoded = enroll.CreatePFX( string.Empty, // no password, this is for internal consumption PFXExportOptions.PFXExportChainWithRoot); // Delete the key var storePath = UtilsCertificate.FindKeyStoragePath(privateKey.UniqueContainerName); if (string.IsNullOrWhiteSpace(storePath)) { logger.LogWarning(false, $"Unable to determine private key store path for key with UCN '{privateKey.UniqueContainerName}', keys will build up in disk storage."); } else { File.Delete(storePath); } // instantiate the target class with the PKCS#12 data (and the empty password) var crt = new X509Certificate2( Convert.FromBase64String(base64Encoded), string.Empty, // mark the private key as exportable (this is usually what you want to do) X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable); // Remove the locally signed certificate from the local store, as we are only interested in // the exported PFX file UtilsCertificate.RemoveCertificateFromLocalStoreByThumbprint(crt.Thumbprint); return(crt); }
/// <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> /// Copy files recursively. /// </summary> /// <param name="source"></param> /// <param name="target"></param> /// <param name="overwrite"></param> /// <param name="ignoreOnDeployPattern"></param> /// <param name="logger"></param> private static void DoCopyFilesRecursivelyFast( DirectoryInfo source, DirectoryInfo target, bool overwrite, string ignoreOnDeployPattern, ILoggerInterface logger) { var files = source.EnumerateFiles("*", SearchOption.AllDirectories); ParallelOptions pop = new ParallelOptions(); // The bottle neck here is disk rather than CPU... but number of CPU's is a good measure // of how powerful the target machine might be... pop.MaxDegreeOfParallelism = (int)Math.Ceiling(Environment.ProcessorCount * 1.5); logger.LogInfo(true, "Copying files from {1} with {0} threads.", pop.MaxDegreeOfParallelism, source.FullName); var ignoreOnDeployRegex = string.IsNullOrWhiteSpace(ignoreOnDeployPattern) ? null : new Regex(ignoreOnDeployPattern); int fileCount = 0; int dirCount = 0; Stopwatch sw = new Stopwatch(); sw.Start(); Parallel.ForEach(files, pop, (i) => { try { var dir = i.Directory.FullName; var relativeDir = dir.Substring(source.FullName.Length, dir.Length - source.FullName.Length) .TrimStart("\\".ToCharArray()); var relativeFile = Path.Combine(relativeDir, i.Name); var destDir = Path.Combine(target.FullName, relativeDir); var destFile = new FileInfo(Path.Combine(destDir, i.Name)); if (ignoreOnDeployRegex?.IsMatch(relativeFile) == true) { return; } if (!Directory.Exists(destDir)) { Directory.CreateDirectory(destDir); Interlocked.Add(ref dirCount, 1); } if ((!destFile.Exists) || (destFile.Exists && overwrite)) { i.CopyTo(destFile.FullName, true); Interlocked.Add(ref fileCount, 1); } } catch (Exception e) { // Reserved names cannot be copied... if (InvalidWindowsFileNames.Contains(i.Name.ToLower())) { logger.LogWarning(true, "Skipped invalid file: " + i.FullName); } else { throw new Exception("Error copying file: " + i.FullName, e); } } if (sw.ElapsedMilliseconds > 2000) { lock (string.Intern("utils-system-filecopy-fast")) { if (sw.ElapsedMilliseconds > 2000) { try { int leftPos = Console.CursorLeft - 80; Console.SetCursorPosition(leftPos >= 0 ? leftPos : 0, Console.CursorTop); Console.Write($"Copied {fileCount} files.".PadRight(80, " ".ToCharArray().First())); } catch (IOException) { // ignored, the console might no always be available (i.e. in a service) // Exception Type: 'System.IO.IOException // ExceptionMessage: The handle is invalid. } sw.Restart(); } } } }); logger.LogInfo(true, "Copied {0} files and {1} directories.", fileCount, dirCount); }
/// <summary> /// Create a user or return one if it does not exist. /// </summary> /// <param name="identity"></param> /// <param name="password"></param> /// <param name="displayName"></param> /// <param name="logger"></param> /// <param name="acp"></param> /// <returns></returns> public static UserPrincipal EnsureUserExists( string identity, string password, string displayName, ILoggerInterface logger, AccountManagementPrincipalContext acp) { var parsedUserName = new FqdnNameParser(identity); if (parsedUserName.UserPrincipalName.Length > 64) { throw new Exception($"Windows account userPrincipalName '{parsedUserName.UserPrincipalName}' cannot be longer than 64 characters."); } using (PrincipalContext pc = BuildPrincipal(acp)) { UserPrincipal up = FindUser(identity, pc); string samAccountName = parsedUserName.SamAccountName; logger.LogInfo(false, $"Ensure windows account exists '{samAccountName}@{password}' with userPrincipal '{identity}'"); if (up == null) { up = new UserPrincipal(pc, samAccountName, password, true); } else { logger.LogInfo(true, $"Found account IsAccountLockedOut={up.IsAccountLockedOut()}, SamAccountName={up.SamAccountName}"); // Make sure we have the latest password, just in case // the pwd algorithm generation changes... try { up.SetPassword(password); } catch (Exception e) { logger.LogWarning(true, "Cannot update password for account: " + e.Message + e.InnerException?.Message); } } up.UserCannotChangePassword = true; up.PasswordNeverExpires = true; up.Enabled = true; up.DisplayName = displayName; up.Description = parsedUserName.UserPrincipalName; // If we are in a domain, assign the user principal name if (pc.ContextType == ContextType.Domain) { logger.LogInfo(true, "Setting UserPrincipalName to '{0}'", parsedUserName.UserPrincipalName); up.UserPrincipalName = parsedUserName.UserPrincipalName + "@" + parsedUserName.DomainName.ToLower(); } if (up.IsAccountLockedOut()) { try { up.UnlockAccount(); } catch (Exception e) { logger.LogWarning(true, "Cannot unlock account: " + e.Message + e.InnerException?.Message); } } try { up.Save(); } catch (Exception e) { logger.LogException(new Exception("Error while saving user", e), EventLogEntryType.Warning); // Sometimes it crashes, but everything was OK (weird?) // so we check again if the user has been created Thread.Sleep(500); up = FindUser(identity, pc); if (up == null) { // Rethrow the original, whatever it was. ExceptionDispatchInfo.Capture(e).Throw(); } } return(up); } }